diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..c667620 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,50 @@ +name: Release + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v5 + + - name: Install uv + uses: astral-sh/setup-uv@v8.1.0 + with: + version: "latest" + + - name: Verify tag matches pyproject version + run: | + set -euo pipefail + TAG="${GITHUB_REF_NAME#v}" + PYV=$(grep -E '^version[[:space:]]*=' pyproject.toml | head -n1 | sed -E 's/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/') + echo "Tag version: $TAG" + echo "pyproject version: $PYV" + if [ "$TAG" != "$PYV" ]; then + echo "::error::Tag v$TAG does not match pyproject.toml version $PYV" + exit 1 + fi + + - name: Build sdist and wheel + run: uv build + + - name: List build artefacts + run: ls -lah dist/ + + - name: Create GitHub Release + uses: softprops/action-gh-release@v3 + with: + files: | + dist/*.whl + dist/*.tar.gz + generate_release_notes: true + fail_on_unmatched_files: true + draft: false + prerelease: false diff --git a/README.md b/README.md index 6e533f2..54c0afb 100644 --- a/README.md +++ b/README.md @@ -1,133 +1,574 @@ -# Welcome to Byaldi -_Did you know? In the movie RAGatouille, the dish Remy makes is not actually a ratatouille, but a refined version of the dish called "Confit Byaldi"._ +# FORetrieval -

The Byaldi logo, it's a cheerful rat using a magnifying glass to look at a complex document. It says 'byaldi' in the middle of a circle around the rat.

+FORetrieval is a multimodal document retrieval library built on top of [colpali-engine](https://github.com/illuin-tech/colpali). It indexes document pages as images using late-interaction models (ColPali, ColQwen2, ColQwen2.5) and retrieves the most relevant pages for a given query. It is used by [FORag](https://github.com/FOR-sight-ai/FORAG) as its retrieval backend. -⚠️ This is the pre-release version of Byaldi. Please report any issue you encounter, there will likely be quite a few quirks to iron out! +Key features: -Byaldi is [RAGatouille](https://github.com/answerdotai/ragatouille)'s mini sister project. It is a simple wrapper around the [ColPali](https://github.com/illuin-tech/colpali) repository to make it easy to use late-interaction multi-modal models such as ColPALI with a familiar API. +- **Four storage backends** — `local` (Colpali legacy `.pt` files), `qdrant` (default, embedded), `milvus` (Milvus Lite), and `remote` (HTTP-delegated vector-DB server) +- **Remote embedding server** — offload all embedding computation to a remote vLLM GPU server; the local machine needs no GPU +- **Metadata generation** — filesystem metadata always; AI-generated tags, language detection, and short descriptions optionally +- **Metadata filtering** — filter the retrieval pool by `ext`, `mtime`, `language`, `tags`, `document_type`, or arbitrary regex patterns before scoring +- **Docling ingestion** — optional semantic PDF chunking using [Docling](https://github.com/DS4SD/docling), producing image chunks aligned with document structure +- **Heatmap and circle visualisation** — relevance overlays for retrieved pages -## Getting started +## Installation -First, a warning: This is a pre-release library, using uncompressed indexes and lacking other kinds of refinements. +```bash +uv sync -Currently, we support all models supported by the underlying [colpali-engine](https://github.com/illuin-tech/colpali), including the newer, and better, ColQwen2 checkpoints, such as `vidore/colqwen2-v1.0`. Broadly, the aim is for byaldi to support all ColVLM models. +# Optional extras: +uv sync --extra qdrant # Qdrant storage backend (recommended for large indexes) +uv sync --extra docling # Docling-based PDF chunking +uv sync --extra embedding_server # Remote vLLM embedding server (adds paramiko for auto-deploy) +uv sync --extra quantization # 4-bit / 8-bit local model quantization (adds bitsandbytes) +``` -Additional backends will be supported in future updates. As byaldi exists to facilitate the adoption of multi-modal retrievers, we intend to also add support for models such as [VisRAG](https://github.com/openbmb/visrag). +## Releases -Eventually, we'll add an HNSW indexing mechanism, pooling, and, who knows, maybe 2-bit quantization? +FORetrieval uses [CalVer](https://calver.org) (`YYYY.MM.MICRO`). Releases are published on GitHub only (no PyPI) and inherit the visibility of this private repository. -It will get updated as the multi-modal ecosystem develops further! +Install a specific release directly from a git tag (requires SSH access to the repo): -### Pre-requisites +```bash +uv pip install "foretrieval @ git+ssh://git@github.com/FOR-sight-ai/FORetrieval.git@v2026.5.0" + +# With extras: +uv pip install "foretrieval[qdrant,vector_db_server] @ git+ssh://git@github.com/FOR-sight-ai/FORetrieval.git@v2026.5.0" +``` -#### Poppler +Or download the `.whl` / `.tar.gz` attached to a release on the [Releases page](https://github.com/FOR-sight-ai/FORetrieval/releases) and install it with `uv pip install `. -To convert pdf to images with a friendly license, we use the `pdf2image` library. This library requires `poppler` to be installed on your system. Poppler is very easy to install by following the instructions [on their website](https://poppler.freedesktop.org/). The tl;dr is: +## Pre-requisites -__MacOS with homebrew__ +### Poppler +Required by `pdf2image` for PDF-to-image conversion: + +**Debian / Ubuntu** ```bash -brew install poppler +sudo apt-get install -y poppler-utils ``` -__Debian/Ubuntu__ +### Flash-Attention (optional) + +Speeds up ColQwen2 / Gemma-based models significantly: ```bash -sudo apt-get install -y poppler-utils +uv pip install flash-attn +``` + +### Hardware + +ColPali uses multi-billion parameter models. A GPU is strongly recommended for indexing and search. Weak or older GPUs (sm_70+) work fine; CPU is supported but slow. + +## Quick usage + +```python +from foretrieval import MultiModalRetrieverModel + +# Index a folder of PDFs +model = MultiModalRetrieverModel.from_pretrained( + "vidore/colqwen2.5-v0.2", + index_root="my_indexes", + storage_qdrant=True, # use Qdrant backend (default) +) +model.index( + input_path="path/to/docs/", + index_name="my_index", + store_collection_with_index=True, +) +# Indexing is recursive: all files in subdirectories are also indexed. +# Use update_index_from_folder() to add only new files to an existing index, +# also recursing into subdirectories. + +# Load an existing index and search +model = MultiModalRetrieverModel.from_index( + index_path="my_index", + index_root="my_indexes", +) +results = model.search("maximum output current", k=3) +for r in results: + print(r.doc_id, r.page_num, r.score) +``` + +## Storage backends + +FORetrieval supports four backends for storing and searching embeddings: + +| Backend | `storage_backend` value | Dep | Scoring | Typical use | +|---------|------------------------|-----|---------|-------------| +| **Local** | `"local"` | — | Exact MAX_SIM (in-RAM) | Development, small corpora | +| **Qdrant** (default) | `"qdrant"` | `foretrieval[qdrant]` | Exact MAX_SIM (native) | Large local indexes, best accuracy | +| **Milvus** | `"milvus"` | `foretrieval[milvus]` | Approximate (mean-pool ANN + late-interaction rerank) | Milvus ecosystem | +| **Remote** | `"remote"` | httpx (core dep) | Delegated to server | GPU/network-separated deployments | + +The backend is fixed when an index is first created. It cannot be changed without recreating the index. + +```python +# Local (on-disk .pt files) +model = MultiModalRetrieverModel.from_pretrained(..., storage_backend="local") + +# Qdrant (embedded, on-disk) +model = MultiModalRetrieverModel.from_pretrained(..., storage_backend="qdrant") + +# Milvus Lite (file-based) +model = MultiModalRetrieverModel.from_pretrained(..., storage_backend="milvus") + +# Remote server (server holds collections; local machine stays stateless) +from foretrieval.vector_db_server import VectorDBServerConfig + +model = MultiModalRetrieverModel.from_pretrained( + "athrael-soju/colqwen3.5-4.5B-v3", + storage_backend="remote", + storage_config={ + "url": "http://gpu-server:18000", + "backend": "qdrant", # server-side backend + }, +) + +# Load existing index — backend and server URL auto-read from index_config.json.gz +# api_key must be re-supplied at load time (it is never persisted to disk) +model = MultiModalRetrieverModel.from_index( + "my_index", + index_root=".", + storage_config={"api_key": "my-secret"}, +) +``` + +> **Note:** The deprecated `storage_qdrant=True/False` flag still works (maps to +> `storage_backend="qdrant"/"local"`) but will be removed in a future release. + +## Metadata generation + +Metadata can be attached to each document at indexing time. Two levels are available: + +**Filesystem metadata (no AI required):** always populated from the file itself. + +| Field | Source | +|-------|--------| +| `stem`, `ext`, `mime` | filename and MIME type | +| `mtime` | file modification time (ISO-8601 UTC) | +| `page_count` | number of pages (PDFs only) | +| `author`, `title` | embedded PDF metadata (may be absent) | +| `image_width`, `image_height` | dimensions (images only) | + +**AI-generated metadata (requires an LLM provider):** `language`, `tags`, `document_type`, `short_description`. + +```python +from foretrieval.metadata import ai_metadata_provider_factory +from foretrieval.models_metadata import build_metadata_list_for_dir + +# No-AI provider: filesystem fields only +provider = ai_metadata_provider_factory(None) + +# AI provider: enriches with language, tags, document_type, short_description +provider = ai_metadata_provider_factory({ + "provider": "openrouter", + "name": "mistralai/mistral-small-3.2-24b-instruct", + "api_key": "...", +}) + +metadata_list = build_metadata_list_for_dir(Path("docs/"), provider) + +model.index( + input_path="docs/", + index_name="my_index", + metadata=metadata_list, +) +``` + +## Metadata filtering + +When an index was built with metadata, `search()` accepts a `filter_metadata` dict that restricts the scoring pool to matching documents only. + +### Declared filter fields + +```python +from foretrieval.models_metadata import MetadataFilter + +# Only PDF files +results = model.search("max current", k=3, filter_metadata={"ext": ".pdf"}) + +# Files modified after a date +results = model.search("max current", k=3, filter_metadata={ + "mtime": {">=": "2025-01-01T00:00:00Z"} +}) + +# Multiple criteria (AND by default) +results = model.search("max current", k=3, filter_metadata={ + "ext": ".pdf", + "language": "en", +}) + +# OR logic +results = model.search("max current", k=3, filter_metadata={ + "ext": [".pdf", ".docx"], + "logic": "OR", +}) +``` + +| Filter field | Type | Description | +|-------------|------|-------------| +| `ext` | `str` or `list[str]` | File extension(s) | +| `mtime` | `dict` | Operators: `>=`, `<=`, `>`, `<`, `==` against ISO-8601 string | +| `language` | `str` or `list[str]` | Language code(s), e.g. `"en"` | +| `tags` | `str` or `list[str]` | Any tag in common (requires AI metadata) | +| `document_type` | `str` or `list[str]` | Document type (requires AI metadata) | +| `logic` | `"AND"` or `"OR"` | How to combine criteria (default: `"AND"`) | + +Any other key is matched by exact string equality against the stored metadata dict. + +### Regex pattern matching + +Use the `regex` field for substring or pattern matching on any text field. Patterns use Python `re.search` and are **always case-insensitive**: + +```python +# Files whose name contains "general" +results = model.search("max current", k=3, filter_metadata={ + "regex": {"stem": "general"} +}) + +# Title contains "motor" or "pump" +results = model.search("specs", k=3, filter_metadata={ + "regex": {"title": "motor|pump"} +}) + +# Combine with ext filter +results = model.search("specs", k=3, filter_metadata={ + "ext": ".pdf", + "regex": {"stem": "^report_2025"}, +}) ``` -#### Flash-Attention +When the filter matches no documents, `search()` returns an empty list `[]` without raising. -Gemma uses a recent version of flash attention. To make things run as smoothly as possible, we'd recommend that you install it after installing the library: +## Docling ingestion + +FORetrieval optionally uses [Docling](https://github.com/DS4SD/docling) to convert PDFs into semantically meaningful image chunks rather than whole pages. Each chunk corresponds to a coherent region of text and associated figures. + +```python +model = MultiModalRetrieverModel.from_pretrained( + "vidore/colqwen2.5-v0.2", + ingestion={"backend": "docling"}, + index_root="my_indexes", +) +model.index(input_path="docs/", index_name="chunked_index") +``` + +Results include a `chunk_num` field identifying the exact Docling chunk within the page. + +## Running the test suite + +Install the dev dependencies first: ```bash -pip install --upgrade byaldi -pip install flash-attn +uv sync --extra dev ``` +### Unit tests -#### Hardware +No API keys, no GPU required — runs in seconds: -ColPali uses multi-billion parameter models to encode documents. We recommend using a GPU for smooth operations, though weak/older GPUs are perfectly fine! Encoding your collection would suffer from poor performance on CPU or MPS. +```bash +pytest -m "not slow and not integration" +``` -## Using `byaldi` +### Metadata tests (no AI) -Byaldi is largely modeled after RAGatouille, meaning that everything is designed to take the fewest lines of code possible, so you can very quickly build on top of it rather than spending time figuring out how to create a retrieval pipeline. +```bash +pytest tests/test_metadata_no_ai.py +``` -### Loading a model +### Metadata tests (with AI) -Loading a model with `byaldi` is extremely straightforward: +Set at least one API key: -```python3 -from byaldi import RAGMultiModalModel -# Optionally, you can specify an `index_root`, which is where it'll save the index. It defaults to ".byaldi/". -RAG = RAGMultiModalModel.from_pretrained("vidore/colqwen2-v1.0") +```bash +export OPENROUTER_API_KEY=... +export OPENAI_API_KEY=... +export MISTRAL_API_KEY=... +export OLLAMA_HOST=http://localhost:11434 # + optionally OLLAMA_MODEL (default: mistral-small-latest) ``` -If you've already got an index, and wish to load it along with the model necessary to query it, you can do so just as easily: +```bash +pytest tests/test_metadata_ai.py -v +``` + +All available backends are detected automatically and the suite runs once per backend. + +### Vector-store backend tests + +Unit tests for all backends (no GPU, backends mocked or run in-process): + +```bash +# Local backend +pytest tests/test_vector_store_local.py + +# Qdrant backend (unit: mock client; slow: embedded Qdrant round-trip) +pytest tests/test_vector_store_qdrant.py -m "not slow" +pytest tests/test_qdrant.py -m "not slow and not integration" + +# Milvus backend (unit: mock client; slow: Milvus Lite round-trip) +pytest tests/test_vector_store_milvus.py -m "not slow" + +# Remote backend (unit: HTTP calls mocked; server app: FastAPI TestClient) +pytest tests/test_vector_store_remote.py +pytest tests/test_vector_db_server_app.py +pytest tests/test_vector_db_server_config.py +pytest tests/test_vector_db_server_client.py +pytest tests/test_vector_db_server_manager.py + +# Factory and backend dispatch +pytest tests/test_vector_store_factory.py +pytest tests/test_colpali_backend_dispatch.py +``` + +Integration tests (require a live vector-DB server — see Remote vector-DB server section): + +```bash +# Set the server URL to skip the skipif guard +export FORETRIEVAL_TEST_DB_SERVER_URL=http://localhost:18000 +pytest tests/ -m "slow and integration" -v +``` +### Metadata filter tests + +```bash +pytest tests/test_metadata_filter.py +``` + +### Slow tests (GPU-dependent) + +Full ColPali indexing and search: -```python3 -from byaldi import RAGMultiModalModel -# Optionally, you can specify an `index_root`, which is where it'll look for the index. It defaults to ".byaldi/". -RAG = RAGMultiModalModel.from_index("your_index_name") +```bash +pytest -m slow ``` -### Creating an index -Creating an index with `byaldi` is simple and flexible. **You can index a single PDF file, a single image file, or a directory containing multiple of those**. Here's how to create an index: +### Markers reference + +| Marker | Meaning | +|--------|---------| +| `slow` | GPU-dependent or computationally expensive | +| `integration` | Requires a live API key or Ollama daemon | + +## Remote embedding server + +FORetrieval can offload all embedding computation to a remote GPU server running [vLLM](https://docs.vllm.ai). The local machine only loads the processor (tokenizer + image preprocessor) — no model weights, no GPU required locally. + +**Requirements:** +- vLLM ≥ 0.19.0 on the remote server +- Only **ColQwen3 / ColQwen3.5** models are supported by the vLLM `/pooling` endpoint. ColPali, ColQwen2, and ColQwen2.5 are not supported. +- Recommended model: `athrael-soju/colqwen3.5-4.5B-v3` (rank 3 on ViDoRe V3, 320-dim, Apache 2.0) + +### Quick start + +```python +from foretrieval import MultiModalRetrieverModel +from foretrieval.embedding_server import EmbeddingServerConfig + +cfg = EmbeddingServerConfig( + url="http://gpu-server:8000", + model_name="athrael-soju/colqwen3.5-4.5B-v3", +) -```python3 -from byaldi import RAGMultiModalModel -# Optionally, you can specify an `index_root`, which is where it'll save the index. It defaults to ".byaldi/". -RAG = RAGMultiModalModel.from_pretrained("vidore/colqwen2-v1.0") -RAG.index( - input_path="docs/", # The path to your documents - index_name=index_name, # The name you want to give to your index. It'll be saved at `index_root/index_name/`. - store_collection_with_index=False, # Whether the index should store the base64 encoded documents. - doc_ids=[0, 1, 2], # Optionally, you can specify a list of document IDs. They must be integers and match the number of documents you're passing. Otherwise, doc_ids will be automatically created. - metadata=[{"author": "John Doe", "date": "2021-01-01"}], # Optionally, you can specify a list of metadata for each document. They must be a list of dictionaries, with the same length as the number of documents you're passing. - overwrite=True # Whether to overwrite an index if it already exists. If False, it'll return None and do nothing if `index_root/index_name` exists. +model = MultiModalRetrieverModel.from_pretrained( + "athrael-soju/colqwen3.5-4.5B-v3", + index_root="my_indexes", + embedding_server=cfg, ) +model.index("path/to/docs/", index_name="my_index") +results = model.search("maximum altitude", k=3) ``` -And that's it! The model will start spinning and create your index, exporting all the necessary information to disk when it's done. You can then use the `RAGMultiModalModel.from_index("your_index_name")` method presented above to load it whenever needed (you don't need to do this right after creating it -- it's already loaded in memory and ready to go!). +### Auto-deploy -The main decision you'll have to make here is whether you want to set `store_collection_with_index` to True or not. If set to true, it greatly simplifies your workflow: the base64-encoded version of relevant documents will be returned as part of the query results, so you can immediately pipe it to your LLM. However, it adds considerable memory and storage requirements to your index, so you might want to set it to False (the default setting) if you're short on those resources, and create the base64 encoded versions yourself whenever needed. +Set `auto_deploy=True` to have FORetrieval SSH to the GPU server and start the vLLM Docker container automatically if it is not already running. Requires `foretrieval[embedding_server]` (adds `paramiko`). +```python +cfg = EmbeddingServerConfig( + url="http://gpu-server:8000", + model_name="athrael-soju/colqwen3.5-4.5B-v3", + auto_deploy=True, + ssh_host="gpu-server", # SSH target + ssh_user="myuser", # optional, defaults to $USER + n_gpus=-1, # -1 = all available GPUs (auto-detected via nvidia-smi) +) +``` -### Searching +The manager pulls `vllm/vllm-openai:latest`, starts the container with `--tensor-parallel-size N`, and writes a metadata file at `~/.foretrieval/deployment.json` on the remote. Subsequent calls detect the running container and skip redeployment. -Once you've created or loaded an index, you can start searching for relevant documents. Again, it's a single, very straightforward command: +### Authentication and SSL -```python3 -results = RAG.search(query, k=3) +```python +cfg = EmbeddingServerConfig( + url="https://gpu-server:8000", + model_name="athrael-soju/colqwen3.5-4.5B-v3", + api_key="my-secret-token", # Authorization: Bearer header + verify_ssl=False, # for self-signed certificates +) ``` -Results will be a list of `Result` objects, which you can also treat as normal dictionaries. Each result will be in this format: -```python3 -[ - { - "doc_id": 0, - "page_num": 10, - "score": 12.875, - "metadata": {}, - "base64": None +Deploy vLLM with `--api-key my-secret-token` to require authentication. + +### SSH tunnel (firewalled servers) + +If port 8000 is not directly reachable, open an SSH tunnel first: + +```bash +ssh -fNL 8000:localhost:8000 gpu-server +``` + +Then use `http://localhost:8000` as the URL. + +### EmbeddingServerConfig reference + +| Field | Default | Description | +|-------|---------|-------------| +| `url` | required | Base URL of the vLLM server | +| `model_name` | required | HuggingFace model ID (must contain `colqwen3`) | +| `auto_deploy` | `false` | SSH + Docker auto-deploy | +| `ssh_host` | `None` | SSH hostname (required when `auto_deploy=True`) | +| `ssh_user` | `None` | SSH username (defaults to `$USER`) | +| `ssh_key_path` | `None` | Path to SSH private key (defaults to SSH agent) | +| `n_gpus` | `-1` | Number of GPUs (`-1` = all available) | +| `port` | `8000` | Port exposed on the remote server | +| `hf_token` | `None` | HuggingFace token for gated models | +| `api_key` | `None` | Bearer token for server authentication | +| `verify_ssl` | `True` | Verify SSL certificates | +| `batch_size` | `4` | Images per request (auto-halved on OOM) | +| `request_timeout` | `120` | HTTP timeout in seconds | + +## Remote vector-DB server + +FORetrieval can offload all vector-store operations (indexing, search, fetch) to a remote HTTP server. The local machine only stores the processor and the shared sidecar files — collections live entirely on the server. + +**Requires:** `foretrieval[vector_db_server]` (adds `fastapi`, `uvicorn`, `paramiko`). + +### Quick start + +Start the server manually on a remote host: + +```bash +pip install "foretrieval[qdrant,milvus,vector_db_server]" # or: uv pip install "foretrieval[qdrant,milvus,vector_db_server]" +uvicorn foretrieval.vector_db_server.server:app --host 0.0.0.0 --port 18000 +# or: foretrieval-db-server (console script) +``` + +Then use it from the client: + +```python +from foretrieval import MultiModalRetrieverModel + +model = MultiModalRetrieverModel.from_pretrained( + "athrael-soju/colqwen3.5-4.5B-v3", + storage_backend="remote", + storage_config={ + "url": "http://gpu-server:18000", + "backend": "qdrant", # server-side storage backend: local | qdrant | milvus }, - ... -] +) +model.index("path/to/docs/", index_name="my_index") +results = model.search("maximum altitude", k=3) ``` -`page_num` are 1-indexed, while doc_ids are 0-indexed. This is to make simpler to operate with other PDF manipulation tools, where the 1st page is generally page 1. `page_num` for images and single-page PDFs will always be 1, it's only useful for longer PDFs. +### Auto-deploy -If you've passed metadata or encoded with the flag to store the base64 versions, these fields will be populated. Results are sorted by score, so item 0 from the list will always be the most relevant document, etc... +Set `auto_deploy=True` to have FORetrieval SSH to the remote host, build the Docker image from the local `foretrieval` source, and start the container automatically. Requires `foretrieval[vector_db_server]` and Docker on the remote host. -### Adding documents to an existing index +```python +model = MultiModalRetrieverModel.from_pretrained( + "athrael-soju/colqwen3.5-4.5B-v3", + storage_backend="remote", + storage_config={ + "url": "http://gpu-server:18000", + "backend": "qdrant", + "auto_deploy": True, + "ssh_host": "gpu-server", + "data_dir": "/var/lib/foretrieval_db", # bind-mounted into container + }, +) +``` + +The manager: +1. Uploads the `foretrieval/` package source to `~/foretrieval_db_build/` via SSH. +2. Runs `docker build -t foretrieval-vector-db:local` on the remote. +3. Starts the container: `docker run -p 18000:18000 -v :/data …`. +4. Writes metadata to `~/.foretrieval/db_deployment.json` on the remote. Subsequent calls detect the running container and skip re-deployment. + +### Authentication and SSL + +```python +storage_config={ + "url": "https://gpu-server:18000", + "backend": "qdrant", + "api_key": "my-secret-token", # Authorization: Bearer header + "verify_ssl": False, # for self-signed certificates +} +``` + +Start the server with `FOR_DB_API_KEY=my-secret-token` to require authentication. + +### SSH tunnel (firewalled servers) + +If port 18000 is not directly reachable, open an SSH tunnel first: + +```bash +ssh -fNL 18000:localhost:18000 gpu-server +``` + +Then use `http://localhost:18000` as the URL. + +### Server environment variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `FOR_DB_DATA_DIR` | `/data` | Root directory where collections are persisted | +| `FOR_DB_API_KEY` | `""` | Bearer token (auth disabled if empty) | +| `FOR_DB_HOST` | `0.0.0.0` | Bind address | +| `FOR_DB_PORT` | `18000` | Bind port | + +### VectorDBServerConfig reference + +| Field | Default | Description | +|-------|---------|-------------| +| `url` | required | Base URL of the vector-DB server | +| `backend` | `"qdrant"` | Server-side storage backend (`local`, `qdrant`, or `milvus`) | +| `storage_config` | `None` | Extra backend-specific config forwarded to the server (e.g. `{"candidate_limit": 128}` for Milvus) | +| `auto_deploy` | `false` | SSH + Docker auto-deploy | +| `ssh_host` | `None` | SSH hostname (required when `auto_deploy=True`) | +| `ssh_user` | `None` | SSH username (defaults to `$USER`) | +| `ssh_key_path` | `None` | Path to SSH private key (defaults to SSH agent) | +| `port` | `18000` | Port exposed on the remote server | +| `api_key` | `None` | Bearer token for server authentication (never persisted to disk) | +| `verify_ssl` | `True` | Verify SSL certificates | +| `request_timeout` | `120` | HTTP timeout in seconds | +| `data_dir` | `/var/lib/foretrieval_db` | Data path on the remote host (bind-mounted into container) | + +### Persistence and reload + +When a remote index is exported (`model._export_index()`), the `index_config.json.gz` on the local filesystem stores `storage_backend="remote"` and the server URL. Sensitive fields (`api_key`) are **never** persisted to disk. To reload the index later: + +```python +model = MultiModalRetrieverModel.from_index( + "my_index", + index_root=".", + storage_config={"api_key": "my-secret"}, # re-supply at load time +) +``` -Since indexes are in-memory, they're addition-friendly! If you need to ingest some new pdfs, just load your index with `from_index`, and then, call `add_to_index`, with similar parameters to the original `index()` method: +## Local model quantization -```python3 -RAG.add_to_index("path_to_new_docs", - store_collection_with_index: bool = False, - ... - ) +For local (non-remote) inference, 4-bit and 8-bit quantization reduce VRAM usage via [BitsAndBytes](https://github.com/TimDettmers/bitsandbytes). Requires `foretrieval[quantization]` and a CUDA device. + +```python +model = MultiModalRetrieverModel.from_pretrained( + "vidore/colqwen2.5-v0.2", + load_in_4bit=True, # or load_in_8bit=True + bnb_4bit_quant_type="nf4", # "nf4" (default) or "fp4" + bnb_4bit_compute_dtype="float16", # compute dtype +) ``` + +## Acknowledgements + +FORetrieval was originally forked from [Byaldi](https://github.com/answerdotai/byaldi), a wrapper around the [ColPali](https://github.com/illuin-tech/colpali) repository. It has since diverged significantly to add metadata generation and filtering, Qdrant storage, Docling ingestion, and heatmap visualisation. diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..db1a0c7 --- /dev/null +++ b/__init__.py @@ -0,0 +1,4 @@ +from .foretrieval import MultiModalRetrieverModel, ai_metadata_provider_factory +from .foretrieval.objects import Result + +__all__ = ["MultiModalRetrieverModel", "Result", "ai_metadata_provider_factory"] diff --git a/benchmark_results/sol_a.json b/benchmark_results/sol_a.json new file mode 100644 index 0000000..be858ec --- /dev/null +++ b/benchmark_results/sol_a.json @@ -0,0 +1,157 @@ +{ + "solution": "A", + "server_url": "http://localhost:8000", + "model_name": "athrael-soju/colqwen3.5-4.5B-v3", + "n_docs": 2, + "indexing_time_s": 47.63, + "vram_before_index_mb": 34916.0, + "vram_after_index_mb": 35180.0, + "vram_peak_mb": 35180.0, + "queries": [ + { + "query": "What is the normal operating cabin altitude in cruise for the A320?", + "latency_mean_s": 0.1782, + "latency_std_s": 0.184, + "latency_all_s": [ + 0.5462, + 0.0887, + 0.0843, + 0.084, + 0.0877 + ], + "top_3_pages": [ + { + "doc_id": 1, + "page_num": 32, + "score": 11.553253573170643 + }, + { + "doc_id": 1, + "page_num": 25, + "score": 11.356019967796211 + }, + { + "doc_id": 1, + "page_num": 18, + "score": 11.180209291807493 + } + ] + }, + { + "query": "How is the bleed air supply to the air conditioning system controlled on the A320?", + "latency_mean_s": 0.092, + "latency_std_s": 0.0021, + "latency_all_s": [ + 0.0962, + 0.0914, + 0.0912, + 0.0907, + 0.0905 + ], + "top_3_pages": [ + { + "doc_id": 1, + "page_num": 5, + "score": 13.838700063406733 + }, + { + "doc_id": 1, + "page_num": 7, + "score": 13.775969503165797 + }, + { + "doc_id": 1, + "page_num": 4, + "score": 13.566474871377235 + } + ] + }, + { + "query": "What happens to pressurization if both outflow valves fail in the open position?", + "latency_mean_s": 0.0912, + "latency_std_s": 0.0004, + "latency_all_s": [ + 0.0905, + 0.091, + 0.0916, + 0.0916, + 0.0912 + ], + "top_3_pages": [ + { + "doc_id": 1, + "page_num": 27, + "score": 12.071879404999464 + }, + { + "doc_id": 1, + "page_num": 34, + "score": 11.958589305897435 + }, + { + "doc_id": 1, + "page_num": 29, + "score": 11.811929385592844 + } + ] + }, + { + "query": "What is the purpose of the Ram Air inlet on the A320 air conditioning system?", + "latency_mean_s": 0.0905, + "latency_std_s": 0.0004, + "latency_all_s": [ + 0.0901, + 0.0906, + 0.0911, + 0.0908, + 0.0901 + ], + "top_3_pages": [ + { + "doc_id": 1, + "page_num": 14, + "score": 12.799402413312945 + }, + { + "doc_id": 1, + "page_num": 5, + "score": 12.754466361753614 + }, + { + "doc_id": 1, + "page_num": 2, + "score": 12.519547142354156 + } + ] + }, + { + "query": "Describe the function of the Pack Flow Control Valve and its operating modes.", + "latency_mean_s": 0.0872, + "latency_std_s": 0.0004, + "latency_all_s": [ + 0.0877, + 0.0877, + 0.0873, + 0.0868, + 0.0867 + ], + "top_3_pages": [ + { + "doc_id": 1, + "page_num": 6, + "score": 9.770793438987917 + }, + { + "doc_id": 1, + "page_num": 15, + "score": 9.762546751862187 + }, + { + "doc_id": 1, + "page_num": 5, + "score": 9.4065154169088 + } + ] + } + ] +} \ No newline at end of file diff --git a/benchmark_results/sol_b.json b/benchmark_results/sol_b.json new file mode 100644 index 0000000..3ac6f5c --- /dev/null +++ b/benchmark_results/sol_b.json @@ -0,0 +1,79 @@ +{ + "solution": "B", + "server_url": "http://localhost:8001", + "model_name": "vidore/colqwen2-v1.0", + "n_docs": 2, + "n_pages": 72, + "indexing_time_s": 11.73, + "pages_per_sec": 6.14, + "vram_before_index_mb": 4881.0, + "vram_after_index_mb": 6665.0, + "vram_peak_mb": 6665.0, + "queries": [ + { + "query": "What is the normal operating cabin altitude in cruise for the A320?", + "latency_mean_s": 0.0498, + "latency_std_s": 0.0023, + "latency_all_s": [ + 0.0544, + 0.049, + 0.0487, + 0.0486, + 0.0485 + ], + "top_3_pages": null + }, + { + "query": "How is the bleed air supply to the air conditioning system controlled on the A320?", + "latency_mean_s": 0.049, + "latency_std_s": 0.0013, + "latency_all_s": [ + 0.0486, + 0.0482, + 0.0484, + 0.0516, + 0.0481 + ], + "top_3_pages": null + }, + { + "query": "What happens to pressurization if both outflow valves fail in the open position?", + "latency_mean_s": 0.0481, + "latency_std_s": 0.0001, + "latency_all_s": [ + 0.0483, + 0.048, + 0.0482, + 0.0481, + 0.0481 + ], + "top_3_pages": null + }, + { + "query": "What is the purpose of the Ram Air inlet on the A320 air conditioning system?", + "latency_mean_s": 0.0492, + "latency_std_s": 0.0018, + "latency_all_s": [ + 0.0479, + 0.0481, + 0.0526, + 0.0486, + 0.0486 + ], + "top_3_pages": null + }, + { + "query": "Describe the function of the Pack Flow Control Valve and its operating modes.", + "latency_mean_s": 0.0482, + "latency_std_s": 0.0001, + "latency_all_s": [ + 0.0483, + 0.0481, + 0.0482, + 0.0481, + 0.0483 + ], + "top_3_pages": null + } + ] +} \ No newline at end of file diff --git a/byaldi.webp b/byaldi.webp deleted file mode 100644 index 1f8d53e..0000000 Binary files a/byaldi.webp and /dev/null differ diff --git a/byaldi/RAGModel.py b/byaldi/RAGModel.py deleted file mode 100644 index 32b66bf..0000000 --- a/byaldi/RAGModel.py +++ /dev/null @@ -1,181 +0,0 @@ -from pathlib import Path -from typing import Any, Dict, List, Optional, Union - -from PIL import Image - -from byaldi.colpali import ColPaliModel - -from byaldi.objects import Result - -# Optional langchain integration -try: - from byaldi.integrations import ByaldiLangChainRetriever -except ImportError: - pass - - -class RAGMultiModalModel: - """ - Wrapper class for a pretrained RAG multi-modal model, and all the associated utilities. - Allows you to load a pretrained model from disk or from the hub, build or query an index. - - ## Usage - - Load a pre-trained checkpoint: - - ```python - from byaldi import RAGMultiModalModel - - RAG = RAGMultiModalModel.from_pretrained("vidore/colpali-v1.2") - ``` - - Both methods will load a fully initialised instance of ColPali, which you can use to build and query indexes. - - ```python - RAG.search("How many people live in France?") - ``` - """ - - model: Optional[ColPaliModel] = None - - @classmethod - def from_pretrained( - cls, - pretrained_model_name_or_path: Union[str, Path], - index_root: str = ".byaldi", - device: str = "cuda", - verbose: int = 1, - ): - """Load a ColPali model from a pre-trained checkpoint. - - Parameters: - pretrained_model_name_or_path (str): Local path or huggingface model name. - device (str): The device to load the model on. Default is "cuda". - - Returns: - cls (RAGMultiModalModel): The current instance of RAGMultiModalModel, with the model initialised. - """ - instance = cls() - instance.model = ColPaliModel.from_pretrained( - pretrained_model_name_or_path, - index_root=index_root, - device=device, - verbose=verbose, - ) - return instance - - @classmethod - def from_index( - cls, - index_path: Union[str, Path], - index_root: str = ".byaldi", - device: str = "cuda", - verbose: int = 1, - ): - """Load an Index and the associated ColPali model from an existing document index. - - Parameters: - index_path (Union[str, Path]): Path to the index. - device (str): The device to load the model on. Default is "cuda". - - Returns: - cls (RAGMultiModalModel): The current instance of RAGMultiModalModel, with the model and index initialised. - """ - instance = cls() - index_path = Path(index_path) - instance.model = ColPaliModel.from_index( - index_path, index_root=index_root, device=device, verbose=verbose - ) - - return instance - - def index( - self, - input_path: Union[str, Path], - index_name: Optional[str] = None, - doc_ids: Optional[int] = None, - store_collection_with_index: bool = False, - overwrite: bool = False, - metadata: Optional[ - Union[ - Dict[Union[str, int], Dict[str, Union[str, int]]], - List[Dict[str, Union[str, int]]], - ] - ] = None, - max_image_width: Optional[int] = None, - max_image_height: Optional[int] = None, - **kwargs, - ): - """Build an index from input documents. - - Parameters: - input_path (Union[str, Path]): Path to the input documents. - index_name (Optional[str]): The name of the index that will be built. - doc_ids (Optional[List[Union[str, int]]]): List of document IDs. - store_collection_with_index (bool): Whether to store the collection with the index. - overwrite (bool): Whether to overwrite an existing index with the same name. - metadata (Optional[Union[Dict[Union[str, int], Dict[str, Union[str, int]]], List[Dict[str, Union[str, int]]]]]): - Metadata for the documents. Can be a dictionary mapping doc_ids to metadata dictionaries, - or a list of metadata dictionaries (one for each document). - - Returns: - None - """ - return self.model.index( - input_path, - index_name, - doc_ids, - store_collection_with_index, - overwrite=overwrite, - metadata=metadata, - max_image_width=max_image_width, - max_image_height=max_image_height, - **kwargs, - ) - - def add_to_index( - self, - input_item: Union[str, Path, Image.Image], - store_collection_with_index: bool, - doc_id: Optional[int] = None, - metadata: Optional[Dict[str, Union[str, int]]] = None, - ): - """Add an item to an existing index. - - Parameters: - input_item (Union[str, Path, Image.Image]): The item to add to the index. - store_collection_with_index (bool): Whether to store the collection with the index. - doc_id (Union[str, int]): The document ID for the item being added. - metadata (Optional[Dict[str, Union[str, int]]]): Metadata for the document being added. - - Returns: - None - """ - return self.model.add_to_index( - input_item, store_collection_with_index, doc_id, metadata=metadata - ) - - def search( - self, - query: Union[str, List[str]], - k: int = 10, - filter_metadata: Optional[Dict[str,str]] = None, - return_base64_results: Optional[bool] = None, - ) -> Union[List[Result], List[List[Result]]]: - """Query an index. - - Parameters: - query (Union[str, List[str]]): The query or queries to search for. - k (int): The number of results to return. Default is 10. - return_base64_results (Optional[bool]): Whether to return base64-encoded image results. - - Returns: - Union[List[Result], List[List[Result]]]: A list of Result objects or a list of lists of Result objects. - """ - return self.model.search(query, k, filter_metadata, return_base64_results) - - def get_doc_ids_to_file_names(self): - return self.model.get_doc_ids_to_file_names() - - def as_langchain_retriever(self, **kwargs: Any): - return ByaldiLangChainRetriever(model=self, kwargs=kwargs) diff --git a/byaldi/__init__.py b/byaldi/__init__.py deleted file mode 100644 index 3b8a6cf..0000000 --- a/byaldi/__init__.py +++ /dev/null @@ -1,6 +0,0 @@ -from importlib.metadata import version - -from .RAGModel import RAGMultiModalModel - -__version__ = version("Byaldi") -__all__ = ["RAGMultiModalModel"] diff --git a/byaldi/colpali.py b/byaldi/colpali.py deleted file mode 100644 index cc11dcb..0000000 --- a/byaldi/colpali.py +++ /dev/null @@ -1,744 +0,0 @@ -import os -import shutil -import tempfile -from importlib.metadata import version -from pathlib import Path -from typing import Dict, List, Optional, Union, cast - -import srsly -import torch -from colpali_engine.models import ColPali, ColPaliProcessor, ColQwen2, ColQwen2Processor -from pdf2image import convert_from_path -from PIL import Image - -from byaldi.objects import Result - -# Import version directly from the package metadata -VERSION = version("Byaldi") - - -class ColPaliModel: - def __init__( - self, - pretrained_model_name_or_path: Union[str, Path], - n_gpu: int = -1, - index_name: Optional[str] = None, - verbose: int = 1, - load_from_index: bool = False, - index_root: str = ".byaldi", - device: Optional[Union[str, torch.device]] = None, - **kwargs, - ): - if isinstance(pretrained_model_name_or_path, Path): - pretrained_model_name_or_path = str(pretrained_model_name_or_path) - - if ( - "colpali" not in pretrained_model_name_or_path.lower() - and "colqwen2" not in pretrained_model_name_or_path.lower() - ): - raise ValueError( - "This pre-release version of Byaldi only supports ColPali and ColQwen2 for now. Incorrect model name specified." - ) - - if verbose > 0: - print( - f"Verbosity is set to {verbose} ({'active' if verbose == 1 else 'loud'}). Pass verbose=0 to make quieter." - ) - - self.pretrained_model_name_or_path = pretrained_model_name_or_path - self.model_name = self.pretrained_model_name_or_path - self.n_gpu = torch.cuda.device_count() if n_gpu == -1 else n_gpu - device = ( - device or ( - "cuda" if torch.cuda.is_available() else "mps" if torch.backends.mps.is_available() else "cpu" - ) - ) - self.index_name = index_name - self.verbose = verbose - self.load_from_index = load_from_index - self.index_root = index_root - self.kwargs = kwargs - self.collection = {} - self.indexed_embeddings = [] - self.embed_id_to_doc_id = {} - self.doc_id_to_metadata = {} - self.doc_ids_to_file_names = {} - self.doc_ids = set() - - if "colpali" in pretrained_model_name_or_path.lower(): - self.model = ColPali.from_pretrained( - self.pretrained_model_name_or_path, - torch_dtype=torch.bfloat16, - device_map=( - "cuda" - if device == "cuda" - or (isinstance(device, torch.device) and device.type == "cuda") - else None - ), - token=kwargs.get("hf_token", None) or os.environ.get("HF_TOKEN"), - ) - elif "colqwen2" in pretrained_model_name_or_path.lower(): - self.model = ColQwen2.from_pretrained( - self.pretrained_model_name_or_path, - torch_dtype=torch.bfloat16, - device_map=( - "cuda" - if device == "cuda" - or (isinstance(device, torch.device) and device.type == "cuda") - else None - ), - token=kwargs.get("hf_token", None) or os.environ.get("HF_TOKEN"), - ) - self.model = self.model.eval() - - if "colpali" in pretrained_model_name_or_path.lower(): - self.processor = cast( - ColPaliProcessor, - ColPaliProcessor.from_pretrained( - self.pretrained_model_name_or_path, - token=kwargs.get("hf_token", None) or os.environ.get("HF_TOKEN"), - ), - ) - elif "colqwen2" in pretrained_model_name_or_path.lower(): - self.processor = cast( - ColQwen2Processor, - ColQwen2Processor.from_pretrained( - self.pretrained_model_name_or_path, - token=kwargs.get("hf_token", None) or os.environ.get("HF_TOKEN"), - ), - ) - - self.device = device - if device != "cuda" and not ( - isinstance(device, torch.device) and device.type == "cuda" - ): - self.model = self.model.to(device) - - if not load_from_index: - self.full_document_collection = False - self.highest_doc_id = -1 - else: - if self.index_name is None: - raise ValueError("No index name specified. Cannot load from index.") - - index_path = Path(index_root) / Path(self.index_name) - index_config = srsly.read_gzip_json(index_path / "index_config.json.gz") - self.full_document_collection = index_config.get( - "full_document_collection", False - ) - self.resize_stored_images = index_config.get("resize_stored_images", False) - self.max_image_width = index_config.get("max_image_width", None) - self.max_image_height = index_config.get("max_image_height", None) - - if self.full_document_collection: - collection_path = index_path / "collection" - json_files = sorted( - collection_path.glob("*.json.gz"), - key=lambda x: int(x.stem.split(".")[0]), - ) - - for json_file in json_files: - loaded_data = srsly.read_gzip_json(json_file) - self.collection.update({int(k): v for k, v in loaded_data.items()}) - - if self.verbose > 0: - print( - "You are using in-memory collection. This means every image is stored in memory." - ) - print( - "You might want to rethink this if you have a large collection!" - ) - print( - f"Loaded {len(self.collection)} images from {len(json_files)} JSON files." - ) - - embeddings_path = index_path / "embeddings" - embedding_files = sorted( - embeddings_path.glob("embeddings_*.pt"), - key=lambda x: int(x.stem.split("_")[1]), - ) - self.indexed_embeddings = [] - for file in embedding_files: - self.indexed_embeddings.extend(torch.load(file)) - - self.embed_id_to_doc_id = srsly.read_gzip_json( - index_path / "embed_id_to_doc_id.json.gz" - ) - # Restore keys to integers - self.embed_id_to_doc_id = { - int(k): v for k, v in self.embed_id_to_doc_id.items() - } - self.highest_doc_id = max( - int(entry["doc_id"]) for entry in self.embed_id_to_doc_id.values() - ) - self.doc_ids = set( - int(entry["doc_id"]) for entry in self.embed_id_to_doc_id.values() - ) - try: - # We don't want this error out with indexes created prior to 0.0.2 - self.doc_ids_to_file_names = srsly.read_gzip_json( - index_path / "doc_ids_to_file_names.json.gz" - ) - self.doc_ids_to_file_names = { - int(k): v for k, v in self.doc_ids_to_file_names.items() - } - except FileNotFoundError: - pass - - # Load metadata - metadata_path = index_path / "metadata.json.gz" - if metadata_path.exists(): - self.doc_id_to_metadata = srsly.read_gzip_json(metadata_path) - # Convert metadata keys to integers - self.doc_id_to_metadata = { - int(k): v for k, v in self.doc_id_to_metadata.items() - } - else: - self.doc_id_to_metadata = {} - - @classmethod - def from_pretrained( - cls, - pretrained_model_name_or_path: Union[str, Path], - n_gpu: int = -1, - verbose: int = 1, - device: Optional[Union[str, torch.device]] = None, - index_root: str = ".byaldi", - **kwargs, - ): - return cls( - pretrained_model_name_or_path=pretrained_model_name_or_path, - n_gpu=n_gpu, - verbose=verbose, - load_from_index=False, - index_root=index_root, - device=device, - **kwargs, - ) - - @classmethod - def from_index( - cls, - index_path: Union[str, Path], - n_gpu: int = -1, - verbose: int = 1, - device: Optional[Union[str, torch.device]] = None, - index_root: str = ".byaldi", - **kwargs, - ): - index_path = Path(index_root) / Path(index_path) - index_config = srsly.read_gzip_json(index_path / "index_config.json.gz") - - instance = cls( - pretrained_model_name_or_path=index_config["model_name"], - n_gpu=n_gpu, - index_name=index_path.name, - verbose=verbose, - load_from_index=True, - index_root=str(index_path.parent), - device=device, - **kwargs, - ) - - return instance - - def _export_index(self): - if self.index_name is None: - raise ValueError("No index name specified. Cannot export.") - - index_path = Path(self.index_root) / Path(self.index_name) - index_path.mkdir(parents=True, exist_ok=True) - - # Save embeddings - embeddings_path = index_path / "embeddings" - embeddings_path.mkdir(exist_ok=True) - num_embeddings = len(self.indexed_embeddings) - chunk_size = 500 - for i in range(0, num_embeddings, chunk_size): - chunk = self.indexed_embeddings[i : i + chunk_size] - torch.save(chunk, embeddings_path / f"embeddings_{i}.pt") - - # Save index config - index_config = { - "model_name": self.model_name, - "full_document_collection": self.full_document_collection, - "highest_doc_id": self.highest_doc_id, - "resize_stored_images": ( - True if self.max_image_width and self.max_image_height else False - ), - "max_image_width": self.max_image_width, - "max_image_height": self.max_image_height, - "library_version": VERSION, - } - srsly.write_gzip_json(index_path / "index_config.json.gz", index_config) - - # Save embed_id_to_doc_id mapping - srsly.write_gzip_json( - index_path / "embed_id_to_doc_id.json.gz", self.embed_id_to_doc_id - ) - - # Save doc_ids_to_file_names - srsly.write_gzip_json( - index_path / "doc_ids_to_file_names.json.gz", self.doc_ids_to_file_names - ) - - # Save metadata - srsly.write_gzip_json(index_path / "metadata.json.gz", self.doc_id_to_metadata) - - # Save collection if using in-memory collection - if self.full_document_collection: - collection_path = index_path / "collection" - collection_path.mkdir(exist_ok=True) - for i in range(0, len(self.collection), 500): - chunk = dict(list(self.collection.items())[i : i + 500]) - srsly.write_gzip_json(collection_path / f"{i}.json.gz", chunk) - - if self.verbose > 0: - print(f"Index exported to {index_path}") - - def index( - self, - input_path: Union[str, Path], - index_name: Optional[str] = None, - doc_ids: Optional[List[int]] = None, - store_collection_with_index: bool = False, - overwrite: bool = False, - metadata: Optional[List[Dict[str, Union[str, int]]]] = None, - max_image_width: Optional[int] = None, - max_image_height: Optional[int] = None, - ) -> Dict[int, str]: - if ( - self.index_name is not None - and (index_name is None or self.index_name == index_name) - and not overwrite - ): - raise ValueError( - f"An index named {self.index_name} is already loaded.", - "Use add_to_index() to add to it or search() to query it.", - "Pass a new index_name to create a new index.", - "Exiting indexing without doing anything...", - ) - return None - if index_name is None: - raise ValueError("index_name must be specified to create a new index.") - if store_collection_with_index: - self.full_document_collection = True - - index_path = Path(self.index_root) / Path(index_name) - if index_path.exists(): - if overwrite is False: - raise ValueError( - f"An index named {index_name} already exists.", - "Use overwrite=True to delete the existing index and build a new one.", - "Exiting indexing without doing anything...", - ) - return None - else: - print( - f"overwrite is on. Deleting existing index {index_name} to build a new one." - ) - shutil.rmtree(index_path) - - self.index_name = index_name - self.max_image_width = max_image_width - self.max_image_height = max_image_height - - input_path = Path(input_path) - if not hasattr(self, "highest_doc_id") or overwrite is True: - self.highest_doc_id = -1 - - if input_path.is_dir(): - items = list(input_path.iterdir()) - if doc_ids is not None and len(doc_ids) != len(items): - raise ValueError( - f"Number of doc_ids ({len(doc_ids)}) does not match number of documents ({len(items)})" - ) - if metadata is not None and len(metadata) != len(items): - raise ValueError( - f"Number of metadata entries ({len(metadata)}) does not match number of documents ({len(items)})" - ) - for i, item in enumerate(items): - print(f"Indexing file: {item}") - doc_id = doc_ids[i] if doc_ids else self.highest_doc_id + 1 - doc_metadata = metadata[doc_id] if metadata else None - self.add_to_index( - item, - store_collection_with_index, - doc_id=doc_id, - metadata=doc_metadata, - ) - self.doc_ids_to_file_names[doc_id] = str(item) - else: - if metadata is not None and len(metadata) != 1: - raise ValueError( - "For a single document, metadata should be a list with one dictionary" - ) - doc_id = doc_ids[0] if doc_ids else self.highest_doc_id + 1 - doc_metadata = metadata[0] if metadata else None - self.add_to_index( - input_path, - store_collection_with_index, - doc_id=doc_id, - metadata=doc_metadata, - ) - self.doc_ids_to_file_names[doc_id] = str(input_path) - - self._export_index() - return self.doc_ids_to_file_names - - def add_to_index( - self, - input_item: Union[str, Path, Image.Image, List[Union[str, Path, Image.Image]]], - store_collection_with_index: bool, - doc_id: Optional[Union[int, List[int]]] = None, - metadata: Optional[List[Dict[str, Union[str, int]]]] = None, - ) -> Dict[int, str]: - if self.index_name is None: - raise ValueError( - "No index loaded. Use index() to create or load an index first." - ) - if not hasattr(self, "highest_doc_id"): - self.highest_doc_id = -1 - # Convert single inputs to lists for uniform processing - if isinstance(input_item, (str, Path)) and Path(input_item).is_dir(): - input_items = list(Path(input_item).iterdir()) - else: - input_items = ( - [input_item] if not isinstance(input_item, list) else input_item - ) - - doc_ids = ( - [doc_id] - if isinstance(doc_id, int) - else (doc_id if doc_id is not None else None) - ) - - # Validate input lengths - if doc_ids and len(doc_ids) != len(input_items): - raise ValueError( - f"Number of doc_ids ({len(doc_ids)}) does not match number of input items ({len(input_items)})" - ) - - # Process each input item - for i, item in enumerate(input_items): - current_doc_id = doc_ids[i] if doc_ids else self.highest_doc_id + 1 + i - current_metadata = metadata if metadata else None - - if current_doc_id in self.doc_ids: - raise ValueError( - f"Document ID {current_doc_id} already exists in the index" - ) - - self.highest_doc_id = max(self.highest_doc_id, current_doc_id) - - if isinstance(item, (str, Path)): - item_path = Path(item) - if item_path.is_dir(): - self._process_directory( - item_path, - store_collection_with_index, - current_doc_id, - current_metadata, - ) - else: - self._process_and_add_to_index( - item_path, - store_collection_with_index, - current_doc_id, - current_metadata, - ) - self.doc_ids_to_file_names[current_doc_id] = str(item_path) - elif isinstance(item, Image.Image): - self._process_and_add_to_index( - item, store_collection_with_index, current_doc_id, current_metadata - ) - self.doc_ids_to_file_names[current_doc_id] = "In-memory Image" - else: - raise ValueError(f"Unsupported input type: {type(item)}") - - self._export_index() - return self.doc_ids_to_file_names - - def _process_directory( - self, - directory: Path, - store_collection_with_index: bool, - base_doc_id: int, - metadata: Optional[Dict[str, Union[str, int]]], - ): - for i, item in enumerate(directory.iterdir()): - print(f"Indexing file: {item}") - current_doc_id = base_doc_id + i - self._process_and_add_to_index( - item, store_collection_with_index, current_doc_id, metadata - ) - self.doc_ids_to_file_names[current_doc_id] = str(item) - - def _process_and_add_to_index( - self, - item: Union[Path, Image.Image], - store_collection_with_index: bool, - doc_id: Union[str, int], - metadata: Optional[Dict[str, Union[str, int]]] = None, - ): - """TODO: THERE ARE TOO MANY FUNCTIONS DOING THINGS HERE. I blame Claude, but this is temporary anyway.""" - if isinstance(item, Path): - if item.suffix.lower() == ".pdf": - with tempfile.TemporaryDirectory() as path: - images = convert_from_path( - item, - thread_count=os.cpu_count() - 1, - output_folder=path, - paths_only=True, - ) - for i, image_path in enumerate(images): - image = Image.open(image_path) - self._add_to_index( - image, - store_collection_with_index, - doc_id, - page_id=i + 1, - metadata=metadata, - ) - elif item.suffix.lower() in [".jpg", ".jpeg", ".png", ".bmp"]: - image = Image.open(item) - self._add_to_index( - image, store_collection_with_index, doc_id, metadata=metadata - ) - else: - raise ValueError(f"Unsupported input type: {item.suffix}") - elif isinstance(item, Image.Image): - self._add_to_index( - item, store_collection_with_index, doc_id, metadata=metadata - ) - else: - raise ValueError(f"Unsupported input type: {type(item)}") - - def _add_to_index( - self, - image: Image.Image, - store_collection_with_index: bool, - doc_id: Union[str, int], - page_id: int = 1, - metadata: Optional[Dict[str, Union[str, int]]] = None, - ): - if any( - entry["doc_id"] == doc_id and entry["page_id"] == page_id - for entry in self.embed_id_to_doc_id.values() - ): - raise ValueError( - f"Document ID {doc_id} with page ID {page_id} already exists in the index" - ) - - processed_image = self.processor.process_images([image]) - - # Generate embedding - with torch.inference_mode(): - processed_image = { - k: v.to(self.device).to(self.model.dtype if v.dtype in [torch.float16, torch.bfloat16, torch.float32] else v.dtype) - for k, v in processed_image.items() - } - embedding = self.model(**processed_image) - - # Add to index - embed_id = len(self.indexed_embeddings) - self.indexed_embeddings.extend(list(torch.unbind(embedding.to("cpu")))) - self.embed_id_to_doc_id[embed_id] = {"doc_id": doc_id, "page_id": int(page_id)} - - # Update highest_doc_id - self.highest_doc_id = max( - self.highest_doc_id, - int(doc_id) if isinstance(doc_id, int) else self.highest_doc_id, - ) - - if store_collection_with_index: - import base64 - import io - - # Resize image while maintaining aspect ratio - if self.max_image_width and self.max_image_height: - img_width, img_height = image.size - aspect_ratio = img_width / img_height - if img_width > self.max_image_width: - new_width = self.max_image_width - new_height = int(new_width / aspect_ratio) - else: - new_width = img_width - new_height = img_height - if new_height > self.max_image_height: - new_height = self.max_image_height - new_width = int(new_height * aspect_ratio) - if self.verbose > 2: - print( - f"Resizing image to {new_width}x{new_height}", - f"(aspect ratio {aspect_ratio:.2f}, original size {img_width}x{img_height}," - f"compression {new_width/img_width * new_height/img_height:.2f})", - ) - image = image.resize((new_width, new_height), Image.LANCZOS) - - buffered = io.BytesIO() - image.save(buffered, format="PNG") - img_str = base64.b64encode(buffered.getvalue()).decode() - - self.collection[int(embed_id)] = img_str - - # Add metadata - if metadata: - self.doc_id_to_metadata[doc_id] = metadata - - if self.verbose > 0: - print(f"Added page {page_id} of document {doc_id} to index.") - - def remove_from_index(self): - raise NotImplementedError("This method is not implemented yet.") - - def filter_embeddings(self,filter_metadata:Dict[str,str]): - req_doc_ids = [] - for idx,metadata_dict in self.doc_id_to_metadata.items(): - for metadata_key,metadata_value in metadata_dict.items(): - if metadata_key in filter_metadata: - if filter_metadata[metadata_key] == metadata_value: - req_doc_ids.append(idx) - - req_embedding_ids = [eid for eid,doc in self.embed_id_to_doc_id.items() if doc['doc_id'] in req_doc_ids] - req_embeddings = [ie for idx,ie in enumerate(self.indexed_embeddings) if idx in req_embedding_ids] - - return req_embeddings, req_embedding_ids - - def search( - self, - query: Union[str, List[str]], - k: int = 10, - filter_metadata: Optional[Dict[str,str]] = None, - return_base64_results: Optional[bool] = None, - ) -> Union[List[Result], List[List[Result]]]: - # Set default value for return_base64_results if not provided - if return_base64_results is None: - return_base64_results = bool(self.collection) - - valid_metadata_keys = list(self.doc_id_to_metadata.values()) - # Ensure k is not larger than the number of indexed documents - k = min(k, len(self.indexed_embeddings)) - - # Process query/queries - if isinstance(query, str): - queries = [query] - else: - queries = query - - results = [] - for q in queries: - # Process query - with torch.inference_mode(): - batch_query = self.processor.process_queries([q]) - batch_query = {k: v.to(self.device).to(self.model.dtype if v.dtype in [torch.float16, torch.bfloat16, torch.float32] else v.dtype) for k, v in batch_query.items()} - embeddings_query = self.model(**batch_query) - qs = list(torch.unbind(embeddings_query.to("cpu"))) - if not filter_metadata: - req_embeddings = self.indexed_embeddings - else: - req_embeddings, req_embedding_ids = self.filter_embeddings(filter_metadata=filter_metadata) - # Compute scores - scores = self.processor.score(qs,req_embeddings).cpu().numpy() - - # Get top k relevant pages - top_pages = scores.argsort(axis=1)[0][-k:][::-1].tolist() - - # Create Result objects - query_results = [] - for embed_id in top_pages: - if filter_metadata: - adjusted_embed_id = req_embedding_ids[embed_id] - else: - adjusted_embed_id = int(embed_id) - doc_info = self.embed_id_to_doc_id[adjusted_embed_id] - result = Result( - doc_id=doc_info["doc_id"], - page_num=int(doc_info["page_id"]), - score=float(scores[0][int(embed_id)]), - metadata=self.doc_id_to_metadata.get(int(doc_info["doc_id"]), {}), - base64=self.collection.get(adjusted_embed_id) - if return_base64_results - else None, - ) - query_results.append(result) - - results.append(query_results) - - return results[0] if isinstance(query, str) else results - - def encode_image( - self, input_data: Union[str, Image.Image, List[Union[str, Image.Image]]] - ) -> torch.Tensor: - """ - Compute embeddings for one or more images, PDFs, folders, or image files. - - Args: - input_data (Union[str, Image.Image, List[Union[str, Image.Image]]]): - A single image, PDF path, folder path, image file path, or a list of these. - - Returns: - torch.Tensor: The computed embeddings for the input data. - """ - if not isinstance(input_data, list): - input_data = [input_data] - - images = [] - for item in input_data: - if isinstance(item, Image.Image): - images.append(item) - elif isinstance(item, str): - if os.path.isdir(item): - # Process folder - for file in os.listdir(item): - if file.lower().endswith( - (".png", ".jpg", ".jpeg", ".tiff", ".bmp", ".gif") - ): - images.append(Image.open(os.path.join(item, file))) - elif item.lower().endswith(".pdf"): - # Process PDF - with tempfile.TemporaryDirectory() as path: - pdf_images = convert_from_path( - item, thread_count=os.cpu_count() - 1, output_folder=path - ) - images.extend(pdf_images) - elif item.lower().endswith( - (".png", ".jpg", ".jpeg", ".tiff", ".bmp", ".gif") - ): - # Process image file - images.append(Image.open(item)) - else: - raise ValueError(f"Unsupported file type: {item}") - else: - raise ValueError(f"Unsupported input type: {type(item)}") - - with torch.inference_mode(): - batch = self.processor.process_images(images) - batch = {k: v.to(self.device).to(self.model.dtype if v.dtype in [torch.float16, torch.bfloat16, torch.float32] else v.dtype) for k, v in batch.items()} - embeddings = self.model(**batch) - - return embeddings.cpu() - - def encode_query(self, query: Union[str, List[str]]) -> torch.Tensor: - """ - Compute embeddings for one or more text queries. - - Args: - query (Union[str, List[str]]): - A single text query or a list of text queries. - - Returns: - torch.Tensor: The computed embeddings for the input query/queries. - """ - if isinstance(query, str): - query = [query] - - with torch.inference_mode(): - batch = self.processor.process_queries(query) - batch = {k: v.to(self.device).to(self.model.dtype if v.dtype in [torch.float16, torch.bfloat16, torch.float32] else v.dtype) for k, v in batch.items()} - embeddings = self.model(**batch) - - return embeddings.cpu() - - def get_doc_ids_to_file_names(self): - return self.doc_ids_to_file_names diff --git a/byaldi/integrations/__init__.py b/byaldi/integrations/__init__.py deleted file mode 100644 index 5841288..0000000 --- a/byaldi/integrations/__init__.py +++ /dev/null @@ -1,8 +0,0 @@ -_all__ = [] - -try: - from byaldi.integrations._langchain import ByaldiLangChainRetriever - - _all__.append("ByaldiLangChainRetriever") -except ImportError: - pass diff --git a/byaldi/objects.py b/byaldi/objects.py deleted file mode 100644 index 3e1f9cd..0000000 --- a/byaldi/objects.py +++ /dev/null @@ -1,35 +0,0 @@ -from typing import Optional - - -class Result: - def __init__( - self, - doc_id: str, - page_num: int, - score: float, - metadata: Optional[dict] = None, - base64: Optional[str] = None, - ): - self.doc_id = doc_id - self.page_num = page_num - self.score = score - self.metadata = metadata or {} - self.base64 = base64 - - def dict(self): - return { - "doc_id": self.doc_id, - "page_num": self.page_num, - "score": self.score, - "metadata": self.metadata, - "base64": self.base64, - } - - def __getitem__(self, key): - return getattr(self, key) - - def __str__(self): - return str(self.dict()) - - def __repr__(self): - return self.__str__() diff --git a/examples/chat_with_your_pdf.ipynb b/examples/chat_with_your_pdf.ipynb deleted file mode 100644 index 60b0e72..0000000 --- a/examples/chat_with_your_pdf.ipynb +++ /dev/null @@ -1,764 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "90d2e6ca-4c5b-471f-830a-f469a303197b", - "metadata": {}, - "source": [ - "### Chat with your PDFs using byaldi + Claude 🚀\n", - "\n", - "ColPali is an *image retrieval model*: this means that it takes in full documents as inputs, such as .PDF or .PNG files, and represents them as is. It doesn't perform any particular processing to extract information for the document. Rather, it's a model that has been trained to be able to \"read\" full-page content, complex layouts, tables, etc... and create fine-grained representations for them. The good thing is: you can use these representations to then query your documents, in plain-text!\n", - "\n", - "It's radically different from how Document-based Retrieval-Augmented-Generation (RAG) is generally done: in current pipelines, your document needs to go through a lot of processing steps (extracting text from the document, re-adding layout informations, representing tables and images so they can be queried, etc...), which often carry a hefty time and complexity cost, and you can lose information in the process.\n", - "\n", - "We think ColPali and similar approaches are about to unlock many usecases thanks to its straightforward-yet-very powerful pipeline.\n", - "\n", - "How does it work, in practice? How do you go from having a document, to being able to retrieve it, to getting your LLM to answer with the relevant page in context? Don't LLMs need text?\n", - "\n", - "Well, it's a lot more simple than it looks like at first glance! A hot topic recently has been Vision Language Models, or VLMs. These are basically LLMs that can read \"image tokens\" just like they can \"text tokens\", which means that all you need to do is give them your page as an image input, and they'll be able to use it, just like they'd be able to use textual input.\n", - "\n", - "In this notebook, we'll show you how easy it is to use Byaldi in conjunction with Claude to answer queries based on a given document. To interact with Claude, we are going to use the [claudette](https://github.com/answerdotai/claudette) library, a very simple wrapper to quickly interact with Claude by simplifying all the cumbersone stuff!\n", - "\n", - "The full steps are as follow:\n", - "- first download your chosen model (e.g. [ColPali](https://huggingface.co/vidore/colpali))\n", - "- then create an index for your pdf\n", - "- search the index for your chosen query\n", - "- pass the top search result to Claude along with your query\n", - "\n", - "We'll show how this works with an academic paper and financial report. Let's get started!\n", - "\n", - "*Note: This notebook will consume a small amount of Claude Sonnet 3.5 tokens, costing around $0.01.*" - ] - }, - { - "cell_type": "markdown", - "id": "c0518e52-b011-4421-87ca-32e17e7b2761", - "metadata": {}, - "source": [ - "### Setup\n", - "\n", - "To get started, you'll need to install byaldi and claudette:" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "4a5dbab7", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: byaldi in /opt/conda/lib/python3.10/site-packages (0.0.1)\n", - "Requirement already satisfied: claudette in /opt/conda/lib/python3.10/site-packages (0.0.9)\n", - "Requirement already satisfied: transformers in /opt/conda/lib/python3.10/site-packages (from byaldi) (4.42.4)\n", - "Requirement already satisfied: torch in /opt/conda/lib/python3.10/site-packages (from byaldi) (2.2.0+cu121)\n", - "Requirement already satisfied: ml-dtypes in /opt/conda/lib/python3.10/site-packages (from byaldi) (0.4.0)\n", - "Requirement already satisfied: ninja in /opt/conda/lib/python3.10/site-packages (from byaldi) (1.11.1.1)\n", - "Requirement already satisfied: pdf2image in /opt/conda/lib/python3.10/site-packages (from byaldi) (1.17.0)\n", - "Requirement already satisfied: srsly in /opt/conda/lib/python3.10/site-packages (from byaldi) (2.4.8)\n", - "Collecting colpali-engine==0.2.1 (from byaldi)\n", - " Downloading colpali_engine-0.2.1-py3-none-any.whl.metadata (6.7 kB)\n", - "Requirement already satisfied: mteb==1.6.35 in /opt/conda/lib/python3.10/site-packages (from byaldi) (1.6.35)\n", - "Requirement already satisfied: gputil in /opt/conda/lib/python3.10/site-packages (from colpali-engine==0.2.1->byaldi) (1.4.0)\n", - "Requirement already satisfied: peft<0.12.0,>=0.11.0 in /opt/conda/lib/python3.10/site-packages (from colpali-engine==0.2.1->byaldi) (0.11.1)\n", - "Requirement already satisfied: requests in /opt/conda/lib/python3.10/site-packages (from colpali-engine==0.2.1->byaldi) (2.32.3)\n", - "Requirement already satisfied: datasets>=2.2.0 in /opt/conda/lib/python3.10/site-packages (from mteb==1.6.35->byaldi) (2.20.0)\n", - "Requirement already satisfied: jsonlines in /opt/conda/lib/python3.10/site-packages (from mteb==1.6.35->byaldi) (4.0.0)\n", - "Requirement already satisfied: numpy in /opt/conda/lib/python3.10/site-packages (from mteb==1.6.35->byaldi) (1.25.2)\n", - "Requirement already satisfied: scikit-learn>=1.0.2 in /opt/conda/lib/python3.10/site-packages (from mteb==1.6.35->byaldi) (1.5.0)\n", - "Requirement already satisfied: scipy in /opt/conda/lib/python3.10/site-packages (from mteb==1.6.35->byaldi) (1.11.4)\n", - "Requirement already satisfied: sentence-transformers>=2.2.0 in /opt/conda/lib/python3.10/site-packages (from mteb==1.6.35->byaldi) (3.0.1)\n", - "Requirement already satisfied: tqdm in /opt/conda/lib/python3.10/site-packages (from mteb==1.6.35->byaldi) (4.66.4)\n", - "Requirement already satisfied: rich in /opt/conda/lib/python3.10/site-packages (from mteb==1.6.35->byaldi) (13.7.1)\n", - "Requirement already satisfied: pytrec-eval-terrier>=0.5.6 in /opt/conda/lib/python3.10/site-packages (from mteb==1.6.35->byaldi) (0.5.6)\n", - "Requirement already satisfied: pydantic in /opt/conda/lib/python3.10/site-packages (from mteb==1.6.35->byaldi) (2.8.2)\n", - "Requirement already satisfied: typing-extensions in /opt/conda/lib/python3.10/site-packages (from mteb==1.6.35->byaldi) (4.12.2)\n", - "Requirement already satisfied: eval-type-backport in /opt/conda/lib/python3.10/site-packages (from mteb==1.6.35->byaldi) (0.2.0)\n", - "Requirement already satisfied: fastcore>=1.5.33 in /opt/conda/lib/python3.10/site-packages (from claudette) (1.7.4)\n", - "Requirement already satisfied: anthropic in /opt/conda/lib/python3.10/site-packages (from claudette) (0.34.2)\n", - "Requirement already satisfied: toolslm in /opt/conda/lib/python3.10/site-packages (from claudette) (0.0.5)\n", - "Requirement already satisfied: packaging in /opt/conda/lib/python3.10/site-packages (from fastcore>=1.5.33->claudette) (24.1)\n", - "Requirement already satisfied: filelock in /opt/conda/lib/python3.10/site-packages (from torch->byaldi) (3.15.4)\n", - "Requirement already satisfied: sympy in /opt/conda/lib/python3.10/site-packages (from torch->byaldi) (1.12.1)\n", - "Requirement already satisfied: networkx in /opt/conda/lib/python3.10/site-packages (from torch->byaldi) (3.3)\n", - "Requirement already satisfied: jinja2 in /opt/conda/lib/python3.10/site-packages (from torch->byaldi) (3.1.4)\n", - "Requirement already satisfied: fsspec in /opt/conda/lib/python3.10/site-packages (from torch->byaldi) (2023.10.0)\n", - "Requirement already satisfied: nvidia-cuda-nvrtc-cu12==12.1.105 in /opt/conda/lib/python3.10/site-packages (from torch->byaldi) (12.1.105)\n", - "Requirement already satisfied: nvidia-cuda-runtime-cu12==12.1.105 in /opt/conda/lib/python3.10/site-packages (from torch->byaldi) (12.1.105)\n", - "Requirement already satisfied: nvidia-cuda-cupti-cu12==12.1.105 in /opt/conda/lib/python3.10/site-packages (from torch->byaldi) (12.1.105)\n", - "Requirement already satisfied: nvidia-cudnn-cu12==8.9.2.26 in /opt/conda/lib/python3.10/site-packages (from torch->byaldi) (8.9.2.26)\n", - "Requirement already satisfied: nvidia-cublas-cu12==12.1.3.1 in /opt/conda/lib/python3.10/site-packages (from torch->byaldi) (12.1.3.1)\n", - "Requirement already satisfied: nvidia-cufft-cu12==11.0.2.54 in /opt/conda/lib/python3.10/site-packages (from torch->byaldi) (11.0.2.54)\n", - "Requirement already satisfied: nvidia-curand-cu12==10.3.2.106 in /opt/conda/lib/python3.10/site-packages (from torch->byaldi) (10.3.2.106)\n", - "Requirement already satisfied: nvidia-cusolver-cu12==11.4.5.107 in /opt/conda/lib/python3.10/site-packages (from torch->byaldi) (11.4.5.107)\n", - "Requirement already satisfied: nvidia-cusparse-cu12==12.1.0.106 in /opt/conda/lib/python3.10/site-packages (from torch->byaldi) (12.1.0.106)\n", - "Requirement already satisfied: nvidia-nccl-cu12==2.19.3 in /opt/conda/lib/python3.10/site-packages (from torch->byaldi) (2.19.3)\n", - "Requirement already satisfied: nvidia-nvtx-cu12==12.1.105 in /opt/conda/lib/python3.10/site-packages (from torch->byaldi) (12.1.105)\n", - "Requirement already satisfied: triton==2.2.0 in /opt/conda/lib/python3.10/site-packages (from torch->byaldi) (2.2.0)\n", - "Requirement already satisfied: nvidia-nvjitlink-cu12 in /opt/conda/lib/python3.10/site-packages (from nvidia-cusolver-cu12==11.4.5.107->torch->byaldi) (12.5.40)\n", - "Requirement already satisfied: huggingface-hub<1.0,>=0.23.2 in /opt/conda/lib/python3.10/site-packages (from transformers->byaldi) (0.24.5)\n", - "Requirement already satisfied: pyyaml>=5.1 in /opt/conda/lib/python3.10/site-packages (from transformers->byaldi) (6.0.1)\n", - "Requirement already satisfied: regex!=2019.12.17 in /opt/conda/lib/python3.10/site-packages (from transformers->byaldi) (2024.5.15)\n", - "Requirement already satisfied: safetensors>=0.4.1 in /opt/conda/lib/python3.10/site-packages (from transformers->byaldi) (0.4.3)\n", - "Requirement already satisfied: tokenizers<0.20,>=0.19 in /opt/conda/lib/python3.10/site-packages (from transformers->byaldi) (0.19.1)\n", - "Requirement already satisfied: anyio<5,>=3.5.0 in /opt/conda/lib/python3.10/site-packages (from anthropic->claudette) (4.4.0)\n", - "Requirement already satisfied: distro<2,>=1.7.0 in /opt/conda/lib/python3.10/site-packages (from anthropic->claudette) (1.9.0)\n", - "Requirement already satisfied: httpx<1,>=0.23.0 in /opt/conda/lib/python3.10/site-packages (from anthropic->claudette) (0.27.0)\n", - "Requirement already satisfied: jiter<1,>=0.4.0 in /opt/conda/lib/python3.10/site-packages (from anthropic->claudette) (0.5.0)\n", - "Requirement already satisfied: sniffio in /opt/conda/lib/python3.10/site-packages (from anthropic->claudette) (1.3.1)\n", - "Requirement already satisfied: pillow in /opt/conda/lib/python3.10/site-packages (from pdf2image->byaldi) (10.3.0)\n", - "Requirement already satisfied: catalogue<2.1.0,>=2.0.3 in /opt/conda/lib/python3.10/site-packages (from srsly->byaldi) (2.0.10)\n", - "Requirement already satisfied: idna>=2.8 in /opt/conda/lib/python3.10/site-packages (from anyio<5,>=3.5.0->anthropic->claudette) (3.7)\n", - "Requirement already satisfied: exceptiongroup>=1.0.2 in /opt/conda/lib/python3.10/site-packages (from anyio<5,>=3.5.0->anthropic->claudette) (1.2.0)\n", - "Requirement already satisfied: pyarrow>=15.0.0 in /opt/conda/lib/python3.10/site-packages (from datasets>=2.2.0->mteb==1.6.35->byaldi) (17.0.0)\n", - "Requirement already satisfied: pyarrow-hotfix in /opt/conda/lib/python3.10/site-packages (from datasets>=2.2.0->mteb==1.6.35->byaldi) (0.6)\n", - "Requirement already satisfied: dill<0.3.9,>=0.3.0 in /opt/conda/lib/python3.10/site-packages (from datasets>=2.2.0->mteb==1.6.35->byaldi) (0.3.7)\n", - "Requirement already satisfied: pandas in /opt/conda/lib/python3.10/site-packages (from datasets>=2.2.0->mteb==1.6.35->byaldi) (2.0.3)\n", - "Requirement already satisfied: xxhash in /opt/conda/lib/python3.10/site-packages (from datasets>=2.2.0->mteb==1.6.35->byaldi) (3.4.1)\n", - "Requirement already satisfied: multiprocess in /opt/conda/lib/python3.10/site-packages (from datasets>=2.2.0->mteb==1.6.35->byaldi) (0.70.15)\n", - "Requirement already satisfied: aiohttp in /opt/conda/lib/python3.10/site-packages (from datasets>=2.2.0->mteb==1.6.35->byaldi) (3.9.5)\n", - "Requirement already satisfied: certifi in /opt/conda/lib/python3.10/site-packages (from httpx<1,>=0.23.0->anthropic->claudette) (2024.6.2)\n", - "Requirement already satisfied: httpcore==1.* in /opt/conda/lib/python3.10/site-packages (from httpx<1,>=0.23.0->anthropic->claudette) (1.0.5)\n", - "Requirement already satisfied: h11<0.15,>=0.13 in /opt/conda/lib/python3.10/site-packages (from httpcore==1.*->httpx<1,>=0.23.0->anthropic->claudette) (0.14.0)\n", - "Requirement already satisfied: psutil in /opt/conda/lib/python3.10/site-packages (from peft<0.12.0,>=0.11.0->colpali-engine==0.2.1->byaldi) (6.0.0)\n", - "Requirement already satisfied: accelerate>=0.21.0 in /opt/conda/lib/python3.10/site-packages (from peft<0.12.0,>=0.11.0->colpali-engine==0.2.1->byaldi) (0.33.0)\n", - "Requirement already satisfied: annotated-types>=0.4.0 in /opt/conda/lib/python3.10/site-packages (from pydantic->mteb==1.6.35->byaldi) (0.7.0)\n", - "Requirement already satisfied: pydantic-core==2.20.1 in /opt/conda/lib/python3.10/site-packages (from pydantic->mteb==1.6.35->byaldi) (2.20.1)\n", - "Requirement already satisfied: charset-normalizer<4,>=2 in /opt/conda/lib/python3.10/site-packages (from requests->colpali-engine==0.2.1->byaldi) (3.3.2)\n", - "Requirement already satisfied: urllib3<3,>=1.21.1 in /opt/conda/lib/python3.10/site-packages (from requests->colpali-engine==0.2.1->byaldi) (2.2.2)\n", - "Requirement already satisfied: joblib>=1.2.0 in /opt/conda/lib/python3.10/site-packages (from scikit-learn>=1.0.2->mteb==1.6.35->byaldi) (1.4.2)\n", - "Requirement already satisfied: threadpoolctl>=3.1.0 in /opt/conda/lib/python3.10/site-packages (from scikit-learn>=1.0.2->mteb==1.6.35->byaldi) (3.5.0)\n", - "Requirement already satisfied: MarkupSafe>=2.0 in /opt/conda/lib/python3.10/site-packages (from jinja2->torch->byaldi) (2.1.5)\n", - "Requirement already satisfied: attrs>=19.2.0 in /opt/conda/lib/python3.10/site-packages (from jsonlines->mteb==1.6.35->byaldi) (23.2.0)\n", - "Requirement already satisfied: markdown-it-py>=2.2.0 in /opt/conda/lib/python3.10/site-packages (from rich->mteb==1.6.35->byaldi) (3.0.0)\n", - "Requirement already satisfied: pygments<3.0.0,>=2.13.0 in /opt/conda/lib/python3.10/site-packages (from rich->mteb==1.6.35->byaldi) (2.18.0)\n", - "Requirement already satisfied: mpmath<1.4.0,>=1.1.0 in /opt/conda/lib/python3.10/site-packages (from sympy->torch->byaldi) (1.3.0)\n", - "Requirement already satisfied: aiosignal>=1.1.2 in /opt/conda/lib/python3.10/site-packages (from aiohttp->datasets>=2.2.0->mteb==1.6.35->byaldi) (1.3.1)\n", - "Requirement already satisfied: frozenlist>=1.1.1 in /opt/conda/lib/python3.10/site-packages (from aiohttp->datasets>=2.2.0->mteb==1.6.35->byaldi) (1.4.1)\n", - "Requirement already satisfied: multidict<7.0,>=4.5 in /opt/conda/lib/python3.10/site-packages (from aiohttp->datasets>=2.2.0->mteb==1.6.35->byaldi) (6.0.5)\n", - "Requirement already satisfied: yarl<2.0,>=1.0 in /opt/conda/lib/python3.10/site-packages (from aiohttp->datasets>=2.2.0->mteb==1.6.35->byaldi) (1.9.4)\n", - "Requirement already satisfied: async-timeout<5.0,>=4.0 in /opt/conda/lib/python3.10/site-packages (from aiohttp->datasets>=2.2.0->mteb==1.6.35->byaldi) (4.0.3)\n", - "Requirement already satisfied: mdurl~=0.1 in /opt/conda/lib/python3.10/site-packages (from markdown-it-py>=2.2.0->rich->mteb==1.6.35->byaldi) (0.1.2)\n", - "Requirement already satisfied: python-dateutil>=2.8.2 in /opt/conda/lib/python3.10/site-packages (from pandas->datasets>=2.2.0->mteb==1.6.35->byaldi) (2.9.0)\n", - "Requirement already satisfied: pytz>=2020.1 in /opt/conda/lib/python3.10/site-packages (from pandas->datasets>=2.2.0->mteb==1.6.35->byaldi) (2024.1)\n", - "Requirement already satisfied: tzdata>=2022.1 in /opt/conda/lib/python3.10/site-packages (from pandas->datasets>=2.2.0->mteb==1.6.35->byaldi) (2024.1)\n", - "Requirement already satisfied: six>=1.5 in /opt/conda/lib/python3.10/site-packages (from python-dateutil>=2.8.2->pandas->datasets>=2.2.0->mteb==1.6.35->byaldi) (1.16.0)\n", - "Downloading colpali_engine-0.2.1-py3-none-any.whl (37 kB)\n", - "Installing collected packages: colpali-engine\n", - " Attempting uninstall: colpali-engine\n", - " Found existing installation: colpali_engine 0.2.2\n", - " Uninstalling colpali_engine-0.2.2:\n", - " Successfully uninstalled colpali_engine-0.2.2\n", - "Successfully installed colpali-engine-0.2.1\n" - ] - } - ], - "source": [ - "!pip install byaldi claudette" - ] - }, - { - "cell_type": "markdown", - "id": "fca57320", - "metadata": {}, - "source": [ - "But there's another catch: to work with PDFs, we need to be able to convert them to images. To do so, we use the `pdf2images` library, which internally relies on `poppler`. Thankfully, it's very easy to install, but depends on your system:\n", - "\n", - "- **MacOS**: `brew install poppler`\n", - "- **Linux**: `sudo apt-get install poppler-utils`\n", - "- **Windows**: Follow the instructions from [Poppler-Windows](https://github.com/oschwartz10612/poppler-windows/) (I'm sorry, there's no one liner here).\n", - "\n", - "Finally, if you want it to go even faster, we recommend following the byaldi setup instructions [here](https://github.com/AnswerDotAI/byaldi/) to get flash attention going.\n", - "\n", - "And that's it, you're ready to go! The next step is importing all we need, and setting up our environment variables:" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "1ab91a5b-8a54-473e-a316-e15f02aaedfa", - "metadata": {}, - "outputs": [], - "source": [ - "import base64\n", - "import os\n", - "os.environ[\"HF_TOKEN\"] = \"YOUR_HF_TOKEN\" # to download the ColPali model\n", - "os.environ[\"ANTHROPIC_API_KEY\"] = \"YOUR_ANTHROPIC_API_KEY\"\n", - "from byaldi import RAGMultiModalModel\n", - "from claudette import *" - ] - }, - { - "cell_type": "markdown", - "id": "110d10d5", - "metadata": {}, - "source": [ - "Now that all the boilerplate is out of the way, let's actually load the model. It's very straightforward, a single call does the trick (notice we pass verbose=1, which means that the model will be quite loud when indexing):" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "a28ec68f", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Verbosity is set to 1 (active). Pass verbose=0 to make quieter.\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "`config.hidden_act` is ignored, you should use `config.hidden_activation` instead.\n", - "Gemma's activation function will be set to `gelu_pytorch_tanh`. Please, use\n", - "`config.hidden_activation` if you want to override this behaviour.\n", - "See https://github.com/huggingface/transformers/pull/29402 for more details.\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "f328cce883284d328c87631fbfcc4908", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Loading checkpoint shards: 0%| | 0/2 [00:00\n", - "\n", - "\n", - "#### Using Byaldi to get relevant context\n", - "\n", - "Let's first quickly define our query, which we'll use later:" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "4a09ea18-d6a1-4e80-8a34-09688f5fac94", - "metadata": {}, - "outputs": [], - "source": [ - "query = \"What's the BLEU score for the transformer base model?\"" - ] - }, - { - "cell_type": "markdown", - "id": "21e2cb2f-2251-48e1-bd1f-934d9dc025da", - "metadata": {}, - "source": [ - "Now, let's `wget` the paper, so we can then feed it to our `RAG` model to index it. If you've cloned this notebook from our repository, it's already in the `docs` folder, so you can skip this step." - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "434b26a8", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "--2024-09-06 15:34:11-- https://arxiv.org/pdf/1706.03762\n", - "Resolving arxiv.org (arxiv.org)... 151.101.67.42, 151.101.131.42, 151.101.195.42, ...\n", - "Connecting to arxiv.org (arxiv.org)|151.101.67.42|:443... connected.\n", - "HTTP request sent, awaiting response... 200 OK\n", - "Length: 2215244 (2.1M) [application/pdf]\n", - "Saving to: ‘1706.03762’\n", - "\n", - "1706.03762 100%[===================>] 2.11M --.-KB/s in 0.06s \n", - "\n", - "2024-09-06 15:34:12 (34.8 MB/s) - ‘1706.03762’ saved [2215244/2215244]\n", - "\n", - "mkdir: cannot create directory ‘docs’: File exists\n" - ] - } - ], - "source": [ - "!wget https://arxiv.org/pdf/1706.03762\n", - "!mkdir docs\n", - "!mv 1706.03762 docs/attention.pdf" - ] - }, - { - "cell_type": "markdown", - "id": "340e14ca", - "metadata": {}, - "source": [ - "This is the full extent of our document preprocessing: downloading it! We're now going to pass it to our model for indexing. In this case, \"indexing\" means creating representations of the document that we will store in-memory, as well as persist on disk to be re-used later:" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "014e932d-b9b2-4dd0-ae1d-1355db06eca5", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "overwrite is on. Deleting existing index attention to build a new one.\n", - "Added page 1 of document 0 to index.\n", - "Added page 2 of document 0 to index.\n", - "Added page 3 of document 0 to index.\n", - "Added page 4 of document 0 to index.\n", - "Added page 5 of document 0 to index.\n", - "Added page 6 of document 0 to index.\n", - "Added page 7 of document 0 to index.\n", - "Added page 8 of document 0 to index.\n", - "Added page 9 of document 0 to index.\n", - "Added page 10 of document 0 to index.\n", - "Added page 11 of document 0 to index.\n", - "Added page 12 of document 0 to index.\n", - "Added page 13 of document 0 to index.\n", - "Added page 14 of document 0 to index.\n", - "Added page 15 of document 0 to index.\n", - "Index exported to .byaldi/attention\n", - "Index exported to .byaldi/attention\n" - ] - }, - { - "data": { - "text/plain": [ - "{0: 'docs/attention.pdf'}" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "RAG.index(\n", - " input_path=\"./docs/attention.pdf\",\n", - " index_name=\"attention\",\n", - " store_collection_with_index=True,\n", - " overwrite=True\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "6e8dd3ea-bc81-4f48-b3fc-4ae4cf4303ee", - "metadata": {}, - "source": [ - "The model's now done processing the document. It has split into pages (if you want to use a smaller unit of information, this is a processing step you will need to do yourself).\n", - "\n", - "With our index being created, let's now ask it our query. This is done with the `search()` method, which will return the top `k` pages whose content match our textual query:" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "30d6488c-ba09-4d0c-ae70-409c08d0f866", - "metadata": { - "scrolled": true - }, - "outputs": [ - { - "data": { - "text/plain": [ - "[{'doc_id': 0, 'page_num': 8, 'score': 18.375, 'metadata': {}, 'base64': 'iVBORw0KGgoAAAANSUhEUgAABqQAAAiYCAIAAAA+NVHkAAEAAElEQVR4nOzdd1gUx/8H8KGDoCJNwY4gdkDs2MFoxIZiV+wNu9HERI36tcWOvaAiKqJYMRbsiihiBwXpqDRBpHe4298f82Se/d0dx9LUXN6vP3i4uS2zu7Ozu5+bnVHiOI4AAAAAAAAAAADAv5/y984AAAAAAAAAAAAAVA0E+wAAAAAAAAAAABQEgn0AAAAAAAAAAAAKAsE+AAAAAAAAAAAABYFgHwAAAAAAAAAAgIJAsA8AAAAAAAAAAEBBINgHAAAAAAAAAACgIBDsAwAAAAAAAAAAUBAI9gEAAAAAAAAAACgIBPsAAAAAAAAAAAAUBIJ9AAAAAAAAAAAACgLBPgAAAAAAAAAAAAWBYB8AAAAAAAAAAICCQLAPAAAAAAAAAABAQSDYBwAAAAAAAAAAoCAQ7AMAAAAAAAAAAFAQCPYBAAAAAAAAAAAoCAT7AAAAAAAAAAAAFASCfQAAAAAAAAAAAAoCwT4AAAAAAAAAAAAFgWAfAAAAAAAAAACAgkCwDwAAAAAAAAAAQEEg2AcAAAAAAAAAAKAgEOwDAAAAAAAAAABQEAj2AQAAAAAAAAAAKAgE+wAAAAAAAAAAABQEgn0AAAAAAAAAAAAKAsE+AAAAAAAAAAAABYFgHwAAAAAAAAAAgIJAsA8AAAAAAAAAAEBBINgHAAAAAAAAAACgIBDsAwAAAAAAAAAAUBAI9gEAAAAAAAAAACgIBPsAAAAAAAAAAAAUBIJ9AAAAAAAAAAAACgLBPgAAAAAAAAAAAAWBYB8AAAAAAAAAAICCQLAPAAAAAAAAAABAQSDYBwAAAAAAAAAAoCAQ7AMAAAAAAAAAAFAQCPYBAAAAAAAAAAAoCAT7AAAAAAAAAAAAFASCfQAAAAAAAAAAAAoCwT4AAAAAAAAAAAAFgWAfAAAAAAAAAACAgkCwDwAAAAAAAAAAQEEg2AcAAAAAAAAAAKAgEOwDAAAAAAAAAABQEAj2AQAAAAAAAAAAKAgE+wAAAAAAAAAAABQEgn0AAAAAAAAAAAAKAsE+AAAAAAAAAAAABYFgHwAAAAAAAAAAgIJAsA8AAAAAAAAAAEBBINgHAAAAAAAAAACgIBDsAwAAAAAAAAAAUBAI9gEAAAAAAAAAACgIBPsAAAAAAAAAAAAUBIJ9AAAAAAAAAAAACgLBPgAAAAAAAAAAAAWBYB8AAAAAAAAAAICCQLAPAAAAAAAAAABAQSDYBwAAAAAAAAAAoCAQ7AMAAAAAAAAAAFAQCPYBAAAAAAAAAAAoCAT7AAAAAAAAAAAAFASCfQAAAAAAAAAAAAoCwT4AAAAAAAAAAAAFgWAfAAAAAAAAAACAgkCwDwAAAAAAAAAAQEEg2AcAAAAAAAAAAKAgEOwDAAAAAAAAAABQEAj2AQAAAAAAAAAAKAgE+wAAAAAAAAAAABQEgn0AAAAAAAAAAAAKAsE+AAAAAAAAAAAABYFgHwAAAAAAAAAAgIJAsA8AAAAAAAAAAEBBINgHAAAAAAAAAACgIBDsAwAAAAAAAAAAUBAI9gEAAAAAAAAAACgIBPsAAAAAAAAAAAAUBIJ9AAAAAAAAAAAACgLBPgAAAAAAAAAAAAWBYB8AAAAAAAAAAICCQLAPAAAAAAAAAABAQSDYBwAAAAAAAAAAoCAQ7AMAAAAAAAAAAFAQCPYBAAAAAAAAAAAoCAT7AAAAAAAAAAAAFASCfQAAAAAAAAAAAAoCwT4AAAAAAAAAAAAFgWAfAAAAAAAAAACAgkCwDwAAAAAAAAAAQEEg2AcAAAAAAAAAAKAgEOwDAAAAAAAAAABQEAj2AQAAAAAAAAAAKAgE+wAAAAAAAAAAABQEgn0AAAAAAAAAAAAKAsE+AAAAAAAAAAAABYFgHwAAAAAAAAAAgIJAsA8AAAAAAAAAAEBBINgHAAAAAAAAAACgIBDsAwAAAAAAAAAAUBAI9gEAAAAAAAAAACgIBPsAAAAAAAAAAAAUBIJ9AAAAAAAAAAAACgLBPgAAAAAAAAAAAAWBYB8AAAAAAAAAAICCQLAPAAAAAAAAAABAQSDYBwAAAAAAAAAAoCAQ7AMAAAAAAAAAAFAQCPYBAAAAAAAAAAAoCAT7AAAAAAAAAAAAFASCfQAAAAAAAAAAAAoCwT4AAAAAAAAAAAAFgWAfAAAAAAAAAACAgkCwDwAAAAAAAAAAQEEg2AcAAAAAAAAAAKAgEOwDAAAAAAAAAABQEAj2AQAAAAAAAAAAKAgE+wAAAAAAAAAAABQEgn0AAAAAAAAAAAAKAsE+AAAAAAAAAAAABYFgHwAAAAAAAAAAgIJAsA8AAAAAAAAAAEBBINgHAAAAAAAAAACgIBDsAwAAAAAAAAAAUBAI9gEAAAAAAAAAACgIBPsAAAAAAAAAAAAUBIJ9AAAAAAAAAAAACgLBPgAAAAAAAAAAAAWBYB8AAAAAAAAAAICCQLAPAAAAAAAAAABAQSDYBwAAAAAAAAAAoCAQ7AMAAAAAAAAAAFAQCPYBAAAAAAAAAAAoCAT7AAAAAAAAAAAAFASCfQAAAAAAAAAAAAoCwT4AAAAAAAAAAAAFgWAfAAAAAAAAAACAgkCwDwAAAAAAAAAAQEEg2AcAAAAAAAAAAKAgEOwDAAAAAAAAAABQEAj2AQAAAAAAAAAAKAgE+wAAAAAAAAAAABQEgn0AAAAAAAAAAAAKAsE+AAAAAAAAAAAABYFgHwAAAAAAAAAAgIJAsA8AAAAAAAAAAEBBINgHAAAAAAAAAACgIBDsAwAAAAAAAAAAUBAI9gEAAAAAAAAAACgIBPsAAAAAAAAAAAAUBIJ9AAAAAAAAAAAACgLBPgAAAAAAAAAAAAWBYB8AAAAAAAAAAICCQLAPAAAAAAAAAABAQSDYBwAAAAAAAAAAoCAQ7AMAAAAAAAAAAFAQCPYBAAAAAAAAAAAoCAT7AAAAAAAAAAAAFASCfQAAAAAAAAAAAAoCwT4AAAAAAAAAAAAFgWAfAAAAAAAAAACAgkCwDwAAAAAAAAAAQEEg2AcAAAAAAAAAAKAgEOwDAAAAAAAAAABQEAj2AQAAAAAAAAAAKAgE+wAAAAAAAAAAABQEgn0AAAAAAAAAAAAKAsE+AAAAAAAAAAAABYFgHwAAAAAAAAAAgIJAsA8AAAAAAAAAAEBBINgHAAAAAAAAAACgIBDsAwAAAAAAAAAAUBAI9gEAAAAAAAAAACgIBPsAAAAAAAAAAAAUBIJ9AAAAAAAAAAAACgLBPgAAAAAAAAAAAAWBYB8AAAAAAAAAAICCQLAPAAAAAAAAAABAQSDYBwAAAAAAAAAAoCAQ7AMAAAAAAAAAAFAQCPYBAAAAAAAAAAAoCAT7AAAAAAAAAAAAFASCfQAAAAAAAAAAAAoCwT4AAAAAAAAAAAAFgWAfAAAAAAAAAACAgkCwDwAAAAAAAAAAQEEg2AcAAAAAAAAAAKAgEOwDAAAAAAAAAABQEAj2AQAAAAAAAAAAKAgE+wAAAAAAAAAAABQEgn0AAAAAAAAAAAAKAsE+AAAAAAAAAAAABYFgHwAAAAAAAAAAgIJAsA8AAAAAAAAAAEBBqH7v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s7///jssLMzU1NTKyqpfv35lju3LD4/OnDlT/sRGRkbSo3N8+PDh9u3br1+/zsjIsLa27tatm62trfS8Fy5cuH//fnFxcePGjW1sbLS0tHr27EkIefr0qaOjY3Z2NiFk+/btKioqhoaGzs7OpeXh8ePHDx48GD16dGktEEsjFosXLFhQmdOctdSTPnx8tWvX7tChw+PHjz98+HD37t2ffvqpzCWz9pL88b6fP3/u7e2dkZFhZGRkbW2tpaXl4OAgMWO/fv1WrVq1fv16Dw+P8m0MAECloWUfAAAAAJTPtWvXXr16RQiZP38+v1uu6vDq1Ss6KDD816SkpKxatcrCwqJLly5Hjx5NSEjo1q3brl27jh07tm7dOkLI58+fraysdu3aNW3atNOnTw8cOHDmzJk///wzi87ExsYuW7asUaNGvXr1cnNzo4nFxcVz5841NDQcMGCAu7t7cnJyz549165d++jRoyNHjgwcOJDf/NDT03PAgAEmJibz5s0LDw+niTdv3uzRo0ejRo2WLl0aERHh5ubWuXPnW7du/f3332vWrOncuXNBQYHEtvz6669OTk52dnabN28WiUTjxo0zNDRs3LixtbW1tbV1aXvgzZs3bERaMzOzMvcYv1kcx3F79+61sbEpKipatmzZjBkznj592r179/Hjx6elpfHnGjlypIeHx7p16w4ePNi1a9cJEyaw91VjYmJYDEv1H3IysHv3blJWuE2mwMDAypzmGRkZdERvJSUl9iJwaTp37kz/efDggZCFP3v2jP7DIoO7d+8eP3789OnT3dzcpk2btnnzZhcXF+kZbWxstLW1PT09ExMThW0HAEDV+U4DgwAAAADAD+3Dhw/0drFv374fPnz48OFDeHj4rVu3aIBPTU1t8eLF/NE2qZKSEjqXlZWVwBWxWSwsLN7xPH369OTJk82bN+/du3dpeRs0aJCcJV++fLlevXrCNxm+PenReJnMzMzw8HAaOapZs+aUKVOCg4OPHDlibGy8Z88ejuOGDh1KCDl69CibZcSIEYQQNzc3+jE1NTUqKopOpqOjw1/4mjVrCCGqqqp2dnZPnjzhOK6kpIQOOGNiYiISiehkkZGRt27dUlZWJoRs27aNzZ6Wlqanp0cIad68+ZIlS3JzczmOe/PmDX2BVGLkX9qw6+DBgyxlyJAhhJCGDRveuXPnzp07pe2c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j48Pz/R89vb2AFBSUkKVlJWVvXz58tixY9zVtLS0yPdODofz+vXrxgWAEPoRnT171tPTs3///lQJjUbbunWrmZkZAERERPDUnz59ekhISK9evc6fPy98glEeR48e5XA4kyZN4pmHzs7ODv7r1EZKampqFi9ezF2HTqdPmTIFAIqLixtwbu1GaWnpkiVLLl68KHAOPtFfmfr4+voOHz5clAkTDx486OnpKSEhcf36dUVFxQacQ8dy69ata9euUbkewsjIaPDgwUD6zgtVUFBw586d0aNH81wyQ0NDbW3t48eP19XVNUucenp6oswDGBQUlJmZOX78eJ5yBwcHzn95JRENHjw4Li4uKSlp9erV7u7uDg4OCgoKANDmM/ZqaGjo6upyj+tvkCZecRHJyckZGBhQoyWE8PHx6dGjB/dkfAAgLi4+ZcqUx48fx8fH89SvqKiYNGmSra3t+fPnuV+E7t2719bW8lQuKSnp3r17Y08CIYSaDSb7EEJto1OnTitXrgQAFot16NAhgXVevnwZGxu7YsWK1glpxYoVpN9HI6xZs4ZkJLlVVlYCgLW1NVUSFxe3fv16/hmpra2tye/2vLy8xgWAEPoRFRYWck9rQNBoNNJ9j+cDwdfX9/79+wBw8eLFbt26iX6Uurq669evA4C5uTnPJvJj+9OnT48fPyYl27dv52+BfJqRqH44S5cuXbVqlY6ODv+mBr0yAjGZzMOHD6elpW3atMnf37+ioqK+mu/fv9+6dSsA/PHHHzwrMv1UysrKcnJy7t69u23bNp5N/fr1A4CoqCjhLTx69IjD4QhMw2lqan758uXly5fNFa0oyMpj/PGoqanR6fTAwED+fJAQioqK7u7uR44cOXDggIGBQUJCgoqKSp8+fZoz4tbV9CvevLKzsz98+FDfvx8AuHPnDnchg8FwcnJasWLFrl27yC1n8qEBAObm5l++fOFppKSkhP/zBCGEWh8m+xBCbWbJkiVkuclz587xLGJIHDlyxMXFhXz3EiIqKurw4cOzZ89etGjRqVOnPn/+3NBI3r17d/r06Ybu9V33799XVVU9fPgwVTJixIj58+fz15SWliaLhPTu3bvZw0AItVu+vr4COxSrqKjA//1AoKYW7dev34gRIxp0lNjY2PLycvjvpzU3JSUlkjd88uRJfbuzWKyHDx+OGDFiyZIlDTpue3Dl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A4HBPoQQQgghhBBCCCGEGggM9iGEEEIIIYQQQggh1EBgsA8hhBBCCCGEEEIIoQYCg30IIYQQQgghhBBCCDUQGOxDCCGEEEIIIYQQQqiBwGAfQgghhBBCCCGEEEINBAb7EEIIIYQQQgghhBBqIDDYhxBCCCGEEEIIIYRQA4HBPoQQQgghhBBCCCGEGggM9iGEEEIIIYQQQggh1EBgsA8hhBBCCCGEEEIIoQYCg30IIYQQQgghhBBCCDUQGOxDCCGEEEIIIYQQQqiBwGAfQgghhBBCCCGEEEINBAb7EEIIIYQQQgghhBBqIDDYhxBCCCGEEEIIIYRQA4HBPoQQQgghhBBCCCGEGggM9iGEEEIIIYQQQggh1EBgsA8hhBBCCCGEEEIIoQYCg30IIYQQQgghhBBCCDUQGOxDCCGEEEIIIYQQQqiBwGAfQgghhBBCCCGEEEINBAb7EEIIIYQQQgghhBBqIDDYhxBCCCGEEEIIIYRQA4HBPoQQQgghhBBCCCGEGggM9iGEEEIIIYQQQggh1EBgsA8hhBBCCCGEEEIIoQYCg30IIYQQQgghhBBCCDUQGOxDCCGEEEIIIYQQQqiBwGAfQgghhBBCCCGEEEINBAb7EEIIIYQQQgghhBBqIDDYhxBCCCGEEEIIIYRQA4HBPoQQQgghhBBCCCGEGggM9iGEEEIIIYQQQggh1EBgsA8hhBBCCCGEEEIIoQYCg30IIYQQQgghhBBCCDUQGOxDCCGEEEIIIYQQQqiBwGAfQgghhBBCCCGEEEINBAb7EEIIIYQQQgghhBBqIDDYhxBCCCGEEEIIIYRQA4HBPoQQQgghhBBCCCGEGggM9iGEEEIIIYQQQggh1EBgsA8hhBBCCCGEEEIIoQYCg30IIYQQQgghhBBCCDUQGOxDCCGEEEIIIYQQQqiBwGAfQgghhBBCCCGEEEINBAb7EEIIIYQQQgghhBBqIDDYhxBCCCGEEEIIIYRQA4HBPoQQQgghhBBCCCGEGggM9iGEEEIIIYQQQggh1EBgsA8hhBBCCCGEEEIIoQYCg30IIYQQQgghhBBCCDUQGOxDCCGEEEIIIYQQQqiBwGAfQgghhBBCCCGEEEINBAb7EEIIIYQQQgghhBBqIDDYhxBCCCGEEEIIIYRQA4HBPoQQQgghhBBCCCGEGggM9iGEEEIIIYQQQggh1EBgsA8hhBBC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/MuXL+nfxTt/9erV9CmPEELi4uL8V+KFCxcsLS3Pnj17//59FxcXcXHxDh068MzgWdPuFSgsLExJSen69etcLpfNZm/btg0hRBa4ceNGEqgNCQnZtGmTsrJy//798YN9586dc3Nznz17pq6ubmRkZGZmhiurHTt28KwFv/eyt7f/+vVrfHz80aNHZWRkunTpwnMEGea8du3akiVLcKl2797t5uZGpktyc3NDCCkrK8fGxvbo0aNz5866uro459SpU+lLGD9+PD5FydQo9agrEhISjIyMZs+enZycHBAQgN++iImJ6ejomJqaOjo61rr/L1++rKSktH///vj4+MDAwJUrVyKE+vfvT5/C6/z5825uboMGDcK1kNs/cNUqUF5eHnkVdODAAXqVlZ2dbWxsrK6uTmqACxcuSEhIHDt27NevX6WlpStWrGCxWPv37+dZ5siRI9H/TtAREhKCf6rIllZVVX3//n3MmDF41TxVJZOzV7jKysoLFy7Mnz/fwsJCR0dn4cKFzL/beuBmfZ07d+b/E5nC7vfv38wXiH/r6zdBR0pKCv9svDz+/PPPGTNm0H+kMDIky5MnT06dOjVp0qTevXvPnDnTzc2N/HhhZBX4LGrgBB38BE7QwQ/fbJ88eXLr1q0SEhKmpqb4zauMjMyXL19wnuTkZBcXF/LanmcCnHnz5g0dOjQ7O5uiqNDQ0J49e5qZmTXutgAA6geCfQC0CvUL9llZWZEnSZxC7+K6ePFikpMe7FNSUiI3TPSGHufOnSP58b01QujEiRM45fnz5zjl7NmzOKWuwb7Pnz9LSEggPnJycps3by4pKeHJj2/isQEDBiQlJVEU9fHjR5KooKBgY2OTm5tLUdTff/9N0lesWEEWEhsbi8M9srKyJPxBRn7p168feVpmnpOiKPLmmX82XnqwT1lZ+e3btxRFZWZmkq/IyclxuVycmd7flh7so/65T0UIsVis8ePH4808ceIEyU8escrKyki3F5KYn5+PnzDFxcXx+/AmxTDYd/78eZxt5MiROIWcVyTYh+HIBQn2JScnv3z5EkeyRo0atWbNmqSkpHnz5mloaHz9+jUxMZE066AXoKqqCrdvUlFRGT16dExMDEVRFRUV48ePRwhpamqSA4FX0b59++HDh5MUMur2rl27Xr9+LfyZgVzCN27cwCkcDichIWHv3r04fdKkSRcvXgwLC7OysjI3N6+srORyuXhik8DAQLKc/v37I4Ru3ryJ/5mamhoVFYXfrvNcZV++fMFLNjAwOHr0KD5pP3z4gI/7xYsXa10CiQhbWVk9e/YMJ7q6uiKERERE6NFkiqJwE6H379+TrcNF1dbWfv369evXr+k7k6G3b9/iArx584YkZmVl4YqC7ASMzWaLi4uTR/GioiI9PT0SOqcoKjc3Fz+iLF++vNZVz549e/To0fQUvDlGRkbkUfznz5+4gaSWltbWrVvxQ11lZSVu3WNsbGxmZhYaGoozR0RE4Kgu2UVpaWk4yIIQ0tHRuXDhQmlpaXFxsaenJ65qlJSU6HFJgdGowsJCdXV1LS2tsrIykpiamoofutauXUsScbtgRPs5IPr06ePm5kb+GRoaKicn5+/vT1LwYzZC6NatW/QFslgs+hcpilq0aBHOWe9gH26EaG1tzbN2MqEqhoN93bp1mzlzZkRERG5u7t27d0mIjR4KuXDhgqqqKj0KgCeflZKSor/OqVOwr1+/flZWVvSU0aNH03fO06dP8dLMzMw8PT3xyZ+VlYWDYsOGDRs+fHheXh7OjH+e5OTk6D9zgYGBoqKiM2bMoK/l1atX+OSnbyDznKQ6xU/dBD4PZWVlhw8f/unTJ5x47NgxnDkkJASnxMfH+/r64sXyzIPMvK7AL8nk5ORIECQ3Nxe/pfvjjz9ev3799evXmnY7dvjwYUS708B2796NEGrfvn1aWho93cnJCSFkaWkpfJlEaGgoriEPHDjA86eBAweSu6CYmBgWiyUtLU3+WlZWJioqKioqSp8olhIU7KMoCncv4AlrXr58Ge9DerCP4dkrxLt37/T19a2srD58+ECvJZpISUlJbN3x3+Dxw307NDQ0+P9EBnpmuE+wpgv2paam4pdSampq9DtnvATyotrKymrNmjX79++fOnUqvgM0MTGhv41uVcE+PT09FxcXPGtwbm4ufps4ceJEsij6BMT0YB++U6L/iH/8+FFfX79xtwUAUD8Q7AOgVahfsI9MsTdp0iSSSNq+6ejokER6sG/8+PEkPS4ujqR37twZtyeKi4sjzU/8/PxwzqioKJzSqVMn/GBT12AfRVFv3rzhn+MM09bWpr+0pyiK9K9B/xsNoXfxII8TMTExJHHw4ME4kcvlkk43/fr1I0ugdx48c+ZMnXLylEF4sO/kyZMknd63FAcuKYrC3eswnmAf7reCkSeB+Ph4kjhgwACc6OvrSxLpsQ/Sf4T+XN1EGAb7Hj9+jLOZmJjgFFJ4nmAfbnhCgn0YDnD07duXP65ERm7iKQBukqOiokJ/CiKPxPTHNtwqlv6Iy+Vyu3fvjhBycnKqdQ/wB/uw5ORknL5+/Xqer+Tl5eHwJf30xjfTLi4u9Jy4wQ7PVUYeSw4fPkxPx8+Z9Fh/TUvAY9ujf7pHYWw2Gz8Me3t7k8SSkhJcVPotvqenJ0KIxWLRGxHXFY5CLlq0iJ6I2xHb2trSE318fIYNG0b+uX37dnFxcZ5nSNxaWUxMDAfHa4KDjC9evKAnkhcG9+7dI4n4PORpu0Q6FJNIH4bb8W3fvp2kkArk6tWr9Jyk/z69MZ3AaNTy5cuRoIfAixcvIoRERERI0K26ulpbWxshNGHCBHpO/BqDBJ4oiho8eHD//v15FojnMyUXZklJSefOnVVVVUkjU55C1jvYh/tI0islUnvTL1Ic7Bs4cCD9Yv/+/TsOv+rq6uKISX5+vry8/ObNm+nr/fbtG17g6tWrhRdGoMrKSjExMXV1dXpQ5v79+1euXCH/JE2Nrl+/Tv/unDlzEEJqamr0bSEDtuJ3PxRFVVVVmZiYIISioqJ41o47z3bu3Blfa8xzUrUF+2RkZOit3qqqqvDADvSfNoqicB95nmAf87oCP/nzVDW4IbChoSFVm+TkZBkZGTU1NfLKDeNwOPgNEP0Ghqp7sI+iKDwpOU9hUlJSlJSUyFHDs8bLycnRezzgbo88wXSBwT5898IT7Ltx4wbeh+S8Yn721uTJkycsFsvW1ramdy0hISG3b98+c+ZMQ2ppuu3bt7erO/5mrfxwnda1a1f+P5GxHSIiIpgXtSmCfVwud+XKlfgtHfHnn3+SDGlpaTixX79+9JPnw4cPuH09/YViqwr20V8dURR14MABhJCqqio9kbxSot8J4LFBeKLzdnZ2jbEFAICGggk6APgXmz9/voiIiIyMzKRJk0gi6d5Y03wd9BFPevXqRe5aMjMz8aPpgwcPyLg2pP8pGRsoKyuLHiKsk+HDh0dERMycOZP/T4mJiRMnTqQHv+jovXfpn8mgIfQRykjzxri4uPfv3+PP9EF8SH83hBC+v2Ses07oY+TRPwucY4TJcgQuhH446KOokM/k4aHFcblc/IHhhCQ8cAx6xIgR/KP21DS1C+5wLSMjQx9JjQyHRA9YBwcHo/89NCwWCz+z0bPVr8xI0BjeSkpKPj4+V69epQeCKYpCCNHHrUM1bB3p000agWJ463jKLHAJpDc6fQlSUlL4qqcvISwsDI+uSN8/eNBDiqL459thDvd4unHjBhkHEyGEG9k9fvyYPnP3uXPn6C2RL168KCcnt23btjU0eMrv6upq+usNfjhad/fuXfp3STNDEhVC/+winsEuSfsyHCAjcCyAhIEQbQ+TPo/Y9OnTceSaxFAEKikpwe3g+EfKmzFjhoKCApfLPXXqFE4RFRXFAevHjx9HR0eTnF5eXpMmTSI1eUJCwvv370tLS9f8L9yeKyIiAkemTp06lZmZOXr0aJ5x1nhOtnrYvHnzkSNHSLHRP+c84jvtEUIWFhb0i11bWxtH02JjY/E23rlzp7CwMDw8nL4t586dw9+iH0rmxMXF5eXlMzIypkyZQhpfW1tbDxkyhOQhR1bgudGuXTt6hdOuXTtcG5Nz4+XLl+Hh4ZKSkvxDYuGTPDMz886dO3XKWSscRKNvAq4rSHiCbD7/d5nXFfwVKfqnrmBSkZ4+fbqsrMzY2JinGCIiIrhV6ZMnTxITE2tdjhD4hVxUVBS9o4CHh4e9vT05auPHjz99+vTTp0/JhqMaKueGaODZm5aWZm9vLy8v7+npWdNIdsHBwceOHVu2bBm9gm2IXbt2Fdfdzp07a10yDrRRgibvIon0IRdbBIvFOnHiREZGxs+fP/fs2YPf+x48eJDMQUeupgULFtBPnkGDBuH3HG/fvn3z5k2zF7x2+NUmgX+zfv78SZ8UTmDlgH8HN2zYQN7mIoT279/fVAUFANQFTNABwL/Y/v37V61aJS8vLyMjU11d/f79+3v37rHZ7DotpFOnTuRZPT4+3sTEhN6cbebMmfhhj36PlZSUREb8qauuXbt6e3uvWbNm7969jx49ot/YxcfHX7hwYc2aNcKXIPCmVmBiWFgY+Ux/JJOUlBQXF8db9O3bNw6Hwzwn/5weDDXKqNICF2JkZEQ+0+f8JY9bPXv2bPiqGwV51q3T1AQ86n0ICHILToKP6J+7WJ5ANo7I4AZTDSSw2GQWhaqqqnv37t27dw9HqUj7gnrAW0fftIYvgdzix8XFaWho4M/q6uqioqJcLpcnklUn9vb2mzdvxlOvODo6IoQoinJ3d5eVlS0tLfXy8sIVwvfv38PDw21tbfG3EhISMjIydHR0eGbjIf8UPikhbk86ePBg+sOYpaUlbhjIPxckj5oiyzhexiQWICoqOmrUKHd3d+EvTqKiovBRII24CTyIp6+vb3h4OEl0cHDYuXNnZmbmoUOH8HigFEVdu3aNPn0H3nZtbe2adh2G4yC17op6UFVVXbt2Lf78/ft3d3d3Uv3yn/b8Nd60adPw5kRHRxsZGeHNMTMzo1eDCCE8fxH/SP8Mubi4bNy48f79+/7+/q6urgsXLpSQkGCyN4ScG2VlZfQGSgihjh078m9gv379xMTEqqur8ZFlnrMe8I87k4nsa1JTXZGWllZWVkZeODGvSPH28p/wCCEyWVN4eHhD6uQhQ4YYGxtHREScPXsWL5PL5V6+fJl0ZsdbQWZd+P37t6enp7+/P/55bUjlzKOBZ++6det+/fo1Z86c/Px8cmopKyvTX4guXLjw3bt3GRkZ9DeXrRPu9iFwLmmSKHDumhbRsWPHbdu2mZmZTZgwobKy0sfHx8XFBSH0+/dvnIGn9R9CaPHixbg3emBg4IgRI5q5wHWFKweKoqqqqgSOwEPMnTt33759WVlZEyZMmDRp0pEjR7S1tVvPbScA/3EQ7APg3+fDhw9ktL5OnTpFR0efOXPm5s2b6urqZHxu5uiTheXk5CCE6K/NHRwc6GPnYfW4z46Li5OVlSUtqvr06fPgwYPIyMg///zzwYMHJNuNGzdqDfYxR49a8jyDkRAem83+8eMH85w8Lz9bg/79+48YMQK/KyaN+FJTU0ngb8KECS1WuP9Fijd8+PCWLQm/SZMmPXv2LCAgIC4ujkz2mpWVxWKx8GQ1TaS4uPjEiRNPnz5dvny5p6eng4MDbp7WqpiZmWlqaqampl66dAl3OEII5ebmcjicwYMH19Q3n4kOHTpMnTr1ypUr7u7uONh3//59hNC+fftWr17t4eGBK4QzZ87MmzePXJv4/QT1T6/nOuFyuTjoPHz48Jpm7WwGOHJUVlYmJA9poJednc1f6+IlJCQkkBQJCYl169atW7fu2rVre/bs6dKly8ePH6urq8khQ//suo4dOwrfdXjV9KhB4/L39//rr786deq0bt066p/u0kyQEeLxQcSbY2BgUI8zQQgXF5eioqJ9+/bl5eUtWbLk9OnTFy9eJNOtNBzevbhnJU8UT1RUtHPnzqmpqfjIMs/ZSkyYMGHTpk1VVVXXrl0jIzziYRbwYKnC4e2lN48lSLC14dvr5OS0ePHi27dvu7m5KSkpvXjxQltbm/8kTE5O/uuvv1JTU11cXJydnTt16lTXl6nCNeTsraioePToEUIoPj4e15zY7NmzyVTvmK+vb53maW0pOJAnMJZKEhvyW9MUxo8fP3HixAcPHpC6mtxR89ft6urqcnJyxcXF9e4c0zrJy8u/e/du/PjxiYmJDx48ePbs2YYNG7Zv3y48RAgAaB7QjReAf5nIyMgFCxbgz+Xl5XPnzjU0NDx16pSDg0NQUNDSpUvr2oKM/h4Vv4uj99NRUFDox6cePblu3LhBn0MDMzIyun///s2bN8kzPOkK0ShIEyTE97qYtGVgsViqqqrMczZi8RrRjRs3cBezhw8fvnjxIisra8uWLfhPa9euxZ1HWhxFUXfv3kUISUhI0DuetxKLFi2aN28eRVEzZszAj6aRkZHe3t47d+5sutCkr6+vjo5OSEjIq1evZs+e3WpvjsXFxe/cuYPnJ8VD83A4nK1bt2pqapLh5+oNv6L4+PEjfgQ6ePDgpk2bcJe6qKiooKAgNpvt6elJAgfon2aSaWlpPKOdMoSbI9FHwGx+OI7G3zeTjnQT5uloieHWTzzNKpcsWaKoqFhVVYWHafPy8po3bx6eOQTDu67WbcfRFoHrbaDy8vL58+ePHj167dq1586d69WrV52+Tiph/IHh5tTD3r17P3z4gJtchYeHW1pakvmaGg4f2crKSvyCjQf9yDLP2Uro6emdPXtWTEzMxcUlICAAIZSfn//XX3+NHj36zz//rPXreHuFnPCoMbZ39uzZHTp0qKiowJNmuLu704cIwI4fP66np6elpfXkyRN6D+5G1JCzF9d+LBbr3f/iifQlJCRkZmbSR0ButfChJy3j6H79+oUQkpKSInNftB5jx45FtL4veF4LVEPAGvdpqKn977+Xjo5ORESEi4uLhIREZWWlq6vrkCFD6jpeDQCgKUCwD4B/mV27dhkbGyOEKisrJ06c6OXlRVGUkZHRwYMH69e9kT4hLH7ypD99hYSENLTE/8AjXvObNm0aeYyv31BuNaF3jaEPWFNRUUFeFPfo0UNWVpZ5zkYsXiNSVlb29fXdsWNHeXn5n3/+OW/evJKSkhUrVvj7+5Npalucl5cXHrNp1apV9KGjWgkREREPD4/NmzcXFBSMGzdu4sSJhw4dev78OR58uil8//7dxsamsLDwypUr9MEWW6c+ffpERkZ27Njx8uXLAwcOnDFjhpaWVkxMDGmuW28DBgzAF+DFixf9/PwyMzNnz56toKAwZcoUhJCHh4e3t3ffvn3pT/j4gYrNZtMHCaIjMxXwExERwYvCkz/yCwgIoHeHbyL4WhDe3RhX9Qghgf00f/78Sc+DycrK4rnUz58/n5OTc+fOHXqrH/TPrvvw4YPA4BGHw8HDCOKGhPQmz41l9erVly5dcnFxqV8AghQbd8DEm1PToczMzIyMjKzrKiiKwmsZOHBgWFiYu7u7nJxcdXX1okWL6ININgTzI1uPc6DFLV68+PHjxywWa/Xq1aNGjVq2bJmzs/Pz58/pveZrgrclOTmZP1KANxY1xvbKyMjgl6bnzp3Lzs4ODAycPHkyPcP169ednZ0tLS1x38wm0pCzF9/vKSkpCf/tePfuHUJI4LWWl5d39+7dffv2+fj40O8DhTt9+nT/ujt9+nStSzYyMsKhIv7C4NdvgwcPboVhMtwgkfQG6NixI76FFthCH8cEhb/j+dfBYU0ZGZmDBw/GxMTgnhCfP3/es2dPSxcNAADdeAH4V7l169bdu3fxqB9v37599eoVTu/duze97UadkAH7jI2NcQO3fv36kb/euHHjyJEj9LFjYmJi5OTk6vGEHxcXd/fuXTs7O/4/kVsfMihPozAyMurUqRO+TaRPV0K/lcTTJjDPyYOMVcTlcut9CBouPT19/vz579+/j4mJqWtLmUYhcFBtuoSEBByDMDIyorfvIDuNZ+Bt4d0bm8iTJ09KSkrw7KX1XgjzEZ0uXLhQWlpqbGxMDyI34oBQjYvNZq9bt+7Jkyd4mtTGtWTJEicnpytXroSFhW3YsAE/0S1atOjq1avXr1//+PEjz2ODmpqaoqJifn7+3r17J02axPME+OLFi4CAANzgQiB9ff24uLjLly+vW7eOp5VQRkbG4cOHfXx8GnX7BMAD1fFE4nh07dpVR0cnLi7O29t73759PK228XgLeOZiOicnpyNHjhQXF9vZ2ZmamvJsIK5sORzOrl27+J/At2/fjudSMDAwiIiI8PX1jY+PF1il1HrJC1RSUoJnR8EzzGJ1OudxM6ipU6fiXyu8Od++fbt169a0adPoOauqqpYuXYrHLqyTysrKHTt24JEBRUREFixY0K9fPwsLi7KyspCQkDFjxtR1gfyGDBmC4xrXrl3jWSCbzc7MzJSWlsYnMPOcdC1bjSQmJl66dCk6OroeL3VGjx59+fLlysrKO3fuzJ8/n2exCCFDQ8NG+Y1bvnz50aNHExIS5s2bZ29vz9OqGjeMpZ+lqC57Ff+u1fqj1pCzt1u3bu3atau1SL6+vlpaWqTzO/Hp06c9e/bs379/woQJoaGh1tbWAQEBTPqFWFtb16OLA/8UQ/wkJSWHDx/+/PnzjIwMnhZ8P378QAi1zs7IMTEx4uLieLxXbNasWTt37nzw4MGhQ4d4MuObzEYcEEC4+tXSdXX8+PFVq1bhMTG6d+/+6NEjR0fHq1ev1m9+JABA44KWfQC0ChUVFfR/CryBe/HiBX4yxK+1v379Sv706dMn/OHevXvkuzX9zNMHnYmMjCQNJcj0iBMnTiTTg+bn5zs4OJCerV++fJk6dSpeBf1GluFN8Nq1awWOXo9fX8vIyJDOp+h/x/ymf6ZvF/ksMFFaWvrYsWP4M71ZRGhoKP7QsWNHHERgnhMjwaCysrLMzEyKosaOHYsHEqqp2AI/15SBSX6ymeXl5aNGjXr16pW0tHTjNo1kjjzGCJwX4sGDB/369SssLDQ0NHz27Bl92B3yEEJmQ0YIvX37NiMjAyHEMzsz3mSBJxvpbc3zfCVwtG+SmX7a+Pv729nZrVmzpn6RPklJSRyzwyVHfHML8hcbd1VLSkoi8eXi4mIcA+IZMh//k2fTapqCA2fmufwFLkHgzqlpCVOmTBEXF6810vfjx485c+ZYW1vjSTkZmjNnjoyMTE5OTlhYGOmGZmVlpaurW1BQkJ+fzz/a17p16xBC4eHh06ZNI0F5PB/F8uXL8V9rgv9aWlpqbW1NHz4pKCho6NCheLJOTOBOJokCj4jAipdnKszY2FhfX98JEybgyBpGDgd9pWfOnEEIpaamklc72Pfv3wMCAkaNGsUTI0AIKSsrOzs7I4Q+fvzIEy5BCBkbG+P3FmfOnHF1dSWnJZvN3rhxY2hoKB7gb+3atSwWi6KoNWvW0H+eSJVY02zvdPxblJmZiT/T24yTa5/JTBFnz56Vl5c/fvw4/qejoyOeEHbhwoV4CDMsKyvLxsZm0KBBZBpigbu3Jg8fPqRvtaGhIR67lozZWtNCcDr/nKE850a3bt02b96MEPLx8eHpt+jt7V1dXf3nn3/iGRWY50QIkY3lqYVwefhLhXc4T9XUwLoiLy9v+PDhY8aMqV/z7ZkzZ+I+sxcvXuS5lPCcs2fPnqW/V8OHiecOigktLS0bGxuE0MuXL+lDBGC4cqafpdHR0Xl5eYjvLMV7hud8wL9rnz9/JpnLy8tJCz7yu8b87OXHYrHs7OwKCgrIsRbI19dXYLO+JUuWLF++3MTEREJC4vv376GhoQyHI9TS0rKrO4Y9r1esWIEQevr0KU/6y5cvJSQkyARNCKGKioo//vhj8ODBQoa4xedPYwW82Gw2f2vTsrKyy5cv79+/n95Ge+nSpQoKCgkJCbgbOxEQEFBWVjZy5EiGvcLPnj07d+5c+tTqdUXONOFvTwXWD+TUpdcPNd1o0VunioiILF68GPFN7wsAaBkUAKAVuHLlCv3C7Nu3b1FREf5Tbm6un58f/Vk3OTmZoqiHDx/SvzJ06NCxY8caGRkpKiriFBERkZ07d757946iKDzFJ0JITk5OQUHh169feOEzZ85ECLFYrK1bt9LLExgYSI/IqKur29nZGRkZtWvX7u3btzgPHuwGExMT+/37t5AN3LlzJ85pbm6emJhI/1NAQAB+qX716lWSWF5eTm88eOXKFZxeUFBA7woUEhKC0/HckZiqqmp1dTVZFBni8M2bNzjFwcEBIdSuXTs8HXA9ctIf0bW0tIyNjZcvX47/RJ8NgyRWVVXR79pv3ryJ0798+UISRURE8Cjs+KDTGysFBATgdPrzgLKyclVVFUVRfn5+9DNBUVGxc+fOXbp06dq168CBA5csWRIVFSXk0DQcl8sl7+2XLl1aWlpaWlqanZ0dEBCwe/duPBBkhw4dtm3bVl5ezv9dHD+SlZXdvXv32bNnp0+fPm3aNPwOX1lZ+eDBgxkZGRRFVVRU4HNy8uTJ/GUg75CPHDlCT583bx5CSFpamlxQ1D9DByKE7t27RxLxmE0SEhL6+voDBw4cO3bslClTHB0d9+zZ8/XrVyb7AU+hqK2t/f79e09Pz02bNlG0S8/NzY0nPwlYDBw40MPDY9u2bYMHD8aNd9TU1Dw8PE6dOoVz4vgLHouNfB03dkAIbdu2jb5Y3CXWxMSEnihwCZ6enngJr1+/Jom5ubn4WXr16tUkkQw/pKamZmFhMXz48IkTJ86aNWvlypXu7u6lpaUkJ4mUqaqqlpWVMdlvGA5Lubq60hMPHz6MENq1axd//tLSUvJ81a5du9GjR0+ePLl79+5KSkqRkZG1rs7e3p5cdwMHDrSzs8Pto/HACFhVVRWuThctWkT/Lqn6Xr16RU/H70hMTU1JChmEtG/fvunp6Tjx58+fxsbGxsbGqamp9K+TVjy+vr709J07d4qIiKipqZGas6SkZPjw4cbGxikpKQK3rqCgQEZGRkFBQeAhCAsLI9W7urr6xIkTra2tlZSULCwsCgsLSTYnJyecZ9CgQVevXv3y5cuxY8dItLdXr15OTk70y4of/xZVVVXhkbkkJSV37dp19uzZSZMm4Sd8hNCCBQvc3NxiYmIoisKt1SwsLAoKCsgCDx8+rKKi8vz5c/pa3N3dSYskAwMDOzu7oUOHSkhILFmyhMnu5YcHgly/fj1Jqaio6N69u42NDUkhsexr167Rv4tfyElJSRUXF5PE4uJifE05OzvT14L7uw0ePJjNZuPE+Pj4Ll26TJ06tbKysh45CwoKpKWlEUITJkwIDAzcsWPHrVu3KIrCw7ayWKzc3Fz6YvFpYGdnRxLZbDaej37ChAn07WJeV9y4cQOvq3v37paWlqNGjbK1tZ07d+7GjRsfPHjA5XKF73yKor5//47b7m3YsIEkPnjwQFpa+tChQzyZ8WRlSkpK/D8utXr58iVCaOzYsfx/InElR0dHDw+PJUuW2Nra4l63/fv3v3z58v379ymK4nA4OIw1dOhQ+tfJL/Xw4cM9PDx27txpaGh49OhRnDhlyhTy08Pw7BUoNzdXRUVl8eLF9MSQkJD4+Hj8GXfD9/T05Pkil8tt166dsbGxh4dHbGxsaWkpvqtsDaZOnaqtrU2viGJjY2VkZPbu3UvPho8dQoj/lMCSkpLwvaKPjw/ztZPAVkpKCqIQ/o+iKA6H07lzZzExsYULF5KavLCwcPLkyQ4ODvxn9d27d6WkpLp165aVlYVTKisrx4wZo6ysjOs3jKwC/0CTX3yKosgsNOPGjWNefh5kRF1paens7GyBeX79+oXPwJUrV9LT9+/fj79L/z3du3cvTgwODiaJmzZtUlRU/PHjB0lxd3cXFxcnN64AgBYEwT4AWtiNGzcETm4rKSmpqanJ31BLTk6O3FjQR2KWlJRctWpVcXHxtm3bSKKjoyN+Nvj8+bOsrOy5c+eKi4tXrFjRq1evNWvW4LtkdXX1Z8+e8RcsMTHR0tKSvmoTExPyA3/y5EmeIQI7duyYkJBQ02bu3LlTU1Nz5syZampq0tLSM2fOPHLkyKFDh2bOnCkqKjp06NDAwECS+evXr2TePUxcXNzT0zM4OJhnbhA5ObkPHz54e3uTkewxQ0ND+h3VjRs3lJWVlZWVV61ahXtbDBo06Pv37wIPB5Oc4eHhZE4PFos1b968qqqq8vJyU1NTnuPl4OBQWlpKnwAEISQmJnb06NGzZ8/yDAKopKSUkJDw9OlTns2RlpZ+9OjRvXv3eBqdGRgYxMfHR0VFCe99IyIiQo9qNaKKioolS5YYGhryr1RaWrpz584WFhbLly+/fv06eUblFxMTQ5YgLS194MCB6upqGxubLl26rFy50tfXt7q6+ujRo2ZmZmSH29nZkdhZUlLSli1bSAtBeXl5Z2fnyMjItLQ0Jycn0jlrxIgR3t7eFEW5urqSOVg1NTX37dtHilFTVyMWi7Vnz55a98br16/x8EksFsvR0TEvL2/t2rWdO3fGC5GVlV26dOmDBw9I/srKSjJRiYSExPLlywsKCt6+fYufn3v06IG7mk6fPp0c32HDhp04cYKiqKtXr5Kwi4KCwvr16zMyMoKCguidOmfMmBEUFFTTErZv3072g46ODt7AW7dukcXKyMg4OzuT6MDOnTvp3fnpNDU1cftWiqLu3r1LztKgoCDm51JwcLCioiI9skNRVG5ubrt27XC0l19xcbGDgwNZHYvFmjlzZlJSEpPVcbncQ4cO4eGWMEtLSz8/P5LB09PT2tqanJZr1qwJCgr69evX1q1bSZMlTU3NnTt3FhQUvH79mrSww8V48eIFRQv2/fnnn1paWqNHj7axsVFUVHRwcCgpKSHrCg8PX7VqFZnGUUtLa9u2bfRQoK+vr6mpqby8vJ2d3eLFiw0MDNasWSM8ujFkyBDysoHf9+/fBw4cSFpIycrKbtu2jT9yd+bMGfL2SEJCYvr06YGBgSIiIkOHDj137lxeXl5NyxeyRc+fPyfpenp6OHJHXp9s374dLyEjI2PhwoUdOnSQkpLq37//1KlTdXV17ezsBD6yvnjxgrRuQwhpaGh4eHiQH8pady+P8vJyFRUVHR2dMWPGHD9+/PTp04MGDXJwcMAnJx5+ngw9oampuWPHjpycnNDQ0OXLl5OzccCAAfjR/fz58/3798eJcnJya9asiY6OJuv6+++/NTQ0OnfuvHDhwhkzZujq6uIWbfwY5ty3bx+5ft3c3NLS0pYtW0auXFNTU1wqLy8v/HICISQiIuLo6JicnLxnzx564pw5c/BbrjrVFUVFRfytTYmxY8fSX8XVpLS0dMWKFYqKioaGhitXrrS2tjYzM/v06RM9z5EjR+gTPfXr12/Hjh0RERG1LpyuV69eT58+5U9PTk4mh1hJScnNzQ33fMcp48ePLy4uPnbsGP2136xZs+ivCnbu3EleTPbv3//bt29BQUFiYmIjRow4ffo0iQFRtZ29wqWnp1tbW0+cOPHixYtubm4zZ85cvnw5iZThruj0KAzh6upKbuEGDBhQUx3b/IqKiqZOnTpo0KDg4OCSkpIXL1706NFj8+bNHA6Hnq2srGzIkCEGBgY8t50+Pj6Ghoa6urpKSkqKior4/z179jQ0NNyxY0eta68p2EdRFHnjLi0tjSfhtbOze/z4cU2LCgsL09fX79Sp0/Lly/ft22dpaTl69GjyQ4kJCfYVFxfj21381rCuTp06xTPAhbKy8pw5c3jOdk9PT3IV4xBzbGxsRkbGqlWryN2pjo6Oh4eHr6+vk5MTeSTp2bPnn3/+iavETZs2mZiYaGlpbdy40cPDw8XFRVdXl+cFOQCgpbCoZunPDwBoIpGRkd++fdPW1jY2NiaPGb6+vj9//rSwsCCt6PPy8goLC0lUMTc3NyQkJD8/X0tLq2/fvkIGm/v582dwcHBxcXGPHj2YjLpSkzdv3sjIyPTv35+iqMjIyM+fP+fk5EhLS/fq1UtHR4eMbdx0qqurP3/+/OPHD0lJSSMjIyFrZJiTzWYHBQUVFhbq6+u3eG8Fb2/vbdu2JScn15RBTU0NDxbTOnE4nPj4+JKSEnIap6WldenSpa5TSzdEZmamk5MTnuQkJyenuLi4tLS0pKSkqKjo69evz58/z8rKItNB1iQvLy86OlpLS4snvCtEQkJCdna2oaEhCUYkJiYWFBSYmJgwGc++2Zw/fz49PX327Nnp6en5+fklJSW4CWd6evq9e/cmT55MxgEoKSnJycmxsLCIi4urdY/RCRweLjY2VldXV8i3qqqqoqOjq6urdXV16zqFDkVRSUlJOTk5vXr1qsck47VKTEzErYH8/f379OkTGxtbVFSkp6cnpHeeEKWlpZGRkWJiYvr6+sJH5a+oqFBRUXn37p3wSrukpCQyMlJBQaFHjx5CRr5PSkrKy8szMTGRlJQsKysrKCggIez6KSkpiYiIkJOTIzMjlZeXBwUF9erVi+eE4XA4379/z87OlpKS0tHRET4X58+fP+Pi4jQ1NTU1NRtYdZSVlcnIyOARPMvLy/X19UnQsynk5ubGxMQoKirq6+sLH/uVSc7k5OT09HQjI6MWmbq0oqJi7dq1I0aM0NbWzszMLCwsJHVFbGzsnTt3rly5gufeYSIpKSktLU1TU7OJZhyOjY3V0dEReLZUV1dHRkZWVFSYm5uTl0ZBQUHy8vK4uXqtfv/+HRMTo6mpibsplJSUVFRU1HTtN+TsLSgoCA8Px3cs9Dpw5syZX758wWMd8ktPT/f393/x4sWVK1ecnJxIS/PWIDAw8MmTJ79+/dLQ0LCzs2O4wxuOw+Hgn92UlJRuXbvhRAr935NyaGhoXFwch8NRVVW1sLBgcn3hl3AyMjK9e/fmr0NY6P8O9MhRI1+/fn3q1CnSzBkhVFxcnJiYyP8KubUpLS2VlZWlKColJeXHjx+qqqq9evWq34SBAIBGB8E+AAD4d6usrDx8+PBdi/5lAAEAAElEQVTt27fV1dXps4hwudycnBxvb288/FBubm5ThDPahh8/fgwePPjo0aMCJ5BBCM2dO9fZ2bnZxtVubXbs2HH//v3Q0FCB8YXExERnZ2d6H/NPnz4dPnyYdJf+z6IH+xp39iHh3N3dr169ytPBH4CmVlFRMXHixF69ep08eVJghosXLxYUFAgfUhM0lk6dOllbW+N+xA4ODjhg+urVq/nz53/48IE0J3R0dBQTE8Mz5/zHCQ/2NTrhwT4AAGi4VtRqAAAAQD3s3Llz//79nTp1+vr1K39bsGnTpvXu3VtdXR0ifUIsXrw4NTVVSDgmKysLT4zzH/Tp06fdu3cvWbKkppZEcXFxePoCLCoqauPGjbdu3WquAoL/UVZWdvDgQf6JIAFoakePHn358iUeKVWguLi4ml6ogMZVVVWVl5eHh8iIiYkhTSNfvXqlp6dHGucWFRWFhoaSwd0AlpGRgf6ZwZiE5JoOk/mOAACgHiDYBwAA/254KtLKysqqqir+YF9ERARCaNWqVc1fsH8RPCXr1q1b//77b56R6dLS0lxcXFauXCmkn2PbhnfO3bt37e3tyZBeWFVV1f37969evUpCe2w2+9SpU3fu3KlTB962iszRyXC+8nqLjIzctm0bHiXz5s2bqqqqEydObNI1AsAP1xVHjx61srJSV1en/6mwsPDkyZMiIiL9+vVrodL9t4iLi69fv/7Zs2cZGRnLly8n6StXrnR2dj548GDXrl3T09PDwsIuXLggcODd/7JVq1ahoOZbXZ0mrwcAAOagGy8AAPy7/fjxw87OLjg42MjICA/eLycnhwdTe/HixevXrzdt2gTBPuEuXry4ZMkSDocjJyc3bNiwLl26iIuL//z5Mz4+XlxcfN++ffSB2P9rCgsLhwwZEh4ejhAyNjY2NzdXVFQsKirKzs4ODw+fM2fOpk2b6JN3A+L169e4Z727uzuZ7LsprFy5koyZ2K5du6CgoGYYCBUAHsHBwaNHj87PzxcTExs0aFDPnj3l5OR+/fqVlpaWmprq4uKycOHC5hyJFRQVFVVXV/OPFpebm5ubm6ukpKSqqtoiBWudKIry8fHBn6fYMR1ZsuHu3L2DEDI1NRU4Xx8AANQbBPsAAOBfj8PhBAYGvnnzBk+eICoqqqKioqKiYmFhMXLkyFY1z0Or9eXLlzNnzvj6+uJ5CTt16tS/f/+5c+fa2Ni0dNFaXllZ2YULF65fv56QkJCfny8nJ6evrz9q1KiVK1dCCz6BiouL//zzz5s3b2ZnZyOE5OXl8RSTDg4OTbG6qKioVatWff782czM7Ny5c9BOB7SUtLS006dPP3z48MePH2w2W1FR0dzcfNKkSQsWLOCZTR4AAAAATQqCfQAAAMD/h38WoflJTbhcrvAJQwEAAEFdAQAAALQoCPYBAAAAAAAAAAAAANBGwAs3AAAAAAAAAAAAAADaCAj2AQAAAAAAAAAAAADQRkCwDwAAAAAAAAAAAACANgKmaASg5RUVFb148eLOnTsw9Wej+PHjx8uXLx89erRu3bohQ4Y00VpcXV0fPXp07Nix/v37N9EqWon09PRHjx7dunXr9u3bysrKLV2cxofnMr5//35BQcH58+fr9F3mJ1tFRcWnT5+ePHmSk5Nz5cqVhhW57WOz2W/fvr17966FhcXy5ctbujig9UpISHj48OG9e/f8/f2bf+1VVVWzZs0qKSm5evVqm6weAcjOzn727NmdO3d27dplYWEhMM/Pnz8/ffrEny4qKqqiotKpUydVVVUJCQn+DM+fP6+oqOBJlJKSGjNmjMAVlZSUvHnzhmHJe/bsqa+vX15e/uLFC/6/qqio0O/fUlJSwsPDefJMmjSJ4boAAKAVgmAfAC3Mw8Nj//79379/Rwj98ccfLV2cf7fMzMydO3feunWrsLAQIbRixYomWlFVVdWePXsqKirOnTvXtoN9a9as8fT0/P37N0KIw+G0dHEa3/v3711dXV+9ekVR1MiRI5l/sU4n26VLl9zd3QMCAhBClpaWDSxzm3f9+vW//vorLCwMIaStrd3SxQGtVF5enouLy82bN8vKysTEWuaG9tOnT3fu3EEIPX782NHRsUXKAEDTcXZ2vnnzZnZ2NkJoy5YtNWWTlJQUExN79+7dkSNHcEqvXr169epVWFiYmpqalpYmJiY2efLkRYsW8fzOSktLf/r0af/+/VVVVQghJSWlLVu29O3bt6YViYmJsVismzdvent745Tu3bt3794df6YoqrS0NC8vD99Ur1+//tChQ6KiopKSkr6+vn/99Reel1JLS2vjxo3dunWjL1lKSqqwsPCvv/769u0bi8VasGDBhAkT6ry/AACgVaEAAC0tPz8fX483btxo6bK0BXFxcXh/Pn/+vOnW8ueff5qbm3/48KHpVtFK+Pj44P2ZnZ3d0mVpKpMnT0YIjRw5sq5frNPJNnPmTISQpaVlvcr438LhcFRVVRFCe/fubemygFbt1KlTCCExMbEWWXt5efmECRNGjBiRk5PTIgUAoKnh9y4IIX9//1ozW1lZ4cz0u6Pv37/Pnj0bpx88eJD/W9OmTcN/xfG4WnE4nE6dOuGvpKam8meIiopSU1NbvXo1PZG0vr99+3ZNS05KSmKxWLa2tkyKAQAArRyM2QdAy+vQoYOoqGgzr3Tz5s1cLreZV9o8NDQ0mmEtu3fvDg4OHjRoEE96dXX11q1bm6EATeThw4cfP36kp3Ts2LGlCtNscFypHup0sjXPmdlSGrdKERERgU6RgIl6X7zCMTyfJSUlHz58+Pr1axUVlaYoBgAtrk6XmKamJv4gLS1NErW1ta9evTp+/HiE0MaNG319fXm+paWlxfNBOBEREbKi9u3b82cwMDDYuXMnbi1IdOnSBX8Q8luspaXVuXPnfv36MSkGAAC0chDsA6BVYLFYzbk6Hx8f/HK1OVfabEREWrJmW7du3fv371uwAA2RlJTk6OhYXl5OT2zmk7NF1Hsb63SyteyZ2aSaokr5L5x4oOGa4jxp2z+RANRJnS4xcXHxmhayZMkS/Pn169c1faumrzNfEWFjY9O7d2+BXxHe619BQaFdu3YMiwEAAK1Zm33wAADUJDo62tHRER5jmsLVq1dPnDjR0qWop5KSEltbWzw8HwDMQZUC2hI4nwFoCj179sQf4uPjm2F16urq8+fPr8cXxcXFW2oMUAAAaFxQlwHQ6rx69er27du/fv0yMDBYtWoV6cvm5uZGZkhYtWqVuLg4m80+ffo0+aKdnR19vOHAwMDbt28fPnw4Kyvr4MGDxcXF69evLywstLW1LS4uRggdOXJEVFS0Y8eO9vb2+CuZmZkXLlxIS0uTl5fX19dXUVEZOnSonJyckNKy2ez79+//+vXLyckpJibGw8MjKSlJX19/woQJAodYfvjwob+/f3Jysr6+/qhRo3i6wVZXVz9+/Dg1NXXVqlXBwcEnTpxQVVXdunWrvLw8RVG+vr5Pnz49dOhQXl6eu7t7cHBwp06dhg0bZmtry3z31lSA79+/P3jwgJ5z7NixFEWRSdxERETWrFlD3/DHjx9nZGQ4OzvjFHd392XLliGE0tPT8RjV5ubm1dXVERER5FssFmv58uVSUlIIoTdv3pChcBYuXCgvL898K9LS0ry8vBITE8vKygwNDSdOnGhoaMiT5+vXr7du3SooKFBRUTEzM5OWlsadaATKysqys7PDRb116xYumJOTk6SkJD1bVVWVl5fXmzdvuFxu//79ly1bxn9PnJGRcfPmzfDwcAkJid69e8+aNUtgLxse8fHxN2/eHDp0qJWVVWpq6pkzZ2JiYjQ1NVevXo2naPj9+/fVq1cDAwMlJCQmT54scI68lJSUV69ehYaGFhQUmJmZDRgwYODAgQJX5+fnd/v27czMTG1t7YULFwopmPAztrEw2WlBQUE+Pj6pqamSkpImJiYzZ87k6WFdWFh4/fp1DQ2N8ePH37x508fHZ8CAAatXr0YIFRUVPXjwIDMzc+PGjb9//z59+nRwcHC3bt0WLVqkp6fHs6K7d+++e/euqqqqa9euvXv3lpaWHjx4cE0l//z5s5AqBa/66dOnISEhycnJenp65ubmNjY2dX2UevHihY+PT1FRkaWlpZ2dHemNRVePI1VeXv7s2bMvX77s37+foqg7d+7cv3+/qqpqzJgx8+fPx+1Z/P39b926lZGRoa+vv3r1av7OxVwu9/nz58HBwVFRUerq6mZmZuPGjVNSUuLJlpKScunSJUdHx86dO7u5uYWFhdnb21tbW+O/stnsa9euhYaG/v7929zc3NbWVvjMJHfv3k1JScGfbW1t8fj0ly9f/vXrF07s27cvGTwLIZSWlnbr1q3u3bvb2trGxcWdOXMmLS3N2NjY2dmZp+YpLy+/cOFCZGSkhIREz549DQwM1NXV9fT0MjMzr1+/jvOoqqrOmTMHIRQfH//o0SPyXWdnZzIkhZDKHGeoX0Uh3M+fP93d3cPCwrp06TJ8+HAyuz3z3VXr+Sxwpbdv3+7atStZHUIoLy/v7t27oqKiCxcuLC0tPX/+vL+/f7t27ebNmzd06FC8f3x8fJ4/f15aWjpw4MAVK1bwjOYRHR3t6em5atUqFRUVXOuKiooOGjRo1qxZsrKy9JwCf+vJdS38/AwKCvLz8yOLkpCQWLlyJf787Nmz6Oho/Hnu3Lmkn3KtB66mU0jIPsTlfPjwoZ+fX1ZWlqqqqrm5+ezZswVWFC9evPjw4UNsbKyamtrQoUNtbW35B0KJiIh4+vRpWFiYrKysubm5vb09vpNp3NO4gWUuLS09e/YsPVvv3r3x6UG/u9PX1ycVhUAlJSXXrl0LCwv7/fu3trb2kCFDRo8ezZMnJCTk5s2b8+fP19HRefPmjZeXV0VFxZAhQxYtWsTf5Pz379+XL1/+8uWLhISElZXVuHHjhKydOTxpBkJIV1e3URYoxPLly+m3x02EyT1zdnb23bt3Q0JCysvLdXV17e3tu3btyrMcJr+SLVKjAgD+9VpqsEAAAB3+Ub927drChQulpaVNTU1xkEVDQ6OgoADnKSsrwzemCKGSkhKcmJubu3//fpyIpwjIycnZtm1br169EEIKCgrp6ekkAjhz5sxr164tWLAA//PIkSNubm7Xrl3Di/r06ZOSktKTJ08oiiosLHRwcEAIJSUl1VTm4ODg2bNn49uaGTNmnD59WlxcnMSGREVFjx07Rs+flJQ0atSo9evX37t37+zZs127dmWxWBs2bOD8P/buOi6q7P8f+Bk6RUUEFEVRJETaFgtbEYvVVdcu7NgVuzuxFRUQW9dmDexOFEEUJRSRRko65v7+ON89v/uZGYahxM98Xs+HDx8zhzP3nnvvmRvvOVFczHFcUFDQ5MmT6YP0gAEDbt26RSNihJAtW7bMnj2bPt4bGBg8fvxYX1+fH4QaPnx4Xl4eWxHrhSoyZ4L0AnAc9/HjR0dHR/pZHx8fmrh582ZCyIIFC9j0FI8ePWJPDv369WMr9fT0pMM/N27c2NPT09PT8/79+7m5uQsXLqTLHDx4ML+cxcXFdOEXL16UuaZwHMd5enqqqqp6e3tnZmampqb2799fXV39/Pnz/Dw7duwwNTUNCwvjOC4yMtLR0bFhw4ZSlnn//n1Wkdzd3Wn58/Pz6fbS9JCQkFatWtWpU6d58+Y0DuLi4iKynJ07dzo5OR0+fPj8+fO0pjVp0iQoKEjKqr29vW1tbekqTp486enpqaen17p1axrJ0tfXT0hIuHr1qqGhoY2NjbW1Nc3p7e3NX4hQKNy1a1ft2rV3794dFRV1584dOrf18OHDv3//zs9ZVFQ0b968evXqnTlzJiYm5saNGw4ODjSwIjJBR6kVhiu5skm0YMECImmCjlJ3WmFh4e+//25oaHjnzp3CwsKwsLCmTZs2atSIHl+O4y5dujRw4ED6lfH09Fy7di37dgQEBLi5uamoqBBCevXq9fTpUyMjIzMzM1qBtbS0goOD+YUZMmSIi4tLamqqUCi8c+dOnTp1Ro0aJWWjpJxSOI67fft2w4YNZ8yYERwcHBwcvHDhQoFA4ODg8O7du1J3F41fr1y5ctiwYfz7Fl1d3Tt37vBzynKkRHz79m3MmDEaGhqEEHNz86ioqA4dOpiYmDg6OtK6vXz58uzs7GHDhunq6rZv375WrVqEEDMzs9zcXJFVOzk5de3a9eHDh1FRUfv27atZs6a+vv6FCxdohqKioh07dnTo0IEu9uXLl7169aIbYmhoyPZS69att27deunSpVWrVmloaGhpaZ08eVLKzikuLt6zZw9djr+/P03MzMy8fv06PdYLFiygidevX2dxzw0bNnh5eeno6FhYWNAebSJblJqaampqSse6Kigo2LNnj0AgYN+1+Pj45s2bE0IcHR1ZMb58+TJ48GC6fHp+k3IyP3DgAP1gOU4UJaGT4SopKT18+JAeJmbEiBF062TfXdLrs4hjx4717NmTBoa2bNlCEx88eNC7d2+aOGXKlFu3bjVu3NjGxoY2a1JUVLx161Z4eLidnV3jxo3btGlDC8D/lq1du5adEu/du+fg4KCkpMQiMra2tnRGAunXerqoUuunUCg8fPgw/ZS1tXVqaiorRm5u7tSpUxUUFPgXl1IPnPQqVJL4+PgWLVp07tw5JCSkuLiYHpdu3bqxWx22Od26dRswYEBAQMDHjx8nTZpECLG3t09JSWF5srKypk+f3qJFi7Nnz4aHh9Nomq6u7suXL9m6KqUaV0qZQ0ND2U8XrITU9+/f69ev36VLF3YTKBGt9jNnzvz27VtBQcGqVasIIR4eHizDrl272EB4Dx8+nDx5cq1atZo2bUpTRo8eLbLAoKAgY2PjiRMnBgYGRkRELFq0qEWLFjSzLBN0sDmpX716xU8vLCxs27YtIaRGjRrR0dEin1qyZAn9lOy3Q+y0Jr5/8vLy6tSpI3vZRNja2u7fv7/UAshyz7x79+6GDRvu2bPn/fv3d+7c0dHRUVJS8vLy4ucp9Sr5k8+oACBPEOwD+CXQYJ+lpeXmzZvpjeb79+/pgyh/5jLW9Ix/N5menk4TabghIyPj48eP9HlSW1t77NixwcHBhw4dMjQ03LVrF8dxFy9epPmLior4ZbC2tu7fvz97m5+fz6JFEtEV0acLAwODkSNHRkVFFRcXv3jxgv1s++DBA5o5NTW1SZMme/fuZR9/8+YNffqlYbX4+PjXr1/TEZdbtmw5bty4z58/z507t169evfv34+OjqbN99TV1Z2dnZ88eSIUCiMjI9nd+apVq9iSJcZfSi0A9fXr15o1axJCpk+fTlN69eq1cOFC/obHxMQEBwfT8BAL9lG0lV+HDh34iUVFRXQvubq6iuzDpUuX9u3bt6Q9LNHr168JIQ0aNGApISEhNEUoFNKUxMREZWXlbdu2sTyfPn3if0QiVpFu3brFT2fBvvbt2/v7+9O1HDlyhCY+f/6c5dyxY0ejRo1ycnJYCj1qTZs2LSgokLLq2NhY+sTbrFmzrVu3FhYWchyXmppKn4I6d+7s7OzMYnbLly+ny2Tby3HcihUryH9OsVdcXDxkyBBCiJ2dHV0gNXLkSDU1tfDwcJYSFxdHDzo/2Cdjhal4sE+WnbZ9+3ZCyB9//MHy7Nq1ixAyYsQI+vbz58+sZUqvXr1Wr14dFhbWt29fCwuLzMzMgoKC7t27E0KMjIz69+8fExNDN5B+T4cOHcoWS2defv36NUvx8fEZNmyY9O0q6ZRy9+5dgUAwbdo0ke0lhGhpaYk/74mgwT5jY+OpU6d++vSJtqGgZ0V1dfXY2FiaTcYjJdH06dPpebJv376hoaE0cdOmTbSEHTt2PHnyJK1mSUlJ9Fnr2LFj7OMpKSl16tSxtbXlV++nT5/SQp47d47juOLi4vDwcDZpz+DBgy9evPjgwYPmzZvTiMzz589FQq50FykrK3/69ElK4TMyMugyWfSKonEiFr2iu6h27dr0+zV37tzs7GyO44KCguiXbt++fSznzJkzaTNqZvTo0fznXtq6mUVJKFYB6MVLysn8yZMnXAVOFBLRYJ+CgkLHjh1pM/MnT56wtkgzZ84s6+4qqT6LCw8PDwgIoGE4FuzjOC43N5cOFla/fv3JkyfTc1dxcTFt+mdlZWVvb//27Vua+e7du7T87BsRExPDfnqxtrY+ffp0fn5+SkrK6tWraWLXrl05Ga71stRPip4fmjRpIrKBq1evdnNzY29lOXClViGJaEvtw4cPsxR6cT948CBLycnJsbCwGD16NDvzZ2Rk0KgHu15zHDdmzBgrK6uMjAyWQmNMVlZWLKVSqnFllZkFW9+8ecMvz48fPzQ0NEQigCIKCwtpI83IyEiaIhQK6ZmKf41jE2LY2NgcOXKE/gqyb98+QohAIHj//j3L+fbtWw0NjfHjx/PX4uHhQT9epmDf5cuXv3z58vnz5xcvXvj6+tIf6iwsLOjeE1GRYN+TJ0/e/SswMPDSpUs9e/YkkpqzVG6wr9R7Zl9fXzU1NXZl4TiO/vSroKCQlJREU2S5Sv7kMyoAyBME+wB+CTTYR7uSMbQ9y6BBg1jK9evX6Z0KP9gnMdxAYwGEkBcvXoisS+KTTHZ2tkAgaN68OT/nxo0b2QNJSejTi0j8Ii4ujvYz6tKlC02ZP3++lpYWP+bCcRz9udjMzIyl0HvlunXr/vjxQ2RFW7ZsIYSoqamlpaWxxMLCQjppmo6ODru5l7hDZCwAx3He3t6EECUlpdDQ0L1799rZ2Um8WxoxYgSRLdjH/Xs4lJWVv337xk9v2rSpSIu8Up04cYL8Z7CPjbLHbh+vXr1KCJk6dSr/g8OHD5e+5FKDfU+fPmWJQqHQ0NCQELJz506akpSUpKWlJVKHz507Rz8rvZkSx3E0rrd8+XJ+Io3F6Onp8e9iWVcg9mz86dMnVVXVZs2a8cN/HMfFxcXRb9amTZtoCp1oeNy4cSJrp2FBfrBPxgpTwWCfjDuNNgbhB/suXLhACLG0tOR/kD719enTR3zVtGaampryt2jjxo2EEBMTE/5WE0LOnDnDUvLz88eOHSt9uySeUvLz82kwUaTOcxxHm9VILCcfDfYNGDCAn0jrNiFk4sSJrMwyfrXFHTp0iJ5V+K3bUlJS6Co2b97Mz0wbVvP3Bh0QSrzl19SpUwkhhoaGrMkJjcjzqyLTunXrnj178lNYASZPniyl8KzuiUSv2rRpQ/4zesX9G9ISOZS0byD/69CqVSsFBQXWipnjuOfPn+/YsYO9pXVYJErCLkz8lsslncwreKIQR4N9hBB+M5b8/HwaC1BRUaFtbWTfXbIH+6j69euT/wz2cf/Wlk6dOkksqkgEh579jhw5wlJevXol8cQyd+5cmn7//n2aIuVaL3v9vHPnDl0I/8cbjuOsrKwePnxIX8t44EqtQhLR9on8wBkdf4B/FZs+fbqamlpcXBz/g1OmTCGEzJgxg76le5i1W6ROnTpFCGnRogVLqXg1rsQy5+fn16tXj4hdso8ePcovs0SxsbH0h42IiAiWaGNjI3IaZxd3fh0rLi6m01D4+fmxxO7duysoKIg0T3v37h39eJmCfeLTegwcOJAf+eKrSLBPU1NT61/8Ph9SylbxYF+p98xRUVHa2tqzZs3iZ/jy5Yuampqqqir9sapMV8mfdkYFAHmCMfsAfiGsqwX/7bdv38qxKNpFS1tbu2XLlrLkV1dX19TUDA0NHTNmzN69e+kv/+PHjy91aC2agT9WICHE0NBwxIgRXl5eDx48KCwsVFJS8vHx0dTU/Ouvv/jZ8vPzCSEfP36kDRBYsVu3bi0+FRpdUc2aNWkjLJY4a9as4cOHZ2RkBAYGdunSRWIhOY6TsQCEkLFjx546dSogIGD48OGxsbH379+XOOmb7HPGEULGjRu3cuXKlJSUHTt20HZDhJAXL16kp6dLGUdPokGDBu3evZuGONnW0Repqam06yv9f//+/ebm5tOnT6f33LR3T0Xw66dAIGjUqFF8fDyrn2fOnMnKynr27Bl/ZMPU1FT64tGjRyKdMUXQ4ysy2A19itbU1FRXV2eJbOiohIQE+lv3nj178vPz7ezsRJ4uDA0N+/Xrd/Hixd27d9NDT58oxPe5yEBsZaowFSHjTlu+fLmZmdnQoUP5JeTnpGiddHZ2Fl8R3b20DxFLpLuXf4ahNcfd3b1u3bq0T7qKigprlVYmV69eDQsL09fXp2vhmzhx4uzZs69evfr582eRk544kekUe/fu7eDgEBgYeOHCBdoZqiJHiu4NJSUl1iuKEKKrq6uqqpqfny8yJCKteAkJCfRtWlqaj48PIcTe3l5ksZMmTdq7d298fPz58+fHjh1LeKcLkaMTGhr6/PlzBwcHfgUghGhoaOTk5LA4e8XRLRUZSVBiBRAKhX369Llw4QL9ctGe++VYY0kn8wqeKEqiqKhIYxyUioqKh4fHo0ePCgoKrl69Om3atHIsU0YSrwVSTmjk3y8aU7du3W/fvrGqRXizhYpcW+fMmbNjx47i4uLbt2/TkTRLutaXqX526dLF1tY2KCjIy8uLDbb78uVLoVDIQioyHrjyVaHjx48/f/58+PDhLEXkFPf169fdu3e3b9+e/sjE7NmzZ9KkSezQz58/X0FBQWSEu6FDhzZv3pwO1FhWUu5JKqvMKioqs2fPnj9/vo+Pz8qVK9m+OnHiBD06UtDxKAgh/CE+xa8OrDrxzwAKCgoGBgYRERHsDPDgwYObN2+2aNFC5LRcvjPAy5cv7e3ts7KyYmNjnz59euLEiQsXLly4cGHkyJE7d+4U6XFfEbGxsfyBR7OysrZs2bJy5crKWr5Epd4zr169+sePH/xxPAkhxsbGnz59UlJSolWiTFfJn3xGBQD5gGAfwK+L/kRZUFBQ7iWIj1pdEoFAMG/evJUrVx45cuTOnTvbtm0bMmSI+BjzsuvZs6eXl1dxcfHnz5+Li4uTk5NtbGxoGwqGvRUJKcpebLoi+iI8PLykYF9YWFiZCuDl5WVlZfX27duJEydaWlrKXpiSaGhozJgxY/ny5QcOHFiyZAkdNfnEiRMjRoygPelkp6qqyh5c4+Pj6UQl9G1RURF94ejo2KlTp/v378+cOfPkyZN79uyxs7OTPt5/OYjUzwcPHtBVsxn3KNqfTvxeVhYSn6LZHmNNGmmzKZHnZ6pdu3YXL178+vVrenq6jo7Os2fPCCH0EVSKslaYcpNxp9WrV4+16Hn79u3BgwffvHlDeEecT/avD90KoVDIUsaMGbNhw4bv37936dJl1KhRGzdu1NfXL1/NkX5Q6Ivg4OBSg33izUMGDx4cGBiYkpLy/fv3pKSkqjhSysrKNFzIRyseq3V0QCUiaRttbGxotI4/OQ8lcnRoBWjWrJnE8pfpF4VyEK8Ac+fOpePEW1hYLF68eN68eaqqquWLklDitbEqThREUj3p06ePurp6bm5uWFhY+ZZZ6Uo6oCJVSwojIyNLS8uQkJDw8HB+uvh+Lmv9nDZt2sSJE0+dOrVt2zZ6hfL29p44cSLLIOOBK18VcnR0pAPmchx348aN48ePv3jxgvBOcQEBAeQ/Q1qUgoKCnZ0dfR0REREVFdWwYUOReaUIIeITWJWJxJNqpZSZmjx58tq1azMyMvbu3bts2TJCSHJy8r1793x9fUstG22WTgjJyck5efLk9evXY2JiSAlXBxEiZ4AnT54QGa6PshMIBNra2ubm5ubm5mPHjt24ceOCBQuOHTuWkJAQEBAg/p2tFFpaWsuXL2edo6tIqffMJR39Bg0asNfluEr+tDMqAMgHBPsA4P8sX748LS1t586dMTExbm5uzs7Ohw4dEmlWIDt2Q5OTk0NnPFRSUuI3TaostWvXps8tOTk5JeWJi4srUwGMjY1nzpy5bt26EydOLFy4sNR4hCymTZu2cePGzMzMgwcPzps3r7i4+PTp06zfUFmFhYWtX78+Nzd3wYIFM2fO5Ld2pC5evOjq6vrgwYOnT586Ojq6u7tv3LhRZA7HykV3sq2trchP2T/Bhw8fCCGJiYnif2L1MCIiQldXNzc3lxBS6n4oa4UptzLttICAgC1btlhbWy9YsCAwMHDAgAGVXp46dercuXOnT58+sbGxR44cuXDhwvr162mnv7KS5aCIBCxkxKYy/P79+087UuLoBhJCEhMTxX8XadCgwcePH0vdQFr+evXq/fzyS9S1a9ejR49OmDAhJydn8eLFPj4+Bw8epNODVpafdqJQUFAwMjIKDw9nU+7KhwYNGoSEhEi53lFlrZ/Dhw//66+/0tPTjx8/7u7unpeXd+HChdDQUJZBxgNX7ipUVFTk7e3t6+s7YsSI/fv3L1my5NOnT+yvX79+JYSwvtgSyZKnclW8zFSNGjWmTJmyceNG2ghdXV397NmzPXv2ZM3YpUtLS9uyZcujR4/+/PPP06dPOzg4yBI4FkfrTNXdJ3h4eJw4cSI4OPjWrVvnz59nYy5XOjpBk3g6iwLzf+EQV1xcLMsPLVLumYuKiuj3RfrRr5SrZDXeegHAr090tnUA+J8lEAh27Nhx48YN+lPk7du3bWxsnj9/Xr6l0Y4GAoGgWbNmdPxy2sSvEgsssi42K4i4shYgLS3t5MmT9evXz87O5jdtqAhdXd0JEyYQQjw9PQsLC+/cuWNgYMDvdya7devWWVtbd+rU6cyZM+JdtKiaNWvevXvX09OzRo0aQqFwz549rVu3ZmOBVQW6k9mAej8T7WQkscM7+828cePGrJdcqV3jq7rGiqyo1J2WnZ09dOjQoUOHbty4ccuWLWz2xqpgbW0dGho6ZcoUBQWFzMzMadOmDR8+XPqjkUT0oCQnJ4s/7fAPSjlKqK+vTwhRU1Nr0qTJTztS4ljXNikVr9QNrMZvTUlGjBgRHBxMW0lHREQ4Ozvv3bu3Epf/MzeZVpUKtur61ZR6vaPKWj81NDToiGYHDhwghJw/f75bt24ivT6JbAeuHFUoIiLCwcHBy8vr8uXL06ZNE4830bVLD3zQPElJSWwylipVKWVmZs+eraqqmpycTCe/On78OBtgTrpr166ZmpomJibeunXLxcWFzdpcDvQSWb6hY2TEeoVX4hgFEs2cOVM8kVV4NoihRNnZ2SId8CWSfs9Mm+BJP/qVcpX8BS8iAPDrQLAPAAghhPa0JYT06NHj/fv3GzduVFZWzszMpHMClGOB9H7R2NhYQ0OjadOmhJDU1NT79++L5xQKhTdv3ix3yfPz82nJLSwsSspT1gK4u7uPGTOGztRx+/ZtOop/xc2dO1dRUfHbt2+nTp2SZTgeiQ4dOrR48eL+/fvTwdclSk9Pz8vLU1BQmDVrVnh4+KhRowghoaGhdCa4KkJ3Mp3OVdzz58+r7gGMzsbw4cMH8T7vtG7Uq1dPV1eXdaj5+PGj9AVWaY0VX1GpO23y5MlnzpxZvXq1SOevSkcf9nR0dPbt2/fmzRs6NOTJkydPnjxZ1kXRg0L+7anERw8KIYTOz1hWSUlJhJDmzZsrKir+tCMljm3g27dvxf9Kt7HUDaTlDwgIyM7OFv9rYmKixIVXHVoBTE1N79y5c+HCBQMDA6FQOGvWrC9fvlTWKn7miYJWlfJVs18WvbZKud5R5aifU6dOFQgEb9++ffnypUgfXiLzgStHFcrNze3bt29wcLCvr29Jw8PR4Oa7d+8+f/4s/tf4+Pji4mIWAGVTk4uspRLbeFZWmdlbAwMDOqnL1q1bo6KiIiMj2YzSUoSGhg4cOFBZWXnfvn0V7/VPL5ERERHl+HVHRrRxPSmtbV1J1qxZw5qsyqKoqGjGjBnsLetOTs8MEhUWFsbFxZU6eIX0e2YlJSW6hMuXL0v8OO1qXSlXyWq89QKAXx+CfQD/TdgQJ4WFhSyx1O48JeEP6ZKVlbVu3Tr6WkVFZf78+XRGwvDwcCl3RVLQBwwaZjIyMqI/k65atUo8dLhmzRr+5pRVSEgIx3EdO3ZknfvElakAJ06cePfu3YIFC3r06EF/Wv/zzz9jY2NlL1JJY+UYGxvTkZI3btzo7+/PH9hbdjt37iSE8JsEiq/u/v37N27coK/r1q175MgR2qVFxt/SZRnrRxx9+Hz48OHt27dF/pSYmLhu3Tr+ENqVq3v37oSQ9PR0NlUrExUVRQihPV7r1q1LG6qUFL1ldaNKayyfLDstNTX1+PHjpLSDXik2b97M+n9ZW1vfu3ePTgJQjprTuXNnOiAULTwfPShWVlblGw2QtpWgsfKfdqTENWnShLa5EN/AwsLCmJgYdXX1Hj16SF8IrQC5ubls3h6muLjY3d1dZGh/PomXA1KBKwIhhP97wIABA54/f16nTp2ioiLWXIWutCJr/Gknivz8/K9fvxobG3ft2pWUa3dV0besIgoKCsLCwmrWrOni4iI9Zznqp6mpKT2XLl68ODY2lk7Rw8h44EqtQuKuXbtGZy2gM5BSIjufTiddWFjIpm1lsrOz58yZIxAIDAwMDAwMCCErVqxgQSVmzpw5LKRe8WpcWWXmJ/7111+KiooRERF//PHHiBEjZAneHThwID8/39LSkp+53PWWjlCclJRUUohKlp9+peehwwISQviTjMm45C9fvuzdu5dOgizjR/bt2xcZGcne2tvb0wZ3UkZQuX37toGBQam/q5V6z0yXcOTIEfHo5JEjR54+fUoq6SpZjbdeAPDrQ7APoPpxHEd/4BW59aTNlPj3bSyexX/29vPzo7eMWVlZ/GUSQiR2batduzZ9QUf6YKu4dOkSf119+vSpX7++lpaWLBOxiTRL4TjOy8urbt26f/75JyFEIBDMnj2bEHL//n06lA/bwJUrV967d4/9gi2l2FReXp7ID8K0zxH/UZl9nOWUvQDfvn2bMWPGoUOH6Ijp27Zt09fXz8jIcHd3FykJPToih4zuW5Edy7ds2TJFRcXQ0NCOHTuK7Nhdu3Z16tRp48aN0m9habMOOj8DxSoDv13buXPn+J+aNGkSKa0/SI0aNeh9p0j52TZKr5/jxo2j95TDhg3j189379517tyZ7n8pJP7OTxNF1stysh01atQo+uRAG2PyHTt2rE6dOmvWrKFv6TQXb9++pdWGCQoKIoSwH8BlrzDilU0KmpmfU5adxjpViR90kZaMUr4+EhPpx/n1TSgUXrhwgb1VU1P7448/SGk1R+IpxdTUlE4OePLkSZEH72PHjhFC9u7dW44eZ3SQrJYtW9KvpOxHSqKSjpqUisd2l4KCwq5duwghb9++ff36NT/nhQsXcnJyFi1axM7Y7FMiB8LR0dHJyYkQsnbt2n379rFsKSkpgwYNsrW1lTJol4qKCg1t8GvOy5cvacNV/uVAfL2UeAV4+/Yt/9m4YcOGdAYkVgHoFn369Ik1PCksLDx9+jR9Lcs1SMYTRURExG+//ebq6lru6TX8/Pzy8vJ27dpFJ8qUfXeVdIksicRrgZQTWkmZJZ75Ra6tZ8+eTU1NXbhwIStkSfu5TPWTobM/3bx5kw46wSfjgSu1Comjp7iioiLWvqmgoIBOdsFOcebm5m5ubuTfOWrZ8YqOju7Ro8fEiRPpyWTp0qWEkMjIyI4dO7K2hD9+/Jg8eXLdunXZ1BMVr8aVWGbGzMyM/gT45MkTGfvw0mK8f/+eTSgUGxtLGxLyrw4l3VOJnAH++OMPeluydOlSfitIdumRpY0YOwmLfwXWr19Pv2729vZsXhGRT5V0/xMWFjZw4MD27duzGSpK/cj9+/eXLFnCj2g3adKE3gudO3fu7t274h/58eOHh4fH5MmTZZk8RPo986JFixQUFIqKijp16sTWJRQK9+/ff/ToUVoxynSVrOAZlRDi6ek5bty46OjoUjcNAOQHBwDVjT3Mr1y5kp9Oh2zX1dUtLi6mKcXFxfRnZF1d3a1bt+7atatbt25Lliyh823Z2tru2rWL5qS3vISQuLg4kdUlJSXRH4FHjRr17NmzqVOnvnjxgo5gsnr1apYtOTlZXV19wYIF0gtP20ypqqqePXuWJU6dOrVGjRo3btxgKRkZGawDRe3atfv27duvXz99ff3mzZt///6dZaPPvebm5kKhUGRFnp6e9OMeHh4FBQU08cSJE8rKyuvWrePnZDffhw4dKlMBCgsLO3XqNHbsWP7SfHx86Kc8PT356XS6TAsLC34i/WmXZr53796wYcMyMzNFNoQ27rty5Qo/MSEhgd3SnTx5Unw/M7169aLZaFByxIgREydOVFNTI4SMGDFi586d7969u3jxooKCwr1799in/vnnH4FAcPXqVSlL5jiOtkSws7N78uSJp6fn7t27OY6jN52EkPv377OcxcXF9erVI4QMGDCAJdJnS8rBwWHw4MFt2rRRUFDYv3+/9PXm5eWpq6sTQpYuXcpPp/epKioq/N1If/EmhOzZs4clvnv3jj7Ibd++nSWuW7euZs2a165dYym5ubn0GyQQCGbMmBEQEHDnzp0hQ4bQH88FAsHQoUPPnz/PyVxjJVa2ktDxyBs2bMiv3qXutNzcXDoBi5aW1qZNm3bt2tW9e/fx48fTj6xfv37x4sUcx2VnZ9PRxydPniy+6t9++40QUrdu3aKiIpZIp30khKSkpLAdXq9evYSEBJbnr7/+0tXVjYmJkbJdEk8pHMdlZWXRxkEDBw5kJ7Hbt29raWmtXbu21N1FP9utW7fc3FyaUlBQMG7cuObNm3/8+JFlk/FISbRq1SpCiKKiYn5+PktkwzmJnP1oO2U9PT1+4rx58wghTZs2TUpKoilfv35t2rSpq6trXl4ey8baXIh/u58/f85G+zIxMRk4cKCzs7OGhgYdKlF6+adMmULLP2/evEOHDo0dO7Z3796DBg2i14iNGzfS879QKKRxrkGDBvE/TpttWllZsRRbW9tevXqxE6xQKGzVqlX79u1Ztfn8+TM921hbWx88eHD9+vU2Njbs5Dxo0KBLly7RnFJO5rKcKNic4+bm5tJ3AnuQvnnzJkt8+vSpoaGhyCVVxt1VUn2WKDMzk/4y5O7uzk+nzQmdnJz4iewCcf36dX46jT25ubmxFPrzAyGke/furBqHhITo6ekNGDCAfSM4qdd6Tub6yRQXFxsbG6uoqLD8fLIcuFKrkDjayokQYmpq6uXltXbt2g4dOgwcOJAel0OHDu3cuZPjuKioKNbQVUlJyc7OztHRUUlJacOGDWxRhYWF7du3Z4Vs2rRp+/bta9So0bdvX34BKl6NK7HMfDS07eDgUNK+ErFhwwa68J49e/r4+MydO7dnz550JF8HBwdfX196OXv//j3NRktFZWVl0cvu9OnTWaKvry+Nc1lYWHh5eb148eLAgQNsrnBTU9MZM2ZkZWWVVJ6ioiLaq5QQsmjRoqysrIyMjMjIyMuXL9PwFiGkWbNmERER/E8JhUI2+eyECRMiIyOTkpJSUlKSkpKioqLu3Lkzf/58+i3z8vKiH/n+/Ts7bZ44cSI+Pj47O7uwsDA9PT06OvrWrVvTpk2jF0Q6LTWTkpJCrxeqqqpeXl5fv36l6dnZ2SdPnjQxMXFxcWFXKylkuWf28PBgVbFu3bodO3asV6+esbFxfHw8/yjIeJWs4BmV3atMnDix1K0DALmBYB9ANfP19WX9BTQ1NWfNmhUfHx8cHDx69GgW/enateujR49o/qCgIHYvVbt27SNHjnAcZ2RkZGVltWLFitDQ0NDQ0DFjxtAbWUKImZnZkiVLvnz5wl/p4sWL6V9r1qxJg3Tp6emGhoZNmzYdMGDAnj17PD09bWxsPDw8CgsLpZefBvu6devm6uraokWL3377rVGjRvb29h8+fBDJmZaW5ubmxjqbqKiozJ49mz3GXLlypXfv3ux+xdnZee3atfy7c3ojXqdOnSVLljRs2NDNzc3e3r5+/foXLlxgeVJTUxcvXsye/PX09P7666/Q0FBZCnDr1i16R9upU6enT5/SxIiICFdXV5pfIBAMGTLk69evPj4+/FnkXF1djx8/TvPTX3HZnhe5y6QOHjyor68vsmOLi4vZyCxTpkyRssM/fvzIKoCRkRFdNZvKc8GCBUKh8OLFi6ampsbGxlOnTj106NCKFSvMzMyOHTsm/VByHOfv70/vj5WUlOiiFi1aRAe5J4QYGxvTh+dLly45OzvTRAUFhdGjR8fGxtIlXLhwgYaeqRYtWvBjbRIdOXKkW7duNH+tWrUWLFgQHh7+6dOnWbNmsbv5du3a0dtWWjNpor6+/tKlSxMTE+ly0tPTR40aVaNGjVatWs2cOdPJycnZ2Vmk5nMc9+PHj9GjR9MnB3oL7ufnN2XKlDp16kydOvXx48fsTlp6hZFe2URcunRp2LBh7Bvdp0+fvXv3sr+WutMuXrzIxgtv1arVy5cvc3NzaYBAXV393Llz27ZtY7tFSUnpjz/+OHr0KP1sTEzM1KlT2Va4urpeu3YtOzv7zz//pC2eCCFt27Y9d+4cx3Fz5syxsbFp2rTp0qVLDx8+7O7ubmdn9/LlS+lHkJN0SqGKi4vXrVtnYGBgYmIyderU/v37W1tb3717t9QFchwXHR1ND6impmanTp0GDhzYtGnTmTNn8iMdlPQjJVFUVJSHhwfr39SjR4/Dhw9zHOfl5cUeOzU0NObMmfPhw4d3795NnDiR1Zn+/ftfvHiRLery5ctmZmZ16tT5448/Ro0aZW5uzn504Tjux48fc+bMobE2Qkjt2rVnz5794MEDfmFCQ0MdHBxYWxI9Pb1du3aVeu7lOC41NZW1W1FSUvLw8MjLyxs5cqShoeG0adPu3btXXFz86NEjdhJTVlaeOnXq+/fvw8LCRowYwarc0KFDX79+zXFc+/btW7Ro0apVq02bNnl5efXu3XvAgAH84C/HcSdPnmS1sXnz5s+ePYuIiFBUVOzSpcuePXsSEhJKPZlzMtR5+kMO/atIAcT5+Pi0aNFCIBA0adJkyJAhbdq0sbOze/jwYTl2F81ZUn3mKygoWL16datWrWhOVVXVyZMnP378+N69e+PHj6dHUyAQTJw48fbt27m5ucuXL2ct6Ro2bLhixYqsrCx/f382h6aqqqq7u3tgYCDHC/atXr26QYMGLi4uzs7OtWvXXr58OTtByXKt50qrn+LWrl1Lxx2TqNQDJ0sVErdkyRK6xxQUFIYOHRoXF/f27Vv6jWvUqNH79+9ptvj4+F69etEW6HQ30hOXyHHhXzs0NTXXrVvHgo9MxatxJZaZr379+vRnNlnk5uayKq2pqbl06dK8vLx9+/bRlO7du2dkZBw9epT+jEcIMTAwWLx48Y8fP65du8bmylBXV589ezb7yefGjRt0SllCiKKiYp8+fYKDgwUCgZOT0/79+1k2EWFhYRMnTixp3pjatWu3bNny999/9/HxETmBz58/X/ZRNb98+ZKUlDR27Fgp4xvwGRoaihc1Jydn2bJl7Iujp6fXsGFDZWVlExOTnTt3yhLp42S+Z/b29mats+nNkngYvdSrZKWcUdPS0uj1jh/wBQC5J+DKNfQ+AFSjoqKiDx8+FBcXW1lZ0TvIr1+/sv4pMqIDi9ja2tJ7Yu7f1lUZGRlhYWFCodDKykqW+cgGDhx48eLFYcOGnTx58uvXr58/f65fvz6LRonLz88PCQlRUVExMzOjcSUZ7dixY/bs2QYGBvHx8d+/f//w4YOWllaLFi1Ynw4ZlbsAMioqKgoODuY4zs7OTmIvxYkTJ9auXXvjxo3if4qOjt66dau+vj571JSooKAgODiYEGJnZ0c3n46IZGJiQu+A8/LyVFRUFBQU4uLiIiMja9SoITKmjxSJiYkfP340MzNjMb5yoD2JmjRpIuMdeSUSCoW0c5apqSmLsIgrKCgICQnR0dGhdTU6Orp+/frseYyvqisMI32nZWRkvHv3rk6dOmZmZjQlPT09NDTU2tpalu+pjLKzszU1NYVCYWRkZGxsbIMGDUxMTGTp0ETETikiYmNjIyIi6tWrZ2pqWqYiFRUVffr0KSkpSVtb29LSkjZFkeinHSmJMjIyQkJCtLW1mzdvLrEilYoeUH19fRMTkzJ1cP78+XNCQoK1tTXd89++fatXr175JuWkFSA/Pz8sLCw9Pd3c3FzieSA7O5vWRtokNi8vLzMzU0qP45JIr/OZmZlxcXEtW7ZMS0uTZZcmJSV9+fKFxsFZtEKcjLtLen2uam/fvqUBmrCwsAYNGrx//56266dT8ZaD7PUzMzMzKyuLNtwuiZQDJ2MVEhcXFxcREWFqasqWGR0dTQ+ByMUrJycnODhYX19fStfg4uLid+/eKSgoWFhYlLS9Fa/GlVtmQsi3b9/MzMxiYmJYN21ZhIWFpaSk2NrasuoRFBSkpqZW6pTNUsTFxUVHR9OvSWFhYUJCQoMGDcq9tF9QcXFxdHR0REREYmKirq5u48aNS530hq9M98yfP39OSkqysrKSfjIp91WSvwQpZ9S0tLS4uDj+KJMAIPcQ7AOACuEH+6p0RfxgX5WuqEplZGQ0atQoKChI4nQiQqGwS5cux44dk7O7agCAcvD397927dqePXuquyA/Gz/Yx+L7IPeWLl0aHx9f0hRSAAAAZVKeH58BAKB8tm3b1rVrV4mRvqKiokmTJo0dOxaRPgCAFy9ebNu2jT9jDIAcS0lJ2bdv3507d6q7IAAAICcQ7AOACqETrkmfrPC/a0WVbtGiRWFhYa1ateI4bvPmzXTCPnFeXl5DhgyRPnkoAMD/goyMjCNHjpw/f56Nq/g/hc1k+t94yQPZBQUFLVu2rGHDhvb29l5eXj169JB9ADsAAADpEOwDgAqJiYkhhLAJUqvO169fCSFpaWlpaWm1atWq6tVVlsTExPXr1xNCaPuUVatWWVlZScw5derUn1oyAIBflY6Ozv9g712GXu8IIVFRURhjS47t37//ypUr9LW+vj57DQAAUHHlGb8ZAIAQcvHixV69er1584YQ8vr16549ey5atCg7O7vSVxQSEjJ69Oi9e/cSQoqLi52cnP7888/Q0NBKX1FV0NfXX7hwob6+vpGR0ZYtW5YuXVrdJQIAgF/Ujx8/Zs2aNWnSJPp2/Pjx06ZN8/f3r95SQRWZMmVKx44dNTQ0unbt+uzZMz09veouEQAAyA9M0AEAAAAAAAAAACAn0LIPAAAAAAAAAABATiDYBwAAAAAAAAAAICcQ7AMAAAAAAAAAAJATCPYBAAAAAAAAAADICQT7AAAAAAAAAAAA5ASCfQAAAAAAAAAAAHICwT4AAAAAAAAAAAA5gWAfAAAAAAAAAACAnECwDwAAAAAAAAAAQE4g2AcAAAAAAAAAACAnEOwDAAAAAAAAAACQEwj2AQAAAAAAAAAAyAkE+wAAAAAAAAAAAOQEgn0AAAAAAAAAAAByAsE+AAAAAAAAAAAAOYFgHwAAAAAAAAAAgJxAsA8AAAAAAAAAAEBOINgHAAAAAAAAAAAgJxDsAwAAAAAAAAAAkBMI9gEAAAAAAAAAAMgJBPsAAAAAAAAAAADkBIJ9AAAAAAAAAAAAcgLBPgAAAAAAAAAAADmBYB8AAAAAAAAAAICcQLAPAAAAAAAAAABATiDYBwAAAAAAAAAAICcQ7AMAAAAAAAAAAJATCPYBAAAAAAAAAADICQT7AAAAAAAAAAAA5ASCfQAAAAAAAAAAAHICwT4AAAAAAAAAAAA5gWAfAAAAAAAAAACAnECwDwAAAAAAAAAAQE4g2AcAAAAAAAAAACAnEOwDAAAAAAAAAACQEwj2AQAAAAAAAAAAyAkE+wAAAAAAAAAAAOQEgn0AAAAAAAAAAAByAsE+AAAAAAAAAAAAOYFgHwAAAAAAAAAAgJxAsA8AAAAAAAAAAEBOINgHAAAAAAAAAAAgJxDsAwAAAAAAAAAAkBMI9gEAAAAAAAAAAMgJBPsAAAAAAAAAAADkBIJ9AAAAAAAAAAAAcgLBPgAAAAAAAAAAADmBYB8AAAAAAAAAAICcQLAPAAAAAAAAAABATiDYBwAAAAAAAAAAICcQ7AMAAAAAAAAAAJATCPYBAAAAAAAAAADICQT7AAAAAAAAAAAA5ASCfQAAAAAAAAAAAHICwT4AAAAAAAAAAAA5oVTdBQCAUuTk5Dx48ODNmzchISHNmzdfvHjxT1hpSEhIQECAlAyKiopaWlo6OjqmpqZmZmbq6uo/oVQVce7cuS9fvvBTbGxsunXrVk3FAQAAAAAAAKgSCPYB/KKKi4svXrx44MCBBw8eCIXCbt26DRkyZMCAAT9n7crKypmZmf7+/q9fvxb5k76+vr29/ffv3z99+pSenk4IUVVV7dq1q7u7u4uLy88pXjnk5+fv3bs3KiqKpUyZMgXBPgAAAAAAAJAzAo7jqrsMACDK399/1qxZNDI1cODA/fv3161b9+cXg+O4li1bBgYGshSBQBAbG2toaEgIKSwsXLNmzerVq9lppEePHqdOnapVq9bPL6os7t+/37lzZ/Z2ypQp+/btE8/27t27Hj16xMTEKCoq/rzCAQAAAAAAAFQGjNkH8GvJy8sbPny4i4tLVFSUpqamt7f3+fPnqyXSRwgRCATdu3fnp9SuXZtG+gghysrKK1euHDhwIPtrQECAm5ubUCj8qaWUWdu2bUvNk5OTM3To0Pj4+J9QHgAAAAAAAIBKh2AfwC8kJSXF2dn55MmT9O2hQ4fGjh1bvUWqXbs2/61AIBDJMHXqVP7b27dv37x5s8qLVS6ytNQbP378+/fvf0JhAAAAAAAAAKoCgn0Av4qMjIz27ds/efKEvp00adKwYcOqt0iEEAWFUs4SjRs3Fkm5fv16lRWnai1btuzUqVPVXQoAAAAAAACA8sMEHQC/iqlTp3769Im+1tfX9/T0rNbiyMrAwEAkJTU1tVpKUkFLly5ds2ZNdZcCAAAAAAAAoEIQ7AP4JZw8efLEiRPs7YgRI9TV1auxPLLLzc0VSTE3N5eYMz09/eLFi0+fPk1LS7OwsLC3t+/Zs6eampp4zoCAgMePH4eFhWlra7dt29bZ2VlRUfHQoUMrV65MS0t78OCBSH4XFxcFBYWIiIjQ0FCRP3Xs2FGWCUPmzJkjEl29dOkS7fbbo0cPdiySkpLOnj0bEhKSlpbWtGnTbt26tWnT5tChQ05OTra2tqWuBQAAAAAAAKCqIdgH8EtYtmwZ/+3o0aOrqyRlxVojMu3btxfP5ufnN2fOnNTU1IEDB/bt2/fChQurVq2ysbE5c+ZMs2bNWLakpKQxY8Zcu3bN0tJy9OjRP3782Lp164QJEwghjo6OK1euzM7OPnDgwLVr1/gLz8vLU1VVTUhI8PPzu3TpUnFxMfvTs2fPWrduLX0TLl26JN6OcvDgwfTFly9fjI2N6SbMmjUrKytr7Nixbdu2ff36dY8ePYqKiggh/v7+CPYBAAAAAADArwBj9gFUv4cPH0ZERLC3NWrU2LRpU4MGDbS1te3s7MaMGXPnzp1qLJ50e/bs4b8dMWJEx44dRfLs3bt39OjRqampLVq0OHPmzPjx4y9cuNC0adO3b986ODh8+fKF5Rw9evS1a9f09PSePn06f/781atXBwUFTZw4kWUwMjK6evVqt27dxEvSoUOHc+fOzZgxo6yb4OzsHBER4ebmxk8MCwuLiIiIiIioX78+IeTx48fjxo1LT09ftWqVl5fX7Nmz/fz8nj17JjKBCQAAAAAAAED1QrAPoPodOXKE/1ZLS0tTU9PKyio/Pz8oKOjIkSPdunWbMWNGQUFBdZVQosLCwk2bNh0/fpylDB06VCT2RwhJTEz08PCgr11cXJSUlAghysrKffv2JYRkZWUtXbqU/vXmzZt0co8mTZrUqFGDJqqoqOzdu1ekdZ74rCCMhYVFWTdES0urSZMmOjo6/MSmTZs2adKkSZMmtMDz58+nDQbt7e1ZHgcHB5FjBwAAAAAAAFC9EOwDqH5v3rzhv928eTPtqbpv3z6awnHc7t27N27cWB2l+w85OTlr1qzx8PAYPnx4vXr1aBRPSUmpT58+169fP3XqlEjIjBCyYsWKrKws+trGxoalN2zYkL44ceJEeHg4IeTFixc0JTAwkM1KTJe/ePFi/jLpaHoSSflTuRUVFb1+/Zq+3rt3L7+bcL9+/fjhPwAAAAAAAIDqhWAfQPWLiYnhv23SpAl9MX78eH50bP369dHR0T+1ZGKKi4s/ffq0adOmkydPpqSk0MT27dv7+/v37NlTYn4fHx/2VldXl71mYUGhUEj7Kaenp9OUwsJCZ2fnTZs25eTk0JQ+ffqcOnWq8rdHNnl5efn5+fT15cuXO3XqFBgYyP7q7+8v3nMZAAAAAAAAoFog2AdQzfLy8pKTk/kp/GHgOnXqxF7n5uZeuXLl55VMEm1tbT8/v0mTJvET79+/v3XrVon5o6OjWZiMEPLgwYMj/2Lt+Agh7969I//ZQzYvL8/Dw6NRo0abN2/OyclRVFRkMdCfT0tLy9TUlL19/Pixo6Njnz59Xr58SQgxNDTU1taurrIBAAAAAAAA8CHYB1DNhEKhSIqqqip7zY8xEUI+fPjwM8pUms2bNzdo0ICfsnjx4rdv34rnFJmrd+/evSv+FRAQ0OhfdDhCNzc3kTltk5OT58+fb2Ji8s8//1T+ZpTFunXrRFKuXbvWqlWr33//nXVSBgAAAAAAAKh2CPYBVDMNDQ1+dI8QwnEce12nTh3+n+jYdtWuRo0aBw8e5KcUFBQMHz48Ly9PJGdcXBz/7erVqz9LcuDAAUKIkpKSv79/y5YtRRaSmJjYv39/fnfgn2/w4MF79uyhk3XwnTp1ql27doj3AQAAAAAAwC8CwT6A6ifSfI/f71VDQ4P/J01NzZ9UptL07Nlz3Lhx/JT379/Pnz9fJJuxsTH/bXx8vPTF1q9f/8GDB+vWreP3ZSaECIXC2bNnJyUlVaDIFTV16tSnT592795dJD0kJGTNmjXVUiQAAAAAAAAAEQj2AVS/du3a8d9mZmay18rKyvw/mZiY/KQyyWDbtm3169fnp+zevfv69ev8lGbNmvHfBgcHS1lgXl5ecXGxmprawoULP3/+vGrVqlq1arG/ZmZm3r9/vzIKXmZCoTA3N5cQ4ujoGBAQ8ODBg65du/IzVPtYigAAAAAAAAAUgn0A1W/o0KH8twkJCex1cXEx/0/izcqqkY6OjpeXFz+F47ixY8eyWXoJIUZGRvzJK65du5aWlia+KNqY8Y8//ti+fTtNqVGjxtKlSyMiIvr378+yxcbGSiwJv+NzVfjw4UP9+vXpwIKEECcnp9u3b587d45tWkkFAwAAAAAAAPjJEOwDqH5dunSxsbFhb/kD8+Xk5LDX7dq169mz508tGSEiw/DxuxgTQvr06TNq1Ch+SkJCwoQJE9hbgUAwa9Ys/sfFe7wuW7Zs4sSJ9PWxY8f4f6pdu/bZs2cbNWpE35qbm9MX9erV42fjD5lXVFTE/5P4/Ccyohuem5u7fv16QkhaWtrVq1f5GQYNGrRlyxaRggEAAAAAAABULwT7AKqfQCA4dOgQm/zhxYsX7E+fP3+mLxQVFffu3SsQCH5y2URm2MjKyuL3MiaEeHp6Ghoa8lMuXbrEn75j/vz5enp67O22bdvmz59Pt+vz58+jRo3y9fXdunUr/evbt28PHTrEX5qKioqRkREhRFNT09HRkSZaWlry81y7do2+ePjw4bJly/h/+vr1K3vNbzJJCBEZAVBkK3x9fZOSkiZNmsS218PDIzs7m5+nSZMm9EWnTp0IAAAAAAAAwC8AwT6AX4Kjo+Pp06fpCH3+/v4sLHXz5k1CiKam5qFDh/it/34CoVAYEBBw8uRJfiLHcTNmzEhMTGQptWrV2r9/v8hn58yZExgYSF9ra2sfPHhQRUWF/XXz5s0mJiYaGhomJia3b9/29/fnRwNnzZrFj/fdu3fv2bNnhJD169ezuYn79+9vYWHB8owbN87JycnMzKxHjx5t2rThl2T27NmLFy+mowHu27eP/6e7d+9++/aNve3Rowc/ljp9+nR9ff03b94sXryYpnz69MnV1ZVFD/Py8jw9PQkhDRs2XLJkiYQ9CAAAAAAAAPDTIdgH8KsYNGhQYGCgs7NzdnZ2hw4dPDw8BgwYcPv27TZt2rx+/XrMmDE/szBnz57V1NTs2bOn+BB7fn5+BgYGTk5OLKV///4jRozg58nOzm7duvWOHTvoW1dX1zt37ogEK/Py8vr06fPixQtra2uWaGVl5ebmtnjxYiMjo969e3fo0KFr165qamq7du2aPn06y6asrHzmzBn2waKiokePHtWsWfPJkye//fYbIURXV7djx44jR44cO3askZGRgoLC0KFD165dyy9AWlpao0aN3r59S9926NBhw4YN/KBkp06dbt26paWlRd9OmDAhNzfXwsKiZcuWLi4uJiYm/v7+Xbt2vXPnDn9cQgAAAAAAAIBqJKjqge0BoKxiY2NfvXqVlJRUs2ZNGxsbkQlt/6sFBwcHBgamp6cbGho6ODiYmpry/3r9+nVzc/NGjRpxHPf69euwsLC0tDQzMzMHB4fatWuLL62wsPDp06fv37/nOM7BwaFVq1aEkKioqMzMTBsbm/J1eY6Li3v9+nVOTo6ZmRmLTqampt66dYuGEb9///78+fMvX77o6Oi0aNGCH6kEAAAAAAAAqHYI9gEAAAAAAAAAAMgJdOMFAAAAAAAAAACQEwj2AQAAAAAAAAAAyAkE+wAAAAAAAAAAAOQEgn0AAAAAAAAAAAByAsE+AAAAAAAAAAAAOYFgHwAAAAAAAAAAgJxAsA8AAAAAAAAAAEBOINgHAAAAAAAAAAAgJxDsAwAAAAAAAAAAkBMI9gEAAAAAAAAAAMgJBPsAAAAAAAAAAADkBIJ9AAAAAAAAAAAAcgLBPgAAAAAAAAAAADmBYB8AAAAAAAAAAICcQLAPAAAAAAAAAABATiDYBwAAAAAAAAAAICcQ7AMAAAAAAAAAAJATCPYBAAAAAAAAAADICQT7AAAAAAAAAAAA5ASCfQAAAAAAAAAAAHICwT4AAAAAAAAAAAA5gWAfAAAAAAAAAACAnECwDwAAAAAAAAAAQE4g2AcAAAAAAAAAACAnEOwDAAAAAAAAAACQEwj2AQAAAAAAAAAAyAkE+wAAAAAAAAAAAOQEgn0AAAAAAAAAAAByAsE+AAAAAAAAAAAAOYFgHwAAAAAAAAAAgJxAsA8AAAAAAAAAAEBOINgHAAAAAAAAAAAgJxDsAwAAAAAAAAAAkBMI9gEAAAAAAAAAAMgJBPsAAAAAAAAAAADkBIJ9AAAAAAAAAAAAcgLBPgAAAAAAAAAAADmBYB8AAAAAAAAAAICcQLAPAAAAAAAAAABATiDYBwAAAAAAAAAAICcQ7AMAAAAAAAAAAJATCPYBAAAAAAAAAADICQT7AAAAAAAAAAAA5ASCfQAAAAAAAAAAAHICwT4AAAAAAAAAAAA5gWAfAAAAAAAAAACAnECwD+C/AMdxZf1IQUFBVZRE7qWmplZ3EaB0QqEwPT29uksBAAAAAADwK1Kq7gIAgGSZmZm3b9++efPmmzdvCgoKAgMDZfxgSEjIrl27jh07lpOTU6UlfPny5apVq0QSGzRosHfvXimfunnz5s6dO0USbW1tV69eXYlly8vLc3Nz46esWbPGxsZGykcyMjJ69er17Nkzc3PzGzduNGzYsBLLI11WVtbvv/9epo8MGDBg/PjxVVSeCpozZ05ERAR7O27cuIEDB1bi8l+/fu3i4hIXF+fi4nL+/HklJVzIAAAAAAAA/j9BOVoMAUCV+vbt27p16/z8/IqLi+fMmTNs2DBra+tSPyUUCv39/Xfs2HHnzh2a8hO+3QkJCX5+fh4eHvzEwMBAe3v7kj7SpUuXe/fusbeWlpa+vr7W1taqqqqVW7avX7/27dv33bt39O2tW7ecnZ2l5N+wYcPChQvpa3d3d+khy8qVkpKip6dHCFFQUBg2bJizs3PTpk07derEMkyZMqVHjx7v37/ft29fbGwsIWTOnDnbtm37aSUsk2/fvrm5uT179oy+3bJly7x588q9tDNnzvTr109DQ4Ol9OrV68aNG/T16dOnf/vtt4qUFgAAAAAAQM6gGy/Ar2X37t0WFhb79u1r1qxZYGDgunXrSo30ZWZmenp6mpqaurq6skjfz2FgYDB//vx69erxYzG7d+8uKf+7d+9iYmIUFRVZyowZM1q2bFnpkT5CSMOGDTt06CB7/uLiYva6sLCw0ssjRVFRESFES0vr5cuXx48fHzduXMeOHfkZjIyMBg4cuHjx4oiIiKFDh4qU9ldjZGREC1lxly9fHjp06I8fP/iJ1XikAAAAAAAAfn0I9gH8KoqLi6dOnTpjxoysrKwWLVo8efLE0tJSlg8+ffq0bt26z58/X7FiRRWXUbIaNWqMGDGCvT158uT3798l5ty1a9eUKVMEAgFLqVu3btUVrEwdPCdOnEh3uLGx8fz586usUBLQ6NXWrVultIik1NTUvLy8jIyMfuVgHyFEW1u74guJiIgYM2aMePqyZctq1apFCOnSpcuQIUMqviIAAAAAAAB5gmAfwK9i5MiR+/btI4RoamqeOXNGTU1Nxg/27Nlz+PDhderUGTRoUFUWUJrevXs3bdqUvs7Lyzt06JB4nvT09HPnzo0dO5afyG/lV73q1q377t27b9++RUZGmpqa/sxVFxUVKSsrjxw5UpbMNWrUGDJkyC8e7Ku4pKQkFxeXtLQ08T85OTklJCTExcXduXOnKtqEAgAAAAAA/FdDsA/gl+Dt7X3q1Cn6evbs2ebm5uVYSO3atSu1UGWgoKAwbdo09nbv3r3i0Shvb+9+/frp6urKuMzMzEwZQ1oFBQVZWVkyLragoKCkqUsEAkH9+vVliT8KhcJS18hxXG5urixFUlBQcHV15XeFlq5Pnz4l7UahUJidnS3xT0VFRSL9YUtV6mYWFRWxDBzHlSkEmZOTk5eXJ/FPcXFxnTt3DgsLK+mzKioqhoaGUhZeUFCQmZkpe2EIIVlZWUKhsEwfAQAAAAAA+AUh2AdQ/ZKTk2fNmsXejh49uhoLU25jx47V1NSkr79+/XrlyhX+X4VC4d69e/kBQYkKCwt9fX3btm3buHFje3t7DQ0NY2Njd3d3iXGf4uLiTZs2WVhYmJiYmJiY6Ovrz5gxIyEhoaSFf/z4sVevXurq6tra2paWlv7+/uxP06dP78Lj7e1N04cOHcpPv3HjRlxc3NChQ+lCGjVqJLEN4/nz53v06NG4cWMrKytLS8tly5ZJj4I1aNDg7Nmz0vcMX/fu3RUUFPgFO3jwYHFx8eLFi3V0dLS1tfv27Zufn08zh4WFubu7N2rUyNraukGDBrVq1Wrbtu2RI0cKCgrYAsuxmbdu3WrTpo22tra5ubmJicnYsWP79+8vcW+IuH37dv/+/Y2NjR0cHGrXrm1gYNC/f//bt2+zDKmpqZ06dfrw4QNLGTRoUJcuXRYtWpSfn+/s7Mwv6uvXr0WWf/DgQWtraz09PTs7O11d3QkTJiQnJ7O/Xrt2jf9xOgmyj49P06ZNtbW1tbW1XV1dY2JiZD0SAAAAAAAAvyAOAKrb9u3b2VeySZMmW7dunTRp0l9//XXs2LHv37/Lvpxv375Vy7fb3Nz84sWLHMdNmTKFrb1r1678PFeuXHFwcKCv+UPp0Q9ShYWFtCfykiVLhEIhx3HPnj1TUVEhhKiqqt67d4+/wMjIyDZt2hBCxo4dKxQKv3//TnOamZmlpaXRPNOnT2crWrlyZa1ataytrVkDOoFA8OjRI5ozLy+PH2NdsmQJTc/JyenduzdLX79+fYMGDSwsLOiYcZSfnx8rlVAonDlzJiHEwMAgOTmZ47ipU6cSQjp27Jifn1+mvco/lGvWrBH5a1FRET9A7OHhQeNWzMGDBzmOe/DggYaGhq6ubkJCAt1MNndtv379CgsLy7eZJ0+eJISYm5t//vyZ47iPHz86ODgQQvbv308z8KN+W7ZsYR/cunUrrRt0b8THx5uZmdFsGzZsoHkSExM3bNhgbGzMlrB48eINGzacPXuW47ikpKQWLVqwP926dYst/Pv37y4uLoQQS0vLnJwcjuNWrlxJjwU/G38y6EaNGq1evVpHR4e/THNz87IeLAAAAAAAgF8Hgn0A1c/W1pYFGho0aPDbb781aNCAvtXX179y5YqMy6n2YN+7d+/4BQgNDWV5evTo4e3tTV+XFOxjTe0GDx7MEvv06cPiMiyxoKDAysqKEKKkpMTioXXq1KE5jxw5QlP4wb7GjRsHBwdzHHf37l2W2L9/f7ZM1o2a8IJ9HMdt2LCBpdevX//OnTscx33+/JlthaWlJcvs5+dHE93d3WlKUFAQTdm+fXuZ9ip/T4oH+ziOu3//PstQr169fv36eXp6sj7IJ06c4DjO0dGRECIQCOi2cxz34sUL9ilfX99ybGZRURENAs6YMYN9PDk52cDAQHqwLz09nRbP0NCQRdM2bdrEctLQIcWfSZlGKhl+TJkfxWOTdRw9epSmZGdn0zVqa2vHxsaynGxATFVV1ZEjR2ZnZ3McN378eLbYM2fOyHygAAAAAAAAfi3oxgtQzQoLC4ODg9nbQ4cOnT59OiQkxMTEhBCSmJg4ePDgT58+VV8By6B58+Zdu3Zlb3fv3k1ffPz48dWrV8OGDZP+8Y8fP9IXly5dysjIoK/Z0Gxfvnxh0cytW7fSwGKrVq3YSIWNGjWiL/T09MQXvn37dtp6q3PnzmxWB350sqSpe/npc+bM6dKlC11Xs2bNaOKnT58KCwsJIfn5+UuWLKGJrKWYkZERfbF27drKHRKOdZomhBQUFJw9e3bWrFnHjh1r1qxZnz59BgwYQMtGCOF4UUj+UHcPHz4sx2Z++PCBzptx7ty5r1+/0r/WqVPH3d2dP8+yuMjISNqdOT4+/ubNm+LlefTokSwbLvFI/fPPP3///Td9bWdnR19oaGjQ8v/48WPevHniS1BXV/fx8aGNPXv27MkyiIStAQAAAAAA/osg2AdQzWJjY/kxoPr16xNCdHR0JkyYQFMKCgo8PDyqp3BlN2PGDPb66NGjNGa3Z8+eMWPGqKurS/+sq6srbZ3XqVOnGjVq0ER+/IhFAHfu3ElfsFAaIcTHx2fatGleXl78qA2jpaUl/rqsczjwF6KtrU1fFBUV0Rk/Xr58yYJfLASpo6NDX6SkpISGhpZpdbJr164dba02bNiwjx8//vPPP3Rvjxs3TiAQ1KhRo1OnTjSnxP0pQvpmsiXExcXZ2Nhs2rSJztGxbNmySZMmSSmknZ0d7e1rbGxsbW0te3lkcfjwYfaaH0Bk3YFPnz4tPnWJmpoaC/yxLSVlrxgAAAAAAAC/DgT7AKrZ9+/f+W9Zc622bduyRBlbPP0KXFxcWHglKyvL19f3x48ffn5+/K6XJWnSpElcXFx0dPStW7cEAkF4ePimTZtYEzAmJSUlPj6evua3brOystq9e/fEiRMVFEo5s0lvgCYj8YXwG2AuWbKkZcuWLVu25B/HqKioiq9XIhYbFbF9+/aYmJikpKR+/frl5eVdvnz5r7/+KtOSxTfT3NyczRadnp7u4eHRqFGjTZs20XZ/0hf16tWrqKioz58/N2jQICUlxdvbe8+ePWUqT0nevn3LXksMVnIcJz3YWim1AgAAAAAAoNoh2AdQzdjwfBSdZYIQ0rBhQ5aYkpKSkpLyU4tVXoqKiu7u7uztnj17fHx8WrdubWpqKsvHlZWVdXV19+7d27Jly1atWiUnJ9PuzHz8LpZS5t79+SIjI9nr3r177/7Xs3+1a9fu55eqfv36oaGh48eP19fX37p1q42NTQUXqKioeOLECX19fZby/ft3Dw+Pbt26ydIgztjY+PLly/369TMyMrpz5w5r4lcRhYWFnz9/Zm+VlZUlvubP8AsAAAAAACCvEOwDqGZ169bl9x8sKCigL/iJCgoK/CZsv7gJEyawCRDCw8MXL148bdo0GT976dIlExOTadOm5eTkBAcHb968mfZr5uPvGZE5SaoXmyGEEKKurt5ajMTBBKtUdnb2kCFDHBwcvL29ly9ffv/+fTaLRUXY2dkFBgYOHz6c34jywYMHy5cvl/7Bjx8/2tvbDxgw4N69ezdv3jx27FjLli0rXh5lZeW6deuyt3RkQIp9oQgh9erVq/i6AAAAAAAAfnEI9gFUP9YpkvAGC+PPQtCkSRM24F1YWNjdu3fz8/N/ZgnLRFdXd/jw4extrVq1+vbtK8sHr1y54ubmlpSURAjx8vISafPIWFhYsB6X79+/j4mJqXCRKweby4IQ8ubNm2osCZWXl9e3b99z584RQrp16zZ37txKWWx6enpcXFz9+vWPHz8eHBz822+/sT9dvHhRygejoqK6du1K+9suXLjQycmpUspD0dmZKf7YfPzXbNYUAAAAAAAAOYZgH0D1409qwUajy8vLY4nt27enL9avX29hYdG1a1d7e3vx2Qz4DZp+MpFVT58+nb2eMmWKoqJiSR/kOI69Xr58OR33TSAQ0JkcJNLQ0LC1tWXr9fLyKm+pK5mtrS0L0d65c0ek1WF4eDibvqOs+HtJdgEBAffv36evK6UBHXX//n02e0zz5s1Pnz598eJFuuF0po6S7N69Oy4urhzloTPYSJ/LuHv37uw1/6uRnp5OX1hZWRkYGMi+UgAAAAAAgP9SCPYBVL8RI0ZYWlrS12wOAdbET1NTc9WqVYSQ3Nxc1k3y/fv33t7eIssRGdevInOblklOTk5ycjI/xc7OjgYoVVRUxo8fz/9TUVERPzLIGl4VFRWxORY4jnv+/Dkh5Nu3b8+ePWOZWcxr8+bNLHHjxo2XLl1ib1+9esUGDeSHhyS+LjWDjK9pwerXrz958mSaUlxc/Pvvv+fm5tK3b9++HTx4cKlTWDA/fvzgvxWfRlakABJbegYGBrLXT58+pS/+/vtvkWKLL036ZhJCbt68+fHjR5bu6upKZ/tlA/Dxt7SoqKik8hQUFFy5ckVieVRVVdnriIgIQsiIESNevXpVUvFmz57dtGlT+ppVJKFQyEZ45M8EUmoFKF90FQAAAAAA4FeAYB9A9VNQUDh27BjttfrPP//QxNevXxNClJSUPD096Z+EQiE/TCYS3xEKhSLhvzNnzlR1yQkhz549+/bt2+HDh1NTU/nptLni4MGD+TM5EEL8/f35kZRz587RUdWUlJSaN2/O0keMGDF8+PB27drp6OiwxB07dtBtdHZ2njp1Kk0sLCwcMmRIt27dFi5cOGDAgFGjRi1btoz+iQaJKLo/CSFfvnxhRc3IyGARUn70ij+1K3/aDbaQvLw8/tSusbGx9MXy5csbN25MXz969MjY2HjgwIH29vbt2rXbvHlzkyZNJOxBSUSOXUBAgHi8LywsjL0OCgoSjyTy5+K4d++ei4uLk5PT5cuXWSfoFy9eLFiwIC0traybWVRUNHToUNaB+sePH3TuCw8PD5ry4MED9ikatxUpz4YNG0aMGNGiRQv+bNTnzp2jcW1CiJ2dHUt3c3Nr3bp1cXGxo6Mjx3H8WY/ZkVJVVfX29q5VqxYh5PTp0zTx+vXrNGy6cOHCjh070sT4+PicnBz6OikpiTXA5Mci2ZYCAAAAAAD89+EA4NeQlJTUp08fgUDwxx9/bN68uUGDBmZmZk+ePOHnmTdvHv3mNmrUKDExkSbevn27Z8+e/Nl7GWtr6wEDBlRRge/cudOiRQsWOdLU1Gzbtm1KSgr9a0FBQb169R4+fMjy//333/xwHqOrqzt37lyO4548ecKf46Jdu3Zv3rwJDAxkE6q2bNny27dvbIG+vr41a9Zk+ZWUlEaOHJmWlsZxXF5ensg0rwKBYOXKlWFhYTQexNSsWfPVq1djx45lG0INGjSI47g+ffqIlHb06NGZmZki04bUq1fvn3/+oaVKS0sbPHgwf2nm5uYix1GKVatWSZy5WEdHp3379lFRUTTbwoUL2cTNVMOGDdevX89flFAoHDlyJMugpaW1atWqwsLCQYMGsT22e/fusm7mxYsX9fX1+/fvb2ho6OrqOnToUB0dHW1t7QMHDtD1urm5iSxtyJAhHMelpKTw433GxsZnz5798eMHq7p169Z98OABXcj379/t7e1Z5v79++fk5OTl5YlUIQUFhe3bt7NNjomJcXZ2JoQMGjRo1qxZderUMTAwoPFl6urVq/w6RnfsmzdvNm/ezJ9shK5RxkMGAAAAAADwSxFw6KwE8CuJiYl59+6dUCh0cHCQOMTYixcvkpOTO3fu/F80P6/s0tPTHz16pKCg4OjoyOZX/fTp06tXr8zNzW1sbESG/xMKhWFhYcHBwdra2o6OjiKtCKtRRkZGYGBgUlJS48aNW7VqJRJJ/JnevHnz8eNHc3NzKysrOrJeYWHh9evXhUJhu3btyjFB8Ldv39LS0lq0aJGfnx8YGBgdHW1gYGBnZ8cPvJakqKjo4cOH379/t7OzY+0cU1NTb968Wbdu3datW2toaLDMQqHw1atXCQkJTZs2Zf3cZREZGfn27dvc3NxGjRq1bt2aP9cNAAAAAACA3EOwDwAAAAAAAAAAQE5gzD4AAAAAAAAAAAA5gWAfAAAAAAAAAACAnECwDwAAAAAAAAAAQE4g2AcAAAAAAAAAACAnEOwDAAAAAAAAAACQEwj2AQAAAAAAAAAAyAkE+wAAAAAAAAAAAOQEgn0AAAAAAAAAAAByAsE+AAAAAAAAAAAAOYFgHwAAAAAAAAAAgJxAsA8AAAAAAAAAAEBOINgHAAAAAAAAAAAgJxDsAwAAAAAAAAAAkBMI9gEAAAAAAAAAAMgJBPsAAAAAAAAAAADkBIJ9AAAAAAAAAAAAcgLBPgAAAAAAAAAAADmBYB8AAAAAAAAAAICcQLAPAAAAAAAAAABATiDYBwAAAAAAAAAAICcQ7AMAAAAAAAAAAJATCPYBAAAAAAAAAADICQT7AAAAAAAAAAAA5ASCfQAAAAAAAAAAAHICwT4AAAAAAAAAAAA5gWAfAAAAAAAAAACAnECwDwAAAAAAAAAAQE4g2AcAAAAAAAAAACAnEOwDAAAAAAAAAACQEwj2AQAAAAAAAAAAyAkE+wAAAAAAAAAAAOQEgn0AAAAAAAAAAAByAsE+AAAAAAAAAAAAOYFgHwAAAAAAAAAAgJxAsA8AAAAAAAAAAEBOINgHAAAAAAAAAAAgJxDsAwAAAAAAAAAAkBNK1V0AABCVlJTk7+8fFBQUHx+voaFhZGTUvXt3JycnRUXFn1OAN2/enD9/PiEh4eDBg5W75Ly8vBs3bogkmpqaWlpaPnz4MDU1VeRPlpaWpqam9HVsbOyrV6/4f+3WrdutW7f4Ka1atTI0NJRehnfv3kVGRrK3qqqqvXr1CggIyM3NlXErWrRoYWJiIj3P3bt3Hz58GBYWZmpq2rVr106dOknMxnHcuXPnXrx4ER0dbWJiMmbMGDMzMymLzczM3L59+4wZM2rXri1LUTdv3pyYmLhlyxYpeV6+fBkXFyfL0urWrdu2bVtZcpZDXl7eo0ePrly5QgjZsWMHSw8NDZ04cWK3bt1WrVol+9Kqrg5XOqFQ+Pz584CAgJiYmLS0NF1dXRMTky5dujg4OCgpKS1dunTatGkGBgbVXUwgQqHwzZs3V69evX//vsiZp9zKV71/cbGxsVevXr18+fLixYvbtGlTdSuq6q85vRafPn3ay8vL2NhYSs7c3NyhQ4cqKSkdP35cXV29Kgrzy7p+/fqRI0devXqlrq7eu3fvlStXqqmpVXeh5E1kZOSlS5f+/vvvJ0+elOmDnz9/vn79+qVLl7Zt22ZpaSn7BxMTE+/evTts2LBKvzv6+f5nq+iJEye6d++up6f3c05lL1++zM7O7ty5c4UKDQCViwOAX0ZycrK7u7uiomLjxo1nzZp17NixXbt2DRs2TEFBoWHDhqdOnarqAsTExLi5udGoYufOnSt9+fn5+UePHjUyMqLnHxUVlSNHjnz58oXjuKCgoHXr1iko/F9z43bt2l2+fDk2NpZ9Njk52c/Pj0Y9bG1tT506lZeXd+3atcWLFysp/d/vFuPHjy+1DM2bN6eZlZSUli1bdv/+fY7jWDBFW1u7UaNG5ubmOjo6NKVx48bNmjUzMDBQVlamKVu3bpWy/IyMjOHDh4ucaYcPH56VlSWS8+vXrx07drS2tn7x4kVqauqePXtUVFRKWnhWVtaGDRtojO/bt2+lbibHcffu3VNQUGjTpo30bCEhITt37qxTpw4tqpaWVufOnbv9q0OHDi1atKB7uFu3brKstxwOHTrUqlUrWoABAwbw/7RgwQJCiIKCwo8fP2RZVFXX4cp1+vTppk2bEkIaNWo0c+bMHTt2LFu2zNXVVUNDQ0NDo0GDBoSQ0NDQ6i4mcPfv33d1daX1qmbNmpW12LJW719cQUHBrFmzatWqRb/L9+7dq6IV/YSv+eLFi9l1ISIiQnrm69ev05zXrl2risL8snx8fLp161ZYWBgSElKjRg1CyNKlS6u7UHIlIyNjxIgR7PZD9g/GxMSMHz9eS0uLfjAoKEj2z966dat3796JiYlcpd4dVYv/zSqak5MzevRoLy8v7ieeyoqLi2fNmrVixYpyFhoAqgCCfQC/ig8fPjRq1IgQ8vvvv4s8+L148YJGBKZMmVJcXFzVJZk0aVKVBkqePn0qEAhogEPkT1OnTqX3GZs2bZL42Xnz5qmpqcXHx/MTBwwYQD+lqqqakJAgZdX89jgDBw5k6TVq1LC0tHz27BlLmTBhAs0WFhZGU7Kzs7dv304IWb16dUnLFwqFXbp0MTU1Xb9+/cGDB+fMmcOa4Lm7u/NzFhQUODo6amhoxMTEsMSlS5cSQo4cOSKyWG9v7wkTJrBGjrIE+zIyMujvt6UG+6gNGzbQhS9fvlz8r8nJyZ06derUqZMsiyq3Pn36iAf7goKCHB0dPTw8yrSoqq7DFVdQUDBu3Dga6Fm0aFFRURH/r5GRkd26daNH5OXLl9VVSBCxadOmyg32la96/+I+ffpU1cE+qqq/5s+fP5fxCTkrK6tnz559+/bNzs6uosL8gkJDQwUCgbe3N3175coVU1PT69evV2+p5NKRI0fKGuyjXrx4UdZgn7+/v6OjY3p6On1bWXdH1eJ/s4rm5uZ27dqVbTX3c09lM2fOXLZsWfk+CwCVDsE+gF9CUlJS/fr1CSFDhw6VmCEsLExbW5sQMm3atMpdta+vb3h4OD9l8eLFVR0o6d27N73ziI6O5qd//vyZNtbo0KGDxA/269dv+PDhIomzZs1ifZyl/2bbv39/CwsLmnPWrFksXVlZWSSkIn47Sw0fPnzRokUlLf/AgQPt2rXLy8tjKQkJCS1atKCL+vTpE0unXWsnT57M/3haWpqKikqdOnX4S+A4jgZ/aRdXGYN9Y8aM6d69u+zBvpMnT9KFb9myRWKGt2/ftmvXTpZFlRt9dBcJ9pVPZdVh8W9HZRk1ahQhRFlZ+caNGxIzFBUV0XjfgwcPqqIAv6wPHz4cO3asuksh2YkTJyoS7Fu8eHHllqfiqqJIOTk5VRHs+/mXqs+fP8v4hFxFcnNz16xZUy2rlsWcOXMIIZcvX67ugsi/a9eulS/Yx8bokDHY9/jxY21t7ZCQEJZSWXdH1eJ/sIoKhUJXV9dhw4bxE3/mqay4uLhVq1Y+Pj5VvSIAkAUm6AD4JUyZMiU2NlZZWdnT01NiBjMzs9mzZxNC9uzZw277Ku79+/fTpk3Lz8/nJ9Jmd1WKteA7evQoP71Ro0a9evUihDx69Ojjx48inyouLn727Nkff/whkq6srNyjRw8NDQ1CyL59+0oaX+bz58/+/v6zZs1in6IvioqKGjVq5OjoKEvJhw4dyh5lxXl7e584cUJVVZWl6Ovr79+/n75mv7ELhcLdu3cTQlxdXfkfr1mzppOTU0pKyt9//81Ppz1x9PT0ZCkhIeTixYuJiYk0diYjtjdKYm1t7eLiIvsCy4F14q64SqnDEr8dlcLPz8/Pz48Q4uHh0aNHD4l5FBUVjx07pqmpKaW+yZ/s7Gw3N7f09PTqLohkFamifn5+Bw4cqMTCVFwVFakSv8hMtVyqfsKlULqpU6cGBQVVbxmkuHPnDqmaww0iyl0Vy3R0srKyRo4cOXHiRCsrK5pSiXdH1eJ/sIru27fv9u3bIiM1/8xTmYKCwpYtW2bMmBEeHv7TVgoAJfkfOv0B/LKCg4MvXLhACOndu7eUkfinTp1Kb1nWrFlTKevNyMgYNGhQdnZ2pSytTHr16kUHifP19RX508SJE+kLHx8fkT/dunVLUVGRNlgToaurO2bMGEJISkoK6/AiYvfu3Z06dbK1tRVJFwgEso/v7uzsPHToUIl/ysrKatOmjfjgx23bttXU1CSExMfH05SnT59++fKFENKyZUuRzDRl3759MpZHXFJS0oIFCw4dOlTpt3d0fLH/EVX37SgsLFy+fDkhRFVVlUbwS6Kvrz9x4sRf7fGp6nAcN2HChHfv3lV3QSrfmzdvpkyZUt2l+A+/YJFKUo2Xqmq0f/9+8YvgL4Vd0UA+LF269PPnz/PmzWMplXV3VF3+16pobGzs/PnzR4wYQbsKVRcnJ6dGjRqxFqAAUI0wGy9A9aNjtBFCevbsKSWbgYGBpaXlu3fvnjx58unTp2bNmhFCCgoKbty48eDBg82bN+fm5u7du/fJkydGRkbjxo2zsbGRsrTY2NiBAwfS1nM+Pj6Ghobq6uqswR3z9etXLy+v9+/fGxkZ/fHHH+LBKaFQePbs2efPn8fGxtrY2Li6urIZMKRQUlJyc3Pbt29fRETE48eP27dvz/5UXFxMX/j5+a1du5Y/B/GxY8eGDx9e0qzEs2fP3r9/v1Ao3L59++TJk0VCXdnZ2d7e3hLjgIqKiiXNlitOU1OzpPkltbS0JDbMFAgEenp62dnZjRs3pikPHz4khNSsWZNNi8HQw/rkyZP8/Hx+C0HZTZw4ceXKlfXq1SvHZ0vi7e1tb28vEid9/vz5iRMnlixZoqend/bs2QsXLqiqqg4cOLB///7iSwgPDz99+nRISEj9+vVtbW179Ogh+wyzwcHBx48fnz9/vq6uLkvMyso6cOBAWFiYhoaGubm5sbGxo6Nj3bp1xT9eah0WV+q3IyUl5dq1a69fv46Pj7exsWnTpk2XLl1k3Jxr167RUG+PHj34WyQR7cQnkpiZmXns2LGgoKC8vLyWLVsOGzZMpNXnp0+fDh06tGjRIoFAsGnTpm/fvs2aNcve3p4QEhoaevLkyWHDhllZWb17987LyysmJsbKymrevHk1a9aknz1y5EhYWJiJicmUKVOaNGkisvZHjx5dvnw5JiamVq1atra2o0ePFqmoUVFRp06datOmTdeuXV+8eHHo0KGMjIwOHTpMnTpVypTihYWFEydOPHXqFCHk1q1beXl5hJAxY8bQXSRliwghkZGRx48f//z5c3FxsY2NzdChQ9ksQGUt0pcvXw4dOpSQkKCrq2ttba2lpSXS/FaioqIiOlFmYmKisbFx+/bt+Z96/PjxwIEDc3NzBQLB1q1bCSHNmjVj7WQlVm+6Q/z9/V+/fk0n9ba3t3dxcRHZ1WlpaefPny8oKHB3d//27dvu3bvDw8MtLS1nz54tvWpJL1J6erqfn19oaGhGRoalpWWfPn3Em/Y8fPjw/Pnz2dnZ9evXd3BwUFdXd3Z2Lml1f//9d3R0NHtrbGw8ZMgQKcXjq6xLFS3zzZs3w8LCmjRp0rVrV4m/G5UkMzPTx8fn2bNnderU6dKly8CBA0UuMXFxcadOnXJ0dOzYsaPIZ+/du3ft2rWoqCgLCwtHR0dnZ2f6848U+/btmzFjBiHk48eP9AA5OzvTM/D379+PHj3q6OjYvn37ffv2PXjwwM3NbfDgwfSDpX4XZK8zJZ1jv3//Tn+ly8jIIIScP38+LCyMEGJhYUHHXaU+fvx4+/bt169fFxUVOTg4dO3aVfzeoErPVKXenEjfk+ISExPPnDmjr6//22+/JScn7927NygoyNDQcNasWWZmZoSQ1NRUX1/fly9famtr//777xIvCg8fPnz69OmbN2/q1Knj4ODQu3dvfX19ievy9vZ+8+aNjo5Onz59SpoXtXw3YBJ9//79wIED1tbW/JuH8t0d5efnX7x4MScnZ+zYsTdv3jx8+LC1tfWiRYtotlJPLz+tispyHZdSSSpeH0o9fNL3pLidO3dmZ2dLf5SoyKlM9hs5Z2fnHTt23L9/X/b6AwBVopq7EQMAx1lbW9PvI38IZInoiP6EkIMHD+bl5U2ZMoVOLtaoUaPw8HALC4smTZrQ+2ANDY1Xr15JWdTdu3dXrFhBl+bh4eHp6bl//376pyVLlhBCOnfufO3atRo1alhYWNAYioqKyt27d/kL+fDhQ+fOnZctW3bp0qWtW7fWqlVLVVWVzv9VqgcPHtC1i0yh6+LiQjeKEPLPP/+w9KysLE1NzcDAQPFF/fnnnyNHjuQ4jj1giw/Rsnfv3iZNmtCOwDTPn3/+KaV4JY1KUw6FhYVKSkpqamppaWk0hT7ompubi2dmfbQjIyNF/sRKLmXMvkOHDv3222/0Ne0LLOOYfazjsMQx+/r27fvo0SP21tPTs2HDhjR/aGioq6urgYEBnTqWELJ3717xUunp6Z07dy46Onrt2rU0m5GRka2tra2tLctG2xmxMfsyMzM3bNjA+hPxtzoxMbFhw4Y7duwoLi7Ozc3966+/CCEvXrxgGWSvwxJJ+XZwHHflyhUDA4Nly5Z9/Pjx6dOntEnpwIEDk5KSSl0yx5uFZv369bLkF3HlypU2bdrs3r374sWLc+fOVVRUrF+//pMnTziOKyoq8vT0bNu2LV1+eHi4g4MDfd2rV6+DBw+y4SNv3ry5ePFiPT29tm3b0sFA27ZtKxQK161bV7NmzXbt2tHnPQMDg7i4OLbqvLw8V1dXU1PTp0+fFhcXP336VEdHp127dqxiX716la3dx8dn1apVNWrUMDMzo88SrGZKFB4e7unpScPfgwcP9vT09PT0TExMlLJF9INr1qzR1NQ8e/ZsTk5OTEyMg4ODoaEhG6CqTEW6c+eOnp4eHSQxISGhW7duGhoa/Aw0FikyZl90dHSzZs1cXV0/ffpUVFTk5eVF/nNCnqNHj44cOZIQoqmpSbfL399fSvXmOO7Dhw8ODg6DBw9+8eJFWFjYunXrlJSUrKys3rx5QzOEhYUNGjSITpM9ZswYf3//OnXqWFhY0Ghg48aNxaf/5pNYJPqngIAAbW3t5cuXJycnZ2VljR07VllZ+ejRo/yPr1u3zsbGho64GhgYaGxs3KdPH/ZXGqgl/zlm35w5cxQVFf/888+vX79KKZi4SrlUZWRkjBw5csyYMX///bePjw99op48eXJBQYGUVdOgPCHk6tWrdPoshrY0pNmOHDni7OxM292LDDcpFAonTZpkbm7+8uXL9+/f03ZPCgoKpqamtra2EyZMkLje3NxcT09PGjexsbGhBygoKOjSpUuurq4qKiqEkJMnT06ePJkWRltbu7CwkCvtu1CmOiPlHJuSkkKLREsyfvx4+pbNe1BcXLxhwwY9Pb0jR45ER0f7+/u3b99eUVFx6dKldIdX9ZmKK+3mpNQ9KeLevXs9evSgh3jFihWnTp2qV69ey5Yt6dp1dXVjY2P/+ecfAwMDe3t7Oi6wkpJSQEAAfyHp6ekjRoxo3rz5jRs3oqOjfX1969WrV7t27VOnToms7unTp/r6+vPnzw8NDQ0NDR01apSlpSUtoezbSCUkJNAPljpmH519SPp9ESXl7igoKGjixIl0XrIpU6YcP36cdaGl97fSTy8/rYpyMlzHpVSS27dvV7w+SD98pe5Jcfn5+TVr1lRUVGSTqzAVP5VxMt/IUWfPniWEuLi4SCwqAPw0CPYBVD81NTV61YyNjZWek/2gx27IaM/KGjVqdO/e/f379xzHZWVl0UYNgwYNkr60b9++0aW9e/eOn06foBo0aNC3b1/6OJebm+vk5EQIcXZ2ZtkSExONjIxOnz7NUs6cOUMIEQgEsgwFLRQKaXhIW1ub3WokJiYqKSnRUfAJIUOGDGH5jx49KjE6xvGCfffv36cfFJ831sLCYufOnRwvZPbTgn03btwg/xnTpD91tmrVSjzz48eP6XrFx7YvNdgXFRXVrFmz79+/07flC/bNmzfv3b+Cg4Nv3rw5e/ZsgUDAD/ZxHHf9+nWav3Xr1leuXKGJ06ZNI4TUrVuX/7z0+vVrZWXl2bNnsxQas9bX179169atW7dYukiwLycnJyoqitV5/lZPnDixYcOG7K1QKGzTpg2/hDLWYSlK+nb4+/sTQlauXMlPpH3PrayspMcOKFoMQsi5c+dkKQnfvXv3atSo8fnzZ5ZCQ4f169fPzc0tLi4ODw+nzzyEkBEjRjx48OD8+fMNGzakLQSjoqJokMvKysrHx0coFHIc9/79e9q6rWvXrn/++SfdhNzcXNq+Zt26dWxd9P5+zpw5LIUe8W3btrGU5ORkOsSkmZnZhg0b6NLomYEQ8vbtW+kbSKcd3717N31b6hbR74udnR1bAn3G6N+/fzmKZGpqOmrUKPY2MTGxVq1a/OJJDPbRBmKXLl1iKTSW9Pr1a5Zy7NgxQkidOnVYipTqHRcXV6NGDWdnZ3qA+EtQV1dnPwPk5+fTlheNGzceMWIE/eJHRETQmAg93UkhXiSO4/Ly8ujHMzIy2N4jhOjq6rK6/fXrV0VFRf5Uj9euXevWrRt/ISInsWfPnjVr1qzcU81U8FIlFAp79OjBD79+/fqVhg927dolZb3sCdnW1vbIkSPJycmBgYGsoyJb4KdPny5fvkwTRZ6Qd+3aRQi5ePEifVtQUECb/PTo0ePWrVsSf7ti6DWIfxH8/PnzuXPn6Ip69+69b9++169ft27dmga+ZfkuyF5nSj3HchxHv1Zs6xj6hb1//z5LyczMpOeTSZMmcTJ8ryt4pir15kT6npQoJyeHRtwaNmzo4eFBb1p+/PhBG+N37Nixe/fu7GcPd3d3epT5O7B169a1a9dmeTiOe/fuHf119sSJEyzx+fPnqqqqCxYs4K+9b9++tLSybyMle7CPzgfl5+cnPRsn9e4oPj7+1atXhoaGdJ/MmDEjMjJyxIgRTZo0SUhIkOX08nOqqCzXcemVpIL1odTDJ31PSjwujx49IoSYmJiI/6nipzLZb+SomJgYQoimpqYsN0UAUHUQ7AOoZj9+/CD/KnWqezbm7tixY2kKffhUVVVlN08cx9GmJfzbIImkP0GZmZnl5uayxNOnTxNCtLW1WcqsWbMMDQ35HywoKKCBy9GjR0tfNTV//nxaAPa77rZt21q0aMFxHP2dX0VFJSUlhf6pR48eq1evlrgcFuzjOI71B+E3bAwICKhRo0ZmZiZXHcG+3377TVdXNzk5maXQZgtOTk7imV+9eiWyTxjpwb7i4mInJyd+W8jyBftUVFS0/kXnPKFE7qHZIwR/MtlPnz7RxKioKJY4duxYQsihQ4dYypMnT2g2ftCKEwv2UW/evBHfamtra1VVVTpJMeXt7X379m32VsY6LIXEb0dubq6JiYmamhr/68ZxXHp6Ou1mJUtjPfq0TwgpqY3h48ePJ0yY0LJlS8d/9evXj95M29raisyyR7uEE0LY5HcvX74sqQpxHEefLfft28dPpE1s+vbty0+kY4Pygzh0BmH+7f769esJIezbR5mbmxNCFi5cyE+kTR74ESKJRIJ9pW6Rt7c3fYBhKU+fPiWEGBkZlbVIqamphJD27dvz84waNaq4uJi9lRjso61c+c+TtBcVv85LjKxxJVTv33//XeSbRbVr144Q0rNnT5ZCvzLt2rXjhwWHDx9OCOFHLSWSWKSoqChCiJKSEjvxchxH42JsGsfz588TQkSm8f3jjz/Ya5Fg37Vr11q2bCky8XqZVPBSRYfEFTmT007HxsbGUtbLnpD5z+TFxcV0NnllZWX+1JY6OjpE7AmZDt3Az7Zu3bpS10uJB/so2gVYpEU8J/N3QcY6U+o5lishkvLu3TtlZeW2bduKFC8gIIAQIhAIHj9+TFOq7kwl481JSXuyJAMGDCCEDB06lJ+4cOFCQoienl5OTg5LpGEXZWVltpPpTDgipyDu319w9fX1WWDIyclJTU2N/wXkOO748eN0X5V1G2UM9hUVFdHLPf8WoiSl3h3R0KSZmZlIM0lZTi9c1VfRMl3HpVSSitQHGQ9fSXtSInpuadmypfifKn4qk/1Gjm0O/avIrSMA/GQYsw+gmtE+AlRRUZH0zGwuQjbOFO3voKyszLq+EkLoQyx7QCofQ0ND1uSQEEJb/v/48ePHjx/a2tpCofDo0aNaWlpz5szhf0pTUzMvL4/dBEg3fPhw2nPE19eX9ik7cuQIfTF69OjAwMCCgoITJ07MmDEjISHh9u3bskxbMXfuXHpTuHXrVtZCcOfOnRMmTKC/DP9kL1++PH/+/N9//80fno/uWE5sLDZ+Ylnnj9uyZYvIeDTls27dOv743EVFRdevXxcfxohWPEIIfwwdNtbPt2/f2ACFr1+/JoRkZmaybK1bt1ZRUSkoKIiNjRXpUSJO4jTBurq6+fn5Li4uZ86cocPVubm5iU9kIb0OS1+vRJcuXYqKimrevDn/60YI0dHRcXNz8/Pz27NnT6nTmLBRKUuaNrpdu3bt2rXz9fWlt9e2trbnzp1TUVF5+/ZtUFCQhoYG/0vHFvLkyRPaEYntsa5du4ovnB44kc2nB05k4D/aIzIxMZGlrF692tbWln5DKVpdRSbPpasQGVypXr16cXFx5TspSdmioUOH/vjxg59e7iJpaWmpqqo+fvx4zpw5mzZtoitduXJlqcU7depUcHAwa3pTUhmkbxqTmJh48uRJQggbkZCZMGHCkydPbty4ERkZSUcoo9tVu3Zt/qBLFTn/N27c+Pjx4zVr1mT7ip2R2ObQP23evNnCwmLEiBE0kc45I87Pz+/gwYM3btyoVatWOcoji1K/5keOHFFUVNy3bx9/L6WkpBBCoqOj4+LiSh3hlH8sFBQUFixYcO3atcLCwmvXrk2fPp2mix/K9PT0z58/k/88+9FWS3FxceXYUv6KKvhdKLXOyHiOFbd79+7CwkLx2tutW7dGjRp9+fJl3759NGxdRWcq2W9OStqTJZFSJE1NTf6werRIhYWFaWlptCfm9u3biaQv9fjx49evX5+YmHju3Lnx48ffunXr4cOHHTt2FDlZibytlBswvpSUFHpwaWkriO7Yjh07svsESpbTC6n6Klqm67iUSlLu+lDWKiq+JyX6+vUrKe0Ilu9URsp+I6esrKyhoZGTk/P161f+qNwA8JMh2AdQzVRUVHR0dOgowqmpqSI3HyLYE4LEiQgYelsgFAorr5iEDQxPf6/78OFDamqqqampyGwV9K2UYfj5bGxsLC0t379/f+fOnejo6LS0tODg4CtXrhBCfv/993nz5hUWFvr4+MyYMePEiRP29vYmJialLtPNzc3DwyMmJubs2bMbN25s0KBBZGTkjRs3aHeqn6ywsNDd3X3Dhg0iY/zTuzEWuuVjiaVO3cAXHBzs4+PDGkpUIiUlpX79+rm5ucmYmb7g1z0azg4PD2cpCgoKRkZGUVFRLCBYVnPnzr179+69e/fMzMzWrVs3adIk2hRR+qdE6jAhJCYmRuRroqamJnG4dCo0NJSIPWpSbdu29fPz+/btW3p6es2aNaUsWVdXNyIighBC/y8Je7po2bIl3Ye0dYCZmZnIl44O+037+/DJ+DUkJdzc05WmpaWxlIYNG7KHk8ePH/v4+Lx48YLI8CsFqaSTkvgWaWhozJw5k76Oioo6cOAAbRtbjiIpKyvPmDFjy5YtdGinPXv2dO3atdRgNCGkbdu2tMFRUVHRhQsXzp07FxISImMZxNE6pqCgIH4GYGOchYSEiE9HUNJ2lRX9sYQQkp6e7uvr++jRI7ohbHOcnJxatWr14sWLkSNHHj9+fNeuXU2aNJFYnp07d164cOHgwYNVF+kTJ/41f/TokYaGBtt7FPsS8QOFJREZwL5jx4516tRJSUmho/6XhP2SFx4ebmdnR1/TdqCy1CvpqvS7QMp7jiUlnyQFAkHr1q2/fPlCvx3St6UkspypynpzIvvay1okWqratWsXFBTQs734bjExMalbt25iYiLdLbQxJhsStySVcgPG9/37d/qiEr+qEotR6umlJD+hihKx67j0bZFIlvpQRVWUHkTpR7B8pzJSrhu5WrVq5eTk0J9VAKC6INgHUP0cHR1v375NCImMjJT+DMAmNJQ+0+5PEB8fTwjR0NBgo36Uz/Dhw5csWcJxnJ+fX2pqaqdOnehAfnXq1Onbt+/FixffvHnz9u1bOg+vLAtUUlKaOXPmX3/9VVRUtHPnzs2bN+/Zs6d///4Vf7gqhxkzZnTo0IHfUI5q2rTpjRs3aIRXBHtiEZlCUbrx48e3b9+e9l9j6JNeUlLS4cOHCSEtWrRo1apVmcrP/PHHH1JCYNK5uLi8fPny7Nmz27dvp8/hxcXFKSkpLVq0KPeUwf369Ttw4MD06dPT0tLc3d0PHz7s6+tbjlkIra2tRZq9dOvW7ebNmyXlpzfE/MZuDDteERERjo6OUpbcokWL58+fk9KCfSzuz4IX9EvXqFGjCn7pKujixYuenp7dunXbtGnTwYMHS23J+BMEBQWtXbtWW1t7wYIFgwYNKmm+7FJt2LAhNTXV29s7LCzM2dn5999/3717tyxNXfLz8/ft23f27Fl3d3c/Pz9XV1d6sMqB1jGhUJicnCzyow6/jpVv4TJKTEzcsGHDhw8fFixYMHv2bHV1ddYclRAiEAj8/f379Onz6tWra9euWVlZLV68eOHChRKfUTmOmzVrVps2bco9SWgFFRYWpqSkKCoqurm5lbW5tBQNGzZMSUmhXb9LoqGh0bVr1zt37hw9evS3336jibRbZa9evSqrJCIq67tQ7nNsqSfJqq69lXVzUokiIiJoMKuk3ZKYmEh3y4cPHwgh/AE0JKq6bZTY4aByST+9yK7qqij59zpejlLJokqraFmPoCynMlKuGzmaTeKv2gDw01TafQ8AlBsd3538+6NuSYqLi2nTHkVFRTreUDWiz3V0BJaKoKNTEUJ8fX1PnDjB7yE4evRo+mL+/Plv3ryhM9jKYuLEibRjhZeXV3x8vLe39+zZsytYznLYtWtXbm4u7bwjgt5ESuzJRe8CdXR06AxuMvry5cvhw4cn/Kf9+/cTQqKiouhbOlh7+fTs2ZMOplYO8+fP7927d0pKytixY+lgXlu2bFFWVqZ9Fctt0qRJr169ouHLV69eOTo6SgnSlWT48OFj/lOPHj2k5KeNrSR2kGTNBGhYWcqS2ZeXjc8okcgv8OTfL11kZKQMW1YlMjMzXVxcpk6deujQoSVLllRKh68K4jhu6dKlrVu3HjlypLe3Nx0cvdwUFRUPHz587tw5+vRy8uRJBwcHOtC4FB8+fLCxsbly5cq1a9dGjhzJH5mhHFiDvtjYWJE/aWlp0WZoVfrTxcWLF83MzFRUVK5evdq5c2eJefT09J4+fbpmzRp1dfW8vLylS5e6uLgUFhaKZJs6derAgQOzs7MHDx7MH532ZxIIBIqKisXFxeynskpB47ClRha8vb0bN27s7+9PJxYoKChYu3atvb39hg0bKrEwVOV+F0h5z7G0AovXXvLvSbKqf3irrJuTSiTlS03+c7fQCFSpffArfRtZCWUZfKAiZDm9yK4iVVSW63gVqaIqSrerrEdQxlNZOW7k6E/X/BFsAODnQ7APoPqNGzeOPsJdunRJSraXL1/Sq/jw4cPL1MezKtBOW9HR0XQgDxHfv39nA89LZ2Ji0rp1a0JIVFRURkYGf2y4vn370ruEgICAli1bSrn3EvklU0dHZ/z48YSQzMzMPn36NG3atEOHDrIUphKdPHny7t27hw8fFg/ZEEK6dOkiEAiysrLEbzffv39PCOnYsWOZGqHs2rXLVwwNcTZp0oS+rZTfkJcvX87v1ykLVVXVf/75Z9CgQcHBwe3btx88eHBycvLHjx8r0tKHPhFZW1s/e/bM29tbS0srLy9vzJgxZe04uWfPHp//xCaIlMjKyooQ8uPHDzbcNUNnFTQwMKCVVsqSBw0aVL9+fULIkydPyhSgpF86f39/iZsZGBhY1kNTVhMmTPD399+yZUu5I7+Vbt++fWvWrBk9erRIT/lyKCoqot2gBg0a9PHjxz///JMQ8uXLF/GWuXxZWVl9+/b9+PGjn5+f9EEYZETrGCHk7du34uuiT1l0hp+qEBQU9Ntvv+nr62/YsKGkU1BaWlpBQYGSktLixYs/fPhAh3i/du3awYMHRXIKBILDhw83bNjw48ePdOrGn09JSYl2iqTTdIjgOO7WrVvlWCz9vltbW0vPZmxsHBQURIcq69Chw4gRIzp37vzs2bNSm26VQyV+F0gFzrG0AgcHB4v/ie60qqu9VGXdnFQifX19el0Q/1KT/9wtdKySjx8/Sl9gpW+jrq4u/ZWiSoN9spxeZFfBKirLdbyKVFEVpUN5lPUIyngqK+uNnFAopCUxMDAoU3kAoHIh2AdQ/fT09OiD5atXr2jbPYno9BRaWlrLli2r3AKUY2wpIyMjOjYKnQiPj+O4GTNmyH6BZ/1z+/Xrxx8kRVlZmbX7k96sLz8/n83/SM2aNYv+dhoUFCQyBLKMfRwq0pnl2rVrvr6+J0+eFBlTWSgU0vmUjY2NafxRfCBtmsJmu5PRsGHDRovp1q0bIURPT4++ld4nRZbtffr06ZEjR8oxps+BAwfatm377t27wMDAc+fObdmypYLRajoLJyFEIBCMHTv2/v37CgoKcXFx4vfuFcf/dnTu3Jk+ooj/mk1H4pflMVtFRcXT05O+9vDwkL2m0SllU1NT9+7dK/KntLS0NWvW8L8+lS4xMZE2DuW3OS3fsHSlkn2xdCzOSinS9+/f2XHR0tLavHkzbZYrfbT7f/755/Pnzzo6OjSAK70MspTNzMyMdiUrqY5ZWlqampqWuhwZiRTpwIEDhYWF5ubm/F8pRDrZBQQEsDmgjY2Nr1y5MmjQIFLCjqpVq9aJEycUFRX//vtvtnsrq7Qyol8cT0/PrKwskT9JTCxVcXFxVFSUkZFRqXM7CIXCP//809vb+/nz548fPz579uzs2bMljupVkmr5LpAKnGNpy+UvX76I91SQ/SRZEZV4c1KJ6G65cOGCyL2KUCiMjo5WU1OjU3jTwxcZGfngwQMpS6v0bVRSUqI9vmUZYa3cd0eynF5kV+4qWinX8Yqooirq5OREZDuCjOynMlLGG7n09HSO4xQUFMo9kgAAVAoE+wB+CcuXL6fX2vHjx/Onu2IuXbp09OhRBQWFw4cP85vVSLxPYgOTS8d64bH+pPTZgPbGEumTxZZJ8ygoKNCpuy5cuODh4cH+mpGR8ccffzRq1Eh8uoCS/PbbbzQwx+/DS7GevAMHDpSyhO/fv9MfJ5lGjRrR509DQ0M2WBLFHu2kz9omYzZxjx8/XrRo0e7du9PT0xP/lZCQ8Pz583HjxrGR7+hIZ6dOneJ/9vPnzy9fvrS0tOzXr5/4ktnQJ1Uxqg7bzJIWHh0dPXr0aHZHKL3i8Rdy4cKFBQsWTJs2rdQy0GWKTCzAlsmvkM+ePeOPiWZvb29nZ6esrMwCLrLUYekkfjuaN2/u7u5OCPHz8xPZA8eOHatVq5b4vbtEQ4YMWbFiBSHkzZs3M2bMkLjPRZ4JCSGtWrWi980eHh788RkjIyO7du06efJk+gTFlibxGEmct4EmiuwWmsiWxlqh3r17l2Wg8R2RE46UulFq1aW7XeSMJGWLaKlYkci/05gUFhby1yVjkS5cuMB/O378eIFAwB99XLyK0gJkZGSwNhppaWm0fS5/t9DtyszMzM7OpsuhKxKv3kpKSjTIePv2bTq7InPs2DFCyK5du1ijmIrvapEi0c15+PAhK8/z58/pa/7mnD9/nr1WVFSk00azHSWy6vbt29NJjf/666/79+/z//TPP/9069Zt7ty54rVdvKikXJcqQsisWbMIITExMQMHDmQTERQVFW3btu38+fMDBgyQsmqJzp07l5aW5unpqampKbJekcLMmzfvy5cv5euuKPG7QP7dvVX9XSj1HEv+vSSJjMk1ceJE2krIx8eHn56amnr16lUnJ6dhw4bxN6SkIpX7TCX7zUlJe7IkZS0Sv1QbN27U0NDIzMzkf3cIITdu3EhJSfnrr79o34UxY8bQ32wWLFjAD0MHBQWxrSjTNrKtK3XGHjqeTGBgoPRsRIa7I+lVtNTTS1VX0TJdx6VUknLXhyqqou3atdPQ0IiKipK9jb/spzLZb+SoT58+EUJsbW2r9DdIACgdBwC/hry8PNqQzdLS8tGjRyy9oKBgxYoVysrKWlpaFy9eFPnUtm3bCCEKCgoJCQkskY0Tl5iYKH2ltDeBs7Pzs2fPFi9e/M8//3AcN2LECEJI/fr1hUIhy8nCUoGBgTQlNTWVTqZBCDE0NHR1de3Zs2eNGjUGDBhQVFRUpm3v3r17zZo18/Pzxf/UvHnzFi1aSPlsWlpanTp1lJWVv3z5wk9/9uwZIWTNmjUi+eljJyHE0dGRv4F8RUVFbHLJffv2yb4hb9++lXJno6Ojk5OTwzJPmDBBIBDcv3+fvhUKhQMHDlRTU3v79q3EhbN2Ma9fv5alMP7+/oSQNm3ayJJ56tSpdOFubm60S3VhYWFWVlZsbOyrV69WrFhBf2o+duwYzc9CG2fOnGELYX1PTp8+zRJp12xNTU0bG5tOnTr17dt32LBh7u7u+/bty8zM5JeBNkVs2bIlP/Hvv/+my3z48CFLNDc3d3Nzo+EJjuOys7MNDAymTJnCMshYh6WT+O1IS0ujHc/d3d1Zzn379mlra1+6dEmWxTK05xEhpG3btrdu3eJ/a+Li4mi3a3V19aNHj7L0hw8fsslDLS0tBw8e7OTkpKiouGXLFpaHHndCyN27d0XW+OPHDxon2rNnDz+dtgho27YtP3HdunWEECUlpaysLI7jsrKyaDdVXV3dXbt2bdmyhT2316hRw8vLa9euXRzHFRUV0SDF1KlT2aIKCwtpryj+TpOIdsCvV6/e3bt3fX19V69eLX2LaPRZUVFx4cKFBw4ccHFxmTRpEs28YsUKOvmPjEWiMyfQsdUoWslPnjzJUuhDoEAg+PHjB01hDXDs7OwOHz68ZMmSzp07d+zYkRDSokULX19fGkCMjo6me37u3LmPHz8ePXp0bGwsV0L15jiOboWjoyPd+RzHPXv2TEdHZ+nSpfxs/fv3pzWBn0ibpVhYWEjf1RKLtGrVKnYe8Pb2njRp0tChQ+l3f8iQIbt27Xr37t2pU6eUlZX5X6Jt27ZpaWmFh4fTtywufOTIEZpSXFxM90nNmjWfPHnCPshOs6tWrZJe2opcqjiO69u3L03U1NTs2bNn//79GzRo0LRpU+mXyJSUFNq3cffu3SwxODi4YcOGCxcu5OdMTk6moXZaYxna8kVPT8/BwaFr166urq4jRoyYO3fu2bNn2emrJEePHiWEKCgoHD169MaNG6NHjxYKhRkZGfSoLV++XCS/LN8FTuY6U+o5lrWfEr/IBgYGGhoaCgQC9t0pKioaPny4qanpx48fWbYqOlNxst2cSNmTJaFjw7m5ufETly5dSghRVVXlX85YK9cbN26wRB8fHxUVlVq1an348IGmJCUlWVlZ9e7dm39jsGfPHvrZli1b+vn5PX/+fO3atax3v5OT09atW2XcRo7jWGcR+pWRIjY2VlVV1cHBQXo2We6OaDeCTp06iaTLcnrhfkoVlfE6Lr2SVKQ+yHj4StqTJZkxYwYh5MqVKyLpFT+VyX4jR9E77QMHDshYcgCoIgj2Afxabty40alTJ0VFRTMzMzc3t86dO9esWbNOnTrTp0+Pj4/n5xQKhUuWLGGt6J2cnE6cOMFx3KJFi1i8qU2bNvRRsySnTp2irerU1dX37NkTHR09ceJE1vm0R48e165dKyoqWrhwIZsXslmzZn5+fvTjycnJffv2ZZMw6ujobNy4UWLMTjp62yfxT5s2bVq2bJnEPyUkJPzxxx+sv0Pt2rVHjhwZExPDMjg7OycnJ7O3Xl5evXr14vcfsba2nj9/Pn+ZSUlJEydONDMzY3mUlJT69eu3ffv2UrciKSlJeucLfqyB2r59u56e3uLFiw8ePNizZ09bW9ugoCDxJfv5+c2aNYvOOkIIMTMzW7p06eXLl6WXR8Zg36FDh2TpwUHFxcVxHOfr68uePRo3bkwf1I8cOcJ6kDVo0GDTpk10+c+fPy9piLf69evTkMeFCxf4PbWHDRt26tSpiIgIDw8PtkubNGmybNky+jjXsmXL5s2bOzk5bdu2be/evW3btnV3d8/NzeU4rqx1WAqRbwdLLywsXLhwoa6urpWV1YwZM5ydnTt06PDp06dSFyguKSnpzz//pGOK1axZ09HRsUuXLpaWlsrKyvb29uvWrfv27ZvIR16/fs0fJadp06bnzp2jf/rx48ecOXPYEOOGhobz588PCQmhf/Xx8WEtjAwNDZctW5aYmPj06dOxY8eyL8XIkSNv3bqVkpIyb948dhrp3LnzqVOnOI47ffo0G2use/fu4eHhX79+pS0CGjZsGBoa+uDBgz59+tAMWlpac+fOjY2NffToEZucRF1dffbs2ampqSXtkPfv39MYnEAgGDduXGpqqpQt4jju3bt3xsbG9K9mZmYBAQFCoZA2LFJTUzt79qzsRUpISKhfv76JicmwYcP279+/ceNGCwsLdtzfvHkzdepU1vyhbdu269ato39ig/opKyu7u7tnZGSwEF7Xrl0zMjJoNtYmol69eo8ePZJSvanjx483bty4Xr16EydOHDJkSPPmza9evcr+Gh0dPWXKFHbuHTly5NOnT2NiYiZMmMDa/Q0dOlTi+YQRKRLHcVlZWZ06daKJtWrV2rFjh1AopA+QhJC//vpLKBSeOnXKzMzM2Nh4zpw5hw8f/uuvv6ysrO7du8dxXEFBwaJFiywtLWl+IyOj+fPnR0dHBwYG0qH9CCFKSkq9evWiIT82kF+3bt2klJOr8KWqsLDwzz//VFdXp39SVFScOHFiqT+GcRx369atDh06KCoqGhkZDRw4sEOHDlZWVgEBASxDfn7+qlWr7O3t6ZK1tbVnz579/v17+ldvb28dHR0iibOzs/RfxXJzc9nYC23atImLi9u2bRs78aqpqU2aNMnf35/lL/W7UKY6I+Ucm5iYuHTpUto5mhCiqak5ZcqUQ4cO8QuflJQ0ePBgbW3tLl26zJgxw87ObtSoUSwoUNVnKq60mxPpe1Lc3bt3x4wZQ/OrqKjMmjXr1atXsbGxf/31Fzu+HTp0oDth48aNbHaUpk2brlu3jgWjAwMDW7VqpaOj4+bmNn78eAsLiw0bNoj/6Hj+/HnWtktVVXXcuHEXL15UVVUdNGjQ+fPn2VZI38aUlJSFCxfSiwshxNjYeMmSJfSCW5KpU6cqKiqmpaVJ/Kssd0dXrlyh/ZEpV1dXfnSp1NPLly9ffloVLfU6LqWSVEp9kH74pO/JksTExKioqMybN0/8TxU8lclyI8fXrl27unXr8qPYAFAtBFzVT7IOAGWVmJgYEhISHx9fo0aNBg0a2NraVnww45LExsZGRUU1b9683HNr5uTkBAcH16xZs2nTpiKj1MkoKysrLS2N/c7JRweDZ71f5U9BQcGbN2++fv3avHlzc3PzqjvQ1SU8PHzhwoUbNmxISEhISkrK+ldcXNylS5dcXFx27txZ1mVmZ2dramrm5uZ++PAhJyenefPm5RhJUBbSvx1RUVHfvn0zMTGhg6xVxMePH798+ZKcnKyurm5oaGhtbU0b/ZUkJSUlLCzM2NhY4rem6qSlpb17987IyIj12YyNjY2NjaX9pyplFRkZGSEhIQ0aNGCRC+ny8vKCg4NVVFRsbGxoLODHjx/BwcEtWrQo04wZQqGwoKBATU0tNTU1LCxMRUXFysqKtaOU7uvXr1++fLG0tGTDuoeGhhJCLC0t+b8uvH//PiMjw8HBQfYZe5OTkz98+KCnp1em6bllJ14kjuNoor29Pd18oVD4/Pnzhg0b0v5xubm5ampqAoHg69evnz9/rlu3rpmZWblPXHFxcX///ffr1699fX2l56z4paq4uDg0NLSoqMjc3LxMU2Skp6dHRkZmZmYaGRmVacDEf/7558aNGzNnzoyOjk5NTWVnv4iIiDNnzhw5coQON1GSgoKCoKAgNTW1UofPpyrru0Aq6RxbVFQUGhqak5NjYWFRLb35Kn5zUhVycnLevn2rqqpqZWUl5VQQHR2dmJhoY2OjqqpKO2ZKPASVuI3fv3+3tbX18PCgnUyrQqmnF9lV1m1AJV7Hy6HSq+imTZu2b9/+9etXiVfkcp/KynQjFxwcbG9vf/LkSTc3t4pvEQBUBIJ9AABQVcLCwjp27Hj16lWJ04PExsZOnz5d4iyZAPA/YvTo0RMmTKA9NOXJmTNn5syZ8+nTJ/54WMzx48dTUlLoeIIAv4iHDx+OGjUqJCRE+q9N8MviOK537969e/euxHNLWW/kOnXq1LBhQzoQAQBUL3lrQgIAAL+OCRMmZGZm2tnZSfxreHh427Ztf3KRAODXsXXrVgMDA/mL9KWlpU2YMMHS0lJipI/g7Ae/JCcnp4ULF06ePBltQf5LCQSCEydOnDp1Spa5VmRUphu5PXv2qKurHz58uLLWDgAVgWAfAABUlfDwcDoQjMi8dUKh8OrVq9u3b2fj9QDA/5pbt26pq6tv3LixugtS+RISEn78+PHw4UM2DQWTk5Pj6emZlZVFB/gH+KVMmjSpZcuWU6ZMkWXaevgF1a5d+8KFC3/++efLly8rZYGy38gdPnz44cOH58+fl32oCgCoUujGCwAAVWXHjh1z5szhOE5fX79z584GBgYFBQXx8fFBQUH9+/dfvnx5uYffAgD4lQ0aNIh2bbOwsGjVqpWurm5GRkZsbGxYWNjs2bOnT5/OxuYH+NU8ePDgw4cPkydPru6CQDnl5uYuW7Zs0aJFFR/RWMYbuadPn4aEhLBJwAHgV4BgHwAAVKG7d+/u2bPn2bNn8fHxqqqqJiYmzs7Os2fPZjM8AADIH6FQ6Ofn5+vrGxoampKSUqNGDSsrq379+k2fPp1NrQ4A8OvDjRzAfykE+wAA4GcQCoXyN9cwAECpcPYDADmAUxnAfxcE+wAAAAAAAAAAAOSEUnUXAADgPwkEFV0CfsMAAAAAAACA/1VoiAsAAAAAAAAAACAnEOwDAAAAAAAAAACQE+jGCwC/qrL2xq14/18AAAAAAACA/3II9gH8Ei5cuLB27do5c+aMGDGi4kvLycm5efOm7PlbtmxZr1496XnCw8MvXrx448aNW7duSc+Zmpp6586dy5cvt23b1t3dXfZiSPfp06fr168HBgbWrFnTwcFh6NChqqqq7K/Z2dmalbUmuXD48OHLly8HBwfr6uoOGzZs3rx5AvkKhr59+/b8+fORkZHHjh2riuXn5ubevn3777//7ty585gxY6piFXIjPz9/2LBhxcXFx44dq1GjRnUVQygUvn379tq1a1evXn306FF1FUOeFBQUPH78+Pz58zVq1Fi7dm11F6dEMTEx//zzz5kzZ44ePVq/fn3ZP1hFVytCSHFx8YEDB6ZMmUJnrlyxYkVAQMCePXvs7OwqcS18X758uXHjxqVLl1atWuXo6Fgpy5S4Y8PCwsaPH9+uXbvNmzdXylr+l8l+oam689vff//t6OjYqFEjGfMLhcIXL16cO3eusLDQ09OzEkvyvyAmJubSpUtnzpy5fv26hoZGdReHEEI4jgsPDw8MDNTS0mrevHnjxo3LertYVFR0//79L1++1K5du2vXrjo6OlVUVAAoMw4AfgFOTk6EkObNm1fK0sLCwugXXCAQ1KpVy8TExNzcXE1NjSaam5ubmprWrVtXSen/wv3Hjx+XsrTk5OSxY8eqq6sTQmrWrCl91X/++Se7ZVyzZk15Sk/I//37l1Ao3Lp1Kz+0R7ciMDBQ+gf/Z82ePXvmzJkcx/n7+ysqKhJC/Pz8qrtQlSY2Nnbo0KF0uxwdHatiFSdOnLCxsaE1zdPTsypWIU9u375N99XZs2erqwz37993dXWltUJJSam6iiFPrl+/bmtrS4/smDFjqrs4khUUFEyYMIFengghX758kf2zlXC1KkFcXFzfvn1v375N3+bl5SkrKxNCpk2bVolrYWJiYsaPH8/i7I8ePar4MqXs2KVLl9LEpKSkiq/of5nsF5oqPb+lpqa6urqeP39elswvXrzo3bs3DQYNGDCgcksi92bPnl2zZk16xLOysqq7OBzHcW/evGnXrp27u/u6desGDRqkrq5uZ2dXpnPIP//8Y25uvmXLlhcvXpw4ccLOzm716tXFxcVVV2YAkB2eigF+CadPn7a2tvb19a2Upb1+/ZoQ4uLiEhUVxRLZT/0sJT09ffHixYSQgwcPlrrMrVu3yhLs4ziuqKhIT0+vEoN9q1ev1tbW/uuvvw4fPrxixQrWMsLIyCgtLU38g9nZ2eVZr7zw8/MjhAQHB9O3O3fuNDc3DwkJqd5SVbqpU6dWXbCP47iioqJatWoh2CeLnJyc3r179+zZMz09vXpLsmPHDgT7Klfbtm1/5WAfdePGjXIE+7iKX60kiY2NtbKyevz4MT9xwYIFDg4OL1++rKy1iHv79m0lBvsoiTs2JCSkZcuW9PckqKAyXWiq7vyWm5vbrVu3CxcuyJjfxcUFwb7yOXPmzK8T7Hv9+nXr1q2Tk5NZyrNnz3R1dQkh586dk2UJ/v7+Kioqd+/eZSnJyclNmjSZM2dOpZcWAMoBE3QA/BJ+++23t2/fjh49ulKWlpubq6end+zYscaNG0vJpqOjs2bNmi5duuTm5pa6zLp168q4dkVFRUNDQxkzl+rTp0+7d+9+8+bNpk2bxo0bt3z58pcvX86cOZMQ8u3bt23btol/ZNSoUZW19nIoKChYsWJFNRbgwIEDhJA6derQtzNmzPjw4YOVlVU1FqniTp8+zR5lKfqIXnUUFRVr165dpasQt3Dhwp+8xkqhrq5+9erV69evV3vnnQYNGlRvAX4FUVFRhw4dqqylyX7mr0blLmTlXq0IIRkZGd27dx89enS7du346evXr3/16lVl9a6VqCoqv8Qda2Vl9eLFCxp4ggoq04Wm6s5vampqJ06cmDZtGv2puFT/FaeFX8HPv3WRHcdx7u7up0+fZreLhJDWrVvTprsTJkyIj4+XvoTs7OypU6e6uLh07tyZJdapU2fWrFmenp4PHjyomoIDQBkg2Acgh3JyclxcXGQcPGvo0KE5OTmlZivTEB50lKJK4e3tvXnz5iZNmrAURUXFzZs3m5iYEEKePn0q/pGHDx/SgFe1mDVr1vPnz6tr7bm5uU+ePCGVegiq3adPnyZNmpSXl8dP/AlDEP7kUQ5Pnz6N8Y8qSJ6qffnk5eUNGTLk+/fvlbXA/4qxPitSyMqtM7NmzWL//2RVUfn/K47+fzvZd3KVnt/09PTmzZs3bNiwrKysUjOjYsiium5dZBQREfH+/fu1a9cmJyfz08ePHy8QCNLS0q5evSp9CVevXv369WuPHj1E0gcOHMhx3M6dOyu5xABQdv/rt8UAcsnCwmL+/PkyZh48eHCvXr2qtDwVkZaWNnz4cJFEFRWVTp06EUIk/vA4bty4OXPmlPqbZFXw8fHZv3//z18vk5CQwJV1FuNf248fPwYNGpSZmVndBalaISEh48ePr+5SwH+9yZMnv3nzprpL8T8qICDgyJEjc+bMoSP0Afx3GT9+/NevX5csWVLdBZEHv/6ty4cPH378+HHw4EE69gujpaVFp+x7+fKl9CXQbv7GxsYi6fXq1VNUVPT39y8oKKjUIgNAmWE2XoBfglAovH///o0bNzZs2MASCwsLAwICAgICduzYUVBQcPDgwbt379atW3fkyJEiXYRElGk6wjp16tA2/M+ePbtw4cK3b980NTVtbGxGjRqlra0t8SOxsbGHDh169+5dkyZNevTo0bVrV9lX9+zZs+vXr79//75Ro0ZOTk505BcpSmqjp6+vTwiR2E/Z2dl5w4YNGzdulLGd1NOnTx8/fvzmzRsdHR17e/sePXo0bNiQ/dXT07O4uJi+njdvHiEkNTXVx8eHZRgzZgwd4uTgwYN0ILnPnz/TIQ7bt2/fpk0bQkhCQsKxY8datmzZqVOny5cvX7x4MTc3t23btsOGDRPvDpOdnX3s2LE3b95kZWU5OjoOGTLEyMiInyEtLe348eMWFhbOzs4+Pj7Xr1/v1auXnZ3d7du3ExMTaZ69e/fSpp1OTk6tWrVin42MjLx169br169zcnLs7e07depkb28vUoDIyEhvb+/p06fr6Ohs2bLl48ePU6ZMoXPIJCUl/f3339ra2n/88UdGRsa+fftevHhRu3Ztd3d3BwcHQkhOTs7x48cfPHggEAj69OkzbNgw8b39999/Z2Zm1qtXz87OTl1dvWfPniUdmvj4+EGDBoWGhhJCjh8//ujRI4FAMHfuXPFsXl5eISEhdevWHTJkyP9j77zDqsi9h58LSG+iFJWiiDQVUVERK6JiLyuKXSzYy669rF2xK/YuKipiQSkiKnbEhiIgvUqRXqSXy533j/PbvPOde+8wNHHdfJ599sHcJJNkUk7OJOeI7JC17Xh0KIry9vb28vIqKyvr27evnZ2d8E0ciqLu3bsXGBiYnJzcuXPnESNGQINwqfvbt2/Hjx9fUlIiJSUFPUdXV3fixIniyiMQCJ49e/bs2TMnJ6f09PTz58+Hhoa2bdvW1tZ2yJAh9Jgiu8rs2bNrbJa7d+8mJSXhaKampsOHD4e/jxw5IhAIEEItW7bElgcyMjLc3d1NTU0ZBUANN76AlJSUCxcupKWlNW/evHPnzoqKiuPGjWM58HLhwoUfP37gf2pra9vb28PfJ0+ehDMXbdq0Ee6omMrKSm9v7+zs7IULFwYEBJw+fVpfX//vv//GLoO+fft2+/btL1++KCoq9ujRY9q0adgbEhAWFnb58uWNGzfKy8tfuXLl1atXCgoK/fr1mzJlirBWiMsIFflaZ8yYsXDhQti2vXr1CvwvTZ48GS8HNU4sgL+/v4eHR2ZmpqGhIUxoXBAIBAEBATdv3jx8+LCsrOyLFy9cXV0LCgp69+69bNkyaKvg4ODr168nJiYaGhouXbpU5DrF3lswOTk5Li4uQUFBCgoKgwcPNjU1FVewug18iqJu3br16tWr6urqdu3adevWTUlJCSZzFsBhsfA5F4RQaWmpl5dXfn4+3edvbZf4T58++fn5hYSEqKqq9urVS7inMaCPYnt7e3jdp0+fxmf5bWxssA8WgGPDfv361c3NbfHixfSXmJSU5O7uDrNfaGjo2bNns7KyLCwsVqxYIVzO4uLiy5cvBwUFSUpKdunSpX///l26dOF+6In9tdaqYWtckmocOPVfFgEuC404apyFSktLz58/Hx4eLiMjY2Rk1KFDB2NjY4aaRkVFxcLC4uzZs+vWreN+vf3z589Xr15NS0vr1q3b2LFjcZ+JiIh4+PAhPebEiRNhOAcHBz979owRKJKoqCg3N7fRo0dbWFjExsaeOXMmPj6+Q4cOK1euhBKmpaVdvnz5y5cvGhoaM2bMEDlI6yn2IG7rOx2OoktZWRmsCJKSktbW1rNnz2YMgerq6nv37gUEBGRkZLRu3bpnz5729vb0OHXeJiCEunfvrq6uXlZWRhcRcbaIw0nSx48fI5rRGIyEhISGhkZ6enpoaGij2i4gEAg103TmAgkEAkVRVHx8/MqVK+EzmpaWFgSWl5cvWrQIDGC1bNkyOTm5a9eubdu2hTVVSkrK19e3tg8SdtCBqaystLOz09XVff36NZ/P//LlS6tWrTp27JiSkoLjXLt2DSGkqqr6+PFjhhLwzz//5PP59Axh/8AweV5UVDRnzpxp06bdunXL1dUVyjNp0iSmlWJuTnVhx3727FnhhMXFxVJSUnJyckVFReyZFBYWzpkzp0OHDg8ePEhOTr5x44aBgYGiouKZM2dwnNLSUqwdgBCBQJCRkbF69WoI/Pr1K0VR5eXlzs7OIF0ZGxs7Ozs7Ozu/f//+zp07gwcPBid6Fy9etLOz4/F4eIevq6sbEhJCL5Kfn1+vXr2OHj16//79TZs2SUtLt2jRws/PD3719PQcN24c7JwvXLiwZs0ayEdSUvLly5fOzs7r1q2DkC1btkAZsEl4gUBw+PDhFi1anDt3Likp6dGjR7a2tjweb+nSpeDShM/nHz161MrKCkTJr1+/YknX3Nz8xYsXtra2IPytXr3ay8tLR0enW7dusFuQk5MLCQn58OFD+/btTU1Nu3fvDpkcPnyYXruDBw+amprGx8dTFBUeHm5sbNyjRw+WF/TixYsdO3bgbubs7Hz06FH4CQwjWlhYvHjxokWLFoaGhjCIJCQkbty4Qc+Ea8cTwsDAACG0f/9+xk5SR0cnODiYHjM2NtbGxmbjxo33798/fvy4lpaWpKTknj17ONb92rVrYGhSSkoK3po429iZmZl//vknbLANDAz8/f2bN29Od1S9cuVK8ILH0lVyc3NrbJaKiooZM2ZAkhUrVlRUVOAypKWltW3btnPnzt++faMo6sqVK0OGDIEeTh84VIOOL+DVq1ctW7b09/enKCo3N3f8+PHofx3y3L9/H/2vAfusrCwbGxvI6syZMwKBAP9UXFzcu3dvY2NjOA8rzJcvXxwdHUHVOH36dE9PT+zB/P79+xDnwIED1tbWly9fvnPnDpxB7tKlS1xcHPy6e/duMzMzSBIYGNixY0dpaWm8VbO2ts7Ly8OPq3GEsr/WoKAgZ2dn+HgwatQo6EhpaWmQkH1iAaqqqpYuXaqnpwdffby8vLp06QJmwtgddKxatQq+viCEUlNTp0yZ0qpVK0tLS2lpaYTQ1KlT+Xz+8uXLVVVV+/TpAyXU09P7H/dK3HoLEBQU1KZNm+XLl4eEhERFRS1btqxjx47wdLofCY4DX3i1EggEY8eOtbOz+/Hjh0Ag8Pb2VlRUXLNmDUsLUP/4xzAxMWGEv3r1avr06QoKCgihyZMnQ2Btl/jCwsL58+d379793r17cXFx8FFQR0cnNjYWxykoKIBGwA46qqur8RevFy9e4Gi3bt2CTnjixIlaNWxhYeG+fftwl46KioKEvr6+WLNw/Pjxw4cPKysrm5iYwLTQp08f+rijKOrTp096enobNmxISUnx9vYGlZaSkpKZmZm5ubmXlxdLO7O/1to2bI1LEvvAaZBlkftCIzy/AeyzEEVR2dnZ+vr6+/bt4/P5FRUVu3btQgg9ePBAuHnXr1+PEPr7779ZXgFFUfPmzUMIjRs3zsnJia57kpWVvXbtGsQRCAS3b9+GPmBkZMQY7x4eHgiho0ePMvoG5sKFC9hVsaen5+7du9XV1Xv37g3+TDp06FBSUuLi4qKmptazZ09oQ1lZ2cDAQHom9RR7IBMu6zsDFtHlxYsXEB4dHW1mZqapqWliYgIhCxcupGeSmppqYmJia2sbERFRXV19+/ZtHo9nZ2dXVVVFNdA2oaysrLS0lBGIb/WePn2aJW1FRQVEi4mJEf7V2NgYIcTd5QuBQGgkiLKPQGhisrOz4+LiRowYgWjKPgBc5crJydnY2Hz+/JmiqIqKCrhy271799o+iEXZt3//foSQo6MjDtm6dStCaNWqVTgElH1ycnL9+vV7+PBhXl7ey5cv+/TpA3kePHiQnqFIZd+oUaPoO8a8vDwQUzZs2PA/peGg7KusrFRXV2/RosWPHz9EJgSPvd7e3iyZCASCfv36KSkpZWVl4cCkpCTQGR04cAAHuru7CzddVFQUBNKVEQsXLkQIDRs2DIdkZ2ffvXsXYurp6R0+fLioqKi4uNjFxQV2wgYGBlhV+ubNGyUlpejoaJx8y5YtCCFlZWVwl5aYmAjiPkJo+PDhhw8fDgsLs7a2trCwgEy+fv1K35vRge5E396Xl5fDIbiRI0dSFFVdXR0bG4vVhfb29o8ePfLz82vfvj04XiwrK4M3q62tvWLFCmj8ioqKXr16IYR69uxpaWmZnJwMmZ88eRK6NAimFEWlpaVJSUmdOnUKF+D9+/dmZmYs74iiqIyMDCjPu3fv6OGg7NPS0ho2bBjsePl8/rhx42AbQI/JteMJAfsHPT291atXJyQkJCcnHzlyBNQ9mpqaWGWQk5Ojp6dHd6WNzyw8evSIY91v3rwJexX2IpWXl0dFRcHBEyUlJVtb20+fPgkEgsjISHye8cqVKxSHrlJjs+Tn50PIihUrGMUwMjLCG/KYmBhfX1/YKdH1Mo0xvkxMTCZNmoT/WVhYqKmpSdeXidwMR0ZGQvGE3Y7r6+u7ubmJa+309HRQfyCErKysFixYkJCQMH/+fF1d3cjISIqiDhw4YGRkRNeE9u/fHyLj+sLmGSFkYWHh6elZVVWVkZHx119/QeDs2bNx2hpHKMXhtcIWa+/evfSK1DixAPb29oqKiqDDBaKjo0GxWKM3Xjc3NyjVwIEDQRtL/fM6eDyejY3NkSNHQA3948cPsMFK1zRx7y2fP3+Wk5NjuIJ1cHAQnvc4Dnzh1Qo6ZEREBA7Zv39/jc5nYRm1t7dnhCcnJ3/58gW0P1jZB3Bf4qdMmdK9e3c85wgEgg4dOjDWGmFlH0VR+Kw3VvYBWlpajFfApWFLS0sTEhKg8yCaso+iqJycHDhLbmho+Pfff5eXl1MU9fLlSxh69LW4uLjYwMCgQ4cOWMuDTYM5Ozv7+/t///6dpZ25vFaODVvjtMxl4NRzWaQ4LzSUmPmtxlmIoqhFixZpaGjQUw0fPtzDw0O4eT09PYW7nzCg7NPV1R05cuSnT59AzoFOxePx6F6n4RijtrY2I4crV64YGxuL0/QBKSkp0A4mJianTp2CCSQ1NVVRUREhNGjQoMmTJ4OuSiAQTJo0CSFkZ2dHz6H+Yg+X9V0k4kQXrOyztrbGU+XBgwcRQpKSkvTpF/YF169fxyFgvoY+mhp2mwAcOXIEeun/CNhCYJ0g6MoZwHcCkEYIBEITQpR9BMIvwfHjx5GQsu/OnTuwlMJJHMDLywtkgsrKylo9gkXZN2fOHPS/HxXPnz8PshQOAWUfQoj+ubioqAiubKipqRUUFOBw4e2Tt7c3QujLly/058KJHnl5edgY/B8clH23b99GCLm6uv5PKC0hyDp//vknSyZwVfCvv/5ihIOTQXl5+cTERAjBu2t6NHw9il3ZB8CFGob2E1v3w3KkhYXFuHHj6HEiIyMhzs6dO3Eg7G2Ed5WUeGVfVFSUtLR0165dGfGxnzh3d3cIwQa/RH7UBcEdqx4A7JaRrjjAO098dBG67sqVKxkZCj+FDruyT09Pj74XevnyJUTGCqBadDwhYA82Y8YMeuDFixcZb2TVqlVqamqMTQscdLKxseFYd47KPgDO9aioqNAPtZWUlIAOpX379rAposR3FY7NAqY/W7duTT+6GxIS0q5dO0Z94fgbXdnX4OMLbuN269aNHm3lypV01YC4ky9wZKZXr170wHfv3qmqqpaVlVGsjB07Fnoao7dkZmbKy8s7OzvTA0+fPg3Ffv36NX4KhNBVMNQ/Q0lSUhJmVO4jlGKdAUQq+7hMLOA5ccmSJYwM+/btizgo+7Bylr7PFwgE4GmUke2ff/6J/jHiDnDvLTY2NlJSUvjEIuDv78+Y97gPfOHVCm7b0c+X5ebmCrcMg1GjRiGEFi9eLPLXCRMmICFlH8clHiYHxjFMOLI3fPhwHCJS2YcDGcq+tm3bov9V9nFsWIqiwsLCIJCu7KMoCny+L1u2jB4I9yW3bNmCQ65fv44QondIgUAAytAdO3YItdz/wPG1cmzYGqdljityfZZFivNCQ4ma3zjOQt26dZOSkmK0BuMIPADzFY/Ho39EEQaUffr6+nRBNDIyEj5h4oWPoqhPnz5BYR4+fEjPYdiwYfv27WN5BAAKRLq6n6IoONNtZmaGVzrqn8Zp3rw5DmkQsYfL+i6SGpV9dNGxoqICpES6Bhbc0N28eROHTJs2jTFMGnabQFFUQUEB3KqmrzgiiY+Ph0fjyZkOnP89duxYbQtAIBAaFmKzj0D4JRBpzxtfGYP9EgAnHaqrqzMyMuCCVf3Zvn17p06dpkyZgkMoikII5eXlMWKqqKjQHeMqKiquWLFiwYIFeXl5AQEBI0eOFPcIFxcXHo936dIluhEQ2M+XlpaGhIQIGw0RR2Vl5aZNm6ZMmTJ9+nRxcVRVVRFCycnJLPkcPnwYISRsumXGjBnr1q0rLS11cXHZvn07x1KxA68S9lcYBweHTZs25ebmPn36dNasWWFhYWDACJ/6QQgJBAIej0dRFPjYBaC31MpU4smTJysrK4Ura2Zm1rNnzw8fPpw8eRI+jOOuKDJ/qAjjHjf0SfS/pltUVFSkpaUrKyszMjJA7INfjx07ZmJiAlsFhNDOnTu510IYdXV1uB8H4BZOTU2Fyz7173iMRps1a9bWrVtTU1M9PT3hrtOVK1fk5OQY5nigod6+fUtRFI/Ha9i6Q+bq6ury8vI4UF5efuHChWvWrImPj09KSoJ9griuwrFZli9ffujQoe/fvz948GDMmDE4LXjro2coPIM1+PhSUFCQk5P7/PnzwoULnZ2dYWu0atUqGOzsrF271tvb+/3792/evMHnkd3c3KZMmcJu9Qz9UzUrKyv6dWmEkLu7e2lp6evXr+nGDVNSUuCPwMBAUJPhaZwx/FetWnXz5s3q6uqXL1+2b9+e+whFtZwBOE4scGQJzpLQEbbHJBJcTfrkwOPxWrVqlZeXxzA9Bjd58WYYce4tL1++fPr0aY8ePfCcI66Q9Rn4kJujo6Onpyecz1JTUwMFJQvfvn1DCMG0I0x9lvgNGzZIS0szrGGuWLFiwIABLMYKawX3hkVi6oL+qQ7dwiZCqHXr1p8/f05NTcUhnz9/RgjRHRfweLx+/fp9+/bt+/fv7OXk+Fo5Niz7tMx9Ra7PsohhX2jENQjHWahly5Z8Pn/UqFF3794FU3e2trb41CcdmE4pisJrKAudO3emdwZjY+MJEya4ubk9ffq0uLgYzt9169bNxsbm6dOnhw4dwr7gcnJynj9/jhWaLLC0rZqaGr0bwKySn59fUVEBc3X9xR6O63uNtRAJfUWQlpZu1apVYmIifaS4ubl9+vQJNJu4POh/JfMG3yYsWrQoOTn58OHDeLkRB5/Pp5eKAQSC7T8CgdCEEGUfgfAvAy/tYCO/QdDW1sbibFBQ0IULF+BjLF7LMcJizR9//LFgwQKEUFRUFIuy7/Xr13JycgyDwdiUcq08iuzZs6dly5Z0E/7CgMCak5MjLgKfz4+OjkYICRvAbt68uZGRUUhISGhoKPdS1QEZGZkBAwZ4eHjExsYihF6/fo0QMjIyYliYhkYT1miAKRyOwIk/kda+e/fu/eHDB+HKcs9f3N4PdjX5+fnwT0tLyz59+rx588bR0fH69esnTpzo2LEjHGpoKLA6BvuAq3/HY/R5SUnJsWPHnjx5Eo4yRUZG5uTkWFhYMN4a/iefz2/WrNlPqDtCyNbWFoy4xcbGgrIPl5kRk2OztGnTZuzYsR4eHufOnQNlX1VVlbu7e1BQEHtJGmN8wa7bycnp7NmzT548OXr06KhRoxi6CXH07dvX0tLy3bt3Bw8eBGWfQCC4desWPlTI5emMEBiwPXr0oO/ZLC0twbOKoaEhe4YWFhZqamp5eXkw/BtvhHKZWAQCwfv37xFCLJby64bIyQGO/+CZgXtvefv2LcdC1mfgz5kz58CBA5mZmVZWVo6Ojk5OTmpqajWO1tzcXCRe2ccdxhIfFxcHLk2EjeUzfGvUB+4NW1ugOnRxBd4+dHsMPFqkxy069XmtwrIT+7Rc2xWZAcdlEcO+0IiD4yy0cuXKJ0+evH371sjIaNu2bcuXL5eWlhapBsL1YhGfxJUZIfTHH3/Ajf7o6GjswmLNmjVPnz719/f/8uULdNrbt28PGjSI4+wtDMusghAqKCiAk3f1n1Q5ru91qwUDkF7o7mt79uwJyuvq6mofH59bt269efMGiZLMGdR5m3Dt2jU3NzcnJye6glsccG0fiif8KwTCIXQCgdCEEGUfgUD4P3x9fQ8fPmxpabl9+3YfHx/8oZudli1bysvLl5aWwlZHJFVVVdnZ2Twe748//qinYOTr6+vh4fHs2TPGKRsGIPZh+8HCJCQkgFBFP12C0dbWDgkJYexGGgNw6ldWVoYQgkMNOjo62F9BAwIbBnGVRQgVFBTk5ORwPMVTN3g8npeX1+jRowMDA1+8eGFubr5y5cqdO3diGb3BacCORwdunJWUlJSXl8Nbk5GRYX9rP6fu2Eck9Chx1KpZFi1a5OHh4efnl5qaqq2t7ePjI3z8R5hGGl87d+7Mz88/ffp0QkLC6NGjR48efe7cObjnVSNr1679448/vLy8YmNjO3To8OLFCxUVFe4HioWBV9+9e/fBgwfXLQdtbe28vDx4WY03QrlMLElJSVAM+jnZnwb33hIREYE4FLKeA19TU/Pp06cjR45MT08/e/bsnTt3Dh8+DC50WBB5vKX+wPl0cBvdeHBs2AZh9OjRe/bsSUlJef78ubW1NQRmZGRISkoK+/Km0+DzOfu03KgrMhfoC424A8gcZyFbW1sXF5cFCxYUFRWtWrXq4sWLLi4uImc/vCSxiE81lhn9o/vGTzczMwsNDT106JCrqytCyM3NbenSpXXIv1bUf1LluL43HlVVVefOnbt+/frcuXMvXrzo6OgIJ4gbg6CgoKVLl547d87R0ZFLfKzIE6l8hECsECQQCE1FDU61CQTCf4GioqIJEybMmjXr2LFju3btwn4VOQK3J8Bej0h4PJ6kpCRFs/FRN8LCwjZv3uzn58e4KCQMfDZnkeHwT/RLExj4FMy4dtcYwD0XMLMFn5Tj4uIa40FQX5bKKioq1tiq9UdNTe3Vq1cHDhxQUFDg8/n79+8fOHBgcXFxIz2uoToeA+jwBgYGsrKy8NbAQDV7qp9Qd7xXhx4ljlo1i42NjaGhYXV19aVLlxBCly9f5rITaKTxJSEhcerUKR8fH9hSent7d+nSBVvRYmfs2LHgEwCsj7u5uc2ePbu2BaBT/wFLH/6NN0K5lBNf6BNZgMaGe2+BctZYyPoP/K5du0ZERMB19dzc3FmzZi1fvpw9CdQCG2VrKOBAX0pKSqPq+zg2bIPQu3dvcOHq6OiYmJiIEAoNDb19+/b+/fvBs5Y4GmM+Z5mWG3VF5gJ9oREXh3shZ82aFRwcDK5mIyIi+vTpgz0j0cFHDuv25Q/KjIQEQjhy7u7unpqampqaGh4eju1CNB71n1S5r++NQUxMTNeuXW/cuPHgwYO5c+fWaHGiPsTHx9vZ2V25coWjpg8hJCcnB0uYsMEf9I+2F+6MEwiEJoQo+wgEAlqwYIGHh8fevXvrZgAIfHIxDNDQkZKSgks6Hh4eIiM8evSoxqfEx8fPnTsXW5xhB4QPlphqamrwKzbVTAdq1Llz5xofVE9ADDUxMUEIgTFEX19fkZu6mJgY2BfVDZC8v379KnytAyrbqVOnOpue4Uh+fn5lZaWkpOTq1atjYmLAYv3bt2/B3URj0CAdTxh6h4e3lpGRAffgGJSXl4Pfg59T97S0NIRQs2bN6IY1halVs/B4vEWLFiGELl68mJ6eHhISMnz48BpL0hjji8/nw/5h5MiRUVFRW7dulZCQyMrK4rg5kZCQALtLly9f/v79+/3791mMfnIBGllcGz579qzGy1b04d94I5TLxIIvfcN12p8M994C5YyJiWHfe9dz4IPmS1VV9cKFCx8+fICRfvz4ceytQiRQhQZX9hkZGSGEKIp68OCB8K/5+fklJSX1fwrHhm0oNmzYcO7cubKysgkTJowePfrgwYP+/v4Ms2jCNPh8zj4tN+qKzIUaJSvEeRaCLm1iYvLq1Ss3Nzc1NTU+nz9v3jzh7ooVN3VT00CZ1dTUGKe/J0+erK2tXVVVdezYsZs3b9rb2zeq6gqo/6TKcX1vDEpLS0eMGBEeHn716tX6GwdgJysra8KECZcvXwZvVEBubi64QWMBbmpnZWUxwisrK3NzcxUUFOpzdp5AIDQIRNlHIPxXECfE5+bmgo2VLl264MAat6mY9PT0kpISCwsLdkUhbGiPHz8uLFy6u7uzW6VBCH3//n3q1KlXr14VPg0EW3dGIAis2BK/SODai4+PD91SOJCYmMjj8fCXZywO0pultLRUXM7cWy80NFRKSgp8+UETFRUVgZtFOuXl5X/99RfH64oigcpmZGQ8e/aM8RPsWMaNG1fnzDny7Nkz/PTWrVvfuXNn7ty5CCEwQ1Mj3FuVTj07nkjgRuGsWbMQQtra2vBxe8eOHcIxV65cCddYONa9bnXEgAWiyZMn13jHrVbN4uDgIC8vn5yc7ODgMGPGDI6m4hp8fP348ePAgQPwt6ys7LZt265cuYIQevfunUibQcLMmjVLQ0OjrKzMzs6Oy2VkdqANnz59SjfVD8TFxV25cgUbThJJbm5uWlqavr4+mABr2BFKb0kuE4umpiaccBFnMr+xdUAcewusMqmpqY8fP2YvZ30G/t69e4uKiuBvCwuLgIAAWBzZZ6r+/fsjbpbOakWrVq3grP3WrVvp9rwQQgKBYOnSpeydHw8uhqV8xvji3rANwufPnwMCAqKioj5//uzt7X316lWOSoGGnc/Zp+VGXZG5QF9oxMFxFtq0aRMOnzx5cmBgoLy8fHFxsbB6HWQnXV3dWllSZpTZwcGBES4lJQVm4M6dO+fi4iIcoTGo/6TKcX1np27L+sOHD+Pj4xUUFOif7uopIYikpKRkypQpJ0+eHDhwID388+fPcnJy7Gnhyx98ZaQDxgesra0bz0gLgUDgCFH2EQi/BCDEM8RxkUI8FvdrK3zjC4MMe174jkNwcDAOBGGXsbUQyblz5yQkJE6fPk03Hw4lp39NBTkvIyNj9OjR+MoYRVHgY3HhwoUsj8jNzR0zZsy2bduaN2+eSSM2Nvbs2bO3bt1ibNopigKJE9sDEsmePXsUFRXLyspu3rxJDw8ODo6IiJg5c2bv3r0hBJuhCQgIwNFu3LgBf9BvYoJDNKx8ZEhmjCMYHz9+/PLly9y5c+HsRo8ePUA7uWXLFtBiAGlpaba2tn/88QcWvODVi+we2M4O4905ODhYWFgghOAyJqaysvL27dsdOnTA9phxvxKZv0h7zziQUV8Ip3fU27dv0yOAXUh2o+zNmzeH/SqjVWGwMIYMrjUuSX06nkgKCwvd3d3Hjh07evRohJCEhMSKFSsQQo8ePVq2bBk+A1JaWrp8+fKioiJsRJ+97tBz+Hw+nDhAYtqfjrA67Ny5c9LS0vRtibiuUqtmUVVVBVfdT548gc2wMMIzWGOMLw8PD3pdpkyZoqCgoKuri/WP8Ku4uVFOTm7dunUIobdv33LfbYprwzlz5igrKwsEAjs7O7rHknfv3tna2q5du5YRnzH8L126VF1dvXv3btDMch+hLEVCoqYgjhMLdON3795du3YNx+Hz+eHh4QihHz9+sDQREm8JHsIZ41R4ZuDYW2bMmAEV3LhxI71IX758gT9wIPceLrxa8fl8uucWJSUl+B7DPlMNHToUIQS+rYSpzxIPmprw8PBBgwbhfXVeXt6kSZP69+8vbCafXhdlZWVwuUAfXE+ePIF5Bg8u7g1LLyH36tDfdXJysq2t7fTp0xkuVrnA8bVyl51YpmXuK3I9l0WRMBYaer3oaTnOQh8/fqTbejMyMoL7vMJdOiYmBiFkY2PDXjxxnDt3rk2bNiLdrC9cuFBTU/PHjx8CgYD7gS+WthU5qyBa+9Rf7OG+vgvDLroIlx/6J+4tIJmXlJRg47YlJSUgpdOlu3puE/h8/rRp0+bPn6+trf3tHxITE8PCwi5cuED/hB8dHd27d+/p06fT3whcLvb19WVkC98MYNokEAhNDEUgEH4BQOaQkpL68eMHDjx27BiM04SEBBwIp/AQQu/fv+eef0ZGBvZo8ezZM/pPpaWlsBlQUVE5fPiws7PzwIEDwRi5pKTk8ePH9+zZQ1HUw4cPEUISEhKBgYE47cOHD1u2bHn69Gl6hhUVFXBIZOHChfRwfEFAVlZ2yJAh48aN09fX19TUjI2N/Z+yIvR//1EURVGFhYU9evRgmcSuXbvGSAjbrWHDhtXYLNevX5eVlVVSUgoODsaP69OnT69evbKysnC0yspK8NCnpaV19OjRI0eO9O3b9+jRo/DNfMCAAVevXoWYeKd67ty5p0+fTp06tbKykqIo+DjcoUOH6Oho/EZMTEwsLS2zs7PxgwIDA/H+wdDQ8I8//hg4cKC0tPTKlStxnKKiItAOrFq1SrhGuHs8ePCA8VNISAgYpb5w4QIOXL58eatWreh9yc/PD3Lw9vYWzh9OrwwfPpweePLkSUgSFRWFA/HmcPfu3RBy584dSUnJt2/f4jhXrlxp1qxZjT0ZjJpZWVm9e/fOycnp+vXrFEXBeQc1NbWqqioc8/nz5/DQu3fv4kCuHU+IXr16IYTGjBlTUVEBISUlJWPHjrW0tPz+/TuOlp+fjw+camhojB49evjw4S1atOjbt29paSnHuqelpYHGauHChYGBgfPmzcNdRZiDBw/C43bs2MHn8yHQ2dlZWlr60qVLOBp7V6lVs8CYGjp0qMjy5OXlga5/7dq19PCGHV9wZmr//v04YVJSEo/HO3LkiHDLZGRkiCxqaWlp8+bNVVVVy8rKREYQBhwvdu/eXfgnMP+HEOLxeJaWlhMmTOjevbukpOS9e/dwHLwDHzNmDJ7eAwICFBUV58+fLxAIcEyOI5T9tYK+o2XLlk+ePLl58ya8ES4TS0lJCXx1kJCQWL169bNnzx49ejRy5EhQxUpLS8+aNev169fiWgmfnaEXVSAQwKm0yZMn0yPDhU15eXn64OXYW86fPw9b6C5duri4uHz48OHIkSPdunWDp3ft2nX9+vUQk0sPF7laLV26VFdXNycnB4fMnj1bT08vPz9fXPUBWKrS09OFfwJrdD179qQHclziKysrsfdPHo9nYmLSu3dveXl5BwcHem64p928eZMeDlOltLQ0XJ6dMmWKvb09nOLR09M7cuRIUVFRrRrW09MTAn19ffFTqqurwQ7azJkz6U+HTkVfMk6fPo0QkpSUNDIysrKysrW1tbOzc3BwcHJyCgsLY29hittr5diwNU7LXAYOVb9lkeK80FBi5jcus1CnTp3GjBmDh1tVVZWhoeHYsWOFmxd0i+/evRP+iQ58NWnbti29JLt3727dunVAQIC4VHA/et++feyZY0pLS0FwZSQBX8Nt2rSprq7GgV5eXtAOb968wYH1F3u4rO/iECm6uLi4QG6fP3/GMUtKSuTl5RFCy5YtgxB8VNPU1PTChQvbt2/v16/fsGHDEEK6urouLi6XL1+m6r1NYD83CsYKgY0bN0Lgx48f6TmcPHlSSkoKT9oURZWWlnbs2HHIkCHsjyYQCD8HouwjEJqYZ8+eLVy4EJsvsbS0PHTokEAg+Pvvv7GB5F69el28eJGiqD179mDvGaamphDIzqdPn2bOnEn3uSEnJzdx4kRXV1ccx8PDA7RRCKF+/fqFhYXl5eXBFkhNTQ1v8M6ePWtqasrj8YyMjCZOnNi9e3dLS8svX77QH3fw4EG4lQay+KJFi168eAE/8fn8jRs3Yh8CPB5vypQpSUlJzBL/r7LP1taWRRZp3rz5/9+0/5PQyckJIfT06VMu7R8aGmplZaWsrDx27NgFCxaYmpquW7cONHR0Pnz4gM8faWlpeXh4UBQlJSVlYWGxf//+xMREiFZRUYHPK3Xp0gWLX9C8K1asMDY2trGxGTVqlJqamqOjY3l5uXB56PeptbW1L1++jDUChw8fxpbOpKWlZ8+efevWLfjp48ePK1euxNamtbS0Vq9e7eXlRc88Nzd38uTJSkpKffv2Xb58ea9evcaNG5eZmQm/FhUV/fXXX9jAtrq6+qpVqz58+AC/Pn/+fM6cOfCTpKTk4sWLAwIC8vLyNm7cSLfJDUL55cuXcTu0aNFiw4YN379/v3PnjqGhoZ6e3rJlyy5cuLBx40ZjY2NGCUVy9+5d0G7IyMg4OTmlpKTMnz8f31QdOHAg7Gq2b98OFp0QQq1btz58+DAk59rxhIiPj58yZYqioqKSkpKNjc3o0aPbt2//999/0zUUuGHHjRuHr23Ky8tv3ry5uLgYR+BS91WrVuGW9/PzYykYbPl0dXVXrVrVrl27SZMmderUSV9f/+XLlzgOS1epW7P06NHj/v37jMCKioqdO3fC6Qmo+JIlS+hyfwOOr5ycnFatWrVv397Ozu7UqVMHDx7s2LEj3jMHBwcvWbIEz2Pdu3fftWuX8IMoiho5cuSiRYtYmhfj7e0Nmytg+PDhwtvUW7du0WfXXr16Mfa6WAXj5OSko6Mzbty4/v37t2zZ8tixY8JPZB+hFIfXGhMTg28X2tvbl5SUQDj7xAL8+PFjypQpeGS1adPG09Nz3Lhx2traa9asYUz1mIqKil27doG6FiFkZmbm5OQkEAg8PT3xoSQpKakFCxZ8+PDh27dvy5Ytg20tQsjW1vbOnTs4K469xcfHBzuebtas2cSJE1+9eiUpKWlra3v16lVQXVEceri41WrZsmVdunQxMjLaunXrxYsXZ8+ebWlpGRERIbL6dLy9vRFCbm5u9MBLly6NHz8et/zEiRPv3btX2yW+vLx80aJFWPGkqqp64sQJrOnIycnZuHEjVkm0bt16w4YNePXJysoaMGAA/CQjI7Nz586qqqrBgwe3bdt29erVeIbn0rBxcXHr16/HR+l1dHQ2bdqUk5Pz6tWrUaNG4ZZcvnx5QkLC58+f8TVJCQkJcMcBpaUPKzqSkpL0zxUiYX+ttWpYLtMy+8Cp/7JIcVto2Oe3GmchS0vLTp06WVlZHTx48MyZMwMHDpw+fXpBQQGjbSsrKxUVFfv168f+CiiKKioqWrdunZaWVrNmzXr06DFx4kRjY+NJkybRP14K8/HjR0lJybS0tBrzpyjKxcVl0KBBUCN1dfVNmzYlJyeHhIQsXLgQz1EjR468deuWQCDYvHkzXv1NTEzoE3V9xB6cA/v6Lg6G6EJR1IYNG/CzDAwMDhw4QFHUjRs3cMeQkZFZunQpzNsbNmwA/bukpKSDg0NOTs6zZ8/gu5q5uXlqamo9twkizYBi5OTk6LrUL1++GBoaDh8+XFj+2blzZ9u2bb28vIqLi0NDQ4cMGTJ8+PC8vLwa24dAIPwEeFRTOBgiEAi/GgUFBV+/ftXU1OzQoQOEZGVlxcfHm5ubM8x2pKenf/v2rbKyUl9fH28MuFNdXR0VFVVaWmpiYoIl1/8B20uu7ez0T0IjQ8NevXpdvXqVe9KysrLQ0FBJScmOHTuKM1NSVVUVERHB4/E6duwIh7CSk5OxfEmPFhISIikpaW5uju0lKSkpFRcXX7t2zd7ePioqqqCgwMTEhMUNXH5+fkRERJs2bfT09Brcb0Z1dXVkZOSPHz+MjIzq5nGvbpSVlcnIyEhISKSlpcXHx6upqRkbG7PbNcOkp6fHxsaamprWucA1dzwxVFZWxsbGZmVlqaqqmpqa4hOywpSVlYWFhSkoKHTo0IFhqoZj3aOiovLy8rp27cpuK+fQoUOrV682MDCIjY3Nzs6Oiopq3rx5x44d69BVuDdLTEyMgYEB/bY+dxpkfAkEgoqKCjk5uYKCgqioKB6P17lzZ6w24ohAIGjTpo2Xlxf7eeHakpycnJKS0qFDB7yRw3z69AmUoampqaqqquHh4VByFvv09RyhRUVFISEhrVu3xm43MFwmloqKitDQUHV1dVAeJSYm6unp1e291w0uvQUhlJKSkpaW1qVLFzk5ueLi4tLSUuHGR3Ua+CUlJQoKCgKBIDY2Nj09vW3bttw9R9vZ2RUVFdXN80+N8Pn8sLAwGRkZIyMjjqYzAYqi4uPjc3JyoLkQQikpKTo6OiIjc2zYOlNSUrJ48eI5c+YoKChkZGQUFhYWFxcXFxcXFBS8efPm06dP+fn5NU5ldZ7P6XBfkhp1RQa4LzTiYJmFoEuXl5dHRkYWFRWJW0kvX768cOHCz58/c/TVRlFUQkLC9+/fmzVrZmxsDHdEWDh16tSDBw/YdUyNRP3FHpb1nYV6ii6pqakJCQnGxsb4ncbGxpaUlJiZmf3MOblGEhMT7969m5SUpKqqOmzYsL59+zZ1iQgEwv9BlH0EAuEXo97KPv127T5//lyj3Pkzwcq+adOmNXVZCP966Mq+pi7LvwxwO/7hw4ef9kS6sq9uNu8J/yJ+/PjRrVs3FxcXuNpJYFBWVjZ06FBbW9u///5bZIRBgwZ5eHj8Usv3fwRQhG3YsAGs1DUGZmZme/fuHTFiRCPlTyAQCAQGv9BnAQKBQGgQ/P39yVaBQCAw4PP5Tk5Oy5Yta+qCEH5bVFRUPD09V61alZGR0dRl+RXZv39/QEAAvrTIoLq6WkZGhizfPx+KopYtW+bg4NB4mr779++XlZWB/1YCgUAg/Bw43Z8iEAiEfxHCl9eaHPBfxvDKRyDUDdKduJOfn79kyRIZGZlevXo9e/assrLyJ7sIFOeUk/C70qlTpzNnzkybNu3y5cvirsr+ZwFnr3v27OnWrVvz5s3pPyUkJKxcuXLLli1NVLT/LtXV1cuXLzcwMGjwxndzc7t27Zq5ubm2tvbmzZtPnjzZSJegCQQCgSASouwjEAi/A0VFRUpNXQZx5ObmlpaWIoQSEhKauiyE34GUlBSEUFZWVnFxcZ3tVf1HePToEbgmvHz5srS09KtXr7Bx958DvCyEUEJCAnZCQvi96d69+40bN/bv33/w4EGi3aCzbNkyHx+fp0+ftm7d2traum3bttLS0llZWdHR0UpKSvv37+/Zs2dTl/E/x9WrVydOnAgOmhuW7du3R0dH+/r6IoTGjh1rb2/f4I8gEAgEAgvEZh+BQPjFqLfNvlonbEz279/v6ur69etXhJCcnNykSZP69u07b968pi4X4V9JWFjYgQMH3N3dKysrEULm5uZDhgxZtGhRu3btmrpovygFBQXz5s179OhR+/btT5w48TMNhxcVFf39999ubm7Z2dkIoVatWtnZ2Y0bNw67mCQQ/oPEx8cfP378wYMHKSkp1dXVWlpavXv3nj17Nrng+ftx48aNnTt3ZmZmTp482dnZmbtfCwKBQCA0CETZRyAQfjF+L2UfgUAgEAgEBrABIScfCQQCgUBoJMg1XgKB8KtC9gAEAoFAIPyOEDUfgUAgEAiNCvHGSyAQCAQCgUAgEAgEAoFAIPwmEGUfgUAgEAgEAoFAIBAIBAKB8JtArvESCIRfDGJxj0AgEAgEAoFAIBAIhLpClH0EQhMTERERGxsrHN6sWTNNTU0tLS0NDY1mzZoJR/Dz86uoqBAOHz16tISE6EO7QUFBaWlpHAtmaWmpqan55cuXb9++Cf/avXt3bW1t/M+PHz9+//6dHkFBQWHw4MEcn/WLUF5e/uLFi/v37+vp6W3YsIE9clpa2pMnT3x8fKZMmTJhwgQc7u/vv2HDhilTpqxcuZL7o4uLi588eeLh4WFrazt9+nT2yHV7BIGAyc3N3bt378ePH799+9auXbulS5f+8ccftc2Ez+cHBgbeu3ePx+MdPny4McrZJLx69So/P184XE5OrlWrVlpaWi1atBCeY4uLi58+fSqcqnnz5v379xf3LHHTuEhGjRolKSn5n5qTfwICgeDo0aNPnz79+vWrurr6hAkT1qxZIykpKS5+ZWVlWFjY169fdXR0OnbsqKmpKS5mRUVFWFhYRESEurp6ly5dWrduXZ9yhoWFeXh4bN26tW7Jg4ODk5OTucfv3bu3hoZGrRamfzUURV2+fNnf37+goCAyMrJNmzZjx45dsWKFSOnr38KRI0du3Lixe/fuoUOHNnVZOFHbwcjgPytHVVZWBgYGenp6VlVVnThxolZpxYmyIvmdGo1A+K9AEQiEJiUpKenu3bv0nba5ufnIkSOtrKx0dHR4PJ6srOyUKVP8/f0ZCV++fLl9+3ZZWVlIpaCgsHPnzkePHgkEAnHPCg8PP336tI6ODiSRkpLq3bv34H+wtra2sLDAW5f79+9TFBUdHe3q6tqtWzdcPDs7uzt37mRlZdFzDg0NPXLkSIsWLRBCMjIy69evf/HiRWM0V+Nx48YNXM0///yTJWZaWtrChQubN28OkS9fvkz/dfTo0fA6+Hw+x0dfvHjR0NAQcjtx4kSN8evwCAIBk5aW1rFjxzdv3lRXV//111/Q8SIiImqVSUBAwIABA/Cc0EhFbRI+f/585coVc3NzqB2Px7O2th4xYkT37t1btmyJEFJXV1+5cmVkZCQ9VVlZmZeXl4ODA54q9fX1z5w58+nTJ5ZnMaZxZWXlgQMH4jm5f//+Xbt2VVJSgl8LCgqo/9Kc/BOoqKiYOHGik5MTRVF37twBHe7p06fFxXd3d+/Ro8e6deu2bNlibW0Njf/t2zfhmF5eXmPGjDl69KiPj8/p06etra1nz56dk5NTt3JWVVWZm5v37du3bskpisK6D2lpaS0tLUNDQwMDAwiRlZU1NjZu3749XYt9//792i5M/14KCgrGjBmzYMGC8vJyiqKePXsmJyeHEFqxYkVTF63uCAQCFRUVhNCwYcOauiycqO1gZPCflaM8PT379esHFR81ahT3hOyirEh+m0YjEP47EGUfgfBLUFFRAWIZQig7OxuHR0RE4E9te/bsEU44Z84c+PWvv/7i+KyrV69CkhEjRgj/KhAIbt26JSkpefv2bRz44cMHSKKurs6yxru5uSGEtm3bxrEkvxoCgQA0oezKPgCfkWRISL6+vmZmZiJfFgslJSVSUlIchdS6PYJAAPr169e5c2f4u7q6eurUqT179iwuLq5DVr179/79lH3A+/fvYYB37NiRHv748WPQA8rKyj579kw4Yfv27SGhp6cnx2fhaXz//v3Cv5aVla1Zs4axNPxH5uTGZvPmzQih3Nxc+OeRI0cMDQ2DgoJERj5x4sTs2bOrqqpwyNmzZ3k8noqKSkJCAj2mj4+Po6Mj48Pbxo0be/fuXVZWVody7tq1CyFUH2XfH3/8ISUldeHCBdBnURSVkZEBXcjS0hJHy8zMXLRoEULoxo0bVC0Xpn8vffv27dq1a0VFBQ558OCBrKzsqlWrmrBU9WfPnj1dunR58OBBUxeEE7UajCL5L8tRffv2ra2yDxAnyorkN2s0AuG/AFH2EQi/CkZGRrDiMnbdfD4f37168uQJI9XOnTvhp+PHj3N80OvXryHJ5MmTxcWZNm0ayPpAeno6JOnSpQtLzikpKQghPz8/jiVpPHbv3p2Xl1eHhN27d+eo7KMoCr48c5GQuADa3t97T/UbIBAI1qxZU+fkde6ZDUVISAhCaNCgQbVNmJWVtXfvXkbg2LFj/13KvhcvXnh5eXGJiSc9CwsLxk8ZGRmtWrVCCCkqKgq/TRsbG0gYFhbGsVR4Gj9z5ozICAKBQFtb+/v378LF+7fMyb8glZWVmpqaEhISXCKnpKT07NlTWK86btw4hr6Mz+e3bt36x48fjJjV1dUdOnS4fv16bcsZHh7es2fPeir7hg8fvmnTJnqISGUfRVF8Pt/c3PzChQvwz99+YfLx8UEI7dy5kxFeN7VsU8F9Zvs1qdVgBERW+bfvruKAYwF1UPZRDS3KEgiEXwrijZdA+FUQZxpGUlISvrQjhB4/fiwuFXfLMlxizpkzB9/25f4UVVVVhJCioiLHkjQSjx8/3rJlS2VlZR3S8ng87pHF2UasG7V6NKGp2LJli7e3d93S1qdnNhRPnjxBte+6fD5/0qRJSUlJjPB/V6dNS0ubPHlyaWkpl8gsc52mpqadnR1CqLi4+O3bt+ISNuCczOPx1q9fLy0tXdun/CJz8q9JaGhoZmYmx7Hw8OHD9PT03bt3M8IdHR0RQu/evYuPj4eQ8PDwwsJC4TaXkJCwsrJ68+ZNrQopEAg2bNgg/NzaUlpaOnPmTC4xJSUl7e3tS0pK4J//rjFeB27cuIEQUldXZ4Tjy/W/PrWa2X5NajUYkfgq//bdVRz1qXjDirIEAuGXggxvAuFfADZEEh0d/XOeOGjQILgUUCtg2wnXKJqKuLi4yZMnV1dXN2EZCL8rd+/erfOu+xfpmQynDRxZuXLlixcvGrosP5WysrI//vgDn2aqJz9/Tl6yZAkY4KsVv8Kc/MtSq7EQGRmZkpKydevWT58+0cNxT/j48SP8kZ6eXlxc7OLiIpxJTk6OsFKJHWdn5+nTp9c2lTCbNm3CRa2RmTNnDho0qJ5P/LcQFhaGEJKRkWnqgtSRhp3ZmopaDcbfo8oEAoHwEyDyH4HwLwC76zU2Nm7sZ1VXV//555/Hjx9v7AeJIycn58aNG2FhYRUVFd27d7ezs2vTpg0jjo+Pz5MnT8rKyrS1tbt27SojIwPO5j58+DBhwgTwpHnq1CllZWUZGZmlS5fihGFhYbdu3UpJSREIBGZmZuPHj8cGtoQJDAx0c3PLyMjo1q3b6NGjO3XqxKX8FEW9f//+5s2b+/btY+wfxBVbmHfv3l27du379+/t27efO3cu472Le0RoaOjt27fHjh1rYWHx8ePHS5cu5eTkdOvWbcWKFfLy8sJPCQsLu3fv3tevX9u0aWNmZmZjY6Orq8uljgihjIyMa9euxcTEFBYWGhsbDx48mIt2uLi42MfHJzIycvv27WVlZWfPng0ICNDQ0Jg8ebJIj6XiOkN+fv6lS5dwNAUFhYULF0ZFRT148ICefPTo0bC/9fb2jomJQQhpaGjMmDGDpYTZ2dnnzp1LSkqSl5c3NjbW19c3NTWFU66XL19esGABRVG5ubmHDh1CCBkYGMA9VoRQWVnZzZs3P336lJ2draura2FhMWnSJPyxvcaeyaXbs1NWVvbgwYMvX75ER0fr6+t37dp15MiR2LcDQuj06dOlpaXv3r1DCH379g2qgBCaPn06i0dRPp+/YsWKU6dOIYS+fPkCqaytrekOIoCkpKSLFy9GRERoamqOGzdOZN/++vWrt7d3WFiYqqqqhYXF1KlTa3V8hr2RgZSUFFdX15EjR3bu3PnMmTMvX74cPnz4sGHD7O3twc6dt7d3amoqQmjmzJl11qH8zDk5ODj406dP8+bNa+wHhYaG3rhxIycnp0WLFp07d5aTkxN2zpiXl3f79u3g4OD8/Pz27dtPnjzZzMxMOCt/f/+goKCQkBA1NbWuXbsOGTJET0+PEUfkm6J7OKlPb2EvgI+PT3R0NFxpFwgEeCwMGDDAwsJCZIZDhgw5fvy4jo5Ou3bt6OFVVVXwBz4d07FjRx6PN3/+/LS0tA0bNuCjl/Hx8a9evTp69CjHKkCST58+rVy5EopaH4YMGcI9cuvWrUX6DmZfmIA6vzU+n+/h4fHhw4fExEQdHZ2hQ4cOHz5c+LxSaGjoq1evvnz5UlVV1bVr1759+wq/Mo4Lzfnz5wsLCxMTExFCfn5+ubm5EE5RVEVFxY8fP5KTk8+dO6esrMzIn33pvHv3Lj4EPWzYsI4dOyKE7t27l5CQAIE6OjqTJk3C8YuKim7fvq2goGBvb//48WMXFxdDQ8ONGzfC4k5R1KNHj/z9/dPS0lRVVc3MzKZPn44n9oyMDJaZraKi4tGjR6GhoX///bdwg3MZpLUVKkTSsIORvcp06iZHVVZWuri4BAcHS0hI6Ovrm5ubKykp9erVi72OCQkJt2/f7ty584gRI3Jzc8+cORMUFKSmpjZv3jywbAvd+9GjR0VFRT179ly+fDn9pDbHtqIX/v79+76+vvn5+UZGRvj2jzBVVVWwaH7//t3MzGz48OFgrKZuNIh8SyAQfipNd4OYQCD8D1iXxLDZV15eDvtqJSUlhhVwiqL27t0LqcQZexIGdvtIjM2+wMDAwYMHMwJzcnIgibD5KkZREULv3r3jWBJhzp8/37t377Nnz96/f3/16tVSUlJqamre3t70OLNmzRo0aFBmZiZFUZ8+fWrfvn3v3r3hp+vXry9YsACKumPHDmdnZ+zKrbq6eu7cuaqqqj4+PuXl5ampqZaWlqqqqk+fPqVnDsLlihUrli9fTp8qpaWlz549yygtnJfBhk4SExPXrl2LJTPGe2QpNgAX7o4fP75hwwZpaWkQMRFCioqKnz9/Zn/EiRMnsGtFLy+vv//+W0lJydjYWFJSEiFkbm5ONyoP7Ny5U0NDw8fH59u3bwcPHoQ9lYaGRpcuXczNzYODg1le09mzZ6WlpZ2dnXNzc0tKShYsWCAhIXHgwAGWJDExMZMmTQLp0MLCIiIiwsjICPw/IoQkJCTc3d0ZSdg7A/Yz07VrV3BcAMI0PKJVq1YxMTE4q6KiomnTpklLS+OWFElwcHCLFi1u3rwpEAjKyso2bNiAEHr06BFFUeXl5c7OzmCOTUNDw9nZ2dnZGRfmw4cPGhoas2bNSkhI4PP5ly9fRghNmDABW3xn6Zk11pQLb9686dChw6xZsz5+/BgdHX3o0CE5OTldXV0oPHDu3DlnZ2crKyuEULt27Zz/AfqkOOLj452dnaHLWVlZQRLcPcBgmZ2dnYeHh6KioomJiZaWFkKIx+MxxktFRcWff/45evTo69evu7u7g//xzp07R0VFcawjeyPz+fzjx48PGDAAdC5PnjzBWl1JSUlPT88DBw7AP2fOnAm1oPu7EIZl0ouLiwPLUF27dq2srGT8OmzYMEjIvWrs07iTk9OuXbu4F48O9zn5/Pnzurq6X758oSgqPT0dakF3WUBR1MWLF3V0dA4cOBASEvLu3bsOHTogIQunKSkptra2PXr08Pf3T05OdnV11dfXl5eXP3LkSHV1NVXTmwKfwvXpLTUWgKIoX19fZ2dnfK310KFD0CXYXScXFBTgHDC3b9+GTOjemXGNOnfu/P79e4qiMjIyzMzMrl27VmP5MQKBYPz48enp6RRFffnyBdXPZp8w4mz2MeCyMAH1eWsBAQGdO3detGjRq1evIiIixo8fjxDq379/YWEhjlNWVrZq1SotLa0bN24kJyf7+vqCOWNHR0dsIbFWC83p06f37t0L6/i4ceP27t27Z8+eXbt2rVmzBi+y4AKbTo1LZ3V19fnz5yE5Fg9KS0s/fPgAn3AGDhwIgV5eXnZ2dqA1+/vvv8+fP4+Vmx4eHhRFZWdnW1hYdOvWLSgoqLq6+v379y1atOjYsWNycjLk8PLlS5EzW0BAgIODA0xTwlMElzFSB6FCmMYYjOKqDL/WR46iKKqwsLBz586rVq0qLy/n8/m3bt2SlZXdsGEDSx0vXbqElZLHjx93dXVt1aqVhYUF5N+sWbM3b95ER0ebm5u3bdu2d+/eoPsePnx4HdoK+PHjx5gxY7p06eLr65uWlubh4dG5c2dQNzNs9gUEBFhaWjo5OXl6eh4+fLhly5aSkpLC5ikZoqxI6infEgiEpoIo+wiEXwWs7AsPD09KSkpISHj79u25c+fga6SpqWlAQIBwqvoo+4YNG/aVRmBgoIuLi76+flMp+86fP6+lpZWfn49DwAelvLx8WloahDx8+BAh9PLlSxzn1atX3bp1w//08/ODomZkZNAzP336NEPAglNg2C0pAEJb27Ztp02bFhYWlp2dfefOHTDGj4Ts3DMkpJycnPj4eHt7e2FhqMZiU/8IqSYmJuvXry8qKqIoKiMjA06RTJgwocZH0J2HHjlyBHQQz58/h83D1atX6c/y8PBA/2uSfNWqVQghSUlJLy8vf39/4R0OJiIigsfjycnJ4ZDi4mIejyclJcWuNqqqqhozZgzsi2xtbWEHWF5ebmtrixDS19enR+bSGfr164cQ6tevHz0hfOI2NzdnPH316tWzZs1iKR5FUb169bK2tqaHDBo0iG4CfP369QghY2NjehyBQAA9BHb1QI8ePRBCd+/exSHieiaXmrLz7t07SUnJqVOn0gMfPnwI/ZPRaRcuXIhqrzWwtLRECC1cuJARDso+fX39sWPHpqSkUBRVVVUFpx21tLToirBp06YNGzaMnhZ8DnTt2pXhtFQkNTZydXV1bGws9nQxduzYS5cuBQcH9+nTx8LCoqqqCmYnhNDNmze5VBlPep07d05KSkpKSoqNjX3+/PnOnTvV1NQQQiNHjoQqM6iPsm/z5s14Qg4LC3v+/PnevXsVFRUbW9mXn58vKyu7fft2HJKXl9eiRQu6qmX//v2SkpL0rC5evAhlwJ9MCgoKWrdu3b59e7pzg6SkJG1tbYTQ+vXrKQ5viqpHb+FSAMzNmzehDCzujGsEejvDtf2PHz9gWkMISUhIODg49O/fnz7/c+HUqVNXrlyBv5tc2ce+MAF1fmuvXr2SkZE5ePAgDsEGEDdv3owDR44cKSkpGRERgUPKy8tB32dlZYUVIrVaaCiKAuULQ4LCt7ChyhiOSyeebRgKlOnTpyOasi8xMfHRo0egPhsyZMjKlSvj4+NnzZqlq6sbHh5OURSs9fv27cM5wOqwbNkyeiMIz2zJyclfv341NTUVniK4j5FaCRXCNN5gZJnM6ylHrVu3Tlpamq7H3LFjx8qVK9nLk5WVBTY69fX1N2/eXFpaSlFUZWVlnz59QBrp1q1baGgoRA4KCoIG/PjxYx3aqrKy0szMTF9fny4zREdHQzemK/uCg4OVlJQCAwNxCL76cO/ePXr5uSj76infEgiEpoIo+wiEXwWs7BO+tzJx4kS6gEunPso+KSkpRRrYrlOTKPvy8vJUVFQY7gLx3aV169ZByJYtWxBC2FEgYG9vj/8Wp1JZsWIFY0sGlnrQ/55eAWUfY08VHR0tJyeHEDIzM6N/YhUpIbm5uQkLQzUWm/pHSF21ahU9ELbEbdq0qfERfD4fAulbJlyjJUuW0APhTsr9+/dxCL6W6OPjQ7Fy7949hJCysjJdIIZzE48fP2ZPC5eJNDQ0QBoGwBkiQgh/nOfYGdzd3RFCEhISdJ3L58+fIRpsljCGhoZ0PZEwlZWVUlJSurq69Fd8586dW7du4X+KVPbl5OTAbu3Dhw84EO4/bty4EYeI7Jkca8pCVVUV3KMUniJALtfT06PvVxtD2derVy96Z8D+vmNjYyHk2bNnSMiZOL7P+PDhwxoLwLGR4UYeQkjYY3KdlX3CczKc9YAzaMLUR9knIyODJ2QFBQX83MZW9sELmj59Oj1w6dKluI6RkZFSUlIMdXlxcTFc9sTKvsWLFyOETp06xcgfvrVISUnRD9eIe1P16S3cC0A1hLLvw4cPkpKSysrKcXFxjJ+qq6tXr16N+4yBgcGzZ8+45/zt27eJEyfifza5sq/GhanOb62iokJfX799+/aMk2KgsMPKvlu3biGEJk2axEgeEREBjzh+/DgO5LjQACKVffjAJuMEE/elEwIZ4gFcxsfKPgBOS/Xs2VNYJdqlSxeE0P79+3EImFixsrLCISwzGxwnZ0wR3MdIrYQKYRpvMNao7KuzHAW3vOnDOTMzk65aFQd8lQdDH5gzZ87AIxie2eFm9549e3AI97Y6cuQIo7cDAwcORP+r7Ovfv3+fPn3ocQQCAXyp6tWrFz2ci7IPqLN8SyAQmgrioINA+OUoLCysrq4uKCiIioq6fv16v379bt++bWpqOn36dGxTpkGws7MrolFVVfXt2zd8HuEnc+fOnR8/fgQHB/9F48KFC/BrQEAA/AFmWf766y8suCOEdu3aVWP+GzZsOHTo0IkTJ3AI9Y84DpbU6DBsmhgaGk6ZMgUhFBoaik8ciEOkc0zuxWYYhNLX10cIpaen0x07iHwEqEIQQi1btqSHw5fhlJQUHFJdXQ3qpMLCQhxoYGAAlunAAg4LI0eOPHXqlK+vL93qPzRmXl4ee1pIIi8vD8pTegnpheTYGcaPH6+hoSEQCOBGJ9CpUyf4xk43kP/y5Ut5eXk4ZiKOZs2aqaioJCcn29nZFRQU4MqCwR0WWrRo4eHh4erqCgfNAI4NwrGmLDx+/Dg0NFReXt7ExITxE+z0vn375unpWWM+9UFHR4feGaDTItoLhRrdvn2bXk3s8YNLNTk2Mh4acN6nQbCwsBAIBFVVVTk5OZ8/fz527JiWltaePXt0dHScnJwa1uPK0aNH8YRcXFzM5/ODgoK4W9KsMzBBXb9+fdeuXbhG69atw6bBjh49yufzsYVKQEFBISwsLCYmBpw5FBcXw+ZW2NLWtGnTFBUV+Xw+GH9ErG+qzr2lVgWoPyUlJdOmTZORkXnw4IGw7deTJ0++ePHizp07WH1gY2OzdetWjpmvXr0aGy/7FahxYarzWwNjdiNHjmS4kblz587nz5+3b98O/4Sbm8Jv1sTEBFqYbgyR40LDgkjfpvVcOsUBY8HGxkb4oVeuXDl69CjdIhvHlYWeM51ajRHuQoUwP3kwMqizHAUz4ahRo7DnJQ0NDYZRF5FAl6MbyUUIYcO7DHuCYCQ3PT0d/sm9rcrLy3fs2IEQGj16NCMm4x3Fxsa+evWqqKiIPh5XrlwJN9yDgoIqKipqrJQw9ZRvCQTCz4c46CAQfjl4PJ6EhISKioqKioqRkdHUqVO3b9++bdu269evp6amPnv2DBsCb3B0dXUPHTr0559/NlL+LMCuoGvXrp07d6aHwz0IbAp6xowZTk5O6enpo0ePHjt27OHDh/X19bFlGRY0NTVXrlwJf8fGxp4/fx6fn8KfrzHCMrednR14hIiIiABLVbWizsUGyUwgEPD5fCx51wqQQQUCAQ6RlJSELsRwJNq2bduUlBQWjyVAs2bN8N4jLy/v8uXLgYGBJSUlSFRLci8hvZAcO0OzZs0cHBz2799/6dKlTZs2wVtzc3OD7dDVq1f37NkDmZ86dQpOtLGzdu3adevW3bt3LyAgYPfu3XPnzpWVlcVbRBbgEApCqLKy0sPD4/79++Cas8YG4VjT1NRUYaUSnAcJDQ1FCGloaAhnbmVlxePxKIoKCQmZNm0ae0lYHlFbsN3uyspK+AOqOWDAAHoftrS0hLOHbdu2RQjx+fy0tDThrMAIIKplI9dtsLAgJSXVokWLFi1adO3a1dHRcdy4cY8ePdq0aVNZWRm+kdrgSEhIdO/efe3atVj73Eh06tRp+PDhDx8+3Lx5861bt44dOzZw4EB6z4dzqcJTlpqaGhwVQQh9/foVhrBwb1RSUurcufPbt2+FHU0IvykuvUUkdStAnVmxYsX37989PT2FfRMtXLgwMjLy1atXcnJyf/zxx6VLl9asWZOfn79jx47mzZvXuMJeuXLFxsYGVEi/JsILU53fmriu1axZs65du8LfAoEgPDwciZno+vTp8+rVq/j4+OLiYvjSIxLhhaa21HPprDFz4cAuXbrA4T6E0OvXr69cuRIcHIzqutSiBhojwkJFIz2ooeAuR61cufL+/ftRUVGdO3desWLF5s2blZWVuYhqIhGpGkMIgWsOrLHl3lYRERH5+fmSkpI1+u+C8di+fXs4lY/B/+Tz+Q3lgbrO8i2BQPgJEGUfgfAvYMuWLe7u7pGRkS9fvrxx4waYfWkkTE1N8aYag2UCdvEOlAXi5Bt2vn//jhDq1KkTtgkiEhUVlefPn48cOTI+Pt7T09PPz2/t2rWbN2/m+NBXr14dOHCgTZs2q1atqqys5OhgF9G0HvjqU62oZ7EbnDFjxty8edPV1XX79u1Y9k1PT1dXV6efnGIhISFh3759qamp69atW7lypbq6et0+FIuEY2dACM2fP//AgQOJiYkvXrwAc3sHDhw4ffr0qlWrsrKyfH19x4wZk5GR8eTJE3xcjoW1a9cWFhY6OTllZ2fPnz//9OnTFy5cEHY7K5KioiJnZ2c/P78lS5ZcvXp12rRpXM6PcKxpjx49hDteeXm5jIwM3GIT2S1lZGTU1dWzsrLwRbO6PaLGtOwIBALIedCgQVhzJ0x8fLywc09LS8u3b9/if9atkRscWVnZCxcutGvXjs/nOzk5zZs3r25aUY4MGzYsKiqKEdjgc/KtW7cmTpzo5+cXFhZmbW09ceLEEydOwOZTIBBAO5eVlbHkgC9UZmRkCDeIjo7O27dva+yKHHtL4xWAI2fPnvX09Hz27JnweeGrV6/evn07NjYWjpXxeLy5c+eOGjVqypQpz58/37p167x581h0UhkZGV5eXnfu3GmQcv4c6vPWvn37hmrqWklJSaWlpUjMRAdaUYqi4uPjsWqskaj/0llbBALBtWvXzp07N2rUqP3797u5uWFTFXXgp42RnzkYGxArK6s7d+7MmDGjuLj44MGD165dO3LkyOTJkxv1odzbCmLKyMgwjsEKA6KFhoZGjUJU/fnV5FsCgUCHXOMlEP4F8Hg88EWAEHr16lVjP2vZsmWMQEVFRTioz37ABI53MS4ycATkZi7Cn5GRUWho6Nq1a6WlpSsqKnbu3Dlw4MDi4mL2VOXl5Q4ODsOHD1+zZs2ZM2dqezoPrl3Q/6gtdSt2I3H8+PFu3bolJyc7OjqCku7ChQvp6elubm7gv48dZ2dnU1PTDh06PHjwAK5QNSzcO0P79u3BQy74Cnjw4EFFRcW0adPAtR8cxjx//vykSZM4dstdu3a9fv0aztkFBwdbWlqCZUB2nj9/bmhoGBoa6u/vP3XqVPhuzwWONbW3t3cQAtLCwCwvL6fbmMOAsoZxp6m2j6g/sDNhr6aKiopwAUaOHIkj1LmRGwNtbW24GiYQCN68edOoz2rfvj29HYAGn5MVFRUfPnx46dIluA52+/ZtMzOzyMhI+JXLG8RX1URqYLl3RS7PatQC1Ag4d37z5o1IywCHDx+eN28ePvAIaGpqPn78eMyYMYWFhXA4Sxzr16/X09M7derUSRpg0ez79+/wT7oG/Behzm+NyxyIryiyvFnEen6woajn0llbkpOTwZvqzZs3169fz+hUdeCnjZGf9qAGZ9y4cREREWCyICMjY8qUKWDxufHg3lag7C4tLa3xHjd3IapB+KXkWwKBQIco+wiEfwf4u3fdbqA8e/bs3LlztUqSmpoKxkEAOJMP9trFJUlMTJSWlq6bhSnI/+7duyJ/TU5Opp9gkpeX37dvX0REBFgYDAwM3L17N3v+y5cvv3LlyoYNG+qmnMrKykII8Xg8KyurOiSvc7EbiZYtW75//75///6fP3+2srKaMGFCaGhoVFQUKM7YuXHjxl9//dWnTx+6+fmGhWNnAObPnw+RCwoK9u3bt27dOgkJCbCD/uDBg7S0tHPnzoH1LJlDSwABAABJREFUOnYoisrMzEQI9enTJzg4+Ny5c4qKilVVVXPnzmW3lRkbGzt69Oji4uKrV6/STURxgWNNnZ2dXYSA3TV450AIgQl/BtBvcRwWWB5RTyQkJGCjIq6ar1+/Likp0dLSEi4AGNpH9WvkRqKec/L+/ftre0Ln0aNH9DZswDm5sLAQTk7Nnj07Li7ur7/+QghlZmbOmTMHISQhIQFfR+7fvy8yeXJyMmqgrsixt4j8qaHGAjuBgYFbt259+fKloaEhDnz06BHYqyovL//69Su+f0pHSkoKPLGwHw8fP3688IVQOKfzy1Kftwbnef38/ESeDU9NTRUIBMrKyqDIY3mzenp6jaFuY1CfpbO2VFRUjBo16uPHjxcuXOBiTYILP2eM/MwHNSwwNnV0dO7fv//s2TOY944dO/bo0aPGeyj3tsIzg/BZbwawOrx69So7O1v4Vz6f7+/vX/cS/y+/mnxLIBDoEGUfgfCrwLJhQwgFBgbCHwx3AeypAD6fv3btWlNTU+5JEEJr1qyhb2LhikpxcfHLly/FJfH29h49enTd7v1B8UJCQoR3C5WVlYsXL27VqhVC6OjRo9iqcfv27X19fcF1hrD9b7pRm8LCQrjFSb/jUyurNzExMQihqVOnirQZVCPci/3T2LZt2+zZs798+fLp06e7d+8eO3aM43bi8OHDCCFzc3N6YJ1NCImEY2cAxo0bp6mpWV5evmzZsqSkpBkzZiCEjI2N+/bty+fzp06d2rp1a5F7b+Gcsfl8SUlJR0fHN2/eNGvWrKSkhCGCMyp79uzZkpISQ0NDuhKKpUHoP9WqpiIZMGAAnHFzdXVl/FRUVJSdna2oqDhkyBD2TDhS57cM1bx48WJSUhLjp+TkZGdnZwUFBfYcatvILHBMxT5Ppqam4lMYdZiTU1JSDh06BD4ZOSYpLy9ftGgRuL0GGnBOfvv2rZeXF/ytoqJy+PBh8AL5/v37qqoqhBBcZr979+779+8Zad+/fw/W5fX09ED/de3aNeEagWujP/74o8aa1rm3NFQBWIiMjFy3bp2fnx/DZtarV6/geI6MjIyqqiqe7RnAHMtwAMVg7NixS4QYOnQoQqh169bwzxq9BnGBoyTAkTq/NehaqampdA8bQF5e3l9//QXGWGESCwwMTEhIYERrkDfLHY5LJ1j3g+GDAZU6R548eRIWFoY4yy1cZrafMEZ+5oMaVvBACK1fvx7/bW1t/f79e1BGN6qoxr2t8JJx9uxZkVnh5DAe+Xw+/Zs9Zv369Q3yJQ/4BeVbAoGAIco+AuFXAUuBwudEduzYAeu9mZnZ1KlTRaYSJ7jn5+c7ODjExcVhu7w1JkEIHT169ObNm7DBAP7++29VVVWE0Pr160VeHIuIiDhx4sSSJUsY4S9evBg2bJiDg4PID4wYBwcH2MfOnj37wYMHODwtLW3YsGGDBw9u3rw5hNCNGUlISMDBLuwAFG+GsbF/Pp+fmpoKlaUfpcEXorEbARbOnj2rpqYGei4MvCnG+8K5MaR89mLj+IxUODe6UCvyEeKOF0Fkxrvet2/fnTt36mb8EU7x0FsyLCwMukSNLSnSdSlOhQvJvTMghJo1azZ79myE0LVr11avXo1vd8LhvlevXnFxzQF4eXnRT5eYmZnBQU5G78rMzISKCAQCgUAADRIXF4ddNBYWFoI5bXqDiOyZtaqpSNq2bQv7k7t37zL8SsPOYfv27XTFBFSwtgYWoZD0ksMfHDvtqlWrEELFxcUjRowAvTnw/v37QYMG0fdX4uDYyLgLCfc0GRkZUDcI10IkLBNyRUWFo6Mj/O3o6Mg4h1XjBBsVFTVu3LiePXtiHVyNSSorK2fPnp2VlUU/WVznOVkkt2/fpv9z9uzZ0tLS2traYHdpw4YNUlJSFEXZ2tp6enricj548GDu3Lnr1q2Df4KK8Nu3b0+ePGGU5NOnT6NGjcIGYVneVH16C/cCINoo4Dgcvn//7ujoePjw4YqKim//EB8fHxQU5OHhAe6weTzewIED6U1E58WLF506daJP+/fv3+/cuTNjZREGchOpCHBwcHB2duZSfgb4kh27Bqqxx/i0adNgBK1fv37nzp04Zzgut3r1alD2OTk5tWjRgqIohvVVPp9/8+bN1q1bY7+9iPNCQ4/MGOa4XoxZgvvSCcbX6HZXkpOTnz9/jmgtTy+PcJlh0kO01ZaiKFCg0Cc9lpkNojHeHfcxUiuhQpjGG4wsVa6nHBUdHQ0KVqB58+Zg844+ZkUisq1wIKM8EE5vQI5tZWhoCH543dzcoC8B1dXVUOwfP35AiJmZGTg6P3nypJOTE654aWnpypUr4+LiBg4cyCgPlyPqdZZvCQRCk0ERCIRfgO/fv2Mlxfnz50tLS/Pz82NjYz08PMaPHw/hhoaGMTExjISwnCOExo8fHx0dnZmZmZOTk52dnZSU9O7du0OHDsFJNDs7O5wE+47s3LlzeHh4bm5uRUVFRUVFdnZ2XFycm5sbGCtRVlauqqqiP4t+c/D58+c/fvzAhd+3b5+SktLu3buFq4ZPMYwbN469Ec6fP4/d4Hbq1GnChAlwamnZsmU4zvr161u2bJmcnIxDzpw5Iy0t/f79e/hnQUEBnP0ZM2bM+/fvN2/efPfu3YqKCtBWyMrK7tix48yZM2PGjFm8eDE8a/78+YcPH4a2HTZsGEKoR48euHYURe3du1dTU9Pf359e2sTEREi+a9cueviuXbsgPCQkhHuxc3Nzoe70ylK0lxUVFcX+CDBzjhDavHkzPQf4wNu9e3d6IHysVlZWNjc3HzBgwMiRI+3t7efPn3/kyJG0tDTRr+cfQNZECM2ePfvSpUvz58+fMGEC3OHq27evi4uLt7e3uLSglZOTkysqKsKB2CgePSGXzoCJi4vj8XgtW7YsKSnBgSUlJeDSmh7IQnl5OUJo3bp19BA9Pb2xY8fikMePH0ORtmzZ8ubNm5kzZyYmJh46dAgC+/btC36B+/fvD+dQWrdu7eLicvLkSUpMz6xtTcWVHPrtgAEDysvLITAyMrJ169ZTpkxhjGLQFoFDFY75UxS1ceNGhJCMjMzt27cfPXo0ZcoUPp9PURQcOencuTM98uvXr6E6Z86cwYFw4hIhJCEh0bdv3wkTJnTr1k1SUtLd3Z1LATg28rt37yDa0aNHhTOBurdv3/7169eXL1/etGkTyxPx7kVaWjokJKS8vDwrKys8PPzs2bN4Ths+fHhZWRk9VWFhIdbP7ty589u3b1lZWTk5OVlZWfHx8f7+/qtWrYKp/sSJEzgVnsahR/348aOqqqq0tDQjIyM8PBzMhCGERo4cyShk3eZkYcAjqq+vLw6Ba2JHjhzBIXRlSvPmzfv166evr6+oqBgUFETPasuWLTweT0tLKzExEbdJ//79u3btmpKSgqOxv6n69BaOBaD+6dUIoeDg4BqzLSgoYLl1qKOjg2MmJye3atVq27ZtjBy+fftmaGj49etXeiD0SSUlperqapanw6Hjdu3aMfob1jp9+vSpxiowuHLlCqRVUFCAy+DC1GphqvNb8/Pzk5WVhbQyMjK9evUyNzeXkJA4e/YsPZqXl5eysrKUlJSPjw8OBPdQz58/p8fkvtDgK9X0mZ+iTThfvnyhh3NfOleuXIkQ4vF4CxYsuHTp0rJly3r27AmOmCUkJDZs2PD582eKoioqKpSVlRFC48ePZzRLUFAQ7l2nT58+ePBgv379oJF5PJ6zszNe68XNbDCxtGzZEqZrDMcxUiuhQiSNNBjFVbn+cpSlpeWAAQPoo2zy5Mlt27bNy8tjKUxVVRWYU3R0dKSHY8X0ixcv6OEwk1haWtIDObZVdHQ0PEtGRgbsCfj5+Q0fPhxkMAkJiSlTpsDHhuDgYGyttU2bNmPHjh0+fLiamlrv3r3p40KcKCuSusm3BAKhCSHKPgKhifHx8Zk5c6a4y6EtW7bs1avX1KlTr1y5wpDy165dy+VyInDhwgWKolxcXGxtbbFmgR1h0ZOiqPDwcGybhsfjtW3bVkNDQ0JColevXo8fPxZZQfxlXlVVtcbW8PPzo9vY1tPTu3LlikAgwBHWr1/fpUuXdu3arV+//tKlS6tXrzYxMXnw4AE9EycnJ0guLy9//PhxCPT19cWbcFNTUyht3759oSI7duyAaGlpafPmzWvevLmsrGzv3r3t7OyMjIwmTpyYmZmJ88/Ly/v777+xwSZlZeUVK1YEBga+ePFi6dKl2M2ioaHhli1bCgsLayz2lStXLCwsIJW0tPSCBQtiYmJSUlKWLVsmLy8P4SYmJlevXhX3CFdXV5yDqqrq6tWr09LSPnz4MG7cONyY06ZNw/uWe/fuifOZqKCgwNg4MUhISIAzLAihli1bHjt2TCAQYNtqY8eOLS4uFk6VkpKydOlSrNEePHiwm5sbRVE7d+7EPk/atm174MAB7p2BzogRI5ycnBiBixcv/vPPP1nqQqe8vFxDQ8PQ0HDYsGFHjx49efKklZXVnDlz6GpfgUAwfPhwKI+2tvarV68oiqqoqMAnFGRkZJYuXfrjx49Hjx7BNS4jI6O4uDhILrJn1ramIhEIBMePH9fR0WnTpo2jo6O9vb2RkZGLiws9zqFDh+gnKXr37r1t2zaG6kEcaWlp2Ji6lZVVamrq69ev8XcIhNDo0aNhTP3999/YQpyamtrGjRtxCffv3083qmVlZRUQEMCxgjU2cmho6MqVK1u3bg1xFBUVFy1axNA7+/v7w4CSkJCYO3euOHXn4cOHJ0yYgLUPdCQkJHR0dAYOHOjo6MiY8bKysmbPns3dtBb0ilpN4yL1YnWYk4Xx8/Nr3769rq7u3Llzz507t3fvXmNj43379jGiubu74xZGCA0fPjw2NlY4t2fPnpmZmamqqtrZ2Tk6Opqamq5evRq3dlFRUY1vqp69hb0AFEV5eHg4OjriV6ynp7du3Tq6rlOYNWvWsLwaW1tbeuTw8HBra2v4+PH27Vtvb+/Vq1f369dPePfr6urapk2bLVu2iHxoSUlJhw4dtLW1lZSU1NTUVFRUWrZsaWBgAAs6RVFgBFBFRSUnJ4dj41AUdeLEiTFjxtB9Zaqrq0+fPp3RYbgvTBC/Pm/ty5cvXbt2xcKJiYnJkydPhKMlJiYOGzZMUVHR2tp6+fLlPXv2HDNmTHp6Oo5Qq4XmzJkz+KisnJzc0qVLIyIikpKS1qxZg11hGBoa7t27F+fPfeksLCyE76YwMGfPnp2fn793715lZeWpU6fevXu3tLT08OHDePjzeLyJEyceO3aMXt9du3ZhL0kw3LKyskCSUVJSwj1WeGa7fPmyvb09bk8bGxv61wWKwxiprVAhjsYYjCKrXH85iqIoa2trMzOzrl277t69+8KFC1OnTu3bty9dRSjMlStXsFQgJyf3119/BQUFZWdnb9y4kd7ltm/fXlhY6OfnN2HCBAiUkJCYMWPG06dPubcVkJycbGNjA2sfQkhXV9fb29vOzk5LS2v58uVv377FMWNjY/v06YO7gaKi4tatW7GEJk6UFVnNOsu3BAKhaeFRDWqzg0Ag/BcoLi6Oj4+Pj4+vqKjQ0NDo2LGjOPEXyMrK+vTp07p160JDQ7nkn5mZGRMTo6enp6Ojw1BNlpSUKCgoUBSVmJiYnJyspaXVoUMHYZ+hiYmJqampnTt3hmtuQFFRUVhYmJKSEvhaRQiVlZV9+vTJyMgIe0MDqqur4+LiMjIy5OTkjIyM6m/2m2Oxfxpv3rw5ceKEk5NTampqdnZ2cXFxSUlJcXFxTk7O48ePJSUl8ZkCkfD5/LCwsMrKyq5du+Jt1cePH5s3bw5moRsQls5AJzk5WUNDg6GgyczMlJaWrvEmLKa0tFReXr64uDgyMrKioqJjx47CaQUCQVhYWHl5ebdu3ei75djY2IyMjE6dOuEksbGxRUVFXbp0ob9okT2zVjVlJysrKyoqqkWLFiYmJngz0CCUlZV9+fJFWVkZmw2qAxRFxcfHZ2VlGRoaYg+b3OHYyCzk5ORERES0a9dOR0entk//lantnMygoqJCSkpKUlIyKysrJiZGVla2U6dOItWdCKHU1NTk5GRTU1PhPkynpKQkNDRUWlra1NS0bj5V6tlb6l+AevL27duPHz/Gx8fr6OiYm5tbW1s3xpwfEhLSrl07OB32K1Cft1ZQUBAeHt62bVuGSUQGfD4/PDwcjHjWoWPUh9ouncnJySkpKR07doTBkpGR0bx581rZNc7MzIyOjm7bti3+iJKVlRUXF2dubo51WKiuM9tPGyON8aDGmMxBVKuqqoqOjs7JyWnfvv3PXyk4tlVJSUlYWJi2tjZ8YUpMTNTT0xO56BcXF4eFhYF41oCm+ugl+aXkWwKBQIco+wgEws/AyclJRUWFo/UoQqPy8OFDe3v76Ohokc4fqqurLSwsgoODf37BCAQCgUD4NSFLJ4FAIBD+XTS8gp9AIBAYuLm5hYSE3Lhxo6kLQkDFxcXTp09v3bq1ODev+fn5+FoHgUAgEAgEsnQSCAQC4V8H8cZLIBAal6CgoLi4uGvXrpFT/b8C2dnZeXl50dHR58+fFz7Z/f79+6lTpx44cKBJykYgEAgEwi8IWToJBAKB8K+DXOMlEAiE/xazZs26evUqQkhbW7tv376amppVVVWZmZlfvnyxtLTcuXMndsVAIBAIBAIBkaWTQCAQCP82iLKPQCAQ/nPcunXr8uXLwcHBmZmZsrKybdu27d+//7Jly+rje4FAIBAIhN8YsnQSCAQC4V8EUfYRCATCfxeBQNCwPlsJBAKBQPi9IUsngUAgEH59iLKPQCAQCAQCgUAgEAgEAoFA+E0g3ngJBAKBQPhd4PHqmwP5BEggEAgEAoFAIPzLIUfQCQQCgUAgEAgEAoFAIBAIhN8EcrKPQCAQCITfjtoe0Kv/kUACgUAgEAgEAoHwa0CUfQRC00NR1NevXx89euTj43Pz5k0tLS3804EDB27fvr1nzx4bG5smLGFFRUVAQMD9+/dlZGQOHjzIErO6unrmzJmZmZmXLl3S1dVt1FLx+fw3b97cu3evWbNmBw4caIxHUBQVFBTk6emZmJh4/fr1xnjEr8+v3z85cv/+fTc3t8+fP8vKyg4aNGjHjh0qKir0CGVlZY8fP+aeoYaGRu/evWuMFh8f7+3tfefOncePH8vLy7PErKysfP/+va+vb2Rk5P3797mXpEYePXr0+vXruLg4bW3tAQMGjB49uj65PXnypLS0lGNkCQmJej4OqP/ckpSU5ODg0K5du4sXL/7exvVLSkqeP3/u6+uroKBQn7mxvLz8y5cvMTExSkpKxsbGJiYm3NO+ffu2devWenp69MCioqJPnz4NHDiwDoX59u3bgwcPPDw8zp492759+zrk8Nvw48ePL1++JCYmtmvXrmvXrsrKyuzxMzMz3717JxwuIyPTokULAwOD5s2bs+dQUFBw9OjRBQsW0Od/BhERET4+Pl+/fm3WrFmfPn2mTJkiJycnMmZubq6bm1t4ePiPHz/MzMwcHBxYshVHaWnp8+fPX79+nZGRUVxc3LJlSwMDg4EDB5qbm0tJSTk5OdnZ2RkaGiKEwsPD4+LihHNQVFRs2bKlkZGRrKwsPdzf37+kpEQ4/vDhw6WlpfE/vb29BQIBPUKHDh1MTU1rWxEWiouLnz59yjGyurq6lZVVAz79dyI3N/fhw4d3795dtmzZoEGDmro4vzT5+flPnjy5d+/e6NGjp06dyj0hi6xIIBCaDIpAIDQpr1+/njBhQrNmzWBIpqam4p+qq6sVFRURQqNGjWrCEt65cwdrNCZPnswe+f379xDz4MGDjVqqgICAAQMGcCxV3fjw4cOoUaN4PB5CyMLCojEe8evz6/dPjuzfv3/s2LF8Pj88PBx2tnPnzmXEoW8IVVRU2rVrZ2xsjBWCurq6RkZGbdq0wTtYa2tr9odmZ2fPmTMHK/iKi4tZIru4uPTt2xdiGhgY1LGeCP3ff/+Qnp5ua2uLEJKUlMS1Gzp0aGVlJXtCFrS1tSEfeXl5HR0dIyOjVq1aQUjLli2NjY319PSwAoLH49WxLv9L/eeWffv2QQ5BQUENUqRfk5MnT3bs2LH+c+P58+cnTJjg5OTk5eV1+vRpXV1dKyurt2/fckw+atQohJCZmdmUKVOWLVs2adKkHj16yMjInDp1qrYl+fHjx7x581RVVaFSUVFRtc3ht6Gqqmr//v2TJ0++ePGij4/P/v37u3TpsnPnzurqapZUeXl5t27dous4zM3N+/Tp065du2bNmvF4PFNT03379hUUFAinzc/P37p1K0yDcXFxIvMXCASbN2+Wk5M7duxYbm5uWFhY//7927dvHxYWJhz5zp076urqc+fO/fbt2/fv3xcvXqyoqHj79m3ujVBYWLh161YlJSWEULdu3davX3/8+PENGzaMGjVKRkZGXl4ePga8fPkS4sfHx587dw4riKWkpPr169ejR482bdrweDxpael+/fpdu3aNz+dD/MDAwKNHj6qrq0P8Zs2abdy40dfXt6qqil4MPz+/ZcuWwWeDdu3anTx5Ulz71JmysjIvL6+ZM2fiF6ejo2NjYzN48ODBgwfb2NhYWVmZmppCGQYOHNiwT/9t2Lx5c5s2baABfXx8mro4vzQ7d+5s164dtNWZM2e4J2SRFQkEQhNClH0Ewi/BlStXRC6Qu3bt6tKli5+fX1MVDGNubs5l61hVVTVhwoT+/fsnJib+hFL16NGj8ZR9wOTJk//Lyj7g1++f7Dx69Agh5OnpCf98/PixgYHB9evXGdFCQ0MRQoMHD46MjMSBDg4OUPeAgAAIqa6u9vHxad68ea9evbg8/cyZM1yUfcDs2bMbUNlXVlbWs2fPJUuWxMXFCQSClJSUJUuWQGG2bNnCkpAdNTU1PT29Z8+e4ZDLly9Dtrt27cKBnz9/trCwQAiVlZXVsTo06j+3xMXF9e7de+rUqeyakd8DMzOz+syN586dc3JyoodERkbKyMgoKip+/PiRSw6g7KOjrq5++fLlupWHoih81vW/rOxbs2bN1atX6SFlZWWDBg36888/a0ybnZ2NdfRYsZWXl7d37174bKOnp0dXz5WXlx89enT27Nn4eKY4Zdbu3bsRQrt378YhP378aNWqVatWrdLS0ugxX758KSkpOXToUIFAgANHjhwpKSn5+PFjDg1ARUdHGxkZIYRUVVVv3rwp/CvWaT58+JD+0+3btyF86NChODAmJmbhwoUQbm1tXVhYiH/as2cPhE+aNImlPPb29gghkWrNhkIgELRt2xYK8/nzZ+EI6enpvXr16tOnT+OV4d9OVFQUUfZxpKCgoA7KPkCcrEggEJoKouwjEH4Jnj171rALZHV19Zo1axokKwAOBzWqWo2djIyM/fv3MwJHjhzZ2KVasWJFYyj7xL2gBn9xDUKD98+fDFwjDQwMZI/2/v17VVXVnJwceqCwsg+4ePGimZkZl6ffu3ePu7Jvy5YtDajsO3jw4JEjRxhRxowZI6JL10bZJycn9/TpU3qISGUfRVGxsbESEhJ5eXm1rMZvy9OnT319fX/Cg0aMGFGfubFt27bKysrXrl2jB/bs2RMhZG9vzyWHUaNGLVu2zM7ObvDgwQsXLjxz5syPHz/qVhjg7du3/3FlX0hIiMgPDMnJyRwnZxkZGYRQy5YtGeFYEda1a1esB6yoqKioqKAoys3NjUXZFxMTIykpqaSkVFpaSg/fu3cvQsjR0RGH8Pl8ODT0+vVreky4YmxoaEjXAIokJSVFU1MTIWRkZPTt2zeRcfh8/rBhwxBCHh4e9HDcf+zs7BhJli5dCj8tX74cB7q6ukLgqlWrWIp0/PhxRUVF9mLXH3yPQZy+9ePHjz179mzsYvx7yc/PJ8o+7sBEUQdl379dViQQfj9+Z5s1BMK/iAY3ILVp06aHDx82YIa8JrXfX1VVNXHiRNjS0PkJpWqkR4h7QQ3+4hqEf7WBM4qi/P39EYdalJaWjhgxokWLFlyytbOzq6qq4hKzVl2oYZu6uLj4zz//ZASOHz8eIYQ3P7WFoig1NTVra2sukQ0MDHr06MHdwN/vTUpKypQpU35Oa9SnI1VVVWVlZRUWFsLAwcDxouDgYI75LFmy5Pbt20+ePDl9+vSCBQtqNC3HTtOuQb8CL168UFNTEw7X0dHR09MLDAysMQd8yY6BnZ2dsbExQig4OBjnIy0tDSbq8IVWkTg7O1dXVw8bNoxhoW/cuHEIoevXr+OpxsPDIzExUVNTs0+fPvSYPXv2bNWqVUxMzJMnT1geRFHUxIkTMzMzeTyeq6urOMOdkpKS169fb968OWOgias7Qujvv/+GP86cOcPn8xnxWRIihFRVVeFcZKPCXgaEUPfu3RvENOrvCpk9akWdm+tfLSsSCL8lZEwSCL8h7u7u8FH9t2HFihWvX79u6lI0GOJe0O/34n4FCgoKysrKuMQ0NDTctGkTx2yVlZWPHTtWj3L9DLZu3SocCLbnhwwZUrc8KYq6evUq982As7MzwxHKf5PS0tLx48dnZWU1dUFqplmzZidOnJg8efKqVavo4d+/f0cIgdMDws8nPT39+fPn4eHhjHCBQJCXl8eukqsRuPeNEIqIiOCeSiAQ3LhxAyHUq1cvxk9GRkYqKiqlpaWXLl2CEDgr16NHD8bswePxwCLH8ePHWZ519+5dOANoa2sL8cWhpqa2fPly7lp1TU1NDQ0NhFBlZWV8fDzHVECzZs2kpJre2yGPx8MqSwKBQCAQgKZfnwgEAgsVFRV+fn7h4eEbN26kh2dlZZ07dy4pKUlRUdHExERPT8/MzKx169YIIRcXlwULFiCEcnJyDh06hBAyMjKim08KDQ318fEJCwtr0aJFjx49pkyZQncwBwgEgnv37j18+LCgoMDExGTx4sXcy5ybm+vh4aGkpATW7gA+n//ixYvbt2+fOHFCSkrq1q1bnp6ezZo1GzZs2JQpU1hy4/P5y5cvP336NEIoODgYajR48OAuXbowYiYkJFy8eDEqKkpLS2vChAkiHa5xqTsLcXFxLi4u0dHRhoaGQ4YMEXm4qbS09MGDB+DC0sDAwNzcfNSoUQoKCjiCuBdU44tLS0t7/PhxcHBwZmammZmZhYXF0KFDGbumioqKx48fP3/+/PDhwwihhw8furu7FxcX9+7de8mSJeBzMCoqytXVNSoqSkdHx8HBAawx1g2R/bO27zorK8vNzS00NLS6urp79+4TJ07k7sQtJCTk1atXwcHBAoGga9euAwYMoFcnJCTE39+/sLAQ/nn9+vWAgACEkKGhochDEK1bt4ZBxJHBgwcjhMrKytzc3D59+pSTk6Onp9erV68JEyaIS5Kfn+/i4vLx40c1NbV+/frZ2dlx3yjWp6Ho+Pj4GBkZ7dq1qw5pEUISEhK18mZoaWkJfyQnJ1+9enXcuHEmJianTp0KCAgYO3YsdvaXkZHh6uoaGxtbWFhoamo6dOhQnBAjcm4pKiry9vaOiYnZtm1bSUnJmTNnAgMDW7VqNW3aNGFfyXFxce7u7iNGjOjatSsOTE1NvXv3bsuWLadNm/bt27fTp0/HxMQYGhouXboUuyKhExIScu/evYiICG1t7S5dutjY2IiMRicjI2PixImfPn1CCHl5eSUlJSGEZs+eTT+oxd6ZGQXgGBOTnp5+69YtfGoJITRo0CB6IzCYPXs2mI/E5OTkgKrF0dGR/VkNRVJS0pUrV8LCwtTU1IYOHSqukdkH4KVLl+iHWBUUFLCNtnv37iUkJMDfeHqsrKy8dOnSly9fJCQk2rdvb25urqKiAqYn2alx5kf1WAeBzp07l5eX9+/f/9y5c/Q6Xrt2TUdHh3FcrrYUFxfDHzV65qUTGhoKFr5EqoANDQ0/fvz47NmzVatWCQQCPP0KxwQzfPgOoEiwKnD69Ok1Fmz9+vUVFRU1VwAhhBBFUVgzWKvqs+Dl5eXv719eXq6jo9OtWzdpaek6f1+pkaNHjw4fPhw37LFjx+inznv16gWunyiKAsEAIaSkpDR//nwcp0bRKCQk5NatWxMmTOjWrdu7d+9cXFzy8/N79OixbNkyhi9jFp48eRIUFBQSEqKurm5ubm5ra0sf1HBEFP5esGABnJc8fPgwRVEQOGDAAPpIZF9QWODz+XDiuKqqqk+fPnZ2di1btmTE4bIescjhmLrJnPHx8bdu3erWrZutrW1WVtaZM2eCg4Nbtmy5cOHC7t27I4QqKyvv3Lnz5MmTsrIyKyurJUuW0B1wAVxmJKCwsNDV1RWcL/Xo0WPRokXiCtZQQgiBQPh5NN0NYgKB8P958eIFDEls5yIgIMDBwQFOxFhaWtIjBwUFtWjR4vbt2wKBoKSkZOXKlegfx3Pl5eXOzs6ghNLS0nJ2dnZ2dsYmosrLy5ctWzZ27Njr16+7u7uDvqN79+4JCQn0/AsKCkaOHNmtW7eHDx+mpqbeunWrU6dO4MuM3QLU9evXR4wYAcqL9evXQ2BlZeWSJUuwAA2+QbW0tPAdHBxTJPHx8c7Ozjo6Ogihvn37Qo1CQ0PhV9CFTZ48+datWwoKCqampmDQh8fjMayYc6y7MHAL0sLCwtXVlSGlLV++nOGeLyAgwMDAYM6cOZ8+fYqKitq3b5+0tLS+vv7z589xMUS+IPYXR1HUhQsXWrRosX///tjY2MDAQNhvDxo0CDsrSE1NnT17NkhyWlpa379/HzZsmI6OTq9eveAG0KRJkwQCwcaNG1VUVKysrKBJFRUVRRr8ZsCxf9bhXZ85c8bKyurcuXP379//888/JSUlNTQ0njx5UmORSktL//zzz9atW7u7uycnJ/v4+AwYMIDH4y1duhSbxgsNDXV2dt62bRs8fenSpdCwtbLaI85mH/Du3Tt1dfU5c+YkJiZWVVWdO3cOITRt2jRs94qiORZ4/fo19loLDBgwICsri54hlFbYZh/XhqrJ9N6ZM2dsbGwYdgm5JGRHnM0+iqL4fP6xY8f69esHiulnz55hvYa0tDRYxD99+rS0tPTx48fz8vKKi4vnzJkjKSl58uRJnInIuSUmJmbSpEkwKi0tLUNDQw0MDAwMDECDJiUl5e3tDTGLi4sPHjyIN4rYpYyfnx8u2Pr1693d3Zs3b25iYgLbV3V19ZSUFEZ1tm7dqqmp6evrm5SU5OTkBBlqamp26dLF3Nw8OjpaZPu8fPkS+wJ2cHCAfohNGXLpzLWNiedGHFJcXCwnJ6erq3vz5k26IwKO7N+/HyE0c+ZMjvFHjRoVFRWVlJR0+vTp5cuXHzt2LCQkhPvj7t27p6KisnPnzqioqK9fv86ZMwe7GKbb7KtxAGZnZw8fPhwSbt68mT4wKysr16xZIyUl9eLFCwgpKCgwNTVdu3ZtRUUFn8+/ceOGtLT09u3bayxtjTN/PddBoKysrEOHDhB//Pjx379/pygqMDDQwMAAr4nsgAJF2GZfZWUlTE0yMjIZGRmMX/FtbmGDcdDgCCGRblvAPGinTp0oioqJiYGYBw4cEI6JlVAipiaKoiiqoKAAfxdJSkriUlkGQUFBkFzYZl9YWBj8ZG5ujgNv3rzJ5dXcvHlTW1ubETht2rQhQ4bA3P7hwwc9Pb0aXbezAx+WRL4CiqKsrKy+fPmC/5mdnQ0qIYTQwYMH6X0+NTVVX1/f0NAQ51OjaHT8+HHsyPjBgwdr165VVlY2MjKCC5tWVlY1WlqkKCo5OXnIkCGWlpbPnj379u3blStXdHV1FRUVT58+jZOXlZXNmjULHoQljby8POzzAfywc1lQhMFOJ1xdXRkfgXR0dBj+2WtcjyhWOZxjw4rk0qVL3bp1g4KdPn360qVLWlpaPXr0AK2orKzsp0+fQkNDTU1NDQwMsHQ3bdo0Rj41zkiYiIiIDh06TJ069e3bt0lJSfv37zc3NwfVIcNmHxchRFhWJBAITQtR9hEIvwTCC2RycnJYWBiY0WEo+7p37z5kyBD8T4FA0KdPH7onu9WrV2MJm469vf2oUaPwP6urq+F8nJWVFQ6sqKjo1KlThw4d6MbUQ0NDQc5mV/bFxsY+e/aMsSEH8K54wIABWDhYt24dyGfp6emszUPBLn3p0qWMcNjQtm/ffvz48eD1r6KiAnyJ6Onp0WVQLnUXCSj7tLS0BgwY8PLly7y8vKdPn+I7RDt27MAxAwMDJSQkGJvhe/fuSUhI8Hi8V69e4UBxL0hcOKgJLl26RA8E4VJTUzM3NxcHwrU7WVnZoUOH4t3XrVu3oLSDBw/evn07KCgrKysHDhyIuO3ea9U/ub/r06dPt27dmt7Tli1bhhBSUVFhqMCEGT58uJSUFF23UlJSAucX6KODoqjU1FQoD97S1woWZV91dTUolz99+oQD4Tbco0ePcAhW9vXu3fvmzZs5OTmfP3+eOXMmBA4aNIiep0hlXy0aSrzOLiMjA3TEqqqqu3btou8A2RNygUXZV11dHRsbi1Wu48aNu3r1alBQEJw3EQgEsNNWUlLCSfLy8qAbFxQUQIi4uaWqqgq89Ghqatra2sbGxlIUVVpaCvbssQeV0tLS+Ph4rG6j+4/Oz8+Hl9ihQwdHR0d44rdv3+BSJN1gP/XPUNqzZw8OgVPPzZo18/X19ff3LyoqEtdE+OTUnTt3GD9x78zcYzKUfXw+f86cOXZ2dnXwkpGamnr8+HE1NbVt27Zx2djjAqxcuXLdunVhYWFlZWWBgYHt27dfvXo1s+OJ4tq1azwe79SpU/TASZMmQQNiZR/HAZiQkAB7182bNzMe5Ojo6ODggP+5atUqWVlZegk3bty4ceNG9tJyn/nruQ5SFBUREaGvrw+ZqKiozJs3b+zYsdyVX+KUffi+tsjKsij78AHhmJgY4YRwBA+GNhzrQwidO3dOOOaFCxfgV3Efn968eQMRZGVludRUGHHKvsrKStD+SEhI0B341lnZ5+3tjRB68+YNDnn8+HGNkgY7WNnn6+v79R+Cg4MfPnw4Z84chBBd2UdR1IcPHyC+sIMmY2Njd3d3/E8uohE249ipU6fjx4+DCOHr6wuBDEcowhQUFLRq1crQ0LC8vBwHxsTEwOClLxkPHjyAPBmqIlVVVfSPsq/GBUVcGSC+gYHBnj17kpOTExIS9u7dCypLNTW1/Px8iMllPaI4yOF1ljkzMjLk5eURQvr6+tu2bQNf9mVlZaAEtLCw6N69O14CHj9+jBDi8Xj0scl9RoqMjFRSUrK3t6e326lTp6Ct6Mo+jkIIUfYRCL8aRNlHIPwSiFsg586di/5XmVJeXi4hIaGvr19dXY0Dr1275uXlhf8pUmcEpq8Zn/Xw7heHHzx4ECF0+vRpRglB3cbFtyN8BGbIx35+fvAgukiamZkJgXQJWyTsyj4rKyv69gw/Cw4+UJzrLhJQ9ikpKdG/GBcUFMCVGSUlpczMTIqiqqqqOnfuLHLPA4U0NDQEoY2qpbIvMTFRXl5eV1eXsUkuLy8HVxKzZs3CgdeuXYNKZWdn48Dq6mo4gkePSVHUmTNnEEKtW7dmqT7AvX9SnN91bm6usrLy1q1b6Wnfv38P0RjhDGAbNnXqVEY43s65uLjgwMZT9mVmZsJWgb5BBd2Tk5MTDsHKPnoBBAIBvotK7//Cyr7aNZQYnd369et1dXXpl74nTZrEJSFHWJR9QGxsLETYsGED4yfwBKqqqkqf0+CMKsOBssi5Zf369QihVq1a4fFFUZS7uzs8rqSkBAempKRAIF3ZR1EU3M8aN24cPRCuMjGcn8J5GfrJUHwmyN/fX2TFMeKUfdw7c626PV3ZV1xcPGrUqJ07d7KXUJivX79Onjx51KhRWlpaU6dOZZx/YWfs2LEXL16kh0CnXbBgAXvC4uLiVq1atWnThnFu2sfHB6qJlX0cByBFUdOmTUMItWnTprKyEgeWl5erqqrSx7WVlRVCCB+XpigqJSVl9erVLKWt1cxfz3UQ+PHjB3ynAQYPHlzj+XQMKPtUVVUTExPT09M/f/7s7u4OymKE0PLly+ljEMOi7Pvrr7/gJ5G+cefNmwe/FhQUeHp6wt9XrlwRjolXrvv374ssOfZp3qZNG3G1e/r06Zw5cyxoDBkyBJ/xx8PE1tY2JSUlJSUlMDDw5MmT4CBYQUGBUbA6K/vAqMXly5fpgRx9WIsDK/vk5eUV/4HuEYWh7KMoCm4NM7yuBwcHa2trY1mCo2hUXl4OIc7OzvSYJiYmCKF169axFx4uzgsreUHalJGRiYyMhBDc0xiSBtwVBWUfwLKgiAQr+7Zt20YPBxMx9FpwWY9qlMPrI3NSFGVgYIAQWrJkCT0QmosxQVH/NA5u3lrNSBASHh5Oj1ZYWAjzKlb2cRdCiLKPQPjVIA46CIRfGmEXbDIyMkpKSgkJCfb29j9+/IDA8ePH12hUCIxku7u7/0UDfy2Hr+5lZWXwoV7YopmwTRPuZUYI4Qs49Hw0NDQgMt6E1w1dXV26vRKQ3RFCWMvDpe7sGBoaKikp4X+qqKiAErCoqAjE00ePHoWFhSkpKeFrVhiwxBcTE/Po0aM61O7kyZOlpaX4YgVGRkYG9FBXrlzBgixuZ3ppJSQkQBxkWHAHk+SZmZkCgaAOBUP1e9d37twpLCz89OkT/aXgCzvsLwWkXuE+3717d1DHHD16tG41qhUaGhp37969fv063fYZRVEIITgLIFw8/DePx9u8eTP87eHhwfKU+jQUZs+ePd++fcvNzT169Cickrh16xZWPfwEcFfBu1bMmDFjTp486evrix35Uf8YaWI0I0t/U1BQoJuOArMDCKG0tDT25DgHxhQHOdCnpqqqKlDtYSuQCKFOnTrBrWE829QW7p25bt0+IyPDxsZm0qRJdbDf37FjRzc3N29v7/T09A4dOlhYWCxcuLCyspJL2mvXrsGZI0zPnj2NjY3Pnz//5csXloTHjx9PT0+HM4z0cOE1iPsAXL9+PY/HS0tLA28SwIMHDzQ0NOim7mCGHDlyJFYlaGtrwwQujlrN/PVfB8vKytasWaOkpHTz5k1wjuzv729mZganyTgCH6tatWrVq1evP//8s7i4eP369aGhoUePHq2tM02sb8Jjlg4O5PF43GOKfBC25sbCoEGDLl68uHjx4qCgoKCgoPT09Lt3786YMYMRzd/fv23btjo6OjY2Nvv27dPW1j58+HBsbCw+bV1PoBetWLHi4cOHOLDONlIZhIaGFv1DaWlpeXm5OHdJcFozKCgITIUCFy9enDdvHpYlOIpGIvstEjVJClNUVAR3vYUnLgcHBxkZmYqKirNnz3KsPoZlQWGHvgojhObNmwdW9vAqzGU9qlEOr6fMCQ1Ol+IQbVETKchlZGTAP7nPSO/evfPx8TEwMDA1NaVHU1JSYpisaRAhhEAgNAnEQQeB8O9j7dq1mzZtunPnzqtXr/bu3evg4CAvLw/H/ll4+fIlj8fr378/XZq3tLQEe8ZwZCYyMrKgoKBZs2YMy2KNh6SkZFVVVZ2VTSKRkZGBP/CmlEvd2REWpu3s7OAGX2RkJEIItAAgdTGA0yIIoZCQkLFjx9ayNv+Xs0hPi1ZWVuDKIzQ0tH///iyZiFRzgDxXXV1dVFT0E/ylMt71y5cvEULdunVjCJpQEYaYS0cgEIA/SnFt8unTp4iIiKqqKnHKnQZk3Lhx8EdFRcXdu3e9vLxAkUH3hIBh9CJTU1NTU9OIiAjoQuKoc0MJ07x58+XLl1tZWQ0YMKC0tNTNzW3YsGHckzcIwnbEpaWlsQug3NxcFxeXd+/egXF9kc3IBbw7rfPcAjnQk0tJScEbjI6OpsfU09PLy8vjMo0Iw70zS0pK1qHbJyQkWFpaysvLT5w4sQ7Fo7Nt27ZHjx6dPXu2tLT06tWrNcaHQ2QMevbsGRUVdebMGThWLBK4M6inp8elVBwHYKdOnUaNGuXt7X3gwIGZM2fCe7x+/TpDHblq1SofH5+IiIhOnTqtXLly06ZNioqKcMpGHA0y83NcB4uKigYNGmRtbQ3KkVGjRm3dutXZ2bm4uHjChAlv3rxh91GLadmyZXZ2Np/Pr78bWexhRqQ3DAiUlpZWVlbmEhOJ/6wIx9iRmO8odLAfjG7duomcHsePH3/79u0Gqb5IZs2atXfv3szMzBEjRkyYMOHgwYNt27Zl70V1RkZGZsaMGXQVNmbo0KGGhoYxMTFnz54FdVtFRYW7u/vnz59xnHqKRsKTpDDh4eEQQXjiatGihaGhYVhYWEhICPuDWBBeUNhhrMJSUlLjxo07depUQkJCRUWFjIwMx/WIXQ6vv8wpjDh5BgQ5PC64z0jcZ9oGFEIIBMJPhij7CIR/Hxs3biwsLNy/f39WVtacOXNOnz598eJFOLcvDoFAAFa3hwwZwnJGDzaTcnJytf28/yvDse61RV1dXV5evrS0FD6oRkREINrHVTpqamoQE58WqRUsOYOTDYRQbGwsu7LvF+T79+8Ioc6dO9vZ2dUqYVJSUllZGWJtk8rKyuTk5LqJ1LWlsLDQ2dn58ePHy5Ytc3V1/eOPP6BqXNDT04uIiBBZEUydG0ocFhYWCxYsOHLkCIz3XwSwqZeRkbFu3brVq1crKSnhe6+/CDweb/To0Xfu3HF1dd2yZQtMkhRFZWZmtm7dmsWzLQvcOzOPx6tDt5eTk6uoqPj27duCBQvwQYw6M3369Hfv3rm6ui5atEjY0zEXlJWV0T/bUXFAtxTpNVIkHAfghg0bvL29w8PDfX19R44cWVBQ4Ofnd+LECXqcfv36ubu7z5o1q6SkZO/evVevXj1+/Pgff/zB8vTGm/mFWbFihbKyMr4MqKCgcPDgwcmTJ0+YMCE5OXndunXs3mwZNIiqC3uAxafL6YAfZOicBgYGPB6Pol2oFI6JEMIeSxh06tQJkpeVlcXHx7PM7XByGdE++4mkxurj5OzKrOrqaoYWpnnz5s+fPx85cmRiYuLdu3cfPHiwadOm9evXN5JuESE0f/58ul9vAPz2LF++3M3N7dChQ0pKSh4eHlZWVtgBbiOJRgxggCCEMjIyhB1q6+johIWFNdQAqRug7aqurs7JycGn52pcj1jk8J/TsOLgPiNxn2kbXAghEAg/jd9nP08g/KfYu3fvy5cvwTvhx48fe/To4eXlxZ4EBE12oQrkg8LCQvo9td8ALnWvA/DtFIxMg0hXUlKCNy3CMfH94loBOYu8KYM/3tYt56YFPsjX4aVg6ZmlTaSkpIT3FY3B06dPDQ0NIyMj/f397e3ta3uWEDoP/F8cdW4oFsA/aZ3PzTU4hw4dMjU17dixo5eXF/1a5a/GqVOnzM3NExISFi1aBGc9jh8/XlRUBN7A65Ah985ct27fqlWrGzduSEpKXr169dixYxxLxefzX79+LXyECruGwKa1xDF37tyePXvSzxABMBUnJyezpIVliKNtB+4DsHfv3uC2BdwK3759e9CgQcJn2CdMmBAeHg6GLL5//z5hwoQNGzawFKDxZn4G+fn5ly9fXr16NeNokoWFxcePH9u2bfvmzZufP6J79uwJf9Dvy2NARwAjWkVFxcjIiD2mrq6uuHm7ZcuW2Esp/XqsMOIuAtcW/NZEaicxJSUlwiebTExMwsLCVq1a1axZs/Ly8s2bNw8dOhSU9Y3B+PHj8Zc/Og4ODqClun79OkLo4sWLYD4P00iiER0uE1fTCjCw/kpISOCiclyPWOTwn9Cw4uA+I3GfaRtDCCEQCD8HouwjEP5lCASCrKwshFC/fv1CQkJOnz6toKBQUVHh4ODAchBGQkICVve7d++KjPDixYuKigp80yQqKqoRyt40cKx7HXLOzs5GCIF1c3ABiRASaY4KYuI4tQJSxcbGlpSUiMwWIcR+rvPXBDqbuJcSHx8vTrJUVlaGT/EsTW1kZMR+rKNBiI6OHj16dHl5uYuLC91gHHdgLGMD+SKpc0OxAIdfYO/d5Li6uq5evdra2nrFihVNXZYaUFdX//Dhg5WV1YcPH/r06WNnZ5eYmBgTE1NnBSX3zlznbm9tbb19+3aE0KpVq7D1dHYcHR379+/foUMHhnoCn5nCM49Ifvz4cenSpY8fP965c4fxE0xisD0WB3R4LmtQbQcg+HJ59erVu3fvhE0Kon92v3p6el5eXv7+/nB8bO/evdjYljCNN/MzAO8oIg+QamhorF27trKyUuT2vlHR1NSE1Qcf4MJUV1fDSxw0aBCEwAVb4Zjon0NGOKZIsDOQ3bt3/4Rjv/jwIPvJ68TERMYVXYgP5y7Dw8PBqNzz588PHz7caIX9P3bu3EnXpSopKc2aNQshdObMmaSkpISEhKFDh+JfG080ovPTBkidgVW4R48eMHlyWY/Y5fCf07Di4N7g0G/j4+OrqqrY82wMIYRAIPwciLKPQPiXUVZWtmPHDvhbUlJy4cKFL1++lJSUzM/PZ1zKY3zkB79p58+fF/6OFxcXd/78eRkZGWyPAyy8CCPStPZPo87HFrjUvbZ5pqWllZSUdOrUycbGBiE0YMAAsJzi6urKiJmZmVlSUtK8eXO6C0UkvjqMcNgglZWVCUta8fHxCKF+/fqJNOP1iwMv5dOnT9hLI6a8vHz58uVgNlsk0CavX79OSkpi/ARtwn7zrlaw9Plz586VlZUZGRnRFQ216qWxsbFycnLz589niVOfhhIH7LeFTdfXmfrMDM7OzgihLl260AO52ONvErZs2bJgwYLg4OCgoKA7d+4cOXIEvN9wR+QA59KZ69ztN27caGtry+fzJ02axH6qDoCdW15eXkxMDD08JycH/mC3DaeioqKrq2tvb4+dsWLA3CG7OylYhh4/fixcTQB3ttoOQFtbW1CWrVy5MioqCjxR0gFtIGBjY/Pu3TvQ+LBYoK/DzF83oJulp6eL/FVbW1tHR6fGhaAxVnDwayHcRMHBwWVlZWpqatheJMT88OEDw8dLVVXVhw8fEEKMc2cMJk+eDNrAjIwMOJ5ZK2pbd0VFRZh7X7x4wXIoz9vbe8KECfSQw4cPY214hw4d/Pz8IEJ9/BhwKfy7d++OHTvGOKy6dOlSHo8XEhKyaNEiR0dHhoWWxhCNGOjp6YGniGvXrgnXAiau8ePHwz9x8Rjqp9LS0noWgwWY5cDlGuK2HtUoh/+EhhUH9xkJZtrCwkLsvJ4Bfl+NIYQQCISfA1H2EQi/BFiSYFiHAZmYIfd4enrSZeXu3bvD3gl886F/TFlnZGRAbtXV1RRFgWu2wsLC4cOHx8XF4eQBAQFDhw6FTY6RkdHIkSMRQteuXXv9+jWOU1FRAdoB7HeMBZFlFrd1h2g1CrJQI/zJGm/nIDnjWbhxcDQuda8tZ8+e5fF4586dg4tj7dq1W7t2LULozp07jFYCkWvPnj34kojIFyQufOrUqf369UMIXbx4kVEGV1dXKSmpU6dO4RBx1oUgnNFQOHKN7V+r/snxXTs4OICZoVmzZtHdwiYnJw8ePHjcuHEs9yKdnJzU1NQoigKfd5jy8vLbt2+3a9eOfvMOf0Kv27d0fIREeL8BcnxsbCyOk5+f//XrV0TrhCw8e/YsNjZ269at+HYk+qf16O1cn4YqLy8XHrMURZ06dWrhwoXCyg6E0OPHj2ssuTAsrYQfCn8Idw9oRvqVz48fP8LumtGM3PsbTkjv2ziQew6MobF79+579+6BkfXaoqCgABs8xjzGvTPXqtvTS87j8a5du6atrZ2dnT1+/HjhM8IM+vfvr6ent3PnTsaOF7QVzZs3p58Pun//fufOnRmnlgYOHLh79256x0YIFRQUBAYGNm/efMmSJSxPX7lyJY/Hq66u/vPPP+lvKjg4GP7AXbq2A5DH48EW/e3bt9OnTxe+8xseHk73ltOyZUuwUcVyzbBWM3991sFOnTqpq6vfv39f5K8vXryo0fsTGLxD/5yv5A6eOUWWcN68eZqamk+fPmUoIsFrxF9//YW9FlhYWNja2hYWFjJ8Bz98+LCgoMDGxqZXr14sxZCUlLx58yb0yX379t26dUtktPLycpHhuNbcNUfg/7qwsHDDhg0i637y5Mnc3FysqwIoiqKfaZWUlAStN70XnTx5ctasWQxlOgu4zOI6SUJCwowZM2xsbBjqPCMjI/hI8PTp09mzZzNScRSNxPVbkZMkAx6Pd/r0aSjh8+fP6T8FBQVFRUVNnjwZvpgimrOIV69e4Wienp5FRUWItsog1gWlVhQUFLi7uw8bNmzy5MkQwnE9YpfD6ylzihTkcKBIQQ43CPcZadq0afAJYfv27XQ7pxERETDkcXLuQog4WZFAIDQZFIFA+AU4f/48DMl3797Rw+H7m6amJuh9KIoCcWfTpk04TmlpaevWradMmYJDfH19Ibft27cHBATMmDEjNTWVoqhp06ZBuKSkZL9+/SZMmNClSxcpKSlPT0+cNioqqnnz5gghOTm57du3v3792tfXd+jQoWA2W0pKavr06X5+fuIqUlJSApL9xIkT6eFYUfXy5UscmJCQAIFbt25lbx+QXWRlZe/evfvw4cPp06cLBAKKouA6WNeuXemRsUEf+JIMcKm7SHbt2oUQUlFRiY2NxYGenp4tWrQ4deoUPWZZWRnsgQcNGlRRUQGBISEh6urqc+bMwW+QEv+CxIXHxMTANYq///4bZ3Lt2jU5ObmzZ8/Sy4Btt2dmZuJAPp8PasRRo0bRI+Pzm3BBjAXu/ZOqzbs+d+4ctq9kZmY2YcKEfv36SUlJrV27lr08FEV5enoqKSlJSUk9fPgQQgQCwcKFC1u1ahUYGEiPiVv1+PHjNWbLoLKyEm8/tm/fzvgVXCEjhAYMGHDp0qUNGzYMGDDA2toaIdS2bdvLly+fO3eOoih8oOPWrVs4bVRUlIGBwdy5c6uqquh5QkdVVlaurKzEgbVoKIT+7z+KEggEOjo6kpKSM2bMSEhIgN/Ly8sXLFgwdOjQ8vJykQnV1dX5fH5tGwrrv6ytrUVGwI1w8uRJxk9gHw0hNG/evEuXLs2dO9fe3h4MpdvY2Fy8ePHRo0eU+LkFzicqKiqWlJTgQKwOe/78OQ7El1gZZYDJjVFyOIUkIyNDbygYhioqKubm5gMGDBg1atTkyZMXLFhw9OhR+ogTBxg469ChQ0BAwMWLF3GP4t6ZuccEvYmlpSUOCQgIgF5kbW2dn5/PUs6MjIyuXbsypoX09HRwsHv37l16OHh4VFJSok8Cqamp/fv3LygowCHV1dXTpk3j8Xg3btyooZkoCnvDHDhw4I0bN96/f79v3z6seezcufOKFSv4fD7HAcjA3NwcIRQWFib8k4WFxaBBg+hvfNy4cYaGhkVFRSyl5T7z13Md9PPzk5WVffz4sXB4ly5d6P1fJJ8+fUL/EBISwh6ZDiyCCKE3b96IjODj4yMnJ0cXQr5+/SonJ2dtbc2YTBISEnR1dY2MjH78+AEhhYWFpqammpqa379/51KY4uJimCR5PN60adOCg4Ppv6ampk6aNAkh1KxZM2dnZ/pPuLe0bt26rKyMy7MoioJrsAihmTNnBgcH42k5ODh41qxZSkpKjDWRoqjVq1draGjA2g04OzvLysp++fIF/omvqI8fP55LGQoKClRUVPDc9f379+Li4qqqqsLCwpSUlLdv327atAkOdV64cEE4ORiSs7OzE5k5F9EIzt8xFkGBQACKrQEDBtRYhb///pvH47Vu3frbt28Qkp+f36tXr169eoEvC5wnDE8lJaXdu3efPn16/PjxDg4O4A6oTZs2Bw8ezM3NpVgXFJGUlZXB9LV06VKQHimKKiwsHDFiRO/evekvi8t6xEUOr7PMWVlZCa970aJF9HA4coj+V2ATCARQNrp0x31GAouuCKF27dqdOnXq/fv3165dGzBgANgcVFdXX7BgQVxcHMVZCBEnKxIIhKaCKPsIhCYmJCRk2bJlWJLr0KHDli1b0tPTL1++DDIrMGTIkDNnzlAUVVxcrKmpaWhoOGLEiGPHjp04caJXr17z588vLi7GeVZXV8O3XISQrq4u3gQKBIK9e/eCP0Sgf//+79+/ZxTp27dv1tbW+Ptwu3btHj16NGzYMG1t7VWrVn38+FFkRSorK3fu3IldNEpKSjo4OMAuffPmzfimm4mJyd69eymKunbtGrY0p6ysvGbNGpYNVUpKCj632K9fv/T09NevX48bNw5XZOzYsf7+/nw+f9OmTfhCgYaGxo4dO2pVd2FKS0u3b9+up6cnISHRuXPnSZMmmZub9+3bV+ReUSAQODs7a2tra2trz58/f+LEicbGxm5uboxo4l6QuHCKooqKihYsWKCqqtqlS5fly5cPGTKkR48e9H1OQkLC+vXrQVGLEOrbt+/Ro0cpinJ3dwdvDAghCQmJ2bNnP3v2LC8vb+3atfjOV8eOHeGlCFPb/lnbd+3r64u1aQghfX19LroAID4+fujQoYqKijY2NsuWLevevbudnV12djaO8OnTp9WrV2P3F8rKyosXLz5//jzHzOfMmUP3+cjj8QYPHrx69Wocp6KiAh+Ok5GRWbFiRVFRkZeXF8jEnTp1whuba9eude3alcfj6enp/fHHH/369TMxMWEoTTw9PadOnQqSN0LI2tp63759+FeuDUVT9lEUhceItLS0ra3t+PHjR48e7erqinc7wgkHDhzIpX0wixcvZjhmNTMzc3R0xFv3oqKilStX4stlSkpKixcv9vX1pTe1sbEx/Kqurn7q1CmBQAAnIxBCkyZNKiwsFDm3pKSkLF26FK4sIYSGDh0K6tTNmzfjs1QdOnQ4duxYQkLCxo0bsaPP5s2bw0XOgIAA+g2yefPmffz4MS0tbeHChdh1prW19YMHD6Cod+/eFedNRVlZucbtzaNHj+Tk5OBZCxYsoOtza+zM3GOeP39+xIgRuNPa29vDFHT8+HHY7iKEWrRo4eDgkJOTI66osbGxw4cPnzVr1t27d4OCgtzc3PT19du2bUt/cYCrq2ubNm22bNnCCP/48WOfPn0OHz78+vVrV1dXW1tbXV1df39/9ibCnDhxAs9mMjIyM2fOfPr0qaSk5NChQ11cXECNyH0A0lm1alXPnj1FPrR///5mZmbdunVzcnI6f/68vb39wIEDYa/LDpeZv/7rIEVRXl5ehoaGM2fOvHXr1vv3793d3SdPnmxvb5+WlsaSKjw8fMqUKXgah2dNnTq1Rr3DiRMnFi9eDJ0WIdS+ffuNGze6uroKxwwJCTE1NbW1tT1//vz27ds1NDTWrFlTWloqHDM3N3fMmDEmJiZHjx49duxYx44dR40alZKSwl4SBkFBQfb29mBEUktLy9LScuDAgcbGxlJSUt26ddu1a1dSUhKOfP/+/VGjRuGpFSHUrl27efPmRUREcHmWp6cnPqMqIyPTvn17ZWVlOTm5SZMmiexgq1ev7tKli76+/vr16y9evLhy5UpTU1O6ivbHjx9wTmrbtm3sj87Kypo9ezZ2EVsjIstTXV2tp6f36tUrkY+oUTRydXXFrlHU1NTWrl2bmZkZGBgIt0BgknFwcKixMZ88edK5c2dVVdVJkybNmzfPxMRk06ZNjG9dFEV9/foVLo0ihBQUFI4cOSIQCPr27du2bdtVq1YFBgYWFhayLyjiePfu3bBhw6SlpdXU1IYNGzZixIj27dtv376doY+ucT0qKyvjIofXTea8cuWKra0trv6qVau+fPmSnp6+bt06+qK2c+fOioqKe/fu4am+WbNmjo6OeA3iKItSFPXixQtsdJLH4/Xp0yc+Pl5WVtbc3Hzfvn30HsUuhIiTFWt8LwQCoVHhUU1qgYtAINQWiqLKysrk5eWLi4sjIyMrKio6deqE7aZjqqurw8LCKisru3XrhnetOIf4+PisrCwjIyM48CWS4uLir1+/6ujogKCZkJDQrl27hvJzVwdKS0tDQkJUVFSwYcE6wLHuIvn27VtaWppAIGjfvr2wD0cGmZmZ0dHRLVu2NDExEdlo4l4Qy4vD5f/+/Xvbtm2x5uI3ICMjIy4uTk9PT6RLQXaqqqrCw8PBdBfsoH4yMTExmZmZsI2BkOjo6LKyMjMzM8aNqpycnKSkpKKiojZt2hgaGtbhWTU3FO5s/yzuoaGhkZGRVVVV6urq3bp1E2vV65+E1Xw+fVf8c6iqqvr69St0e7hcSVHUhw8f1NXVGVdBm5bXr1+fOXNm165dKSkpOTk5xcXFxcXFJSUlWVlZfn5+LVq0qNEDRnZ2dmRkZPv27UVu4Ll35p/T7YOCgt69excXF6elpWVubt6/f398JZMLRUVFz549+/Dhg5ycnIWFRb9+/Wrrszg+Pj4vL69Lly7S0tKFhYXl5eXYBTmG+wAE+vXrN3PmTEdHR+GfSkpKFBQUqqqqoqOjc3JyDAwMauvUu8aZv/7w+Xw/P7+vX79mZGQYGxv36tVLpNeOJiE5OTkoKAg+SrGvsPn5+WC8z8zMjK5BqBXV1dWhoaHfv3/Pzc1VUlJq1aqVmZlZrbood7KysuLj4xMTE2VlZTU0NLp37451oAygF1EUlZiYmJycrKWlZWhoyOiKhYWFSUlJP80xRWRkJNagiaQ+olGtKC4uDg0NlZGR6dixozinOtXV1dHR0aWlpWZmZvAtJzk5uaEEntLS0ri4uNzcXDU1NVNTU5H+u2tcjzjK4egnNqw4OM5IWVlZcXFxHTt2BG1dfHw8/UsnnfpIawQC4edDlH0EAoFAIPwuCCn7Gj3hf4YHDx5MmzYtOjpa5OG+qqoqS0tL+k1Jwi9IaGjooEGDkpOTG0klRCAQCAQCgfCLQBx0EAgEAoFAILBRXFw8c+ZMbW1tcdd4MzIyOnXq9JNLRagt27dvnz9/PtH0EQgEAoFA+O1h3hEjEAgEAoFAINDJzs7Oy8vLz8+/dOnSnDlz6D9RFPXmzZtt27aBr0PCL0VhYeHixYt5PF6vXr0iIyNfv3599uzZpi4UgUAgEAgEQqNDrvESCAQCgfC7QK7xNhozZ84EdZ6Ojk7fvn01NTUrKioyMzODg4P79++/c+dOYsPoF+TOnTsTJ06Ev3k83q1bt+zs7Jq2SAQCgUAgEAg/AaLsIxAIBALhd4Eo+xoTd3f3y5cvf/nyJTMzU1ZWVl9ff8CAAcuXLzcyMmrqohFEU1hYOH/+fF9f3zZt2uzZs4fuw51AIBAIBALhN4Yo+wgEAoFA+F0gyr6fgkAgEOnplUAgEAgEAoFA+BUgoiqBQCAQCARCLSCaPgKBQCAQCATCrwxx0EEgEAgEwm8HPqlHIBAIBAKBQCAQ/mOQT9MEAoFAIBAIBAKBQCAQCATCbwJR9hEIBAKBQCAQCAQCgUAgEAi/CUTZRyAQ/ovk5+d7eHjMmTPn0KFD9PBv376dP39+zJgxL1++bKqy1ZnY2Nh+/fotXbq0qQsitnl/KQQCwfv379evX79gwYIaI4urkbe3t4WFxcWLF2v16NDQ0B07dtjZ2dWuxFygKPivuKjI28trtoPDqZMncSCn/34uxcXF3t7es2fPPnXqVK0SUhQVEhKyb9++Pn36VFZWNlLxGpCMjAwXF5eRI0cGBQU1dVl+NrGxsYcOHerbt2+NMSsqKl68eLFmzZqZM2f+hIL9iwgMDJw1a1anTp1MTEzmzJnz/fv3pi1P3aa+JsTV1fWPP/7Q19fv2rXrli1bajVpFBYW3r59297e3sfHp/FK2EjUXzDYvXu3paXl27dvG7BU/01+tVHcGFRUVOzYsWPo0KFt27bt16+fq6trU5eIQPhvQxEIBMJ/jHXr1unr68McuH79eghMS0tzdHRUUVGBcD8/v6YtZB3Yvn07FD4xMbEJiyGyeX81Pnz4MGLECB6PhxAaPHgwe2SWGllbWyOE9PT0OD43LS3N3t5eSkoKIWRsbFy3wtfI+fPnO3fuDAXetWtXIz2l/tS5nK9fvx4/fjw0I0KovLy88QrZIKxYsUJLSwtKGxAQ0NTF+XlkZ2fPnj1bXl4eISQlJcUe+dKlS1ZWVtBKlpaWP6eE/wo8PT0tLCyKiooyMjJgLho6dGjTFqm2U1/TsnHjxtmz/x97bxlQ1fI9fg8tISgSKmAhkgrYhaJiYaBi17VR7O5rX1uxEwUVFRTFwi5AUVBKQEpQUkK6OZz9vFj/7zz7d87hsGmvd31enTN79vSsPXvtmbVm8/l8b29vOTk5Qsg///zD8V5HR8f27dvDsLx582adlrMuqOHCoLS0FFrsr7/+quWS/cf4DWdxrZOfn29lZeXk5MQwzMGDB2HgvX//vqHLhSD/XVDZhyDIf5Hy8nItLS1h3U1kZOS/V9kXHh7evXv3+fPnN3RBKmze3w3YW1epso+puEbu7u6dOnU6c+ZMlfJdtmxZnSr7GIbh8XjNmjX7zZV9TM3Kee7cuX+Lso9hmKCgoP+gsg84efIkF2UfMGXKFFT2sUlKSpKRkdmzZw/8/fLli76+/tGjRxu0UNUUfQ2Cm5sbIeTdu3fw9+rVq3p6em/evOGeQmZm5r9X2VfzhcHWrVs7d+7s7e1di6X6r/F7zuJax97evlGjRjweD/4uW7bMzMwsPT29YUuFIP9l0BsvgiD/RSQlJbW0tJKSkgTCdXR0GqQ8tYKhoeHHjx8buhSEVNy8vxuampocY1ZUo3Hjxo0bN66q+aqrq1f1lqoiJSWlpqb269evus6ohtSknP+u2cp9sP15VKnu/6Ju5fF427Zt27NnT53mcunSpbKyMio0TExMIiIi6jRHARISEu7fv7948WJ2YPVEX4MAXwVoA06fPn369Oli4gvXt2nTplJSUuXl5XVaTkLI/fv3mzVr1qdPn1pMs+YLg507d+7cubO2yvP7s3Hjxj179khK1qapqwafxfVAQUHBtWvXVFVVpaSkIOTYsWMNWyQEQdBmH4Ig/1FEruRqd3n3X+Zf0ZJwjJcjtVijKuX7m+dSc6pdzn/FGKP8W7qjLmioiVbXrF692svLq65zefHiBWm4ZikpKRk/fvzPnz8bJPeaw+PxwAIvxwasqL71MH9jY2NnzZpVXFxc1xkhYrhz587+/fuZ2jZf27CzuH7w9/fPzc39s+uIIP86cEIiCIIgCIIgSBW4evXq8ePH6yGjlJSUesilIuzt7f38/BqwADUkPT2dx+Nxj99Q9c3Pzx87dmxWVlb9Z41QwsPDZ82aVeuaPtLQs7h++C/UEUH+deAxXgT5XXB3d3/9+nVZWVnr1q27dOkiLy/fr18/QoiDgwM9PLJs2TIZGZmioiK250pbW9s2bdrQvx8/frx169ahQ4dSUlL279+fl5e3Zs0aQ0NDQkh4eLizs/OyZcs0NDSuXbv28uVLKSmpvn37Tp06VVFRkV0YPp9///79t2/fpqSkaGpqdu7cedq0adQcPlBcXHzhwoUvX77Iysrq6ekZGxtraWlBRkBycvKFCxcSEhJUVFSMjIw0NDQsLS0bN27MTuTTp0+enp5hYWFaWlo9e/acMGGCwPfzSnMRSX5+vouLS1BQUFZWlq6ubv/+/YcMGVJ5H1SRtLQ0FxcXIyOjoUOHPn/+/NatW9nZ2d27d58wYULr1q3ZMb9//3758uVZs2a1bNnSwcEhKCho5syZw4cPpxHu37/v4+MTFxdnZGQ0ePBg6rby+PHjZWVlNJqlpWWXLl0IITk5ORcvXoRAXV3dMWPGwO+IiIgbN27MnDlTV1dXoLT+/v4+Pj6BgYGNGjUyNze3srLS09OjV93d3b9//w6/x44dC9ajnZyc6PnK7t27W1hY0PhV7ZeIiIhHjx6xQ8aMGQOFjIqKevDgAQQOHTrUxMSkokS4NHhV5wslNDTU2dk5Li6uU6dOI0aMgHauFIZhvL29PTw8jhw5wg7n3j4ZGRkXLlwICAho0qTJyJEjbWxshOMkJSW5uroGBwfLysp26dJl6tSpysrKAnF4PJ6bm9vz588LCgpMTU25+15kGObdu3eurq67du1q0qTJhw8fnJyc0tLSzMzMVqxYARl9+/bN2dk5PDxcS0tr9uzZZmZmwumIH2DVKGdRUZGLi0tgYGBWVlbnzp3Hjh0rPKpFwkXyCFOp0ODxeC9fvrx3797p06fLy8udnZ2fPHmioqJia2s7bNgw4QSzsrKcnJz8/PxkZWUtLCysra25FJ4iXjZWqTD+/v5ubm7Z2dkaGhrm5uby8vIjRowQiFORCAKKi4sfP37s5+e3d+9ehmFu377t4eFRVlY2dOjQOXPmQMF8fHzc3NySkpKMjIyWL1+upqYmsl5paWkXL14MCgrS1tYeOHDgyJEjubdJpROhes8LkYgfDxcvXly0aBEhJDExETx0d+7cGXxWVC9BYTIzMy9fvkwISUtLI4Q8ffo0OzubEKKurk5dFYt53BNCPn36dOfOnfj4eDk5OVNT0ylTpog0IBASEuLp6RkUFKSoqNi5c+eZM2fCZOHxePb29pcuXSKE+Pr6QjVtbGzAW0VFoo8Qwufznzx58vnz59DQUC0tLXNzc2trazDNSanqbBLJ9+/fnz9/HhgYmJ2dbW5u3rt3b/YB2ODg4BcvXtBH2MWLF2FMVtRT4uvLBp4+v379MjY2XrZsmfBQr6rsSklJsbW1DQkJIYS4ubmBfc8lS5aAZwzx6wcuvSxyYRAQEODq6jpnzhx9ff2XL19eu3atpKSkf//+8+fPF9icVVRU9PDhw6SkpBUrVtDA3Nzce/fuJScnr1+/Pisr6/Tp058/f27Tps38+fNFzriEhITr168HBQWpqqqampoOHDhQuGHFUNEoZSN+PFAqWmx/+PBh7NixeXl5hJDDhw9LSUmx51pF5Obmenp6BgQExMXFGRoadu7ceeTIkXS1zGUWi4SLuKi0B58/fw6DCpCQkACbeoSQly9fUjOy8+bNo77pRFLpjH748GFkZCRoyfPy8mDukAqmD4Ig9UpDGgxEEOR/jB8/ftSoUZmZmXw+/9WrV2pqajNnzoRLhYWF1L5Mfn4+BKanp+/duxcCwZVEamrqli1bOnToQAhp0qRJYmIi1WhMmTJlz5499P38zZs3Xbp0kZaWpus5MzOz+Ph4WpiUlJSOHTtaWlp++fKlvLz8yZMnsrKyVlZWNHeGYTIzM/X09LZv315WVlZaWnrq1CkJCYlLly7RCL6+vs2aNXv06BHDMDk5OX/99RchJDY2lkYoLCy0s7ObOHHizZs3r1+/DoutIUOGZGRkcM9FJN7e3k2bNl22bFliYmJpaSnYmlm/fr1AtJ49exIhfwv0BI14Bx23b98eMmQIrOdOnjw5a9YsQoisrCzcq6qq+vr1a4ZheDzesWPH+vbtCy/D/v7+9DWmRYsWkFRsbOzgwYPXrFlz9+7ds2fPtm7dWkJCYu3ateXl5dDRtOMuX74MgUBYWJiioqKVlVV6enp+fv7BgwfNzc0h5qdPn9ilzcvLs7Oza926tYeHR0JCwq1btzp16iQrK7tnzx5qR7m8vPzUqVNw+8OHDyEwNzcXel+gobj0i0Dz8vn8O3fuQIu1atWK3csMw4BFoXXr1pWVldWkwRnO8wUATZOVldXx48fZrzdSUlJHjhwRKINAjWJjY9esWaOtrU0IUVJSYsestH12795NCDEwMPj48WPz5s11dXVbtWoFWR8+fFgg3+PHj1tYWDg6Ot65c2fu3LmEEF1d3aCgIHacjIyMgQMH9u3b9+XLlwkJCY6OjiYmJk2aNCGVOb5Yt24d9RIbExMzb948DQ2NXr16ycvLE0IsLS0Zhtm7d6+KikrPnj2hkEpKSsHBwexEuAywqpbz5cuXPXr0OHz48L1793bu3KmgoKCkpHTjxg12nCdPnkDJ2Q46KpU8IhEvNEpLS+3t7aGcjRo1+vnzZ+/evVu1agVNJ1IoBQUFtW7dev78+Z8/f46Jidm0aRP1O1ypgw7xsrGqhTl27Jienl5ERATDMN++fevatWurVq3YEcSLoMTExFmzZoE7XQMDg9jY2L59+7Zr165r164g1rZt21ZQUDB58mSwNda0aVNCiL6+flFREc3i9u3bhBBpaWloZ8Ji2rRp7JgMw2zYsIGIctBR6USo3vNCJOLHQ3FxsYODQ//+/Qkhbdu2dXBwcHBwePv2bbUTFElmZiakDO/htra28NfFxUX8455hmLKysilTprRo0eLVq1dlZWURERHt27dv06YNDANKfn7+kiVLOnbseOvWrejo6LNnzxJCmjVr5u/vzzDMt2/fHBwcwDHR4MGDIfdv376JEX0Mw8TGxlpYWAwcONDb2zs2NvbMmTNNmjTR1NS8e/cuRKjGbBKGz+efOHFCVVX15MmTsbGxr169AgOCU6dO/fXrF8QJCQlxcHDYvHkztMz69evF91RF9YWr8PRxcXGZN2+evLy8mZkZaOJ0dHSys7PZ6XCRXQK8ffuWPqEWLVoEWRcWFopfP1TayxUtDE6cONG2bVsI9Pb2trOza9q0KVXKsL3u+vj4ULXayJEjITAqKmrChAnw/B02bJivr6+2tra+vj5EU1JSCgkJEaigq6urqqrq5cuXExISLl26BPc2a9bM1NTUzMxMYMUigPhRyn08AGIW2y4uLiBY4EEMc01MwRiGefnyZatWrZYuXRoSEhISErJx40YJCYkuXbqEhoZCBDGzWEyylYoLjj1YVFS0ceNGCLS1tWU/KMvLy8FbroeHh/g6VjqjGYbx9PR0cHCYNGkSIURRUdHhf9DpgyBIQ4HKPgRpeO7cuUMICQgIoCGXL1+ePHky/Xvv3j14WrPVbfB5kPxPeZGTkxMZGQlrwcaNG8+ePTskJOTixYstWrQ4ceJEQkICXUp26tTJ1dW1pKQkIyNj165dEDhw4ECaMuwtcnR0pCG2traEkAsXLtCQZcuWqaiosGvx119/nT17lv7t1KnT6NGj6d+SkhL6wgmMHTt24sSJ9G9hYSEsstkLzUpzEaasrAy+N9JFBp/Phw/v0dHR7Jg1Ufalp6e/fv0aYmpra+/ZsycrK6uoqOj27dtKSkqEkKZNm2ZnZ5eXl0dHR9OXDVtbWw8PDy8vL2NjY3gly8zM1NXVPX36NE05MDAQVvaXL1+GEB8fH7j96tWr7DKUlJQoKCjAmrKwsDA2Npb2psDSecCAAY0aNWLrc9PS0vT19QkhS5cupYE5OTlwO1X2AaBtZDcUl34R2bwzZsyAZbqADujly5fNmjUT41OVY4NDZC7zBQBln7a29uDBgz98+PDr169Hjx7RFfOdO3fE1CgjIyMmJgYmi8Abb6XtA8o+NTU1KyursLAwhmH4fP6cOXMIIaqqqoWFhTTmsWPH2rRpww4ZO3YsIaR9+/alpaUQUlhY2KFDh44dOxYUFNBojx8/hlpU6uX21q1bELNPnz4PHjyAQHqQzcrKatWqVZBXWVnZoEGDCCHz5s1jp8BxgHEv58ePHwXeGMHOt4yMTFRUFA0UqeyrVPIIw1FobN++nRAiLS09aNCg9+/fMwzD4/GmTp1KCGnZsiVbER8cHKygoDB37lx2LuvXr4fSVqrs4yIbORYmNTVVRkaGrbmOiorS0dGhf7mIIOZ/M6Vx48YjRoyAEcswzIEDB2Dw9+vX78aNG3w+n2GYtLQ0aLpr167R20HZJykp2a9fPw8Pj1+/fr1//55udVy2bBm7+iKVfVwmQjWeFyLhOB5gl1Pfvn1rK8GKALXayZMnaYj4xz3DMEePHiWEzJgxg95y4sQJQsi0adPYKc+aNcvExCQnJ4eG9OrVixBiYmJCQ0D+b9myhYaIEX0ZGRlqampmZma0UxiG8fX1BWWxu7s7DeQ+m0QCt9+6dYuGlJeXg3d1c3Nz9kejmJgYGGZU/yIe4foCoOwzMjI6ePAgCJzw8HCoF5h4AzjKLmHoE+rFixe0RuLXD5X2spiFwZs3byDQ1NTU2dkZGvzMmTOEEAkJifDwcIiWkJAQEhIC+wGpso9hmNLS0sGDB8PTc/To0QkJCQzDZGZmGhgYEEImTZrErlpERIS8vDx7TUt3g7q5ub148SIrK0tMy3AZpRzHQ6WLbQ8PDyiYwBJFJK9fv5aQkFi8eDE7ELpbSUnpx48f7HDhWVwRHMUFxx7k8XjwJcDGxkYgo61bt44YMUJ8YbjPaIZh4LiJmppapXVEEKTeQGUfgjQ869atg0UPDSkpKZk9ezb9S99p2coLkWopWOoRQvz8/ARy+fTpk3B8hmFWrVoF4fRzN+wXYCv7li9fTgixt7enId27d5eUlPz58ycN+fjx47Fjx+B3QUGBhISEsbExO6P9+/fTDUGenp6EEF9fX3aEpUuXwgthYmIil1xEkpSUBG+qMTExNNDU1FSghZmaKfsAUDMJpHD//n1IYceOHRDy5csXCDlw4IBACuvWrVNSUhLYzgY7gPT19WkIbO0ZPnw4O9rt27cHDRrEDomOjhZe01+5coUQMn/+fIGs3dzcoLU/fPggUHcBZZ9wQ3HpF5HNS0+UCPTF3LlzBV74RcKxwbnPF1BhtGjRgr23KCEhQVVVlRBiYGDAXvGLrNGFCxeI0Btvpe0Dyj5NTU32lhDqm4++KKalpSkpKcHBSYq7uztEo1tFwBnolStXBJoLDnNVquyjL8N0dyQA56kFXgZgNd+2bVsawn2AcS9njx49hg4dyo6TkZEBhbSzs6OBwsq+SiWPSDgKDfoeyH6Lo24u2brOwYMHS0pKCmwnDA0NhZjilX0cZSPHwkBqbNHNMMzUqVPpb44iCPq9UaNG7JlCO+XgwYPs22FrLfsRBso+Qgh7I15JSQkcFpaVlWW3lbCyj+NEqMbzQiQcxwN3ZR/3p5JIKlITiHncL1iwgPxfNdDdu3cJIUZGRjQEOoW9PYdhmJs3bxJCOnbsSEMqUn6JFH3wxUJ415K9vT1IWirxuM8mYaKiouTk5Dp06AD6ZUpycjKo5NiP2tpV9gkMwsmTJxNCxo0bR0M4yi5hhJV9gJj1A5deZipYGNDsnJ2daWB5eTk8ZAWk9LRp08j/VfYx/xv/enp6bOmxf/9+Qki7du3YMZctWyYg4X/9+gWb+9ifBETCZZRyHw+VLra5K/tKSkpAs0llMsXY2JgQYm1tzQ7kruzjKC649yAIChkZGYGitm/fXuCLpjDcZzSDyj4E+S1Bm30I0vDAu+6iRYs0NDTgZJCsrCz9nFslZGRkCCGNGzfu1q2bwCVqQ0TAYNnKlSuPHTtWXl7+8uVLUCq5uLh8/PgRvrEDDMMQQjIzM9ll5vP51tbWd+/ehcN93bt3p5Zr5OXlFRUVw8LCZs2adfr0afgGOHfuXFoGMItz7do1V1dXmmZYWBghhM/n+/r6widZ8bmIpGXLlqBlYNumES5/rQDVEWjPUaNG6evrR0ZGvnz58u+//yb/6xRCCOyKYpfq8uXLioqKa9euZYeXlJQQQiIjI+GbKiFk1apVXl5eT58+/fHjBzVOd+nSJTs7O/aNNCM21J6UQPiYMWM0NDTS0tJOnz7do0ePKlW8Gv0CdOzY0dra2tPT8+DBgxMmTKD1dXd3f/XqVaW3c2zwqmJkZARWbABtbe3Zs2cfPnw4IiIiIiICFu4VIbLNObZP06ZN2YZy6KmcxMRE0La4ubnl5+d/+PBh5cqVNBodxj4+PpMnT87Pz4f3K2ErbM2aNUtPT6+k8izJIGACSUtLKzQ0VMD8k4aGBiGE7aqS4wDjXs6wsLCPHz926dKFXWtCiIKCQmFhId3oKpJKJY9IOAoNmgjbVhHsuSOEJCYm6ujoEEK8vLyeP3/esWNH2qEAlwlCOMtGjoWB7jt79qyBgcGSJUvgHRIOhZGqiCDITlpamj1TmjVrJicnV1JSUukgAaSkpOCVFZCVlV2/fr2Pj09paamnp+fixYsrahMuE4HUQC4JUOsPkTp6Kol53G/btk1fXx9O1VWU3bp16yQlJdmmYwkhkyZNMjY2BputXHJnk5WVBebJhKXBggULTp8+nZKScufOndmzZxPOA1gkp06dKikpMTc3F7Dw26JFi5EjR3p4eJw8eVJgSNcWApMa/iYmJsLfmsiuiqho/UC49TKp4CElsv0lJSWbN28eExNDa1RpCioqKmzpCj0ocHtAQAAhJDc3l4aoqqqamJgEBAQIxBSGyyjlPh5qcbHt6ekZERGhqalJBy1l/vz5K1as8PT0jIuLExgwXKjJ80hkD86ZM2fHjh0ZGRnHjh2D7diEED8/v+zsbOHHMZsqzWgEQX5PUNmHIA3PrFmz9u3b9+vXrwEDBsycOXP//v2ampocTdGLREpKintkbW1tIyOjL1++0M+/Xbt27dq1KyGEYZinT5+6uLjAmT62S7tVq1aBTWJDQ8PNmzevXr1aTk6Orr0kJCRWr169Y8cOZ2fnV69eHTlyZPz48ewViZeXl4yMDNvhAyGkZ8+e8KW6U6dOXHKpCHgZJoQUFhbeuHHjyZMnCQkJAuWvU4YMGRIZGUnbkyLQLxEREenp6aamprBljEL/0sXcyJEjW7VqFR8f7+joCC/qycnJAQEB8BlfDDwe7+vXr+R/a1w2MjIyXbt29fT0ZNtv5kj1+gVYv369p6env7+/l5cXKJcfP37cunVraleoGlTU4BwReEMghNja2oIOq1Jln0iq1z7UBGFpaSn88PLyIoR07dpVwNMFnH+Ed4zQ0NDc3FxFRUXYjViLiHzBg0IWFRWVlJTIyclxH2Dcywm17tChg8h5IbJUlEolT0VUW2jQScrn8+HH+/fvCSHUCGNV4SgbORama9eu/fv3f/v27bJly27cuHHq1Clzc3P6cOEugipCRkYGNINsYJAI+xUVnmjW1tby8vJFRUV0W6tIuEwEUjO5JECtP0Tq7qkk8nHfsmVLumc/ODj4woULgYGB7OxiYmJiY2NbtWoFhufYiHGRJB7YOkdESQNTU1NQeIl/3AgPYJHAZjeRzkZ69+7t4eERHx+fnZ0NZgHrFGg9AYldPdlVKcIdXWkvVwPoAvHtX9XboeICD+jWrVsHBASIX+hyHKXcx0MtLrbFZwo/QkJCqqHsIzUTF8JdoKCgsHTp0m3btp07d27Lli3g1Oj69evTpk2jqw6R1HxGIwjS4KCyD0EaHjU1tVevXllbWyclJTk7O9+9e3fv3r2wSb5+0NHR+fLlS2FhIQ3h8XiXLl1ycnKaNm3a2bNnt2zZEhUVxb5l4MCBV69enTdvXmFh4ebNmy9fvnzhwgVLS0saYdu2bVlZWcePH09ISJgwYcKgQYMuXrwIe7LKysrS09MZhhkxYgScOKiISnOpiKysrEOHDvn4+KxZs8bV1bVLly7Cb551B2xJYLenSJKTkwkh0tLS7C/zIpGSkrKzs9u8ebOjo+Pff/8tLS3t7Ow8Y8YM8Qs1QkhsbCy8h6SmplZUzmroyKrdL4SQfv369ejR4+PHjwcPHgRl3/Xr12v4ZZhjg3OHbp+knhyrRE3ahw2MEDMzMzEeS0HXJuBNu97gPsC4lxNq3bJly0rnhUjESB7x1JbQqEmPcJeN3PHw8LCxsfHy8vL19e3ateuiRYv2798PxeMuguoISUlJbW3t6Oho8RONy0QgtTfvgFp/iNT/U+nZs2eHDh3q1KnThg0bPn/+TJ22E0Li4+MJIdS4Qa0AI58QkpqaKqxh19HRqcknGeGMxMgcQkhMTAx8s6xPaii7qoeYXv5NGDVq1OvXrz09PdPT06naKDU1VVlZWaS3XArHUcp9PNTiYptLpjUZ7bUrLhYvXrx///7c3NwLFy6sXr26vLzc1dWV2sGoiHqb0QiC1B2SlUdBEKTu6dSpU1hY2MKFCyUlJXNzcxcvXjx16tRqf1ytKvBWCfZHCCExMTFdunQ5f/78/fv3Fy9eXNFb67Rp00JCQgYMGAC3DBo06PTp0/SqhITEsWPHnj59Cl9NX758aWpqChZ5JCQkwPMpNaYjBvG5iOTx48d6enqpqakvXrwYNWoU28tq/SDQnhUBBYuLiysvL680zXnz5snKyiYnJz969IgQ4uTkNG/evErvokfYRB6WgWV39b48V6NfKOCp4NGjR+Hh4bm5uU+fPmWfGa8GHBucO3AOkdRgk0tN2ofCZZrAYcn09HThDVb1APcBxr2c3IWDSMRIHjHUotCAmlZ6PE0kVZKNHGnSpMnr168dHByUlZX5fP6pU6d69OgBdsSqJILqCE1NTVLZRKvT54VIav0hUs9PpYKCgkmTJk2aNGn//v2HDh0Ce2FsoABpaWnUL1PNqbvHjciMxORSWxlVlVqfvOKptJd/E5YuXTphwoSSkpLJkyfDeHv06FFAQMClS5datGgh5kaOo7RK46G2FtuQaXp6urAusuaDsNbFRbNmzWDF6ODgUFZW9urVq+bNm7ONKoik3mY0giB1Byr7EKThgZdDFRWVM2fOBAYGggG1Gzdu3Lhxo34KAA9yQ0NDQkhRUdGIESNCQkKcnJzEWDuCMuvp6b169eru3bvNmzfn8/nLly///v07IaS8vByMcA0ZMiQ8PHz//v0yMjK5ublgSVpaWhp2ToFnNGGePn3KJReRhIWFjR07VkZG5syZMzU8NVNt2O0pBvD6mpmZ+fbtW+GrfD7/+fPn9K+GhgYYuTt//ryPj0/Lli3Bj4p4VFVVW7ZsSQgJDg4Wvgp9JP5goEiq0S9sbGxs9PT0GIY5fPjw3bt3Bw0aJPIsDHc4Njh3oGUkJCSqp+yrYftQYIRUNE0+fvyYk5MDKi2GZYW9PuE+wLiXE2r97NmzgoIC4aupqaki8wLES56K7qpdoQE1jYmJqcYLJHfZyJHs7Ozi4mJJScnly5dHR0fPnDmTEBIWFrZx40ZSRRFUR6SlpZHKpBCXiUBqb97V+kOk/p9KdnZ2bm5uu3btqsg8Av008uDBA+GrRUVF1djUDJZGSW0/birK6OvXr/T8rEAuLVu25HJ4v9apieyqBpX28m+CtLS0m5vbggULEhMTBwwYMG7cuPv373/58sXW1lb8jRxHKffxUIuLbTraqQcVgUxJdUd7HYmLVatWSUlJJSYm3rx5k+Nxinqb0QiC1B2o7EOQhufgwYN0f36nTp3evHkDxxupOWdq6qisrIzeVVuHFktLSyMiIpo0aTJq1ChCyOPHj6OioqSlpdmmyoQNhcC7IjBmzJiPHz+qqanxeDzYQZOfn//PP//AVVlZ2XXr1oGPs+joaHi1A73MmTNn4C+bN2/evHnzhksuIjl37lxJSYmRkRF7kVRv1voAWBjNmDFDfDRtbW1wibBz505hTcTu3bvZ3U0IAQP2T5482bFjBxjw4sLgwYMJIc+ePRP21RAbG0sIoad+RA4zImqkVaNf2EhKSoJTvGvXrp04caLm1p0FGrzm8wUUUra2tmDdpqrUsH0oME28vb1fvnwpcCk1NfWff/5RUVExMjKCEHCEJ4wYJVetwHGAcS8n/epATYlTysvLFy1aJGYzSKWSRyS1KzSgpmlpadRPtADie4SjbOTI27dvqX5QQ0PD2dkZDNXDw6WqIqjWKSkpiY+Pb9269cCBA8VE4zIRSO3NuyqNBy7jpJ6fSpmZmS4uLoQQ9s4dgeyaN2/evHlzQsj27duLiooEUli5cqWAuopLaXV1dWGbD+TOpqysLCEhQV5efsiQIVWoSQWAzMnOzgZn02wEHmrVpnq9UxPZVdWsufTy78PNmzfV1dXDw8MDAgLu3Llz7tw5UIyKh+Mo5T4eKl1sUyptSUtLS7COJzzaIVMTE5PqWQOsI3HRunVrcGS0f//+hw8fcjlOUW8zGkGQugOVfQjS8PD5fLanhUaNGoHagm6PpxbE2CuSK1eugFIjPz+fBsILm/gzWQKL+Fu3bmVmZm7cuBEM58MmKR6PRz9XlpaWgoMO9lfT4ODgb9++0b+tWrUaOnQou8z37t1jr06sra21tLSUlJSoe1lCSEZGxogRI9gHBO7evTtnzpw1a9ZwzEUYSC08PJyeFkxKSoqLixMoP20lgd03tOm478oRaM/4+PgnT54MHz6cvr7St2iBfpGQkFixYgUh5O3bt2BnCsJLS0t37Njx5s0bMD9P6dWrl7m5OZ/PDwwMHDdunHBJaAXZr+h79+5VVlbm8XhXr15lR05KSnr9+vWgQYOoV1xZWVlYWLOHmb+/f2RkJPm/w4xLv4hsXsqsWbPatWtXWlqakJAg4GivUiptcO7zpSLOnTvXuHFjBwcHdqDIGkGbC+hEKm0fiC9wF/1LJ86cOXNAizF58mR2XUJDQy0tLWHwmJiYwKuOo6Pj58+faZz09HSYC5Ue06uojyBcoJA0Mh3VHAcY93J27doV3FPs2bPnzJkzNKOMjIxx48aZmZnRQ9YiZ6t4ySMSjkJDpFylEWg5Z8yYAXlt3bqVvT0KbOeTynqEo2zkWBhCiLu7OzsOfCeAochdBFVjkHDRMl+5cqW4uPjEiRPgN5ldNXaOXCYC4TDvnj17NmTIkIULF7IdgwrDcTzAExPMtBGxb+Pcn0oigbuEz79X9Linw4YOOfI/YcjObuvWrYSQb9++9evXj25+zMvLs7Oz09DQoB5mKqqmsOiTlJQ8ceIEISQ4OBgcsFLu3r1bWFi4adMmKpy5D2BhZs6cCXuywHU1m2vXrqmpqe3evZuG0HbjaOhAZH0ZhoECCwx1KDBtE+6ySxhlZWVQHglnDX8FWoxjL5MKFgYVrRIhskD7i3zMielBgdufPn06e/bs1atXV8l3HMBllHIfD5Uutqn/qErntZ6eHvhcvnHjhoAi8tq1a4SQ06dPs4/fVjSLhanJ84hU0AXA33//LSUlFRYW1q9fPy5uyqs0o0lV6oggSP3BIAjS0KxYsaJly5Y/f/6kIWvXrm3WrFlCQgL8LS8vh312zZo1O3z48IkTJ6ysrLZs2QJeCM3MzE6cOAExYWFECElOThbIJSgoCC4NHjz4169fEPjlyxd1dfUxY8YUFRVBiK+vL0TT09M7f/78nj17+vbtO3bsWMj94sWLx48fZxjGzMxs2LBhpaWlcBefz+/evXufPn14PB7DMNnZ2YSQXbt20dzT09Pl5eU3bNhAQ6i7MVlZWTjZoa+v37hxY39/fxpHfC4i2bdvHyQ7dOjQy5cvr1q1aujQoZ07dyaEdOnSxcnJ6c6dOwzDlJWVgWJr+vTp7NupivPixYvie41hGHD217p16y9fvkBITk5Oz549DQwM4uLiaDS6G+XGjRsCKeTk5FBnkaqqqiNGjBg5cqSmpqaxsTHtIzawK2rlypUiy0PNLbu6urLDXV1dFRQU5OXl379/DyHFxcWjR482NDSMjY1lx1y4cCEhREpKavXq1RcvXpw9e/bw4cNBsdisWbP9+/fDuKq0XypqXjZnz54lhKxevbqiCMJwbHDu82Xbtm2EkObNm9O5xjDMuXPn1NXVHzx4wM66ohpt2LAB2jw1NZUGVto+YD1HUVGxsLCQ3kWNYTs4ONBAWGoDXbp0sbW17dmzp6Sk5NmzZ2mcL1++wA5EZWXlffv2vX///u7du/3794eDSyoqKvPmzaMtJgx4kCSEvHz5kh1uZmZGCOnZsyc7kL5NhYWF0UCOA4x7OT9+/EhNhbZr127s2LGDBg1SUFAA40rCjRMTEwMhXCSPMByFBrX+FhISQu999eoVBN6+fZsGOjk5gWbZ0NDw/Pnzfn5+586dow469fT0li5dmp+fX1F5uMhGjoXx8PCQlJR88+YNjfPo0SMJCQlPT0/4y1EEgRNwKSmpkpISGgitTQgRaF44LKyurk5DXr9+DTGfP39OA319fVu0aLFjxw6B6sPhvlatWonsazETodJ5R7er29nZVdT4DOfxAJtGYcK+efNm8uTJubm5NUlQJOnp6aAiES5zRY/7oqIiEJVKSkoHDhw4ceLE4MGD586dC5H37t27efNmhmHKysrY7hHat2/fp08fZWXlESNGsJ+woEtVUVF59OjRnTt3Fi1aBOEiRR/DMKtXr4bU0tLSICQ+Pr59+/Y2NjbFxcU0GvfZJJLQ0FBQ9Bw9epQG/vPPP02aNHn8+DE7Jj39Lfz8FYnI+lLNmsBwBUcczZo1Ky8vhxCOskskIHLNzc3fv3/v4OBw8uRJpuL1A8deZipYGISHh0MgrOiA/Px8eXl5QsiSJUvYeYHsMjQ0ZAdOnDiREKKhocEeLX///Tckm5GRQQOnT59OCGnUqJGJiYmFhYW1tfXEiRPnzZt3+PDhHz9+iG8TjqOU43iodLGdlpYG++lmzpz54cMHe3t7Pz+/isqWn5/fv39/QsjYsWPpAHj58qWSktKePXvYMcXMYmE4iosq9SAFNvcJLG/Ew3FGMwyzbNkykRIJQZAGBJV9CNLwrFy50tTUtH379lu3bnV0dFy0aJG5uTn7vY5hmKCgIHrqQVVV1dnZmWEYbW1tExOT7du3h4WFhYWFzZo1q1GjRhBHX19/y5Yt379/Z6cAl3bt2qWjozNq1KhBgwapqqpu27ZNYA26ZcsWeFOVlJScNGlScnJycHAwOH5t06ZNeHg4wzB9+vTp2LFj9+7dDxw4cP78+eHDh48ZM4YuobKzs1u0aNG+ffsxY8acOnXKwcHB1NR0/fr1ZWVlNJfy8vLt27ezPU6OGjXq69ev7JKIz0UkRUVF9FiBoqLi1q1bi4uLz5w5AyGDBw/Oyck5cuQIfH4nhMjIyMybN8/T0zMzM3Pz5s30vVddXX3t2rVsjYYwsNRetGhRx44d+/fvP2bMGDU1tQkTJuTk5ECEvLy8lStXgpIIOm7FihVeXl7sRLKysiZMmEDPa8jKyq5YsUKkpo9hmIKCgqZNm0ZFRQmEx8bGbty4kdrnbt68+YYNGxITE2mEr1+/9u/fv3HjxtbW1vb29p06dVq4cGFBQYFAOpmZmbT1pKWl169fX1xcPH369BYtWixevPjNmzewohXfLyKbV7gucFRWjBKqGg1OqXS+0PYEJaC0tLS5ufnEiRNNTEysra3j4+PZqYms0Zs3b+zt7WFhTQjp3Lkz1TGJaZ+EhISFCxdSN8q9e/e+fv06wzCHDx/W19eHQBUVlfXr19Pc7969C2pKoGPHjgJvswzDxMTE9OnTh55fNjY29vf3NzAw0NfX37FjR3R0tMj2LC0t3b17NzX+aGBgsHv37qKioufPn0+ZMoXmOHXq1CdPnuTm5q5fv57uTDE1NWW/VnEcYNzLGRYW1qVLFxpTXV39xIkTVIAEBwcvWbKEHrLu2LHjjh07cnNzuUgeYbgIja1bt9K6m5ubnzp1imGYkydPUj+/Ojo6+/bto2k+ffqUXpKSkrK2tg4JCZGQkLCwsDh79iz7TViYSmUj98J4eHjo6em1bt3a3t7+4sWL27dv19fXv3btGjs78SIoNjZ2/fr1sLGOEDJkyBBHR0eGYc6fP9+7d28IVFBQWLly5devX0NDQ+fPn0+H9+jRoz08PCCdy5cvd+zYUUJCQldXd/z48T179jQ3N/f29maX5N69e5MnT6bbYaytrU+fPk2vVjoRKn1egCUEGOo1HA8Mw/B4PHjVJ4To6+uHhobWMEEB0tPTt23bRs1mycjIzJw5c//+/QzDVPq49/DwgAPahJDu3bv7+/sXFRXBHhx5eXl3d3eIVlpaunz5cqqcUlRU/Oeff6i2FIiLi6PNPmLEiOzsbDGiD7h//76+vr6amtqMGTNmzpxpYGBAv68AVZ1NIsnOzp45c6aysnL37t2XLVtmYWExaNAgdiN8/vx5zZo11CBss2bNVqxYcfPmTfHJCtfXycmJGsVTVFRcvnx5SkpKSEjIX3/9RcfqwIEDfXx8IAXxsksMDx8+lJOTI4RIS0tv2LAhNzdX/Pqh0l6uaGFw9epVUCxC4ObNm/Py8h4/fty3b18IlJeXX7FiRUZGxuXLl9mW9WxsbFxcXBISEuzt7anEsLGxefz4cUFBwZo1a+gW3V69etFhFhgYSM04CCAvL8/+ACASLqOUy3hguC22N2/eDBk1adLk1q1b4stWXl7+zz//NG/evF27dvb29qNHj+7UqdPr169pBDGzuCK4iAvuPSiQ+IULFzQ1NbmMRjaVzug7d+7MmTOHSn5DQ8MtW7b4+vpWKRcEQeoCCaaOrfkgCFIpBQUFioqKfD7/27dvSUlJOjo67dq1oytFCo/H+/r1a3l5uYmJCRz3iI+Pp2dtKiU4OBgWBxERETo6OuHh4bABiv1KSUlOTo6JidHT06MmZn78+JGWlmZmZgYrPChzSUlJREREdna2gYEBOFUEGIYpLi6Wl5fPycmJiIjg8/kmJiZ0VcqGz+dHRUXl5OQYGBjQ90mBlqkoFzFERERkZGSYmZnR2gUFBTVq1KgWHbYSQpo2bZqdnX327Nl58+ZFRUWlp6fr6+tzLKEAJSUlX758kZWV1dfXh+V+RURERFS7FpBLeXm5kZGRyO4A4uLifv782alTJ1heJyYmtmzZkn0gpdr9wsbFxeX48eNVMqdVpQav0nyJi4tLSkqSlJTs0KEDl+Mt4qmV9hEADvLo6uqKsfqUm5sbGhravn17eJH+9u1b9WwGVRuOA4x7ObOzs8PCwjQ1Ndu1a8fFIyF3ySNMXQiN5OTkHz9+wFQqKyv7+fOnjo4Ox3vFy0aOFBcXy8rKSkpKJicnf/v2TVlZWcAUFIW7CKoJaWlp379/B5UE1exUCTETgcu8+/nz56tXry5fvlyp7xEu44HH48HGNHNz80rHZ/08lSg5OTmhoaFqamr0KwLMpk6dOgnMiPLy8tDQUElJSUNDQxCVAhQUFAQHB6upqXHxCsUuwJcvXxo3bmxsbCwy2VoBpkl6erqenh7Vi9WQ6tVXgKrKLiA1NTUyMpL7QoJ7Lzcg379/X7ly5d69ezMzM9PS0vL/x69fv168eFFWVkY/RYuh0lEKiB8PHBfbYOPVzMyMahgrJSkpKSYmpmXLlnp6ehxvEU8diYv58+erqqru37+/GvfWz4xGEKR2QWUfgvxXYCv76LoQqQlU92RnZ9fQZfn3YWFhYWdnBwd8OIINjiBIDVmzZk2PHj2orVIEQeqOmJiYfv36OTs7g81WAeCs/adPn+q/YP9BcnJy2rRpExQUxDa0hyDInw066EAQBEHqm9evX8fExIDRHwRBkPrh/Pnz2dnZqOlDkPphwYIFKSkp1GKpAPn5+dXb4YtUgyNHjgwcOBA1fQjynwJ34SLIfwXq3FCM00CkSkCTYntyxNXV9cqVK506dWrbtu22bdt27NhBLbxwBBscQZBq4+3tnZube/78+YYuCIL8V4iMjCSEbNmy5dChQwIGBIKCgtatW0ddtSB1waZNmyIiIrp3784wzMGDB/38/Bq6RAiC1Cuo7EOQ/wrx8fHwIzY2ljolRKpNdnZ2Xl4eISQ2Nrahy/LvYNu2bZGRkZ6enoSQQYMGzZ8/v0q3Y4MjCFITLCwsqKcdBEHqgS1btixevPj48eNXr14dOHBgy5YtGYZJTU0NCQkxMjI6ceIEdaWF1Dqpqal79+4lhNy9e5cQsnPnThMTk4YuFIIg9Qra7EOQP5+8vLwtW7bcvHkzLS2NEKKurj5hwoThw4ePHDmyoYv2b+XAgQPXrl378uULIUROTm7ChAndunVbtmxZQ5frt+bGjRu7d+9OSkqytbU9efIkdebIBWxwBEEQBPnX8e7du9OnT3t7e6ekpEhKSuro6FhYWNjb23fr1q2hi/bns2nTpkuXLsnIyKxYsWL16tUNXRwEQeobVPYhCIIgCIIgCIIgdQWfz+fumBhBEASpOajsQxAEQRAEQRAEQRAEQZA/BPzAgiAIgiAIgiAIgiAIgiB/CKjsQxAEQRAEQRAEQRAEQZA/BFT2IQiCIAiCIAiCIAiCIMgfAir7EARBEARBEARBEARBEOQPAZV9CPL7EhISsnPnzvHjxzd0Qf40Hjx40LVrV0dHRy6Ry8rKJkyYMHz48IyMjLou2O/Dz58/L1++PGLEiE+fPlXpxqysrDt37syZM+fw4cN1VDYEQRAEQRAEQRBEDKjsQ5DfkeTk5MmTJ3fp0mXbtm1hYWENXZw/jaNHj37+/HnXrl1cIvv6+t6+ffvJkycPHz6s64L9JqxYscLc3HzOnDmenp4lJSXcb9ywYUPXrl1tbW0vX778n9KNIgiCIAiCIAiC/D6gsg9Bfkdatmx58+ZNe3v7hi7In8mSJUs6deq0YcMGLpF79OgxatSoQYMGWVtb13XBfhMcHByePHlSjRv37dsXHR2tpaVV60VCEARBEARBEARBOILKPgT5Xbh///67d+/YIerq6g1VmD8GHo+3efNmgcBx48YFBwcvXLhQOP7GjRv5fD47RE5O7v79+y9evNDQ0KjDgv5maGpqVu9GSUlJVPYhCIIgCIIgCII0IKjsQ5DfgtjY2FmzZhUXF7MDJSQkGqo8fwyrV6/28vLiGPnOnTv79+9nGKZOi/SvoCZjT1ISnywIgiAIgiAIgiANBr6SIUjDk5+fP3bs2KysrIYuyJ/G1atXjx8/zjFyeHj4rFmzUNOHIAiCIAiCIAiC/KuRbugCIMh/nZSUFFtb25CQEEKIm5tbUFAQIWTJkiVycnLsaBkZGRcuXAgICGjSpMnIkSNtbGyEk0pKSnJ1dQ0ODpaVle3SpcvUqVOVlZW5lKGkpMTNze3Tp09JSUlt27a1tbXt2bOncDR/f38fH5/AwMBGjRqZm5tbWVnp6ekJxImKinJ1dbW0tLSwsIiPjz9z5szXr19btWq1fPlyXV1dQkhWVtbVq1c/fvwoKys7ZswYgYoUFRV5eHj8+vVryZIlX79+vXTpUmxsrJGR0ahRo7p37y5cpPv37/v4+MTFxRkZGQ0ePLhv37700sWLFxctWkQISUxMBM+wnTt3HjBgACGEYRhvb28PD48jR45A5A8fPowdOzYvL48QcvjwYSkpKXV19ZkzZ8LVtLS0W7dutW7deuTIkYSQ48ePl5WV0YwsLS27dOlCCMnJybl48SIE6urqjhkzhks5KyIjI8Pd3V1KSmrevHkFBQXnz5/38fFRUlKaPXu2paUlIYTH4925c+fJkycFBQV9+vRZvHixlJSUQCJ8Pv/JkyefP38ODQ3V0tIyNze3trZu1qyZcHZZWVlOTk5+fn6ysrIWFhZiDBR++vTJ09MzLCxMS0urZ8+eEyZM4LgN0N/f383NLTs7W0NDw9zcXF5efsSIEVxuRBAEQRAEQRAEQaoAgyBIg/L27du9e/fCfFy0aJGDg4ODg0NJSQnDMLt37yaEGBgYfPz4sXnz5rq6uq1atYKYhw8fFkjn+PHjFhYWjo6Od+7cmTt3LiFEV1c3KCio0gK8evVKX19/3bp1Hz9+DAwMBDXf7Nmzy8vLaZy8vDw7O7vWrVt7eHgkJCTcunWrU6dOsrKye/bs4fF4EOfSpUtmZmZQvBs3bjg4OKirq/fo0QMsD2pqav78+dPT07NFixampqadOnWCmJcuXYLbP3/+PG3atMaNGxNCJk+efPr0aRkZGarxlJKSOnr0KLvYsbGxgwcPXrNmzd27d8+ePdu6dWsJCYm1a9dCsYuLix0cHPr3708Iadu2LbTq27dvY2Nj16xZo62tTQhRUlKiqbm4uECjQds6ODi4uLgwDHPt2rWhQ4eCEu3QoUMQOT09ndb08uXL7IYKCwtTVFS0srJKT0/nUk6ReHl5DR8+HDJduHDhixcv2rZta2pqCqpVKSmpFy9eREdHm5ubt23btmfPnrKysoSQmTNnCqQTGxtrYWExcOBAb2/v2NjYM2fONGnSRFNT8+7duwIxg4KCWrduPX/+/M+fP8fExGzatKljx45QQR8fHxqtsLDQzs5u4sSJN2/evH79er9+/QghQ4YMycjIYKcGQ2jDhg3swGPHjunp6UVERDAM8+3bt65du7Zq1aqiFkAQBEEQBEEQBEGqDSr7EKThyc7OBsXKixcv2OGg7FNTU7OysgoLC2MYhs/nz5kzhxCiqqpaWFhIYx47dqxNmzbskLFjxxJC2rdvX1paKibr8PBweXl5qnFjGObp06dQmNu3b9PAAQMGNGrUKD4+noakpaXp6+sTQpYuXUoDk5KSQPHUoUOHw4cPl5WVMQyTmZkJyjVLS8tBgwb9+vULIm/btg1KyOfzGYbJycmJjIxs06YNIaR58+bTp0+PjY0tLy/38/MzMDCAInl5ecG9mZmZurq6p0+fplkHBgbC/rLLly/TwBUrVhBC+vbtS0MyMjJiYmJgOyFb2ccwjIeHB+RC1ZcMw0RHRz979gyM0FFlH8MwPj4+EPnq1avsREpKShQUFEJDQ6tUTmGKiopgt6CWlpadnR00Wnl5OWwtNDEx6dy5c3BwMER+/fo1IURSUvLHjx/smqqpqZmZmbEHgK+vr4KCAiHE3d2dBgYHBysoKMydO5ddgPXr1wsr+8aOHTtx4kT6t7CwEHxx/PXXX+x7hZV9qampMjIyR44coSFRUVE6OjpiWgBBEARBEARBEASpHqjsQ5CGR7yyT1NTMzs7mwZGRERA5JCQEAhJS0tTUlLau3cv+153d3e6ya6ifEtLS83NzU1NTUHdBvB4PBMTE7Y+6MqVK4SQ+fPnC9zu5uYGOqYPHz7QQNDrbdu2jR1zyZIlhBB1dXW2OjImJgZKyFZRgTKrZ8+e7NuTk5MVFRUJIQMGDICQdevWKSkpgTKRApvR9PX1aYiwsg+4cOECR2UfACottrKPYRjY1zZ8+HB24O3btwcNGkT/ciynSKZPn04I6d+/v0D6UEh/f392ODS7s7MzDQGlMOxPZGNvb08IadGiBR1UgwcPlpSUjI2NZUcLDQ0VUPZ5enoSQnx9fdnRli5dCmMgMTGRBgor++Bee3t79r1Tp04V3wIIgiAIgiAIgiBINUCbfQjyu9O0aVMVFRX6t23btvAjMTER1EZubm75+fkfPnxYuXIljZaZmQk/fHx8Jk+eLDLlGzduBAYGbt68mW1zTUpK6v379wkJCUZGRhBCDd4J3D5mzBgNDY20tLTTp0/36NEDAqWlpQkhcBqXAsoyRUVFeXl5GqihoQE/fv78SY8nw+2wv4/SokWLadOmnT9/3svLq6ysTFpa+vLly4qKimvXrmVHKykpIYRERkbCpjaRVQZkZGTEXOUYf9WqVV5eXk+fPv3x40fr1q0h8NKlS3Z2dvCbYZialFNMSxJC4HA0RUNDIzEx8efPn/A3Kyvr8uXLRFSvLViw4PTp0ykpKXfu3Jk9e7aXl9fz5887duxIxxUgXLBLly4RQq5du+bq6koDw8LCCCF8Pt/X13f8+PEV1QVKe/bsWQMDgyVLlsB427lzZ0XxEQRBEARBEARBkGqDyj4E+ZcB52QJIaWlpfDDy8uLENK1a1cBdxngY4Gqh4R59uwZIQT8ZrBp3Lgx1fTxeLyvX78SIe0SIURGRqZr166enp7gXUQMIpVltCJc3BAPHTr0/Pnz5eXlcXFx5eXl6enppqamAl5E6F9Qk9U1I0eObNWqVXx8vKOjI+itkpOTAwIC7t69CxEiIiJqvZwVqSmhMWlLwjliIqrXTE1NFRQUCgsLodfev39PCKHKVjF4eXnJyMhYWFiwA3v27LlgwQJCCDXCKJKuXbv279//7du3y5Ytu3HjxqlTp8zNzYUHHoIgCIIgCIIgCFJzUNmHIP96kpOTCSFmZmZwBpY78fHxhJDi4mIxcWJjY0GrmJqaKnxVR0eHEBIdHV2lfKsBZEQIKSws/PXrFyFEWlp60qRJdZ2vGKSkpOzs7DZv3uzo6Pj3339LS0s7OzvPmDGDKjGhXxqknKCfJYSkpqYK+97V0dGJjIyEXoOYcEpaDGVlZeByZMSIEUpKStUokoeHh42NjZeXl6+vb9euXRctWrR///5K80UQBEEQBEEQBEGqimRDFwBBkJoC7iOoCbyq3iheVUePcyYmJgpfhY1jAidA6wJQMElISHTo0AGKDVv86jpf8cybN09WVjY5OfnRo0eEECcnp3nz5tGrDVhO7r0GJ39FRmMjISFR7WEGNGnS5PXr1w4ODsrKynw+/9SpUz169MjIyKheagiCIAiCIAiCIEhFoLIPQf71tG/fnhBy584dkVc/fvyYk5Mj8hJ4uX306JFIbVRCQgIhRFVVtWXLloSQ4OBg4Tjp6emksiOctQJoo1q3bq2goAD1zczMfPv2rXBMPp///Pnzui4PoKGhMWHCBELI+fPnfXx8WrZs2aFDB3q1AcsJxhwJh16Ds7QxMTF8Pl9MgtLS0mCXsKJhRp04iyQ7O7u4uFhSUnL58uXR0dEzZ84khISFhW3cuJFLdRAEQRAEQRAEQRDuoLIPQX4jeDxeNe4yNDQkhHh7e798+VLgUmpq6j///MP278HG3NycEBIVFQW+F9iEh4cfP34cfg8ePJgQ8uzZM1ASsYmNjSWEjBkzphrFrhKgtAIlkba2Nrit2LlzJ1imY7N79+6ysjJ2SFVbtUrxFy9eTAh58uTJjh07wHodparlrEV0dXVh456Li4vApbKysoSEBHl5+SFDhhBCwDhjWlra/fv3RSZFSw7D7MyZM2lpaQJx3rx58+bNGzHlefv2LdUGamhoODs7g9MSHx+fqlUMQRAEQRAEQRAEqQxU9iFIw6OsrAy+GsDKG/mfvgmUQQIqIfqX6qTmzJkD6rzJkyeztSehoaGWlpYrVqyoKN+//voLbOHZ29ufPHmS7u/z8vKaOHHiunXr4O/evXuVlZV5PN7Vq1fZtyclJb1+/XrQoEGwuw0QuUEMAgUqQmMKK8IKCgrYfxmGOX/+vIaGxpo1awghEhISUKm3b9/OmzevsLAQopWWlu7YsePNmzfgmYQQoqqqSoRalfzPt4lAeSAy9/hAr169zM3N+Xx+YGDguHHj2Je4l1MkYlpSuDAQTltSUlLyxIkThJDg4OCAgAB2zLt37xYWFm7atAl26s2YMQPO/G7duhWMIQKBgYHwg24LXbVqFSEkIyNjxIgR7GO/d+/enTNnDnQNAANJoPzu7u7sv6AYZR8Af/LkyZQpU7y9vStoDwRBEARBEARBEIQbDIIgvwFmZmaEEHNz8/fv3zs4OJw8eZJhGDAAp6ioWFhYSGNS3wsODg40EDQ7QJcuXWxtbXv27CkpKXn27Fnx+Xp4eFCHEoqKin369DE0NJSVlX379i07mqurq4KCgry8/Pv37yGkuLh49OjRhoaGsbGxNFpxcbG8vDwhZOvWrezbQeclKyubm5tLA2FXICHk1KlTNBA2CcrJyd26dYsG2tvbKysrP336lIbk5OS0a9cObldVVR0xYsTIkSM1NTWNjY1//frFrh1tqzdv3kyePBkKsGHDBghPTU2lkdPS0sDX7cyZMz98+GBvb+/n58cwTG5uLjTRokWLRLbhxYsXCSErV64UvsSxnCIZOHAgIcTCwoIdSGv05MkTdjho7iZMmMAOXL16NSGkffv2aWlpEBIfH9++fXsbG5vi4mIazcnJSUJCghBiaGh4/vx5Pz+/c+fOUZfBenp6S5cuzc/PZxhm/PjxECgrKztgwIBx48bp6+s3btzY39+fplZWVta8eXNCyPTp09nFlpSUfPPmDQ159OiRhISEp6cn/C0vL9fQ0IDsxDcLgiAIgiAIgiAIIh5U9iHIb8HDhw/l5OQIIdLS0hs2bIiPj1+4cCFVw/Xu3fv69esMwxw+fFhfXx8CVVRU1q9fT1O4e/eulpYWVfl17Njx8ePHXLL++PGjsbExvbF3797BwcHC0b5+/dq/f//GjRtbW1vb29t36tRp4cKFBQUFNIKzs7OVlRUk0rRp0w0bNkRHR0dFRS1fvpw6Xe3duzfoHx0cHExNTSFQU1Nz69atoHcDZZ+VlZWNjU3Hjh0nTpzYpk2bzp07f/36VaA8WVlZEyZMAPUcqJ9WrFghoEHj8Xj9+/eHCPr6+qGhoW/evLG3tweNJCGkc+fOu3btovE3b94M4U2aNLl161ZpaemuXbu6d+8OgXJycnZ2du/evRMoSUFBQdOmTaOiokQ2L5dyCvDmzZu5c+eCAk5CQmL+/PkvX74sKiratm0bKPUIIa1atdq+fXt+fv7Dhw+pF2Y5OblFixZ9/vyZJnX//n19fX01NbUZM2bMnDnTwMDgxIkTwjk+ffq0TZs2kIiUlJS1tXVISIiEhISFhcXZs2czMjIgWnl5+fbt29neeEeNGsXumiNHjlhYWMAlGRmZefPmgTrPw8NDT0+vdevW9vb2Fy9e3L59u76+/rVr19hl6NevHyFk3LhxYloGQRAEQRAEQRAEqRQJRugAHYIgDUJqampkZKS+vr6mpma1E0lKSoqLi9PV1W3RokWVbszIyIiMjOzQoQO4aq2IkpKSL1++lJeXGxkZgUG62mXs2LEeHh6TJ0++ceNGfHx8XFyclpYWeLoQUx5ZWVl9fX3QlgrA4/FCQkIYhjE3Nwd/suKJjo5OS0szMzOjCkouREREgLeTapezTsnJyfny5Uvjxo2NjY3hwLhIkpOTf/z40alTJ0VFxbKysp8/f8IpbwH4fH5UVFROTo6BgUFF5iAFKC4ulpWVlZSUTE5O/vbtm7KyspGREVWAAmVlZUFBQV26dOHSTQiCIAiCIAiCIEhFoLIPQZDfCLayr6HLgiAIgiAIgiAIgiD/PnADBYIgCIIgCIIgCIIgCIL8IaCyD0GQ3whw4Urd4CIIgiAIgiAIgiAIUiVQ2YcgyG9EQkICIYQ66kUQBEEQBEEQBEEQpEqgsg9BkN8CDw+PYcOGBQYGEkICAgKGDh26adOmgoKChi4XgiAIgiAIgiAIgvybQAcdCIIgCIIgCIIgCIIgCPKHgDv7EARBEARBEARBEARBEOQPAZV9CIIgCIIgCIIgCIIgCPKHgMo+BEEQBEEQBEEQBEEQBPlDQGUfgiAIgiAIgiAIgiAIgvwhoLIPQRAEQRAEQRAEQRAEQf4QUNmHIAiCIAiCIAiCIAiCIH8IqOxDEARBEARBEARBEARBkD8EVPYhCIIgCIIgCIIgCIIgyB8CKvsQBEEQBEEQBEEQBEEQ5A8BlX0IgiAIgiAIgiAIgiAI8oeAyj4EQRAEQRAEQRAEQRAE+UNAZR+CIAiCIAiCIAiCIAiC/CGgsg9BEARBEARBEARBEARB/hBQ2YcgCIIgCIIgCIIgCIIgfwio7EMQBEEQBEEQBEEQBEGQPwRU9iEIgiAIgiAIgiAIgiDIHwIq+xAEQRAEQRAEQRAEQRDkDwGVfQiCIAiCIAiCIAiCIAjyh4DKPgRBEARBEARBEARBEAT5Q5Bu6AIgCCJIWlraw4cPg4KCUlJSFBQUtLW1raysLCwspKXracIGBQXduXPn58+f58+fr/XEX7x4UVBQwA5p2rRpv379IiMjIyIiBCKrq6v37t0bfpeWlj5+/Jh9tWfPniEhIYWFhTSkXbt2HTt2FF+AtLQ0X19fdsiAAQNiYmISEhI4VqF58+Y9evQQHyc8PPzp06cBAQEaGhrdunWbMGGClJSUyJhxcXHu7u5fvnyRlpbu16/flClTZGVlxaTs5eUVFRU1b948LkV98uTJqVOnbt261ahRo4ri5OXlvXr1iktqhBALCwtVVVWOkatKXFzckydP7t27d+TIESMjIxq+efPmt2/fnjt3ztjYmGNSOTk5jx8/dnNzW79+faWdVc88fvy4tLSUS0xDQ8MOHTrUdXmqRJ0Kh4aluLjYx8fnwYMHhJBjx47VSpq3bt3av3//xo0bbW1tayXB34GkpCRPT8/79+9v3ry5Z8+eDV2cWqCwsPDFixe3bt2ytraeMmWK+MjVEEe/M7Gxsffu3bt9+7aXl1dFDylh+Hx+YGCgp6fn27dvX7x4UaclpMTHx9+7d8/V1dXHx0d8zLCwsPnz51tZWe3cubN+ytaAvH37Njs7Wzi8adOmLVq0aNGihZKSkvBVf3//5ORkgUAJCYnRo0eLzKUa64TAwMD4+HjhqyNGjKCr2fz8/JcvXwpEsLKyUlRU5JjXH09ERMThw4cDAgLy8vK6deu2e/futm3bcrz3+vXrcnJy4p8+Hz580NbW1tbWrmrBeDze27dvv3//rqqqOnDgQBUVlaqmgCBIfcAgCPLbkJGRYW9vLy0t3bZt2+XLl1+7du3kyZNTp06VkpLS1ta+fv16XRcgISGBqqUsLS3rIosXL1707duXiqCNGzcGBAQwDBMfH3/lyhUNDQ0I19DQuHLlSmhoKL2xrKzs/v37cG+zZs2OHTv269evd+/eHT9+vHnz5nCXnp5eeXm5+AIsW7aM5j5hwoR79+4VFBRMnjwZQho1aqSlpaWvr9+iRQsI0dTU1NfX19HRUVBQgJDhw4eLSb+8vHz//v0yMjJsSdulS5eoqCjhyKdPn1ZSUtq7d29GRsaXL1969+7dsWPHuLg4kSl//PhxyJAhhJBZs2ZV2s4Mw6Snp2tqahJC8vPzxUQrLCy8d+/e+PHjaWmNjIys/sfAgQO7devWsmVLuPTmzRsuWVeVhISEuXPnNm7cGHIJCgqilwoKCmBArl+/nmNq58+fp6vhOipwTXj9+vWmTZvoe3XLli0HDRoErT1o0KBevXpRBd/27dsburD/P/UgHBqQixcvUqXwmDFjaivZXr16EUJ69OhRWwk2LKWlpcuXL2/atOlvO7mqwfXr1zt16gQ1Onv2rPjI1RBHvy05OTlz585VVlaGuvN4PI43vn371sbGBtqhSZMmdVpIytKlS2lRK428ceNGQoikpGReXl49lK1h+fz588WLF9u3bw+No6ioaG1tPWTIEENDQ3l5eUJI7969nZ2dS0pK2Hd9+fLl+PHj6urqcJe8vPzOnTtfvHhRUS7VWCdERERcvnxZX18fAiUlJdetW+fp6ckeaUVFRe7u7tbW1hBn1KhRrq6uxcXFdddc/y4CAgIMDQ2TkpLy8vI6d+5MCOnVqxfHe+Pi4pSUlLZs2VJRhJSUFFiKODo6VrVgjx49MjAwOHTokJ+f3/Xr183NzXft2lXp8htBkPoHlX0I8rvw9etX0FBMmTJFYIXq7+8P7//z5s3jviKvNgsWLKjT9/ns7Gyqnvv+/Tv7kpubG4QPGTJE5L2wue/mzZvsQAcHB7oAvXv3rpisc3NzqUZJRUWltLQUwkePHq2goHDr1i3avNeuXYNoJ0+ehBA+n//kyRNVVdX+/fuLyWLbtm0qKiobNmxwdHT8+++/6e6Pjh07Cqy279y5QwhZtmwZDfn161fTpk0NDQ0FBkBkZOSmTZvGjRsnKSnJXdk3btw4yFq8sg8oLS1t0qQJIURGRkagnAzD8Pn827dvS0pKPn/+nEvW1ePz58/Cyj6GYdauXdutW7eQkBDuSdE9C7+tPmLEiBFQwtevXwtfDQsLa968+ebNm+u9XJVQ18KhYRk1alTtKvtAkSQgr/7txMbG/uaTq6qUlZWBIqlSZR9TLXH0O+Ps7FxVZR9w+PDh+lT2MQxz/fp1jsq+4ODgrl27/gEKWe64u7tD48ydO5cG5uXl7du3Dz492tjYCHfxwYMH4S47OzsuuVRjneDh4QFZDB06tKJkeTxeixYtdHV1uZThv0NZWZmWltbMmTPh748fP0xMTHbt2sXx9sGDBxNChJV9nz9/trGx6dGjh7W1tYGBASHk4sWLVSrYw4cPZWVl2UuX9PR0XV3dlStXVikdBEHqAbTZhyC/BRkZGVZWVnFxcZMmTbp+/brAsYuuXbs+evRIRUXl4sWLixcvrt2snZ2dIyMj2SH0Y28doaKisnLlSvj99u1b9iVbW1s9PT1CiJeXl8iTKZGRkZqamgKnElq3bk33ScEbSEU4OTmpqqrC5+7mzZvT/XdFRUU7duwYP368mHNMEhISQ4cOPXToUFFRUUVxvn79eu7cuaCgoL17986ZM2fHjh2BgYEzZ84khHz58uXy5cs0ZnFxsZ2dnYSExPr162mgqqqqnZ3d169fT58+zU62ZcuWe/bscXd3Nzc3F1M7Ns7Oznl5eRwjE0JkZGS0tLQIIXJycsLniCUkJGxtbW1tbcvKyrinWVV0dHREhh84cMDPz6/SA9psamsMFxcX19FBsDZt2sAPqn1mY2RktHXr1jpt7epR84YVFji/DxWNQC6IHCpTpkwJDg6eNGlSzcpVm0WqOdU48PWbIy0tTbcrVko1xNHvDN1NX1VqMlmqB3fh06lTJ39//3379tVpeX4rWrVqBT9geQMoKSmtX7/+wIEDhJB79+5R1R6FPoboD/FUY50A8YnYASMlJdWjR49u3bpxKcN/B09Pz6SkJDU1NfjbqlWrL1++bNmyhcu9jo6OaWlpIi/p6uru3r375cuXjx49MjExqWqpCgoK7O3tR40aZWlpSQPV1NSWL1/u4ODg5eVV1QQRBKlTUNmHIL8FCxcuTEpKkpGRYW9SY9O+fftVq1YRQs6dO/fw4cPayjc8PHzJkiXFxcXsQAkJidpKvyLmzZsHK8WrV6+ywyUlJRcuXEgIKS4upp/x2fj4+EyZMkXAfKGMjIy6unqfPn0ggp+fn8hMGYY5efLksmXLQKPHPmlbVFRUqakmYNy4cWIMrjk6Oh45coS9bpaRkTl58iS8pXz48IGGX79+PT09nX3yBYC9RefOnWMYhgZS5S/Ht534+Ph9+/adPHmSS2R2UcVHmDt3Lt2SWRfAvsVaobbGsL29fWBgYK0kJUClrT1y5EhTU9O6yLom1LBhRQqc34eajMC6GyrVpo6KVIvz9PehHp56vyfVrnj9D4P/bB9xQcwDZf78+dBZwtYV6V2VPo+4ZAQIrBNofPFWp1VUVETaFvwvA0YSqzHRkpOTPT09Z82aJfKqioqKiYlJta0ienp6xsfHg0kZNmPHjmUY5vjx49VLFkGQOuIPXLEhyL+OkJAQONE5fPhwMcqURYsWgZaqtjZr5OTkjBs3Lj8/v1ZSqxKqqqrDhw8nhLx69UrAhPNff/0lJydHCLl06ZLAXXl5eY8ePZoxY4bINFevXg0/Ktrc9+TJk+Tk5Llz5wpf2r59O/3+LB4VFZVDhw5VdDU3N1d4F0/jxo3BjH1KSgoNvHHjBiFE+FO2ubm5lJRUbGzs06dPuZRHGIZhZs2adeTIkWbNmlUvhYoYOnQo962FfwDnzp1jb8asZ1q1ajV16tSGyr0uaECBU9c07FARyW9YJARB6h9FRUWwQRwVFVUP2VVvnSAjI1NvPuj+LbBXjFVi9erVhw4dqiPlOCxNW7duLRDesmVLKSmphw8fcvQ/hiBI/YCCFUEaHmdnZ9jGNXToUDHR1NXVO3bsGBQU5O/vHx4eDu5KS0tLnz596u3tfeDAgaKiorNnz/r4+Ghra8+ZM0f8tqCkpKRx48bBeTpnZ2ctLS15eXl7e3uBaPHx8efPn//69auWltb06dO7d+8uEAGstHz48CEpKcnU1NTGxoajj8KpU6feu3ePz+dfvXp18+bNNJxhGD6fTwj5/Pnzly9f2Kel7ty506ZNG7BSLIyNjY2uru63b9/c3d2/f/8ufCzl+PHjs2fPFukybNCgQVzKXGnkilyUgqMM6jWCx+PBLj84s8xGXl5eW1v7x48fr1+/HjZsGPdSUY4ePaqvrz98+PBfv35V43aRMAyzevXqI0eOsAOzsrLu3LlTWlq6aNGipKSkkydPRkVFGRoarlixgh48ofD5/Bs3brx79y47O9vMzKxXr169evXiuLgvKCjw8PAoLi4WUNR+//7d0dExJSVFVVXV1NRUSUnJxsZGZOHd3d0fPnxYVFTUvXv3pUuXivd3DJw9e3bJkiWEkOjoaFAfDxo0yMzMjEbw9vb29fUNDAxUU1Pr0qXL8OHDoZdrDsMwy5cvZ38hj4qKunTp0oYNGyQlJQ8cOBAfH798+fIuXbrA1by8vKtXrwYFBRUXF3fr1m3y5MkCO0Dj4uJu3rzZo0ePgQMH+vv7X7x4MTs7u0+fPosXLxY+t56fn3/z5s3Pnz9nZmZ26NBhxowZFXkErlQ4sBEvcLy9vV++fLl9+/aYmJgDBw7Iyclt2bKFtqePj8/9+/cTExNVVFTMzMz++usvAe/SHz9+vHHjxubNm9XV1W/fvn3nzh05ObkxY8YIjwdvb++7d+/m5eVpa2t36dJFQUFh4MCBYooN5OTkXLlyJSwsLDs729DQcPjw4ezKihkq5eXlr1698vLy2rVrl0CaGRkZjx8/DggISElJMTU17dmz54ABAwTiVKnj2IgfveKrU42GunfvXkxMDP3bqlWrCRMmiCleRYiZU8nJyfCBhBDSpk0bsOTg5+fn7e0Ngerq6mAwASgtLfXw8CgoKJg9e/aLFy8cHR2NjY03b95c6QtweXn5nTt3Hj9+XF5ebmFhMXHiROoXAqhIHBFCoqOjXV1dv3z5oqWlZWZmNnjwYOroSTxhYWE3btyYPHmyiYlJWFjY+fPn4+PjjY2NV61aBd7Po6OjnZ2dv3792qZNGzs7O5FTkqNESk1NvXTpUmBgoIqKyvDhw6njKQGq/WQXCY/Hc3d3f/fuXWpqaqtWrXr37j127FjhaNyFT35+/qVLl96/fy8vLz9s2DDhb2whISEuLi7r1q0T+OhVkbRMSUkROEywePFikDNv3ryh9mRnzJhBDz77+Pg8e/YsIiJCV1d34MCBYCKNTfWkTaXirqoUFhaC2ggMtNUdItcJtQ6X5/7379/v3r0bEBAgJSXVqVOn2bNnC5/Tj4yMfPnyZUBAAI/H69Kly8CBA4VHeA2fvBVRVlb28OHDgICAiIgIPT29zp07jxo1Cj5yA6dPny4qKgoNDSWE+Pv7gwxXUlKys7OrNPHr16/36tWLu8feqvLs2TNCiPAaT1JSUkNDIyUlJSQkpGvXrnWUO4IgVaahjAUiCEKhrgA/fPggPiZYxyeEnDlzpri4eOHChfAe0qZNm5iYGCMjI11dXVjTyMvL+/v7i0nq9evX27dvh9TWrl3r4OBAbZODTRBLS8snT56oqKgYGhrC6lZGRkbAWVtERMSAAQP+/vtvDw+Pw4cPN23aVE5O7vz581xqXVhYCNbK2rdvzw4/duwYfbkSMPc7aNCgf/75Rziphw8fNm/enGGYEydOwI0rVqwQiBMZGSklJRUdHc0wDJwWMTExEVM8YQcdNQH0g48ePYK/sIYjQp5GAPAKOmXKFOFLoP4T46AjNDTUyMgIPHJkZGRALlwcdDAMA7oAJSUl4UuBgYFWVlb0b0RExLhx40BVN2vWrEePHqmpqRkaGsI7SZs2bQQcjKSnp/fu3dvGxubbt2/v3r0DNbScnJyxsbGZmRkdeLTA1EGHl5fX1KlT4XV03rx57DRfvXqlrq7u5eXFMExqaurQoUPl5eXpVR6PB0k9efJk9OjRysrKnTp1Av3IwIEDK22KoqIiBwcH2I/ZsWNHBwcHBwcHWqrs7Oxp06YZGRk9ffr0x48foLpSVVWtkh+GFStWQAk/ffokcMnf3x88YPB4PAcHB/DoSgiJiYmha2jaHY8ePerZs+fJkyc9PDxWrVolJSWlpaX1/v17uOrp6Ulvv3z58u7du5WVlfX19eFk0Pjx4wWyfvjwYbt27Xbv3h0UFOTr69uqVStJSckjR47QCNyFgwAiBU5qauqmTZt0dXUJIQYGBmFhYfTNfPfu3QzDFBcX29jYtG/f3tfXt7y8/MOHD02aNOnRo0dmZiYk6+DgQI1VhYWFjR07tnnz5tQ4lMDk/eeffzp16gROgQIDA9u2bSvgWRsUZAIOOp49e9a4ceNt27alp6cXFBTMnTtXRkbGyckJrlY0VL59+7Zy5UrYqS0g4hiGefDgQfPmzbdu3RoZGfnhw4c5c+YQQsaOHZuWlla9jmMjfvSKrw6XhqKTi+2gY/Xq1ZKSkqtXr46PjxdTtorgMqcSExPB5eiwYcMgpLS0NCIiAjSVZmZmEBgUFDR//nzQkS1cuPDGjRv0HJyPj09FBYCPQ0ePHhU4nmZkZBQTEwNxxIgjhmEuXryopqZ2+/btHz9+UGtxoPWjZRPmwoUL9IPWixcvQGHdq1cveDh2794d3Ls3adKkV69esP1cXV1doJG5SyRfX19NTc1169aFhYWFhYXNnDkTvhqS/+ugg8uT/fbt24Sbg44fP3506NDBxsYmKiqKx+NdvHiREDJ//nyBaJUKH3oKNS4uTl9fX0tLi7qg3blzJ8TJzc3dt28ftUSWmJjIzkK8tPTz8wODd8rKyuwbS0pK1q5dKyEh4ebmRnOZOXPmrFmzbt++7eTkBEoiOzs76vKL4SBthKlU3IkhKCgIqrxkyRKBS5s2bYJLjx8/FrhEvWccOnSo0iwA7usEgYItXLhQTLLz5s0THwEQ/9xnGIbP5+/bt69du3ZOTk6RkZG3bt2SlJRUVlaGW4Dy8vJ9+/apq6s7OTn9+PHj4cOHffv2lZKS2rp1K/RgzZ+8Yvj69WuXLl3GjRvn5+cXERGxd+9eGRkZExOTwMBAGufs2bMODg4wN/v27QsynIsbjbS0NBsbG/CKC0aBxHjjZRgG3Ctzd9BRUlIC7RAVFSV8FbTJ4r3kIQhSz6CyD0EaHvrZNikpSXzMv//+G2JSZdaGDRtgbTpkyJDw8HCGYQoKCkBbZGNjIz61xMRESE3A+Sm8z+vo6IwYMeLHjx8MwxQVFfXr14/8Xy+caWlpOjo67NcJ8KUrISEhkGBF0AO53t7eNNDc3Pzw4cPwkqyurk5Xz4mJiVJSUnFxccLpUGVffn4+6DqVlJSys7PZcZYsWTJ69Gj4Xc/KvtzcXAUFBV1dXfo29fr1a0jc09NTOD5sEOjbt6/wJfHKvtLS0q5du9LGrJ6yT15ePpTFx48fXVxcDAwMBBbxJSUlMCTatm07bdq0X79+MQzz7ds3UNQePXqUHXn8+PGSkpLp6enwNz4+HhSFq1atevHiRWxsrECB6fiJj48PDAwEhwACb9d6enrUSx3DMOnp6U2bNqV/qT7C1NT00qVLZWVlDMN4enpCIEcvovPmzSNCqh8+n9+jR4+mTZuyX8DCwsJg4F2/fp1LygxL2Xfz5k3a2p8/f3Z3dzcxMYGJVl5eHh0dvXbtWog5ffp0Ly+vO3futGrVavv27QzDeHl5KSsr0wZkGAb2ymlpaRUWFtKWgQFvYGCwb98+mFDwok4IYb9gBAQEyMjIPHjwgIZQE6IJCQkQwlE4iERY4OTk5ERERMBul1atWs2cOTM6OnrPnj0tWrSAqbFnzx7yf5X+4KHo4MGDNOTJkyeQbI8ePWjhQW3HFiDx8fFSUlKXLl2iNz5//lxA8yus7CsuLga1S05ODm1PQkjTpk3ZziiFh0p6enp0dDRojgSUfWB0ddu2bexA2LJhYmJCC8y940QicvRyqU6lDSWs7Pvw4YOenl61nfNyn1Pz588nLGUfsHv3bsJS9qWkpHz69Am21PXr12/p0qXfvn2bPn16u3btxDxhQdnXpk2bLVu2/PjxIzY2dteuXbAN0NjYGF6exYgjmDtLly6lIfBlTk1N7fnz5+L14LGxsZCRiYnJpUuX+Hw+wzAREREgJAcOHLhq1SrofdhARAj5+++/q9F6/v7+jRo12rBhAzt36hacPp44Ptm5K/vgiXbv3j0aAtox9vdILsKHKvuGDBlCH3Pr1q0jhDRu3Dg3N5dhmMLCwtjYWKreYuvsuEjLjRs3EkJkZGQKCgrYVTh69OjIkSPp32HDhrE1U/Hx8bAt6/jx4zSkUmkjDBdxVxFUpzZz5szv379///49NDT0/v3748aNI4SoqqpevnxZ+K5qK/s4rhOY2lb2iX/uMwxz4sSJpk2bsrXhY8aMIYSwXf3CI5XtTDYvLw/26y1YsICppSevSJKTk5WVlQcMGADTHIAtpfLy8t++fWNHHjlyJBGlvRXDzJkzIyIi4HddKPvgeUEIESgqAPMaziohCPKbgMo+BGlg2C5TBdaXwtDl74wZMyDk5s2bhBA5OTn68sYwDBwmbdmypfjUxCv79PX1i4qKaKCrqyshREFBgYasWLFCQ0ODfWNpaSkoLmnxxEPf0ufOnQshwcHBUlJSP3/+pAb46EfCAwcOiNR/MSxlH/M/7Sch5MCBAzRCTk5O48aNX716BX/rWdl37tw5QsizZ89oiLu7OyT+8uVL4fiwwmvdurXwJfHKvk2bNq1bt47+rZ6yjxCixIJa1xZexIMrld69e7OXrWBmburUqTTkx48fhBDaQYC1tTUhZNq0aexAYWUfAId02G/XmZmZhJA+ffqwo82ZM4cqSqg+QmDLkqGhocDYEINIdQn0JrudAZg1mpqaWVlZXBKnyj55eXnh1mYrzvz9/SHw6tWrAomYm5tPmDCBHUJPNTo6OtJA+N6+ceNGdkzYJUR36xQXFxsbGw8YMIAdJyMjo0mTJtLS0lTJzlE4iKQigQM+IqWkpGAXDBs4mMneqLt3716BAfbz509I9unTpzSQWqeiGxDALurmzZvZ6QtIKmFlX2xsLCFEWlo6IyODBsKLPXtrg8ihwjDM0aNHyf9V9hUVFbVr105WVlZgnOTk5MCWsb1799JALh1XESKLxKU6lTaUgLLv8ePHXbp0Ee477nCfU9BBAso+MKIqsHsOdFj6+vqg6K8UUPYJbDejG/TYr6/C4ohhmNmzZxNC6CZlhmGonyjYTi6eJk2aCD9rLCwshCsLRWI/Crm3nqWlZaNGjdhdzzCMi4sLlJMq+zg+2bkr+2DvrYeHBw0BiyVVFT5U2ccebKmpqRDo5+dHA6lfGrayj4u0/PnzJ0yHK1eusGP27t2bfpkDBVlYWBg7Amze19HRgb9cpI0wXMRdRVCdmvBB9Y4dOz558oT9mKZUW9lHOK8TalHZV+lzPzIyUkFBYceOHewIMGz09PSgBUJDQ2VkZLp37y6QOESTkJB49+4dhNT8ySsMOIIT/srbt29fQsjQoUPZgVVV9t29e3ffvn30b10o+759+wbVFPndHU4pUZU3giC/A+igA0EaGLb5MPoSVRF0Cz012AQf/2VkZNh2hcDBa0pKChi/qx4tWrRgW4qBd6HCwsKsrCxCCNjak5OTW8li3bp14OHr3bt3XLIYNGgQnAF0c3MrLCwkhDg5OQ0ePFhTU/Ovv/6CONTG/LVr16ZNm1ZpmkuWLIF15/Hjx8vKymgibdu2FTaJVQ/k5+fv3Llz+fLlbIM+tGEZlstdCgRW1QWbr6+vp6dnzZ23KCkp5bEoLS1NTk6Gb+MCwNhTVVVlv13A2KNqHUJIQEAAISQ3N5d9L+wFS0pK4lIkYfd/SkpKcnJy7969W7lyJe3lv//+W7jRBEw3wl928aoKqG+EDZDPmTNHQkIiNTWVanI54u3tzW7txMREAQtEtPoC9p5CQkICAwMTExPZc5AqqdlzEHpKwHYVbH2iTXH9+vWwsDC6zQdo1qxZeHh4bGysQDOKFw5VBSrYrl07YZvfu3fvPnLkCN2nQ/43O+Ctj107gQpSJ9e0gnD14MGDVLtBCKEniyuibdu2169f9/DwoInTOcsug/iqsbl3715sbGzbtm1Bv0NRVlYGO3enTp0SqJr4jqsSXKpTpYa6cuXK7t27nz59Ktx33Kn1OUX+1/L9+vWrktV/AYOwS5YsgQfr/fv3BVIWQFjKde3aFc6EcpFyUEgBb6QwhgVsY8ETk2q4COfWe/PmzZs3b7p37y4wnAT+1sqTXQBXV9ezZ8+yZYvALK6S8CH/1zmAhoYGtDN7Rgj3EUdpqampOXnyZEIInDUGoqKiEhMTqUllZ2dnSUnJ8+fPs5OCL1UJCQkJCQmkutKGi7irlMWLFzMMU1xcnJiYeO/ePTs7u69fvw4bNqxPnz616KCD+zqhFqn0ub9nz57CwkKBgTRo0KCvX79+/vwZFionT54sKysTni8DBw5s27YtwzBnzpyBkJo/eQVITU0Fw6PCuYMB0KdPn1JtWlXJyspydHRcs2ZN9W7nCH1JEbN2pV2DIMjvADroQJAGRlZWVkVFJScnhxCSmZkpYAtcgOTkZPhBTUSLBN4cQKNfW+WkxoPB09bXr19//frVvn17sAxFgb8cFVXS0tITJ048efJkXl6eu7v7lClTXFxcwBRxx44dO3fuHBAQ4OnpmZqampqaGh4eDh8hxaOlpTV58uSrV68mJia6urpOnz6dYZiTJ0+yfYDUJ1u2bDE1NRVwEAz2pAhLe8sGAqvkSxdsb12/fp1t47m2aNGixeHDh7lYhib/G3tsLTOos8FGODVXD3s9qm1DGk7MHTp0yMHB4cmTJydOnLCysuKiboD2od7i8vLyhN+jmjdvXlEzlpaWgiMCYTvcbdu21dTU/Pnz55cvXwghZWVldLZSmjRpItI/DBstLS0HBwfYKCSAgE8G2EpgYGAgMAdBqS3Grzcg0FOwr6Fdu3YC0bh4GBAQDtVDpMcJHR2dlStXwu93795dvnwZNkxV+l2EqnhoBS0sLLp37+7n5zd9+vRr166dOHGiffv2wvUVBvZiEEKys7OdnJx8fHwg90rLIJKwsDAiavwQQnr16uXs7JyYmJidnS2gCmQjPMWqRKXV4d5Qx48fv3v37vnz52vi+Jv7nKoG4t2YCCOwK0pRUXHIkCG3b9+OiIgQfyNIObaXEgkJCR0dnaioqJpIuYoyolp17q3n6+tL/id4xVArT3YBevbsCbfzeLy7d++6u7tDkeiQq4nwIYTIyckVFRWJFz7cpeWiRYucnZ29vLwiIyP19fUJIc7OzrNnz6Z19/b2VlRUpAbdaB3hB+zPrZ60qba4E0ZOTk5LS0tLS2v06NHTpk0bOnSor6/v4MGDQ0JCKn0GVY8qrROqR6XP/YoGEtszSUUSWEJCokePHnFxccLSpraevJA1EeXdgg6nL1++gBHbqrJ+/fo9e/ZUVeJVFfqGUl5eLnwVAutogCEIUj1Q2YcgDU/Xrl1fvnxJCPn27ZvwR2w2cCKSECLe0249AM7d5OXlhb3gVYmpU6eePHmSEOLk5KSiopKfn08/Ds+aNQscpV27di01NXXgwIHCKySRrFq16urVq4SQw4cPT58+/fHjx7m5ufQVtz5xc3P7+PHj8+fPBVZgdDEHSl4BsrOzCSFgGYoj27dvb9as2efPn6nTQEIIPSHu5OTUqFEjVVVVkQ4QudCuXTsw/VMNLCwsQJ199epVsK9ECIGjl8OHD69emoSQffv2ZWZmXrp0KSIiYvDgwZMmTTp9+jTVonLk0qVL9DgtxcfHp0+fPiLjx8TEwHsXe2cNRVtb++fPn/DuHR4eznbdC+zevZuL0rlNmzZc9NowB1u3bl3DOQjAhpTi4uKaJ1Xr3Lt3z8HBYdCgQQcOHLhw4QI9ql8lJCQkHj58aG1t/enTpydPnpiYmGzatGnz5s1c3o7S0tL27dsXHh6+YcOGFStWyMvLi3zV4QKojSoaP/CDbQ++LhBfHe4NJSUlxTDM8uXLe/bsSV0iVBXuc6pBAO1YpVurRo0a5e/vf/v27WPHjsGmVz6fn56ebmRkVKl+rSZwb72vX7+S/6mixFBbT3YBSktLT58+fevWrUWLFl25csXGxgYyAupB+HCXlj169DA3Nw8MDHR0dDxw4ACfz3dxcXn79i1cLSsry8jIkJKSmjBhghjVZ02kTa2IOzYWFhbz588/fvx4fHz8wYMHwcZlXSBynUA/BYn/PlFeXg47NMUj5rnP4/Ggl8UPpEolcKXSptpPXvrNIC0tje49Z2fNJXeRPH36VEtLi/r6qzuoIk+kAhoCxW9ZQBCknkFlH4I0PIMHDwZln6+vL1h+EQmfz4cvipKSklZWVvVXPlHAmrXaJw4ovXr1atu2bVxc3OvXr0HTR48yTZkyZfXq1WVlZZcuXcrJyan0CAzFzMxs4MCBr169CgoKevXq1fHjxxctWlQXW97E8+nTpyNHjjx+/FjgcBYhRF1dvXXr1j9+/BDe/EX+t5Ts3bs397y+f//u4+Pj4+Mj8ioYujIzM6u2so8QsmjRourd2Lhx4xs3bowfP3737t29evWysLD4+fPn6dOn7ezs4Nxi9ZCSknJ0dBwxYsSyZcuSkpJcXV0/fPjg5eVVpVdrIyOjWbNmCQSK2TZLdzCJPJoHmwVAX6+qqiqcMve1ONhlF09tzUEAXlwbUKsiktzc3OnTp3/69MnLy4t63qw26urqvr6++/fv37NnT1FR0bZt296/f3///n22LQVhPDw8Zs2aZWdn5+npWb2dTWxgCIk8hEs3m4j/5FNDuFSHY0PZ29vDXi1bW1t/f//qveNxn1MNAkgD6rK2ItatW+fr6/v48ePZs2dfvny5UaNGR44ckZSUBNcWdQf31gPtRqWnv2tXqgARERFjx45t2bLl48ePRQ6SehA+VarXokWLFixY4OzsvGfPnrdv3xoYGNDtYxISElJSUuXl5d+/fxe/U68a0qZ2xR2bvn37Hj9+nFT3IDZ3hNcJrVu3lpCQYBhG5KdNSkFBAZeNnOKf+1JSUjweLyYmRkxSzZo1S09Pr4m0qfYcYc9WAWWfoqKivLx8UVFR9WTd8uXL7ezs2CYgyP92IPr7+0P4qFGjav7hAUwM5+fni/z+8evXL8J5Qy6CIPUD2uxDkIZnzpw5sBfg3r17YqIFBATAo3TSpEnij/HWA7A3LSEhAWwVCZCVlcXeYiYe2HPHMIyfnx/bKp+amhrYXgkPD09NTRVjDkb4tPKqVavoj7dv31ZbUVVtvn79unjx4rt374JLRGFAqwu7Ldjk5OTAMrR///7cs1u8eLGTEGfPnoWrZ8+edXJyqrk5P0JIREQEtWjDneHDh3t5ecnIyKxatWrIkCErV648fPgwLV414PF4MBfGjRsXGRm5fv16CQmJHz9+0DNQHBk8ePBlIfT09CqKr6mpCdtLQ0JChK+Co7qOHTsSQnR0dIRTHjVqVJWKJx6Yg48ePRJpIicwMBCaiCNwYO3Bgwcir8LWm/pn3rx5Dx48OHToUM1ffbOyskpLS6WlpTdv3hwREQGy5enTp+DfoCKCgoImTpyoqam5b9++mmv6CCGwA66oqEj4XRHGT/PmzTluYa4GXKrDvaEkJCQcHR1btWoVFRUl8uA5F7jPqQYBClCpml5OTu7Ro0fjxo378uVLnz59xo8f//Pnz8jISHBPWXdwbz3QTEVGRopPsBaf7EB+fr61tXVERMSVK1cqUgfXg/CpkrScOnWqsrJyWlravXv3nJ2dwdcNIC0tTf2NiMwITpJWT9rUorgTgG52q97x/02bNrF3YlYKe50gLy8P2p+0tDQxt3z//r3S46vin/vS0tIwyEUOpNLSUigASOAvX74Irxs5SptqP3np9mfh2VpQUFBUVMQld5EsW7ZMWIks8ltyzQG3xcK9WVpa+uvXL0VFxe7du9dFvgiCVA9U9iFIw6Ourg5WdT99+lTR5ixCCCyeFBQUuO9x40g1LMJoa2vDhrVdu3YJXGIYZsmSJZqamhyTAv+thBA1NbUhQ4awL9G9UZaWlmJegEtKSgQOblhbW4OVluDg4MmTJwsUhospw5qYO4yPj581a9bNmzeFv3AeO3YMTPKBWvP9+/cCET58+EAIMTU1rdKCydLS8i8hpk+fDlenT5/+119/Vapp4lLlNWvWVMngPfDr16+DBw8GBAT4+/s/e/bsxo0bo0ePrmoiAgmCWXpCiKKi4r59++pu24LA7AAt7b1792BpTmEY5vv373JycgJjuCJqbk8TRnhmZubp06cFLmVnZ2/fvr1Kh5rh0LG/v7/wS6y7u/uzZ89qUlQBOAqc1NTUW7duEULAhzJQ7fOzz5498/Lygt+tWrW6f/8+bCwVP2bOnTtXVlZmYGDAtuZWURm41MvS0hK0bOBInU1cXBwhpIZTQ3yRuFSnSg3VtGnT69evS0lJ3blzB7ziVgPucwqKLfCODc6d6oiYmBhJSUkqS8Vw4cKFXr16hYaGfv78+fbt24cOHaqJKUPucGw9mETfvn2jnSsASKRafLIDjx49iouLU1FRAS/SgMCQqwfhUyVpqaioOGPGDEKIg4ODt7e3wJSEpBwcHKihDMrx48fBCkc1pE0NxZ34BwpYbCSECKwruDyGoqKibt26RRcz1VgngHrI19dXuMWAtLS04ODgSpcolT73YSCdOnVKeOPeunXrwNIlzJeEhATh1RdHCVztJ6++vj4c161I+BsaGlZPz2tvb79YCPjS0K1bN/hbW/YEwPqKcAvHx8cTQgYMGCB+pzyCIPUMKvsQ5Ldg27Zt4PBr7ty5An5LgYcPHzo5OUlISFy4cKFDhw40XORakFqqFr8soysS+gEQ3gzhVUrghYqmCXEkJSXhcKiHh8fatWvp1ZycnJkzZ2ppaXE3OWdsbAz7JiZNmiSgSLK2tgYdn3iDcb9+/crJyWHb55aQkKCbvJYvX86OzOfz4eVQ4NVIgPz8fPhR1TfJtLS0MWPG/PPPPwoKCqksvn79eujQoeDgYDhQPGDAgB49enz9+lXgG6+rqyshZP369SITB0VhLfpdYQM1FZP4wYMHHz16RE+aixl77ETKy8uHDx/esWPHSh1o0AQFdh9AmgID8u7du+xc5syZIykpSW3h08gihzFHZRNMEIHZsX//fgUFhby8vDt37rAjP3v2LCMjY+3atRzt8XMfYLSaAg3evXt3MBC+fv169svDt2/fBg4cuGDBAqrQ4dJTf/31F7zOTZ069caNGxDOMMz169ePHj1K1e5chENFVCRwIC/hQtIjh2DlAGLCWyt7snOXgWynrpKSknPmzCH/10sMJMUeflAGb29vWmV/f3/4zS6DyKFCRA1dY2Nj2Gh87do1gZK7uLg0adKEbVGL4xQTicgicayO+IYSyLpPnz47duwghGzcuPHVq1fsS4cOHerfv3+le3i5zymQIcHBwXT65OXlwUYeGsIuZLVVw0BSUpKnp+eiRYvYXnpFiiMPD4/169dzOX0vEpH7rSBQYE5BILsLOLbejBkzYJv5hg0b2G0VFBQEP+CUJfcnu/BkEQkMuZycnE+fPkFIdnZ2eHg4YQ25Kgkf4TYRluo0ZXoLd2kJwCR99+7d+PHjBZQXsKJISEgYM2YMOOGF1nBwcLh586atrS2EVCptRDYUqUzcVQRdzwj3iJeXl7OzMyGkWbNmAjZq6dOnInkSHh5ua2traWkpcAv3dQIhZPfu3VJSUsXFxevXrxe+kWGYFStWjBkzRqTbIgHEP/dhr19hYWGfPn3o1tTi4mIwFgwbSOfPnw8LTicnJ3bK2dnZnp6effv2pfada/7kFUBaWhqUla9evQLVGAW8Nh8/fpxt1RHWe9V2e0XnUbXjREZG9urVa/r06exBNXfu3EaNGnl6egpEBqU8OLMGHj16ZGVltWrVqt/TFjCC/FdgEAT5PSguLoZFhpGRkY+PDw0vKyvbtWuXrKysoqLi7du3Be46cuQIIURSUvLnz580kH78TEpKEp8pfEW0tLT88OHD1q1b79+/zzAM7GLQ0tLi8/k0JiihCCEfP36EkMzMTB0dHQhs0aKFjY3N0KFDlZWVR40axePxqlT3ffv2EUJ8fX2FLy1btkxCQiI5Obmie3k8HuxcuHLlCju8qKhITU3N0tJSID79CCwrKysmWfqCYWtry70iOTk55ubmYkSul5cXjRwUFASbGWlzff78WVpaesqUKSITLykpgTcia2trLoWhb3T5+fmVRk5OToZXGgkJiVevXqWlpRUXF8O5jG/fvt2+fRuOUbdp04beAh/AjYyM2OnY2NgQQvT09GgI9W3Xpk2bHj16DB48eNy4cTNnzvz777/Z45z534sNIeTRo0fscDjYMnDgQBoC/j2OHz9OQ0BnevXqVfhLzT+dP3+enRRsmhg+fHilDcIwzLVr16BBnJycnj17BktehmEuX74sKyvbtGnTr1+/Qsz09PROnToNHTq0oKCAS8p8Pp/62Dl06JD4yA8fPoSYr1+/Frjk7e0NFgCgI2xtbS0sLKSkpA4cOEDj8Hg80PvY29vTwLKyMlCjz58/nwbevHmTatubNGliYWHRunVrDQ2NuLg4GoejcKgIkQIH9jUrKirm5eWxI+fn58O5v2bNmh07duzw4cP9+vWDd4nGjRufP3/+2LFjDMPQlzo3Nzd6b2BgIAS6uLjQ2snIyHz+/JnGOX78uKKiYkREBA0ZOnQoIaRbt240hB5+t7W1vXTpkp2d3aRJk8Df4rhx406ePBkSEsJUPFRAb6KoqFhUVETTzMrK6tGjByFk0aJFNPDcuXNKSkoeHh7V6DiRiCwSl+pU2lAw+wghzs7OEFJeXg5mB1RUVOikjo+Ppy+9b9++FV9ajnMqPDwchmifPn0cHR3//vvvjh07gqqRELJ69epPnz5BTPBw0r9/f/H5UmDz0YQJE8rKyiAkKytrwIABw4YNy87OZscUFkcMw4CKR1FR0dTUtH///iNGjJg8efKiRYvOnDmTm5srPuu8vDzY7MkWaAzDgIaFPRoZhjlw4AB0a1ZWFg3k2Hr0ZGW3bt2uXLny8ePHf/75hx4t7Nu378GDBxnOT/a9e/dCSQSmrQB0g5upqamjo+OWLVssLS379etHCDE2NnZycnJ3d2e4CZ+LFy9ChKioKBpIR+P+/ftp4O3btyGQ/cDlIi3ZWFhYEELCw8OFL8GxXOjxoUOHjh49WkdHp127dikpKRCBi7QRgKO4q4gTJ05AkQwNDVNSUgoLC2Hz2po1axQVFaFVnz17JnDX0qVL4a7hw4dHRkampqZmZGSkpaXFxsa+fv163bp1sCpwdXWF+NVYJwDUzcjMmTODg4NhlpWXl79586Z///4dOnTIzMwUUzug0uc+u0aEEB0dHQsLC1VV1b59+xYXF9M4nz9/btGihYSEBGiWGYbh8XjTp09v3749u4Nq+OStiAULFhBCunbtShdmfn5+TZo02bx5Mzsan8+HU8kDBgyoNE1heDwebCCYOnUq+2HNpqCgAPSeK1asEBln06ZNUEd/f392+KlTp6SlpQMDA2lIYWGhsbHx4MGD2dHoueydO3dWowoIgtQKqOxDkN+Lp0+f9u/fX0pKSl9ff8KECZaWlk2bNlVVVbW3txfQTPH5/C1bttCDQhYWFtevX2cYZsuWLU2aNIHA7t27w0q6IlxdXWGFLS8vf/z48YSEhAULFsjIyMDtQ4YMefToEY/H27RpE7USqKenR9Vq6enpI0aMoJ8iVVRU9u3bx15UceTHjx/t27cXeSkgIKB3794V3bhy5Ur6riIlJTV69OgXL17Qq3///Tf75dnb23vKlClsdxmampqzZs0SUIlu2LABXkUoPXr0sLe3Ly8vr7QisLqqiA4dOgjE//79e48ePfr373/+/PkdO3ZoaGhs3769tLRUIFpcXNzu3btpqSQkJGbOnLl///7U1FQxheGo7Pv58+esWbM4Hs6aO3cuwzA/fvxYuHAh7ffp06f7+vomJSXNnz+fGgKbMGFCQEAAwzB8Pn/JkiUVeSHcunUrwzAZGRkbN26kx0xat269ZcuWpKQkR0dH0B5CradOnfr48WMos5aWVrt27aZMmXL27Nn9+/cbGhrSdwAnJydqY6tp06br1q3Ly8t7/fo1PSUkISHx119/iVH1AkVFRd26dYNbunfvnpiYSC99/vy5e/fuKioq48ePnzdvnqGh4d69eytaUguwZs0atgkwKSmpkSNHOjo6CsfMy8tbuXIl3e/QokWLdevWBQcHs+MEBASw7YLp6uqyPwl4eXlZW1vDJSUlpVWrViUlJb1//56ei2zUqNHy5cszMjIg/uvXr9mGk8aMGZOQkACXqiocRCIgcMLCwmbOnEk3znTu3Hn79u3stz43Nzd4UyWEWFlZRUVFxcfHQ4i2tvaXL1+cnJyoBGjbti28VFy9epUehdPS0tq3bx/DMDdv3tTX12/Tps3KlSsdHR3Xrl1rbGz88uVLyOju3btsdzGTJ0++efMmwzD5+fl0V0vTpk2PHj3K5/Pp++TKlSuh04WHyqtXr+zs7KhfoH79+p06dYrWq6ysbOPGjc2aNTM2Nl66dKmVlVWfPn0iIyOr3XHCiBy9XKojpqFKS0s3bdpEx5u2tva6devi4uI+f/5MPWtLS0sPHTr0/fv3paWlcN6NELJ7924xowLgOKcuXLhAvXb27ds3Ojraw8NDXl7e1tbW1dW1oKDgwYMHoLQFbGxsBJRoIomOjp44caKCgoKKisqQIUOsra3bt29/6NAhdgEqEkcMw3z8+LGi83ctW7akk0iYy5cv0x5p0aLF1q1bU1NTfX19Z8+eTVWlU6dOffbsWWZm5tq1a6kRWAsLi2vXrlW19e7evUvPY8rJyc2ZM8fDw0NWVnbs2LHu7u702S3+yR4YGGhvb08nZq9evf755x8xbbtmzRqoi4yMzMKFC3NycqgyztLSkmotxQgfhmE2btxIJSGV9o6OjtTpubKy8saNG2NiYtavXw/6a0JIu3bt/v77b/oEFC8tBXBxcalIz1JWVrZmzRo6DqWkpObNm8f+4Cpe2lREpeJO5F2HDh0aPXq0SPMajRo1MjY2trGx2bt3r8Bq4eLFi+KXKxQJCYn09PRqrBMEePbsGT2VIi8vr6urq6Cg0LRp0+XLl4vXF1PEP/cpx44do9NEVlZ2w4YNhYWFAnHS0tJsbW0bN25saWm5dOnSzp07z5gxIycnB67W/MkrHhcXl7Zt27Zo0WLevHkTJkwwNjZ++PAhO8KBAwcGDx5MEx83btw///zD5astwzCvXr1q3769hoaGsrKyqqqqsrJyixYt9PT06PhJTEw0MjIyNDRs3ry5qqpqs2bNVFVVW7dubWxsLPAhOSgoqEOHDsOHD6efQCi7du1q06bN/fv38/PzQ0JCBg8ePHz4cAGNLexmhcHMsWUQBKl1JJi6OQ6GIEhNSEtLCwkJSUlJady4sY6OjpmZWUW6kpqTlJQUGxtrbGxcJQtfbAoLC0NCQpo0adK+fftqGHQDvn79yjZVw/HSn0FSUpKfn5+KioqZmVm1e+G3hWGYPXv2aGho9OrVKykpKTs7Oz8/Pz8/Py8vLyAg4MGDB0lJSVW1A8Xn80tLSxs1apSZmRkRESEjI2NiYkJfvWqRsrKyoKAgOTk5kRb6CwsL4Vy2sbFx/bt7ZpORkREREdG6dWu6JacmpKSkxMXFGRkZ0c8GtUhVBU5WVlZoaCi840FIcnJyQkKCubl5lWwDFRUVNWrUSEJCIiEhITY2VkNDo0OHDlzkKsMw4eHhOTk5nTt3ht0cfD7/48ePOjo6bGMF4odKRcTGxiYmJrZr14673QPuiCxSpdWpdkMJwzDMjx8/Nm7cOHToUGHn1CLhMqdyc3PDwsK0tbVhtGdlZcnIyAg7Pa8GJSUlUVFRGRkZqqqqxsbG3B9n0dHRGzdu3Ldv38+fP9PS0vL/R3Jy8r1796ytrQUcZdYRHCXSjx8/UlNTTU1N5eTkwJCZSEdStfJkBxISEkCkUNu74eHhfD7f2NhY4MxjnQofgKO0LCsrS0hIEONyt7y8PCwsjMfjGRgYKCgosC9VexLVlrj7bUlLS4uJiYmLi1NUVGzRokWXLl24Dy3uz32GYaKiovLz801MTMRMBB6PFxYWVlhYaGBgUJEvNfHU5Mmbnp7+9etXdXV1ASOq/xbi4uLc3d2/f//epEmTYcOG9e3bVzhOcnLy7du3AwICBA5NIwhSb6CyD0EQBKlDli5d+u3bN2ELL8CCBQvs7OzgDB2CIH8YpaWl/fr1e/r0qYqKSkOXpa6IiIjo16/fw4cPRXpVSk5OtrOzq8jVLIIgyB/MX3/9NW/ePDgUjyBI/YMOOhAEQZC64uXLlydPngQLZSJJSkoCG1gIgvxhlJSUzJw5c9OmTX+wpo8QMm/evOzs7IqstUZFRfXq1auei4QgCNLgHD58uHnz5qjpQ5AGBJV9CIIgSF0RFRVFCHFycoqIiBC4lJaWNm/ePLbJNgRB/iTOnj1rZ2cHnnz+YKKjo8vKynbs2CHgtZPP5z9+/PjQoUMCLlARBEH+eF68eCEvL79///6GLgiC/KfBY7wIgiBIXZGent63b9+oqChJScnevXsbGBgoKytnZGTExsbm5ubu2bNn5MiRDV1GBEGQ6nPs2LGVK1cyDKOpqWlpadm8efOysrKUlJTAwMCRI0du376d+tFCEARBEASpN1DZhyAIgtQh2dnZp0+fdnNz+/btW0FBgZqaWteuXSdNmjR9+vS6czuDIAhSb7x+/frUqVMfPnxISUmRlZVt167dwIEDV65cKca9A4IgCIIgSJ2Cyj4EQRCknuDz+ZKSaD4CQZA/ExRxCIIgCIL8JqCyD0EQBEEQBEEQBEEQBEH+EPDzI4IgCIIgCIIgCIIgCIL8IaCyD0EQBEEQBEEQBEEQBEH+EFDZhyAIgiAIgiAIUgkuLi53795t6FIgCIIgSOWgsg9BEARBEARBEKQSdu7cWV5e3tClQBAEQZDKQWUfgiB1i7e394wZM4yMjIyMjObPn5+WltbQJfp/3L9/v2vXrk5OTg1dkAqJj48/f/78qFGj3r17RwMZhgkODt63b1/v3r3r4pWjJv2Vm5vr5uY2ceLEx48f13rBfluioqL69u27fPnyhi6I6AHzu5Genu7s7Dxy5MjAwEDxMSsa6qWlpba2tiNGjMjMzOSebz0MTh6P5+3tvWrVqpoPhurVsRpkZ2e7u7vPmjXLwcGhTjOqH/Ly8jw8PGbMmFFVwV5aWvrmzZs1a9bMnj27bopWy/xbChwSErJjx45JkyY1dEFE8OHDh8TExKrekpaWNmrUKPHRNm7cGB0dXe2C/c6NJp6aC65du3b16tXLz8+vdgv2H8TJyWns2LHt2rXr3Lnzrl27eDwe93sTExNPnz5taWmZnZ1dZwVEEKReYBAEQeoMd3f3Hj16FBQUJCUltWrVihAybty4hi7U/2PAgAGEEF1d3YYuiAgSEhIWLFigrKwMgvrFixcQ/vbt27Fjx0pLS0M4j8er3Xxr0l+Ojo66urpQsNu3b9duwX5nduzYAbVOSEhoqDJUNGB+N7Zs2dKiRQso5IcPH8TEFDPU3759C4FXr17lmG89DM4XL14MGjQIshgzZkwNU6tGHavB2rVr27VrBxlt2bKl7jKqH06fPm1kZATVcXBw4H7jxYsXe/fuDTf27du37kpYW/wrCpyUlDRp0iSYwiYmJg1dnP9DSkrKpk2bpKSkHB0dq3Sjvb39ggULxMe5detWtYXw79xoXKih4CotLZWTkyOEzJ07t9bL9p9i7dq1MFBfvXolIyNDCDl+/DjHe1esWNGkSRPox4yMjLosJoIgdQ4q+xAEqSvi4+OlpaUPHDgAfz9//tyhQ4ezZ882bKkot2/f7tix4/nz5xu6IBUSFhYmUndz6tSpulD21by/fv369R9U9oWHh3fv3n3hwoUNXZAKB8xvRVBQEBdlHyByqBcXF48cOdLKyio9PZ17vvUzOAcPHlwryr7q1bEalJeXN2/e/M9Q9jEMU1JSoqioWFVlHzBhwoTfWXcmzL+iwEuXLv199FafP3+2sbHp0aOHtbW1gYEBIeTixYvcby8tLW3WrJm3t7eYOL9+/dLU1KyhEP6tGq1K1Fxwbd68uXPnzu/fv6/dgv2nuH79OiHEz88P/l68eLFDhw5cHrgUd3d3VPYhyJ8BHuNFkD+WsrKyLVu2NGABHB0deTyempoa/O3cuXNkZKSdnV0DFomNra1tSEjI/PnzG7ogFaKjo1Ol8BpS1f76/v37mTNn2CGqqqpSUlJ1UbbfGUNDw48fPwo0RYNQRwOjdoE3YY6IrJGcnNyDBw+eP39OxyoX6mdwamho1Eo61atjNZCUlGzZsmWdZlGfyMrK0j0pVeU3mT537tz5+PEjl5i/SYHZCBe+tmZEraCrq7t79+6XL18+evTIxMSkqrd7enoqKyv36dNHTJy1a9c2bdq0SsmuX79eIKR+Gk34CV5zai64du/e/fnz5169etVuwX5bhHu/5pw7d44QQrtg7ty5kZGRPXr0qCi+8Ej4raYtgiA1AZV9CPLHsnLlyoY13fXixQtCiKQkyplqUlHT1VGTVqm/SkpKJkyYIGzRT0JCovZLhnDjXzHXqjRCardG9TA4/43j/18xbLhT7S74HdohJiZm7ty5xcXFXCL/DgVmI7Lwv9WMUFFRMTExgb2f1eDq1aszZswQU6NHjx61adMG9gxy5Pbt2wcOHBAIrIdGq+gJjtQnInu/hpSWlnp7e5OareV+q2mLIEhN+L0WCgiC1BZXrlyhJ+AaipSUlIYtAFIlqtRfixYt+vTpU90VBkEQ5D9Ffn7+uHHj/qUW8f/VhedCdnb2o0ePpk+fXlGE3NzcM2fObNiwgXuaYWFhDeVfBZ/gDU4d9X5qaiqfz+ceH0cCgvzZSDd0ARAE+f/x8/Pz8fEJDAxUUFDo3Lnz4MGDqfV0QoiDgwN1SblixQopKan8/HzYrg9MnjxZS0uLEHLx4sVFixYRQhISEg4fPkwI6d69u4WFBSEkNTXVxcWlU6dOVlZWT548cXd3z83N7dGjx6RJk+BeATw8PN69e/fjxw8jI6OhQ4cKnK3Izs6+fv16u3bthg0bdu3atQcPHlhaWk6cOBE8IWZkZBBCHj9+DD+0tLQmT55M783JyfH09AwICIDEu3TpMmLECIFPkWVlZQ8fPkxKSlqyZMmHDx9Onjypo6OzefNmJSUlhmHevXt38+bNAwcOKCgovHv3ztnZOSMjo0uXLitWrIBP9xEREVeuXImMjGzTps2CBQv09fXZifP5fC8vr4cPHx46dIid48uXLz09PY8fP87j8S5duvT8+fNmzZpNnDhx4MCBwu3z48ePGzduBAUFqaurm5qaDhw4kN1l4omPj7927VpsbGxhYWHHjh3HjBljaGjI8d4qIb6pf/36xaW/KDweb9GiRZcvXyaEvH//HgaYra1tmzZtBGI+fvzY3d09Ozu7Y8eOy5cvFz5eV1hY6OLiEhAQkJub27lz53HjxrVt27bS6nAfw7GxsU5OTvPmzVNXVz9y5AisrcGqGlDR8GbPNUKIlZWVqakpISQzMxMqTggxMDAYMWIE/A4PD79x48acOXOEy19bkxpISkq6cOFCYmIibFFRVVUdNmwYWDQXyaVLl7Kysujf5s2bT5s2DX5fuHAhNzeXEKKqqirmlYPP57958+bJkycHDhxIS0u7cOFCYGCgtrb2oEGDqEtKPz8/2EpAWKLm2bNnX758gcBWrVqBfTEBioqKrl275uXl1ahRIwsLi4kTJzZq1KiikrBJTU29detWu3btrK2t2eF+fn5ubm45OTnNmzc3NzdXUFAYNmyYyBRqa3C+fv369u3bP3/+1NXVrZKNgqKioosXL3758kVOTk5PT8/Q0LBt27bt27cXX8f4+Hg3NzcDA4ORI0eGh4efPXs2OTnZ3Nx8+fLlSkpKAlkwDOPu7u7t7Z2ammpiYtKtW7f+/ftzbGHAz8/v8ePH4eHh2travXv3HjduXKU7PmoumWnWYiYOhcfjubq6Pn/+vKioyMzMbMmSJRUVLCEhwc3NLTg4WF5evkuXLlOnThVusYoageOgqhQxMj8lJcXW1hamjKurK7x+g0yoRkaVdlylkqRKteZY+NTU1AsXLgQFBampqdnY2AwfPlwgnfLy8vv373t5eaWkpDRv3rxLly7Tpk1jrwqq+oCuRdzc3MzNzfX09CqKsGHDhp07d4IzBC58+PBh7Nix+fn5hBB4jLZo0WLq1KkC0SptNFLFeSr+CS5yUQfrSUJIXl6ei4tLcHBwVlZW+/btBwwYQL0SsQtcE8FVWFj48OHDnz9/Llu2jAbW58KMEBIUFPT48eOgoCBlZeUuXbrMmDFDeDdoXFzc8+fPAwMDc3Nzzc3N+/bt27NnT+Gkbt++/fr1ax6P16ZNmy5duigqKsIxcI69X6VMg4KCXr58mZ6eDn/Pnz+vqqpKCOnZs6fIs+cc13IlJSVXrlx58+aNhIRE3759FyxYILxnsHprOQRB6oMGthmIIAjDMAyTl5e3YMGCdu3aPXjwICEhwdXV1cjIqFGjRocOHSovL4c4hYWFU6ZMgZlbXFwMgWlpadu3b4dAHx8fhmGKi4sdHBz69u1LCGnfvr2Dg4ODg4OPj8/t27cHDx4Mi+8zZ85MmzZNQkKCLkw1NTV9fX3ZRYqJibGyslq/fr2Hh8fp06e1tbUlJCQ2b97M5/MZhrl3797YsWPhvfHkyZNbt26lUiUwMBAybdy4MSFkwoQJ8NfNzY0m/uLFCx0dnVWrVoWGhgYGBq5Zs4YQ0qtXr8jISIgQFBS0YMECsDkyfvz4x48fy8rKQvrOzs5r1qwBo/KEkPj4+L/++qt58+a9evWC8lhbW/P5/L///ltFRaV3796gK2nWrFlcXBwk/u3bt9WrV0N4kyZNILC0tNTe3l5FRQUCk5KSunfv3rp1azAxJikp6erqKtBr169fV1VVdXZ2TkhIOH/+PDSmmpqaqampmZlZWFiYmB4/dOiQnJycs7NzXl5eRkbGsGHDlJSUPD092XFgIUiETH0/fPgQwrk46Ki0qTMyMirtLzbfvn1zcHCA9h82bBhE/v79O1wFN4I3b96cOXOmgoKCmZkZvEDq6uoWFBSw03n27FmPHj2OHj167969bdu2NWrUSFlZ2d3dXUxdOI5hHo937NixPn36wAtPQEAAfSFp27YtJCV+eKempnbs2BFuuXr1KgQCgYGBsrKyo0ePzszMzM3N3b9/v5mZGcQMCgpil7YWJzXw/v17NTW1J0+eMAyTlZUFN6amptIIwgMmPT2d1v3EiRPsihQWFg4dOlRbWzs+Pl5ka6empq5YsQKmiba29tu3b9XU1NjqgLlz55aWlkLkjx8/wgzdsGEDhBQXFwcEBMArlpWVFU3258+fcLu7u7uA2azu3bsnJSWxyyA81K9cuTJkyBAYAwJOGBwcHPT19aOiohiGiYqKMjU1NTQ0ZEeo3cHJ4/FWrlypra3t7u6emJjo6elpbm4Or0mVOuj49etX+/btd+/eXVZWVlpaeuTIEUIIlTAi6/jkyRP6wubg4HDy5EllZWVDQ0OolKmpaVlZGTuL7OzsgQMHDhw4MCIi4tOnT/BKLC0tbWBgYGZmtnTpUhqza9euRMhBR35+/vz58ydPnuzq6nrt2jXw/Tpy5MisrCwx9aqhZAa4TBwgIyPD0tLS0tLy9evXCQkJ586d69ixI+gOBMbG0aNH+/fvf/nyZXd391mzZhFC9PX1Q0NDBQpPhPxdVDqouCNe5r99+3b37t3QdEuWLAG5Kl7Ciywwl46rVJJUtdZiCg/hJiYm79+/V1dXb9++PTU1eO7cOXYiSUlJxsbGgwYNCgsLKy8vf/DggbS09IgRI4qKiphqPaDFM378eFIVBx19+/Y9ffp0RVdfvny5adMm+D1mzBjhp7YwLi4uMBRhuDo4ONy8eRMucW+0aszTip7gYhZ18Hx/8+ZNkyZNVq1alZycXFJSsnnzZkLIzp07aco1FFxeXl7Tp0+H+UtFaD0vzPLy8uzt7U1NTd3d3WNiYo4dO0YI0dTUDAkJoXH4fP7x48ebNWt25syZuLi4Fy9ewHev2bNnZ2dns1OztbUdM2ZMVlYWn89/+vRpkyZNFi1aVGnvi4RLpkFBQQ4ODhs3boRkN27cCClX5O1EzFrOx8cHEgkLC+vcubO6urqxsTEsqKZMmSKQTjXWcgiC1Buo7EOQ3wJLS0sFBYXk5GQakpycDF/GNm7cSAOphyyqF2BYL89svQBscLC0tKQh6enpHh4eEFNHR2f//v05OTmFhYU3btyQl5cnhDRv3rywsBAi//r1q23bthcuXKC3U6vbsLSKi4t78OABhAwbNuyff/75+vXr0KFDTUxMYGnOMAysIYTdub569UpCQmLFihXswH379hFCmjZt+vPnT4ZhUlJSPn/+3KpVK1ABzJ07Ny4ubunSpdra2p8+fWIY5tatW5C7hYXF48ePIZG3b99CoJWV1datW2ERWVxcDJaJqRoiPT09JiZm5MiRhKXsA7Zt20YIkZOTGzRoEPgyKysrGzduHCFEV1eXHTMsLKxRo0bTp0+nIf/88w+sPj08PF68eJGbmyvUz/8Pf39/wlI80RA9PT12tJor+7g0NaWi/hIJqGm2b98uEA4reGNj46NHj8IoDQ4OBjXQqVOnaDRfX18lJSX2shss18jJyf348aOiTDmO4fLy8ujoaHgVIYSMHz/+wYMHr1+/1tfXnz17NsNheDMM8+bNGwi5ceMGuwx5eXmysrKxsbEMwxQWFsbGxlLFnICyr9YndceOHceNG0f/FhUVtWnThq2qEzlgvn79Cmt0dvsD5ubmZ86cqai1i4uLIyIiYHeGoqLi4MGDP378yOfzo6OjYe4QQo4cOcIuHnuWAQsXLiQVKPuMjIxOnz6dkpISFha2fPlyCOzduzf7duGhHhUV9eTJE6gRW6Hz8+dPGRmZ48eP05DQ0ND27duzU6vdwTlt2jQFBQUYCcD3799hA0ilyr6lS5eqqqqyQyZNmnTlyhXxdfz16xfY/u/QocP69etBXPv5+cHb9dWrV9kJzpgxgxASExMDf3Nzc+HbyYQJE168eMEeqyKVfTY2NtOmTaN/8/PzwWQ7fVmtiJpIZoDjxCksLNTT0+vcuTN94jAMc/v2bciI3W5HjhzR1dVlR4NRbWJiwtYeCuvOuAwqjnCR+bClmhDy5s0bLmmKVPZx6TjxkqR6ta6o8KC3UldXHzJkyNevXxmGKS8vhx1MLVq0YGuoYaO0s7OzQIiLiwsN4f6ArpQqKftiY2Pl5OR+/fol8mpBQYG1tTUV4ByVfQxruAqEc2+0as9T4Se4+EVdaWkpbH+mMpDH4ykrK0tKSiYkJEBIDQVXfHx8UFAQzHQBEVo/CzOGYWbMmGFqasqOY25uTgjp2bOnQGE8PDxoSFlZGTwT+/TpQ7+owZOdrSU8c+bMnDlz6N+Kel8kHDNlGCYiIgKSjYiI4JKyyLUcVfaBGIf0z549C4HszyTVW8shCFJvoLIPQRoeZ2dnkYsz2F0vLS0dHBwMIfTVl60XoFZyxCv7APhqKvBed/PmTUjh8OHDELJu3ToVFRUBXRIcYOnSpQsNadasGSHExsZGZL1EKo9KSkoMDAwkJCRSUlLY4Xw+HxZ5EydOpIG2trawuhXYd8MwTExMDJTZ29ubHQ6JTJo0iR0IB3W7devGDgTvYwLKPqpLYm9zePXqFQRmZmbSQHt7e0LIvn37aMjPnz9Bm8BekInk6tWrAi9+1F4ee5VZQ2VflZqaqVVl38GDB9mB8Al65syZNKR79+4jRoxgx0lKSoJKrV69WnzWHMcwPUN69OhRgRQ4Dm/YJTF69Gh2NCcnJ4EB//XrV8iIrUCp9Umdn58vISFhamrKTm3Hjh2w+4bGETlgbGxsCCEC90ZERMjLywvsRBAG5o6CggJ7ZJaUlMC5Zg0NDfqFAHRGAoqbFStWkAqUfQLKR3oA8969ezSwoqEO2zrY75Oenp6EkGXLlrGjsV+DmVodnPAuZGdnJ9BccNSxUmVf9+7dpaSk0tPTacjbt2/Pnz8vvo4Mw8A2UoFx1a9fP4G6p6amEkIaNWrEjgYtbGZmJlAYYWUfNKa/vz87Gjgul5aWZotBYWoomblPnD179hAhXTyPxxPY2ZeWlqaoqCjQ6S4uLlDIBw8e0EBh3RmXQcURLjK/5so+Lh1XqSSpXq3FK/tatGiRk5NDAz9//gyR2bpyXV1d8n+VfXAuftWqVTSE+wO6Uqqk7Nu5c+fYsWMrugrO0Ojf2lL2VdpoNZmnFT3BK1rUxcfHE0IkJCTY+3DhAP79+/fZMWsiuBiGmTRpkrAIrZ+FGXyoYMsE5n+Sp1evXvA3MjJSTk5OeKNrbGwsaDnp9s9169YRQu7cuUPjFBQUzJ8/n/7lruzjnilT28o+9uji8XigtL106RINrMlaDkGQegBt9iFIwwOWMjp37iwQPnHixBUrVuTk5Jw7d662vG3AokfAJMeECRPWrVsXHx//8uVLWFhfvnxZQUEB3iUoYFwsICCgsLBQQUGBEALnI6pkK+fRo0cRERFaWlr0tBcgISExd+7cLVu2uLm5nTx5Ul1dnabfs2dPyE64IoQQOHxKadmyZVxcHNxOgW/dVMsAiLStQ5OFJS9NE34kJibCWocQEhAQQAgBq2eApqZmhw4dwsPDExMTxbQAIWT8+PHZ2dlsA4gMw8CPzMxMgRpVmyo1de0iYK4FxhttlrCwMD8/v27duq1cuZIdTVZWtrS0lK4yK4LLGCas/hUYn9yH96pVq96/f//o0aOkpCRqOO/y5cuwiKeIHEi1PqkVFBQUFRWDg4Pnzp178uRJ2Mm4aNEiLibY1q9ff+/eveDg4BcvXlhZWUHg9evXx44dC8ejxACtraqqyh6WsrKyS5YsmT9/flpaWmhoaLdu3bhXhCLQOBs2bDhz5kx5efndu3dHjx4t/l7hNodta6dOndLX14f3PULIjh07hO+tlcEJZ9wELAbSYlSKmppaeXm5tbX1nTt3tLW1CSF9+/aFjcxi6kj+1x1s6UT+J6DYYicwMJAQUlJSUlJSQk9e9+vX7+TJk5VKJ0LIpUuXCCHOzs5UKUYIiYyMJITweLyPHz+KMd9WQ8nMceLk5+fDDmWBLpCSkmrSpAnVehNC3NzcCgoK3r9/z+5Q6nrSx8eHblMVhvugqpT6kflcOq5SSVKLtaY0a9ZMWVmZ/qXSOzExkc7HGzdu+Pn5ga4HgCbKzMykIdwf0LXLtWvX9u/fL/LS+/fvJSQk4MtQ7VJpo9VknlZERYs6HR0dNzc3KSkp9pNXuINIzQSX+NtJHS/M1q9fLy0tLdBos2bN6ty5c4cOHeDvqVOnSkpKhAVU27ZtBw0a9OLFi5MnT4KJQ5hHdnZ26urqYFdHQUFh/fr14ssgEu6Z1jrsx6WUlJS2tnZWVlZtreUQBKkHUNmHIA0Mj8eDD3HCOhcFBQVTU1MvL6+QkJA6LYOkpKSVldWlS5eio6MJIREREenp6ebm5gL2hulfAVdfVbIgHhoaSkRVlhBCl8shISFsq89VSl/kShFO6rE9FVQJutBkVxwyghajtG7dOjw8HHYoiKFRo0Z0H1NycvL58+fpR3sej1e9QgpTjaauI0DdUFpaCn+9vLwIIR06dBA5wODls6oIjGE2AuOH+/C2sbHR0tJKSkq6dOkSKHe+ffsWFxdX6RtUXUxqCQmJVatW7dy589KlSy9evDh69Oi4ceM4Kmp79eplYWHh7e198OBBquy7ceNGTT4hDB06FH5ER0dXT9knYEJeS0urZ8+e7969oxsTqkS3bt369evn5eW1ePHi69evnzp1ytTUtNKZSKo1OBmG+fDhAyFEQD3HnVWrVj1+/Njf39/AwGDLli2rVq2SlZUVdnHDERBQwtKJYZhv374ZGRlBYOvWrQkhXNrEy8tLTk4OXlApPXv2BNUPfe+tElwkM/eJExoampeXp6KiwtaGVFQXQkjXrl0FKg4KZWiTiqj2oBKmfmQ+l46rVJLUYq0rgiqg6byDfEGS8Pn8x48f37hxA2ZZpe0j8gFdi3z8+DEjI0NYs08IKSkp2bNnDz26XqcIN1pdzFNA5KKLOlkqKChwcXF5/vw5qOmrPYCFBVc1bie1tDCLiYmJjY1t164dTZbSqVMn+lv8surFixeRkZHwiWX27Nn79+9PT0/v16/frFmz9u3bp6GhUb15xD3TaiReJephLYcgSO2Cyj4EaWBiY2PhwQkHrwQAq8zC+otaB3aXFBYWEkKSk5MJIbKysuxv7LUFnHkUU1lCSHR0dD1ooGrIqFGjvL29H/5/7J15QE3N//inVUpKaRFFRVHaCKVsZUuJkFD2Jbs8yO6x70v2UJKyS1mKZItEhBYqrbTv+173nt8f7+8zv/O593Y7bfJ45vXXve8zZ8571jPzPjPzfvSosLAQXJ4hhHJycmRlZQcPHswkhtjY2P3799fV1W3evHnZsmUc6+9azm+b1VDBevTo0boVjF6HG306k+otLCy8ZMmSv//+283NbevWrYKCgleuXJk/fz63KzoO2qhR79y5s7Cw8MyZM6mpqVOnTh07duylS5cY2pucnZ3fvHnz9OnTqKgoXV3d8PDwqqqqlhR9t27dhISEWCxWoxnOnJ49e759+7agoKB5t/v5+VlbW4eEhLx9+3bgwIErVqw4ePBgU+cbTCrnjx8/qqqqEELcLhoZYm5ufvXq1cWLF1dUVGzevNnDw8PNzQ28GLcKRkZGXbt2zc/P9/T0xCuSoDby9OZJp66uDvYXW1tb/+LZGvOGA50bk/yHAh0wYEDzXOi2SqXCtGmfz7zgGu1JWjfVzKmvr3dzc7t69ercuXMvXry4bt26pKSktn5oo3h5ednZ2WEXYXR27dq1fv167p0Hv4B2aaeFhYVHjhx5//79hg0bbt++raWlRV9D9zvQ7IEZ7FOurq7mH3+jwyoWi5WSktK3b9+uXbs+f/7c0tIyIyPDw8Pj3r17hw4dapLH9mY8tBmRt4Q2GssRCIRWpJEZC4FAaGvwti98zgUd+JT3C3zYwzlHMFYAW0ZycnJbfCeH9Obk5HB/DcbfLX9BeluOk5OTjY1NVVXVzJkzy8rKEEI+Pj6xsbFeXl54iMmHffv26enpjRkz5ubNm3D8Wavz22Y1VLBWN2HT63CjT2dYvZcsWSIiIpKamvr06VM2m+3t7b1gwYJG72qjRi0gIHD69OmAgAC49+nTp/r6+rBrqVEsLS21tbXRP9skr1+/PmfOnEatlnwQFhaGj/ytOMGATZ0cLnqZ06VLl+Dg4OPHj0tKSrJYrFOnTg0dOrSp63mZVE687ZTJltiGcHBwiIiIGDFiBEIoPj5+5MiR7u7uzY6NA3Fx8du3b0tKSp48efLBgwcIobKysoMHD5qbm2PHNQ0hICAAmYBP3/tlMG84UATZ2dmNripqYW/TKpUKaOs+n3nBNdqTtGKqmZOYmDhgwIDLly8/fPjQ0dGxXSxo3NTV1d26dWvOnDncl5KSkh48eBATE3P2f0lOTkYI+fn5wV84IKLV+fXtNCAgoE+fPkVFRUFBQRMmTOBYmv2b0OyBGWRmZmZmRUUFn2DQR/HpoAQFBbHdXE9P79u3b4sXLxYUFCwpKVm6dCm4CGtqopr00F9JG43lCARCK0KMfQRCOyMjI9OtWzeEUGRkJPfVvLw8hBC4uWxTYNbar18/hFDv3r3h0bBEnwMWi/Xy5ctmPwhm8mw2GzYm0IHEol+S3pYjIiJy7969efPm/fz5c8SIEVOmTHn27FlMTEyjC2cQQm5ubtu2bZs0aRLPKURr8dtmNVSwJ0+e8BxVZ2RkYJcXTYJehxt9OsPqraioCC7/Ll68+Pz5cy0tLSZD6rZo1CwWC47At7CwgPVBwsLCRUVF8+fPZ3K7gIAAnFF448aN1NTUW7duzZs3r0kKcFBUVARr+hrNcOZAztD3TDGnuLi4pqZGUFBw7dq1CQkJ9vb2CKGIiAjsK5khTCon3ooFx2M1A1ijoamp+erVKx8fH3l5eTabvWLFCo5zRVvCqFGjPnz4IC4uvm/fvlGjRi1cuHDhwoVBQUHcm9Q4EBYWhs2t2E80B0+fPm0tJTlg3nCgCNhsdqOWDijQhtLy9u1bPktTW6tSoV/S5zMsuEZ7klZMNXOqqqosLS2jo6M9PDw4TnZrXx4/fiwtLc2xSxEQFxfneVAaLHdqa35xO/327duUKVM6dOhw5syZRvuQdqTZAzP81Qq7JKZTVlZWUlKC/hlWxcTEcH9mgA5KXV0d7NTQyUtJScGeffCDdOXKFexshDnMH/qLaaOxHIFAaEWIsY9AaH/GjBmDEHry5An3l/OUlBSEEDh3Q7RTrurq6nAYPnMV5mepwEFIDg4OCKEePXrAeeG7du3i/gi5Y8eOliwIGjlyJIwUb9y4wXEJEjto0CB87vJvzrVr1+AsmM+fP9+7d+/8+fMMT906deoUQoi+uKMVj23C/IKsbp7aYB6qrKw8cuQId4QrV66ECX9ToddhPjS1eq9YsQIh9PDhw4MHD4KjQya0eqPGHgkQQh06dNi8eTMcFBUVFUV3R8CHWbNmde/eva6ububMmaqqquB9uNlAbk+YMAHPzCEh9FTwTAgfEhISREVFZ86c2Qx9goODg4KC4LeCgoK3t/eaNWsQQm/fvm1SPEwqp7y8PKTazc2NZySNrt2gL6+bMmXK+/fvpaWla2pqwsPDm6QtH0pKSnbt2vXu3buwsLCXL1/evn17wYIFDBfjQCacPXsWu1jFBAYGhoWFtZaS3DBsOPggwoaWQ+IigLQEBwdzf6PKyMg4deoUn0lya1Uq1MQ+v9mvAyYF12hP0sJUN0/5x48fx8fHCwsL45JtdlSti5eXV0P22W7duq3ghbS0NEJo8uTJ8JfJocPNW/3X8nbKPIcvXLhQU1OjpaVFt/T9DgXETfMGZoqKirCtfufOnTU1NRxXV69eDTt8oYPKz8/HbQTD8WY/cuQI2AcRQvr6+q9fvwaTMXc7arT0mT+02fxWYzkCgdCKEGMfgdD+HDhwQFJSsqamhsMok5SU9O7dO2tra3wyND5NnO7l6urVqzCFo8/5YcMC/sLM8SLn+AoXHx8fHBxsa2s7ZMgQhJCAgAAM7l+9euXo6AinUyGEqqurt27dGh0dDVvP0D8TqoZGKjBgop/AjRDS0NBwcnJCCF27do3jkre3t6Cg4Llz57CET/wN7cEEOUd6QcgxA4enc9gmeD4L60mPITAwcPHixX/99VczTJ+wBg08ZgK4QOl5gpXhSGxDcg6alNWogfJqCJ4VjKIo0I0jVyFOXCiGhoZwpvi+ffsuXryIczU3N9fa2nrYsGEwWeIP/zqMaIXFUabMqzcwbNgwHR2d+vr6mJgYno47cY7RU90WjdrPz4+elkmTJnXt2lVOTg72L6PGKoaoqCjYmEJDQ5u6rK+qqoqj+Vy4cEFQUBBbDXBC6DOZnJwcmJwwMUdGRER8+PBh48aNmpqaWNhQing2Xo4VLosWLUIIqampwd/WrZzgfPDTp0/gEBM/AmygeI7XEJ8/f4ZJGgBOFenaNpRGPh0UvYAoipo4cWLPnj3pmdkQECc9hyF1+fn5cOAUlt++fXvlypXQqzREC3tmhg2nf//+4Grm4sWLEREROFhmZiYsdcFFsGDBAnDiMWPGjNDQUBwyIiLCzMwMPHc3lA+osUpVVVW1atWqUaNG+fv7N5wlCDHr86WlpfFeQpDwn4E3u+Aa7Un4p5onDSkPFZhno6MnEPKnvr4er0Ovrq4G2zfPdyLP2Jq0QRIC87+luLj40aNHjX5AakbMAN5Y2rxMa0k7bWiI2NCgCwooJiYGG8JSU1PT0tIQ15ihJR1XM25HrTcw27ZtG0Lo+/fvI0eOhKQhhEpKSubOndu3b18FBQWE0Ny5c+HsPw8PD/q9bDb7+vXrSkpK4MsLdKYv4uvYsSMsksXtqKHS54b5Q9E/AznUsrEczv9fMJYjEAhtC0UgEH4Dbt682bFjRwkJiQ8fPoCksrJy7Nixenp6aWlpOPD+07MAAQAASURBVFhdXR3sNZCTkzt+/PipU6dGjRq1Z88eONFj8ODBFy5cgJB3796FNn7mzJlXr17NnDmzsrKSoih49aqqqn779g1CFhYWGhgY6Onppaen4wcVFxfjM8VkZWWtrKwsLS3l5eUHDBhQUlICYcrLy+HQrhUrVnCnKCcnB6wVq1at4rhUVlY2fPhwhJCdnR2bzQZhQECAuLj4sWPH6CFhGNG/f3/u+PEezODgYLoc1iuNGTOGLjx+/DgEzs7OxsINGzaAMD8/Hwux/ev79+9YiLd1PHnyBAthAtCxY0cdHZ1hw4ZNmDDBzs5u8eLFx48fz8jI4FaYDngyFRAQWL16tZub24wZMxwdHeH7/4IFC44dO5aUlERRFJ7EXrlyhX67i4sLyH/8+MH/Qcyzmk958QQ8S3bp0uXJkyd3795dvXo1RVF4fLx//356YLCRKSsrY8m7d+/wghp1dfUpU6aYmZmJiYktXry40UczrMPPnj2D+O/cucMRA5PqTcfV1RUhtHnzZp764Hm+j48PXd66jbq4uBghdODAAXxjVlaWiIjInj17sKShCoOpqalRVFTs2LEjz2TyBFe2rVu31tXVgfDy5csiIiIuLi70kN7e3hBy6tSpHh4e69evNzAwgFYmKCi4devWyMhIiqJKS0vhOPmdO3fie9PS0vT09Ozs7MCqyP10elUvLi6G1SX0uurn5yckJBQSEoIlPj4+goKCL1++xI9oxcpZWVkJixoEBAScnJyePXv2/PnzyZMnw0ROSEho1qxZ9O6CA319fSsrK5yfLBZLT09v9OjRuJHyTCOLxYK1JzNnzqTHNnDgQISQkZERluDNrcrKyoMHDx49evTkyZNnz569cePGhw8f0u+tra2FU5/mzZtHl9vY2EAMoqKiZmZmNjY2ffr06dKlS3R0dEOJAlreMzNsOFFRUWDFk5aWPnLkyLt373x8fEaOHAlCGRmZJUuWJCYmUhR18uRJ9A+DBg2aOnXq4MGDBQUFvby86MrA6hhVVVUsabRSYYukmJgYXTduGPb5sF/P0NDw3bt3R48evXTpEp84uRWmGBRcoz1Jo6luCJ7KL1y4ECEkKSlZXV2NQ+IVrO7u7iB59+4dSDQ1NS9durRnzx5TU9OJEycihBQUFNzd3c+fP0815QXNn4qKCjguwMnJCTc6bi5cuGBqasowTqCoqEhKSgoh5Orq2mjg7OxsaOYLFix49+7dsmXLIiIiKMaZRrWgnfJ8g/MZ1B04cAAeZGFh4eHh4eTkZGFhAXvqjYyMLl++7O/vT7W446IoCjwy6+jo0IW/ZmBWV1c3dOhQ3FdoaGiYmJh06tRpypQpLBYLB4uOjobTPM6ePYuF27dvl5WVffHiBZY4OTn16NEjNzcXS1atWtWtWzcsaaj0ecLwoRRF3b59G/TnGJA0BM+a4OXlBZGEhobikLW1tV26dEEIzZ49GwtbMpYjEAi/AGLsIxB+F2JiYoYNGyYpKWllZbVs2bL+/fuvWrWKY+pLUdSnT5/wh8GuXbtev36doqiuXbvq6+vv3bsXj4Rqa2vBUoYQ0tLSiouLAzkYSlasWKGtrT1q1KhJkybJysra29uXl5dzPKiwsHDKlCl4y4aYmJizszO2ERw/fhwfrSUiIjJ37lzQhKKonJycHTt2gDcAGIPOnz//6NGj9MhZLNbevXsVFBR69+69YsUKKysrPT09+uzi4cOHdOeJY8aMwVP02travXv3amho4NTt27evtrb28ePH06ZNA6GAgMC8efNevHiRl5e3bt06GKAghIYMGXLp0qVXr14tW7YMO7AzNDSEyLdv3449VxgaGsLs4sSJE+DmFSGkpqZ26tQpUOPLly8NnVbWqVMnelq4+f79Oz72S0VF5datWxRFwcRGQEBg9+7d+fn5W7ZswQYpBQWFjRs3fv/+/cuXLytXroRdqAghPT293bt3V1RU8HlWo1nNpLy4SUpKwluAJ0+eXFpaeuXKFX19fZwDTk5OOTk5nz9/tre3x5sHLSwswsPDIYbo6GgDAwOcaYqKihcuXKAPqRui0TpcVla2du1a+A6PEJKVlV27di19zEo1Vr05KCsrk5KS+vnzJ4c8OTl58+bN3bt3h0iUlJQ2b96clZWFA7Rioy4uLu7WrVvv3r2nTJly9uzZEydO6Orqbt++HXKsoQrDnZZp06Y5ODg0mskYMLcpKipu3ry5Z8+etra2+vr6ysrKjx8/5g7s5OSEy3rKlCnZ2dlnzpyRlJScNWuWr68vTvjr16/NzMyEhYUVFBQmTpxobm6uoaHh4eFBj4pnVS8uLt6zZw/MBqHUli1bFhYWRlGUn5+fhoZGz549V65c6ebmtmPHDk1Nzdu3b0NsbVE5S0tLHRwcsI9ORUXFW7duOTg4KCoqrlmzBhuqeGJqaqqjo2NkZHTkyJELFy6MHTvW1tYWvjrU1tbyTGNISIi1tTUIO3TosHLlyri4uK9fv9LdIM6ePTsmJgYesX79+oaO1poyZQoYOI4ePYpfE6KiokuWLHn69CnczmKxtm/fjpd6IYSmTp0KtrOGaHnPjKNi2HASEhKMjY1xCerp6UVGRvbo0UNbW3v//v0pKSk4pI+PD31D2YABA54/f46v+vr6zpgxA8czceJEWKXCv1JRFBUXFweWHYTQ3bt3+WROo30+BLt//z7UKBERkR07djQUW0MKMyk4/j0Jk1Q3BIfyaWlpjo6OuIEMGzbs5s2bFEUdPHgQZ0XXrl1x2rdu3QopAlt5dnZ2WFgY1GENDY2kpKQmvaB5kp6erqWl1a9fP0VFRRkZGVlZWRkZmZ49e2pra0+YMIE7vKmpKc7YRoF0ycjISElJdenSpVOnTj179tTT0+N/18aNG0F5GRmZ+/fvNzXTmtFOAe43OJ9BHUVRVVVVsJkUIdSpUyfY6wrunhBC1tbWDXXOzDsuDw+PqVOnYqGNjQ00k182MKMoqqamZuXKldh6JSkpeeTIEfxVBlNUVOTg4NC5c2djY+PVq1cPHTp0/Pjx9G+NFEWtXbtWT09PQ0Njx44d7u7uS5YsMTQ05DDncZQ+f90afejHjx/XrVuH3RzJycn99ddf/PslildN2Lx5Mx4+qaqq7t27l6IoHx8fvOlBWFh48eLF+DN5s8dyBALhFyBANd0rEIFAaDtqamqioqIoitLS0qIP4OjU1dXFxsay2ez+/fvDUDgtLU1ZWZkjWH19fWRkpICAgL6+Pt7R0KVLl+LiYjc3t7lz58bHxxcUFPTt2xcPpLiprq6Ojo7u2LGjhoYGHoC2Iunp6UlJST169MAD2X8LKSkpf/3118GDBwsKCnJzc8vKysrLyysqKvLz8wMDAyUkJOi7Mrmpra2FHX8GBgawvqO2tvbjx499+vQBn6StTqtndXl5eWRkJJgRmx1JUVFRTExMt27devXqxXDfTVPrMB+YV++4uLhmu51tlUZNUVR1dTUsyoNDr3V0dCQkJJqkCUVRqqqqHh4eo0aNYnjLyZMnYYVCWlpaQUFBbGxs586d+/fv31Bh5ebmJiQkaGhoQInk5uZKSUnBahEOysrKEhMTi4uLFRQU+vXr10LfjtXV1aKiooKCgpmZmYmJiV26dOnXr18LD5JnUjlramqio6O7dOkCzerHjx/KysqNntJVUVEhISFRU1MTGxtbWlrar1+/5lVgPhw5ckRYWHjcuHHp6elFRUXQO5WXl8fFxd25c+fhw4ewDZY/bDb7+/fvoCGsmPuVMGk4CKGSkpJv375paGjALDcpKamhLi4tLe3nz599+vTBU1n+MKlUdXV1GRkZkydPvnDhAj5DgCcM+/ycnJz4+HhNTc2WvAj4FFyjPUlLmlILlc/IyEhKStLQ0ICFYAih5OTkoqIiPT29X+8UIiUlRUtLKysrq623IsbHx+fl5RkYGDTbx0Lz2mkz3uCxsbEFBQX6+vq4PX7+/LlTp07YxN/utHBgBrBYrOjoaGFh4b59+/KpeJDtBQUFPLsU6OTBiVBmZmbPnj179erF/Zpraunzf2jzaK+xHIFA+AUQYx+B8N8CG0pgnwiheSQkJAwfPvz69es8jSYsFsvIyOjjx4+/XrH/AqQON5uAgABnZ2du78x8oBv72k4xQuuybt268PDw4OBgnlfPnj0rJiZGmk9rkZGRYWdn9+bNmxYarAm/FXv27ImOjsabIgm/P2RgRiAQCBwQ0zuBQCA0mSVLluTk5DS0jqOgoOD3+dBNIABsNnvv3r2rV69ub0UIbUtwcPDx48eNjY0bChAfH8/nKqFJ5OTkzJ49+/z588TS94fh7e09e/bs9taC0ATIwIxAIBA4IMY+AuG/BU+XiISmEh8fT1HUtm3buHMyPDzcwcGB7qWU0LqQOsyc4uJie3v7efPmubq6zp49u7CwcO7cuU2KgeT2v474+HiEkLe3d2RkJMel0tLSPXv2SEtLa2lptYdqfyBHjhw5f/48eCog/DGEhYUVFhbSDw4m/P6QgRmBQCBwQLbxEgj/IYqKimRlZSmKcnZ2PnToUHur8y/m3LlzK1eupChKRkbGzMysW7duFEVlZ2dHRkYaGBgcOHAA+1sgtC6kDjeJmzdvzpw5E36LiIg8f/582LBhTYrBycnp5MmTIiIieXl52BcB4XemoKBg2LBhsbGxAgICgwYN6t+/v7S0dGFhYVpa2o8fPzZu3Lho0SKyDI1A4MPKlSsFBQVPnTrV3ooQmgAZmBEIBAIHxNhHIPxXOHz4sJeXFxzX1aFDh+nTpxsZGS1fvry99fq38ubNm3Pnzr19+zYzM1NYWFhFRWX48OErV67ETj8JrQ6pw02lpKRk0aJFT548UVVVPXnyJHO/HAih6OjoI0eO3Lp1q7a2FiGko6Mzbty4RYsWaWpqtpm+hNahtLTU1dX11q1biYmJZWVlXbp00dfXnzRp0pIlS8TExNpbOwLht6aurk5JSSkgIAD7liX8WyADMwKBQKBDjH0EAoHQIthsNnE9RiAQfk9IB0UgNInU1NTFixcHBga2tyKE5kP6PQKBQEDE2EcgEAgEAoFAIBAIBAKBQCD8MZCPHgQCgUAgEAgEAoFAIBAIBMIfAjH2EQgEAoFAIBAIBAKBQCAQCH8IxNhHIBAIBAKBQCAQCAQCgUAg/CEIt7cCBALhNyU1NXXevHlKSkpXrlwRFiZ9RTsTHh7u6uoaGRlZVFSkq6u7devWgQMHNjWS8vLyoKCge/fujRs3zsHBoS30/C9TWlr6/PlzX19fExMTR0fHJt2bnp7+7Nkzf39/W1vb6dOnN1uHtLS0z58/Mw+vqqqqq6vLYrFCQ0N9fX0FBASOHTvW7Kfz58uXL6mpqczDDxo0SElJqY2UaSE+Pj537tz58uWLsLDwsGHDdu/eLS8v395K/VLq6+tdXV1XrFghICCQnJz86NGjO3fuBAQESEpK8r/x2bNnmzZtsrOz27BhQ1MfWlVVdfHixVWrVv3rjt5PT09//PjxvXv39u/fb2BgwPzG2traDx8+PH78+MuXLwEBAW2nYaO0pOBaSPO61qysrBMnTnz48OHnz5+9evVatmxZS7pW/oSHh2dkZDAM3NY9W0lJSXBwcEBAgLS09MGDB7G8eYO6X/N2aAm/VeY3j98/kxlSV1cXGhp6//79qqqq8+fPt7c6BAIBIYpAIBB4gQccISEh7a3Lf51r166pqKhkZ2dXVFQMGzYMIdSlSxcWi9WkSC5fvqyhoQFleubMmTZS9T/L0aNHcfYePHiQ+Y0ZGRlLlizp0qUL3Ovm5tYSNdzc3CAeQUFBWVnZ3r179+3bV1RUFIQaGhoaGhqKiopYsnjx4pCQkJEjR8LfadOmteTp/MH2ZREREUVFRQ0Njd69e4NETEysb9++6urqsrKy2I5z7dq1tlOmJSxbtmzcuHF1dXWJiYlg45s0aVJ7K/VLSU5OHjdu3KtXr/Ly8hYsWCAhIQFFVlxc3Oi9EydORAh17Nixtra2GY/28/MbP358bm5uM+5tFyorK5cuXSorKwtZ9P79e+b3Xr58GTp8hFCPHj3aTkkmtLDgmk3zutaYmJiuXbsGBwezWKz169fD7Z8/f24jJb99+3b+/HllZWV4kLCwsJGR0eh/GDVqlKGhoaKiIly9efNmG6lBUdTGjRv79OkDD3JwcKBfasag7pe9HVoCd+YbGxu3S+Y3j39FJjPh4cOHw4cPh4SMHz++vdUhEAgURVHE2EcgEHiTnJxsYmJia2tbV1fX3rr8pykpKenUqdOyZcvgb15e3qBBg+bNm9eMqCoqKuB7PjH2tQVVVVViYmJNNfYBeGFCC419p06dQgitXr2abg3p378/RF5eXo5VdXd3FxYWtre3B4mxsXFbzzSmTJkiLCx86dKlqqoqkOTn54NihoaGOFhubu7q1asRQpcuXWo7ZZrN8+fPEUK3bt2Cv2/fvu3du/f58+fbV6tfSVxcnKamJt1ugk3MTIx9AQEBurq6+/bta7YCwcHBgwcP/hfZ+yiKevXqVTOMfcDixYt/B2Nfywuu2TSjax02bFi/fv3gN4vFmjNnjqGhIZP62RKuXr0KpTx27Fjuq2w229fXV1hY2MvLq03VYLPZYB7lMPY1e1D3C94OLQdn/rhx47iv/rLMbzb/ikxmAhguibGPQPhNIFvzCAQCb1RVVUNCQtpbi1YgJCSkqKgIViU0FYqiNm/eTN8I8+vx8vIqLy+Xk5ODv127dv3w4QOTGwsKCtzd3Z2dnbFEXFxcQkKipKSkTRRtG1pSfL8YMTExKSmp6urqZtyrpKQkKCjIZrNbqENVVdXQoUNdXFwEBAT4BBMTE1uwYEFcXFxSUhJIfsEu1Kqqqg0bNixatIh/MDk5ORcXl7CwsKqqqrZWqRnA1iTcHocOHZqQkNCuGv1SUlNTzczM9u7dS9+LqqCgwDwGCwsLCwuLlugwfPjwBQsWTJw48c2bNyIiIi2J6pfRpCziAC9ZYgh3z98qtLzgmk1Tu9avX7++efMGLzISFBT09PRsM+3+P6qqqvCjc+fO3FcFBAQmT548e/bsurq6NlVDQECgR48e8fHx3Oo1b1DXim+HgwcPOjo64pXsrQjOfJ4nCfyyzG82f8xBEC3p6wgEQqvzLzv0hEAgEJpEZmbm9OnTKysrm3f7rl277t+/37oqNZWgoCCEUFPPqGKxWHZ2dikpKRxy/jag340WFt+vpyXZ2yrHkFVWVjo4ODBUY9asWThvf0HFqKysnD17NpOQAgICM2bM+D3LvXnt8c+AzWY7ODioqanNmzePLv/1vcqSJUsqKip+/eFxzeaX9QwN9fz/dpqUgc+ePUPt0UiZmJ7nz5/fq1evttakddPeWg382bNn27Ztq62tbZXYOPh9Mr95/LvGZnz4YxJCIPwZkJV9BALhj6WqqmrKlClZWVnNu93Hx2f37t2ampqtq1VTaZ7+a9euff78OT67599IC4vvv4m1tbWKigrDwPr6+n/99Veb6kNny5Yt/fr1Yxh45syZ2dnZbapPM6iqqvp3LYxtXU6dOvXmzZtHjx61+3ROQEBg5cqVS5cutbGxGTFiRPsq81vxB/T8Led3fmvgQxj/ayQmJtrZ2bFYrHbU4T+b+QQC4T8LMfYRCL8F+fn5t27dUlRUnDp16ocPH7y9vTMyMnR1da2trTk895WVld25c0dCQsLOzi4oKOjy5cu9e/fesmVLx44dcVQ3btyIioqqqakZMGDAtGnTevTogRDKycnx9vamRzV58mR1dXWEUG5urpeXFwhVVVWnTJkCv5OSkm7fvm1ubj548GAOhbOysp4+fRoREZGZmdm/f/8BAwZYWFjgj8kfPnx48+YN/NbW1h4/fjxC6PPnzy9fvsQx/PXXX/QZY35+/sWLF5OTkyUkJDQ1NdXV1TU0NPC+DD74+/s/ffq0qqqqe/fuBgYGIiIisNUoOzvbzs4uLCwMIfTw4cP09HSEkIODA95iABkSHx9fUlKiqalpbm6Od/0ghK5cueLo6EhRVEFBARxrraamZmNjgwN8+/btwYMH0dHR0tLSAwcOnDVrFi4ChvDPw8LCQg8PD4RQWloaQig0NBTUEBIScnJy4hNtfX39mjVrzp07hxCKiIiAu0aMGGFoaMgRMiws7Nq1a+np6WpqagsWLNDS0uKOLSgoKDg4+Pv372pqaiYmJtbW1k1KI/9MBj5//nzlyhUXF5eysrIjR46kpKQsXry4b9++/IuPG4qiPn78eOvWrTVr1qioqMTFxV26dCkpKUlVVdXJyalnz54IoZKSkuvXr799+1ZAQGDMmDFz5szhjqeqqsrf3z8yMjIuLk5NTU1fX3/ChAlSUlLcIcvLy728vEJDQ+vr6wcOHLh8+fKGdKurq7t58+anT58yMzN1dXXHjx/PXRytwoABA5oUfsyYMdzC1NRUNze3b9++ycvLW1tb89y711A/w4exY8cyV0xBQQHKmn+PV11dffPmzfDw8Ly8PGVlZUNDQzs7Ow5TVGZm5t27d6WlpefMmZORkXHu3LnY2NjevXs7OjpCB0inof4kMjLy2bNneLHhrVu3wsPDEUK9evWaOnUqvp1h5eFZ5+Goo+Tk5Dt37ujo6EyYMKGwsNDV1TU8PFxaWnrhwoUmJiYIIRaLdf/+/YCAgJKSkoEDB65Zs4a752m0vrFYrMDAwKioqE2bNiUkJEAvsXHjRj5dbk1NzaFDh8TExEaNGsWn4EpKSjw9Pd+/f9+5c2dTU1NbW9sOHTpwhHn//v2tW7f27dsnLi7OcenJkyfPnz//8eOHpqamnp7e6NGjG9rxN27cOITQtm3b8LuGD0VFRX5+fiUlJU5OTvX19V5eXoGBgQihCRMm4E4AtEpNTe3Tp8/SpUt5rgCKjo5+/fp1RERETU2NgYGBiYkJ98sRSEtLu3LlSlRUlJSUlJmZGZ+G2fJXCcC/5+df4hRFPX36NCgoKCMjQ1paWkdHx8HBgXsvKs+Ca1L7QszeKcy7Vg7Onz9fWVkZGhqKEEpLS8MuKWbNmtWtWzcc7Pnz5+Hh4ZGRkV26dNHX1x89ejR3zW90rNVUKIpauXLl2bNnOVL66NGj2NjYXbt2gbPpN2/eyMnJTZ8+nWdDy8vL8/Ly+vLli5iYmK6uromJib6+PvN1fA0N6qKjo69fv56XlycjI6OjoyMhIWFtbc3TYy+TtwM3Hz58mDp1amFhIULo3LlznTt3FhERgbNZAeav3eYBLXfFihVYkpaW5uXlZWlpqaOjc+HChZcvX44dO5Z+ykSjDTMqKurOnTuTJk0yNDQMDw93d3fPz8/X19dfs2ZNp06duHV4/fr1y5cvv379KiMjY2hoaG9vz90BAk3N5GYMuZmUOJOuiaKo+/fv+/v7FxYWampqLl26lL+qBALhV9OeBwYSCASKunv37vjx42EDwp49e5ydnQUEBPDsSFhY+OzZsxDywYMH06ZNg8HBtm3b3N3d8ZsbnxZ/6dIlIyMjV1dXPz8/Z2dnERGRLl263L9/H65GRUVJS0vDLRzu2D5//tyhQ4exY8fm5uaWl5cfPXp00KBBENLPz49D5ytXrsjKyh44cCA+Pj4sLGzVqlUIIWNj47i4OHqEoCp2JQHOK2GGhhCqr6/HgSMiImRlZW/cuMFms6uqqrZt24YQwmrzYf78+SNHjszOzqYo6suXL3369DEwMIBLwcHBR44cgWfNmTPHxcXFxcUlJycHZ5SoqOiJEycKCgoqKipWrFghICBw4MABuFpdXe3i4mJubo4QkpeXh3uxPjU1NU5OTlZWVt7e3rdv3542bRpCSFtbOyYmplGFmedhYWEhPBe8yJmbm8Pf06dP8485KSnJxcUFbFtwfJuLi8unT5/gKlSA06dPb926VVRUVF9fH2Z04uLiHz58oMeTlpZmZWW1fPlyHx+fK1eu6OvrI4RsbGxKSkoYppF/Jufk5OzYsQMv9crLy9PT04Pf/fr141983GzcuLF79+4Q/vPnz5s2beratauxsTFYCpSUlPLy8h4/fty9e3cdHR0DAwNoO3///TdHPO/evdPU1JwzZ87Hjx/j4+OPHz8uLi7eo0cPf39/jpBxcXF9+/adPn16aGjoz58/T548qaOjA3We4xT5kJAQIyOjffv23b9//8SJE3JyckJCQjt37uSIEMbZLXTQwRNuBx3cTJ48GSE0bdo0X19fSUnJvn37KikpwV24C8Lw72eYw9NBB6bRHu/jx4/y8vJz585NTk4GCw5CaPLkydXV1RDDkydPRowYAXetX7/ex8dHVla2X79+EKeUlBS9y6L49idRUVEuLi579+4FBRwdHaFO+vr64tsbrTz86/zly5exSe706dNeXl7dunUbOHAgWJ2EhYWDg4MTEhIMDQ179uxpbGwM864RI0Zw5Bv/+hYREbFs2TIwpJqamn7+/Bm/FLAXIJ7AqWc8j11/9OgRxPD69WsOm++gQYPS0tIgWEpKirOzM3RNiMubR1VV1eTJk/X19T9//hwbGzt//nwI1rNnT319fX19fforA4Co3r59y0ftN2/eWFlZQeOaNm1aeHh4//79NTU18Ve0/fv3V1RU2Nvby8jImJqawmmM3bp1y8zM5FBvw4YNioqK169fT01NDQgIAOP1/Pnzi4qKOB764MEDaWnpv//+OyYmJiYmZsWKFbgN0h10MHyVQK1r1EFHQz1/oyWen58/ePDgAQMGhIeHs1isjx8/ysnJ9evX78ePH/wLrqnti+E7hXnXys3FixddXFxMTU2h5rj8Q1ZWFgRIT0+3sLAwNDR89uxZamqqt7d37969O3bseOTIEezgnslYiyfv37+HYDzdLISHh5uamuK/8fHxdnZ2MNgzNDSMiYnR1NRUU1Pr2rUrQkhQUJDbHfmjR4+6du167Nix9PR0Pz8/cPEsISGho6Ojr69P97s1evRoRHPQwX9Q5+7u3qNHjy9fvlAUlZOTY2lpiRCiF0qT3g48uXbtmqOjI9yye/duFxcX+o3MX7t84J/5+/bt27ZtG0VR9fX1p0+fHjFiBFhIg4KC5s6dCzcKCgpCPWm0YZ45cwa7kn/w4MG2bdsgW4SEhBBCOjo6HB6r09LSbGxszMzMHj16lJiYCC1aUVExMjKytTK5SUPuRkucYddUWlo6adIkXV3dgICAjIwMX19fHR0d6CiIgw4C4TeBGPsIhHYmLy/v7du3MERQUlLasGFDdnZ2XV1dcHAwHluDYS4lJeXp06cQcsyYMWvXrk1OTp4/f76ysnJUVBRFUZcuXVJQUKBPPDZv3owQEhMT+/nzJ0hcXFy4pxwURVVUVIiKisKLvLKyMikpCX8S5xgXHj16FCHk6upKF+7evRsh1LlzZzyvoyjKyMiIPvIArly5wj3yGDJkyLBhw+jBxo4de/v2bf5Z9/jxY4TQ8+fPseTt27daWlr4Lz7P++bNm/QbY2NjBQQEOnbsiCWVlZVCQkJCQkL0Cd6mTZsQQn379uV4rr29PYevPXCjpqOjwz0d5QnzPKT+sdRs2rSJScwYyPylS5dyyGGa169fP2dn57KyMoqi8vLyYNhqbW2Ng5WWlvbr12///v1YkpeXB2bB5cuXM1Gg0UwuKSmJj4+HAS5CaOHChW/fvn3w4IGqqio8oqHia4iAgAAIb2Bg4OnpyWazKYrKysqSkZFBCI0aNcrMzCw/Px8Cw/IKSUlJuv0rLCxMSEhoxowZ9GiDgoJERUXR/1qf4+PjO3fubGNjA08Brl27BgrQZ6RfvnyRlJSk29ZhSRFCiKOG/w7GPjU1NWtr69TUVIqi6uvrYc2anJxcTU0NDsmkn2EIf2Mf/x6PzWbDXCgsLAzfAi2RPiEvLi6GYL179543b15hYSFFUVlZWbDZefbs2Thko/0JXeEnT55waMuk8jRa53Nzc2FJiKqq6tatWysqKiiKqq2thfWwurq6BgYGMEOjKCoqKgrmq69fv8ZPbLS+ZWVlRUREwDZPbW1te3v7xMTEw4cPKygoeHp68imsmTNnIoQ2b97MfQkb+4YMGeLt7Z2XlxcZGbls2TIQDhgwAJpJfn5+UlKSnZ0dyDmMfbDMB6eFxWJBaaqrqz979uzZs2f0tgbMmjULIbRlyxY+alMUVVNTA9v3FBUV7ezsoHpT/9Tbzp07Dxs27Nq1axB/cXExrEfbvXs3PZKJEycKCQl9+/aNHi2soBk0aBC9579x44aAgICLiwv9dvCoy/HmZfgqYWjsA7h7/kZLfMaMGQihQ4cO4VsgZ7Dxl0/BMW9fDN8pzLtWPqxcuRIhZGRkxCEvKSnp3r27qqpqZWUlFqampsJY66+//gJJo2OthsD2pjFjxnylERoa6unp2bt3b7qxj6Kouro6WNgoLy8/duzY2NhYiqJqamrA+KKiokLPhJSUlM6dO5uZmWEJdFkIoV27dj179iwxMRFf4jD28RnUFRUViYmJbd++HUuKi4vl5OTo3q6Zvx348OTJE3g6fE3BMH/t8odn5kdHR7979+7UqVOdOnUCYx+LxUpISMCfbSZNmuTu7h4RETFs2LABAwaAkY5Jw8R+0rS1tY8fPw43vnnzBuzC7u7u+N6cnBx1dfVJkyZhJ8hsNhs6mcGDB7diJjMccjMpcSY5UFtbq6enp6qqCq0eSEhIgK9QxNhHIPwmEGMfgfBbAKu3Fi5cSBcmJyfDcGfkyJFYCKPSwYMHc8x8CgsLpaSkOGZi3759g9f8mjVrQFJZWQkrF1auXEkPefXqVY53Mz71hj4uTElJERcXV1RU5PhuyWKxYPmJpaUlFsKuNI6Rx82bNzlGHrW1tcLCwt27d6dPb/z8/K5evdpAbv0fO3bsQFwms6lTp+LfDVmLfH19EUKSkpJ47EVRFCx8CAgIwBKexr4XL14ghAIDA+lCvDGHvsynIZqUh1TbGPvwrAYAd8MKCgpYsmPHDhEREQ7bkK2tLUJIWFg4Ly+vUQUYZjJsVUYIPXr0iCOGphr78BFvN27coMsdHBwQQoqKivQJXllZGQR+8eIFSOrq6mCZ1devXzliBrcSSkpKeJY7adIkhBA2u+AYYJhLn5EOHz7c2NiYI0Jog3p6enTh72DsGzJkCL288PQJZqEU436GIfyNfUBDPV5+fj7MxunGPrBHODs700PCMh8rKyu6EFq3hoYGljTan1ANG/uaVHn41HmKovr27YsQgjMEMG5ubnALR5UbOHAgQmjHjh1YwrC+wWIWCQkJPqtlOYBdkEePHuW+hI19T58+pcuxvc/b2xsLb9y4AUK6zai8vBxKky6EtYQCAgL0WSidtWvXov+dMDcEbC7r27cvvRYlJiaCJidOnKAHXrduHUJo+PDhWHL79m3Ea8VQYmIizO2PHDmCE6KkpKSgoICXlwLYCyo29jF/lbTQ2AfwKXGounRjH7ic5shYngVHNaV9MXmnMO9a+dCQsQ+2A9NXwAHu7u4IIUFBQboptqGehw+4wxQSEupEA++O5DD2URQF+xjk5eXprydsF6N/fdy4cSNCyMnJCUvYbLaamhriGs5RXMY+gOegDuohh63NyckJ28Qpxm8H/vA09jWp5+QPz8yXkJDAqzLB2AdgDzbr16/niIdhw6yvrwcJR5cIDXDJkiVY4uDgICwsnJCQQA925swZxMvY15JMZjjkbrTEGebAiRMnEEIcXzWof+oeMfYRCL8J5Mw+AuG3AMaCHKfGqKqqTp8+3dvb+/Xr1zU1NbDdAzb8mpubc5xLdffu3ZKSksjISJj/AGw2W0BAgKLNNDp27LhixYqdO3d6eXkdPHhQQkIC5G5ubjDDwfB0bXbu3LnKykpTU1OOq4KCgosXL966dau/v39SUhLPk3oaQkREREpKKiMjY9q0aR4eHmCNsrCwaPR4fpjBbtiwoXv37lZWViA8cOBAo0+0tLQ8d+6cjo4O/YASiqIQQnCmDB9g1u3j44M/qqN/jtVDCIWEhOBlOw3RFnnYVGCGgIGKl5ubW1tbC/Zld3d3SUlJmIdgUlNTEUL19fXv37/HGd4QDDMZZwLPw+OaBH6QpKQkXQ7bezt16kQ/bqZTp07i4uKVlZW4mj19+jQyMrJDhw7cZxcuXbrUy8sLjqZauHBhWFjY/fv3lZWVYRsaXQEpKamqqiosSUhIeP36ta6uLr1VIoQgk6OiosrKyji0bV+UlZXp5YV7pLS0NDBCMexnWpGGejxZWdl79+6VlpbSD5/i2YohRbA5DgO7TeEsSKDZ/QlqSuVBjdV50JZnHcZKYsB6jusw8/oGOujq6srLyzNJYG1tLVgKYJ1sQ+B9gsDWrVsvXbpUX19/7949e3t7EPJ8s0RERMDJ/aWlpfigLjizjKKozMxMjoQDsEn/58+fjeoPudqpUyd6LWKYqwghOFKA+9w9dXX10aNHBwUFnTx5cv369QihM2fOZGZmzp49m+OkQo7qh1rjVdIk+JS4p6dncHAw3jeNGmhHDXk7Zdi+mLxTmHetzaC8vNzV1RXxKscZM2asW7euuLj4zJkzQ4YMAWFDPQ8TbGxs7ty5Q5ekp6cvW7asuLiYIyTknri4OP31hPfCp6en46MGP336hBAqLS3FwQQEBEaOHJmcnEzPZz7wLEGo/Ddv3tTS0tqyZQvY3Dds2ID3emMafTs0gyb1nAzhyHwWi/X582cwK2P4dMIMGyZkFGqs5mdlZV27dk1HRwdv+wVWrFhhamrK7f+tLTKZg0ZLnEkOVFdXw2aUiRMncsTP3dcRCIR2hBj7CITfmvHjx3t7e7PZ7KSkJPpgCI8zMK9evUIIGRgY6Ojo0OVDhw5FCImJiWGJo6Pj/v37S0pKbt68CUOo+Pj4lJQU2DnCn6ioKIQQz/khHB6PEIqMjGyqocrZ2Xnjxo1+fn4hISH79u1btGiRqKhoo05FZ8+evX///qysrIkTJ06aNOnYsWPq6upMvBCKiIjgJSdFRUWenp4hISGw1At/rW0IyOfhw4fTB2RGRkYwlASd6+vrMzIyOG4UFRWFUXur5CH/RzQVmJdSFFVXVycqKpqQkJCRkaGpqQnfqDH4Lxw6xl+HpmYyd5VuLRqao4qKilZWVuIJLZSLnJwc99RuyJAhwsLC9fX1kZGRCKG3b9+if8qaP1Bb1NXVG8rJ3xxssKitrYUfzPuZ1oVn9cBH+9fV1fn6+vr6+sI6iEZbMfpnjs1ms7Gk2f0JakrlaTRRPOFThxHNKNPU+sZcgYKCAvjB39jHkfzu3bsPHTr09evXMTEx/OPHCfz+/buysjK+XUhIiM1mN+Q2BIx9sDe/GRYZhrnKZrNh7WpDnXZQUFB6enphYaGMjExTOwf+r5JWh2eJ6+np4bMjQ0JCPD09v3z5gpi1o4bgaF8M3ynMc68ZfP36FfThLkdxcXF9ff1Xr161pJHyp0ePHi4uLvPmzWMSGFcJegcF1fX79+/0kLD8sCWfBvv3729hYfH48eMdO3bcuXPHxcXFzMwMnxbHB+63Q35+fkVFBUcweXl5Pi5NmPeczR7zCAkJDRo0aNu2bTy/CjQ0lm52w+So+bDYmcPSB+BGxwfuTG45jZY4kxyIiYkpKioSEBBo1DEXgUBoX4ixj0D4rcEDC+wFsiEyMzMRQtra2vhgnYZQVFScNm3a9evXXV1dwdjn7u6+aNEiJoNamLPxXHOHVU1ISGg0Hg6cnZ1LS0v379+fn5/v6Oh47tw5d3d32KHGBykpqZcvX1paWiYlJd2/f//x48cbNmzYsWMHTNUaJSUl5fDhw6mpqc7Ozk5OTt26dWt04QCbzYa0m5mZ8Rli/vz5k3tsZ2ho+PHjR9RKecj/ES0E6hL1z6bIlujQjExuL6BcYMMgx8RDSEhISUkpNTUVygVC4lWxfICclJOTa7RV/ltg3s/8GsrKyk6dOhUQELB8+XJPT8+5c+fCWqFm0JL+hHnlaVParr7BUi/6D4aAMaLRZdoGBgYqKiqpqakeHh6wCwwhlJeXx2Kxhg8fzu0WFoCiYbFY9fX1DVnuWs6PHz/g/dtopz1kyBCGnQPDV8kvg81mX79+/cKFCxMmTDh48ODdu3dhHVlrwfCdwrxrbQbY4pydnc1tHYNybNNGqq6uzrG+rElMmjTp8ePHoaGh379/xyvCsrKyBAQEJkyY0BLFbt++bWtr++TJk+joaHNz86lTp549e5aP4/uGWLt2rbe3N4fw0aNHfD4kM+85WzjmGTt27OfPnxsN1uoNEyyMv9uwh0+JM8wBKDhRUVGG420CgdBeEGMfgfBbA6NeAQGBRhfwg6kuPj6eSbSrVq26fv16eHj4p0+f9PT0rl27FhYWxuRGOTm5tLQ0vJ6fDv5a3tAqDP7s3bvXwsJi2bJl0dHRkZGRRkZGnp6ecP46HzQ1NaOionbt2uXi4lJbW7tv377nz58HBgY2NDnEnDx5cuPGjbt374bDiZgjLCxcW1sbHx/PZxgkKSnJ/QEfzuNDrZSH/B/RQqAupaWlVVdX81mr1agOzc7kdgH2ttTW1ubk5MAZmnTk5eVTU1OhXGAozLMEOWhSq/xX8Ful6NWrV7NmzTI2Ng4KCgJHhC2k2f0J88rTprRd6eCdWdz7EPkDE8hGDQciIiJ37961sLC4cePGsGHDHB0d2Wz21q1bVVRUsHMGboqKihBC0tLSbWfpQ7S0M+m0mXcOTF4lv4bU1FRbW9uioqLnz5/jZZWtC8N3CvPcawZ4szafcmzrRgpeaJrH4sWLw8LCPDw8ZsyYERAQ0K1bt+jo6OvXr+/cudPMzKwlWnXq1Onx48ceHh4bN27My8vz8fF58+bN8+fP8WGvDBk2bBh9LRjAf+UX856zhWMeFRUVhitGW7dhQs3/BV96mgT/EmeSA9BUa2pqcnNzGZ4FQSAQ2gXB9laAQCDwAw7+UFZWBheNfIBvnhzHxGAyMzOjo6PxXyMjI0NDQ4SQq6vrw4cPBw8ejE8v4o+uri5CKCUlhX5wDJCbm0sPwxyKonJychBCJiYmERERbm5ukpKS9fX1ixcvzsvL43MjjDbExcUPHToUGxsLH7ffv3+/Z88e/k+8ceOGk5OTkZGRs7Nzk1QVFBSEcWdD+RwaGlpWViYvL+/Bxd9//w1hWiUP+T+ihUBdqqqqwufucwAnbfPXodmZ3F7gPOfeyYX+KRoIA6tCfvz4gV2INATk5Js3b6CGc8BisZ49e9YyrX81Tepn2pTExEQrK6uSkpKrV6+2iqWv2f0JakrlaVParr6JiorKysqif+xrzAFNwIcDfwYNGhQdHS0nJ3flyhUTE5MZM2aoqqrGxsbyMRbA5uK2NpZ17twZbAp8CldeXh5mvNA5xMXF8Y+T4aukRXozo6amxsrK6sOHDxcvXmwjSx9i/E5h3rU2g9+kkSKEcnJywB1QkxAUFLx8+fLmzZuLi4snTJhgbW195MiRJ0+eNCMqOqWlpbBwdf78+YmJievXrweXOPQzHBmyZMkS7iEB/82qzAulTcc8mFZvmPCdPikpieebsby8vKk9asvhX+IMcwCvjW20ryMQCO0LMfYRCL81ERERCCEmoy440e/bt2/gN5BOXV3d0qVLOQ5hWbVqFULoxo0bJ06cAE+FTBg7dixCqLa29u7duxyXkpKSEEL9+/fX0NAAiaCgIDydHox7P3JtbS0esQkKCi5cuDA0NLRDhw6VlZX8t12cPHkSe5dTU1N7+PAhuG/j9hLAcfaQi4sL4jowpaHziTjkkM9XrlzB3twwGRkZR48ebdTfQpPysIU079AlRUVFOJlr7969HMWHEAoMDAwNDW00kiZlMn9acnQUc0aMGAEbUrhXElVVVWVmZnbs2HH8+PEIIW1tbRA2tOYIb3WE2sJisXbt2sUdrIWTNOY0deslH5raz7SdYpcuXaqoqOjduzd9019Lqgrz/oQb5pWnTWnT+gauHrE/YobEx8cLCwvD64Y/VVVV69at8/f3f//+fWho6J07d7Zu3crfjAvGPiaWxBYCnfb79++xA18MdNpTp06Fv9A5vHr1qqH1lRydQ0teJXxg3hBevHgBZgh6X93qXS7DdwrzrrUZ9OzZE3a/Xr9+nTseKMcpU6Y0O37m6m3atKl5R7D5+/uXl5fHxcV9+fLlwYMHV69eNTY2bkY8dN69e/fgwQP43blz5yNHjly8eBEhFB4e3hYmV/S/tasVe87Wes21bsPU19cH71WbNm3i1nDlypWNHtHDHIZD7kZLnEkOQFNFCF26dImnMq046iAQCC2BGPsIhN8IjrON6+vrL126pKioCG7+AHiDco/F582bBysvFi1a9PDhQyzPysqysrIyNTWFqxg7Ozs5ObmKioqMjAyeTiHxYJQ+dJg5c+aIESMQQu7u7hzvck9PTwEBAVdXVxhwoH9Oa3r//j2Oqrq6Gn8tLC8vx/c+ePCgpqYG/+3fvz/M3zicxnJD//YoKCi4ZMkS+l0dOnQAQwA+1xnyDXbx0C2JMTExMImlD8Ehx3JycsBNJEVRbDYbfBZXVFRYWFjQj8oODw8fOXIkvaQaokl5iBCCnKHnDxNAeY6Eo39Kk2M4iFONg0EyIyMjp0+fjjfuURR17dq15cuXczhu5gnDTMY5wF2lGyq+hqCfZc4t555hghwr0KtXr82bNyOE7t27x/Gx/fr16/X19du3b4cFPrNmzYLFRHv37qXvCEtNTYVD/UtKSkCiq6sLjev8+fP79u3D+ldVVW3cuPHLly/4eDKsD0cqXrx4MWPGDH9/fz4JbxTc1vjMKxhWjKb2My1XrKEeD3I+OTkZ53ZZWRl8HeGYSEP75QDCcDRA/v0JojVDjvbIvPIgvnUeNVCNsZCjgDjqMPP61lCu8mHcuHEIoSadChoREREWFrZu3Tq6IYnnmwUhNG3aNBEREVhyzhBYVGJubt5oyJbkKkJo3759ULEvX77MEdLb21teXn7//v0g+euvv2Buv3btWnolAZcXiNY5MH+VQAVuqH/joKGen387Qv/bV4OBm6MdNVRwDNsXk3cK866VPw29NOFAidTU1KCgILo8MTExNDR0zJgx06dPx8JmtBHcj/GxdLi6unp6eoL5GPHNPY54QkJCpk6dunbtWg5HzzzhWWcaKkGOLzdz584VExNTUlLCG64Zvh34g18N9MrZpJ6TP0zeJhg+nTDDhtlQe+So+erq6g4ODgihgIAAGxsb7OmoqKjIwcFBU1MT76ppeSYzH3LzL3EmOaChoQF+eG/duvXixQschsVigdMVhk2VQCC0ORSBQPgNgJ1KcnJyb9++BUl9fb2Dg0OXLl2eP3+Og9XU1MDpUZMnT+aOxM3NDZ9wrK2tPXXq1JEjR4qKijo6OvJ86JYtWxBCBw8e5HkVr2c5duwYXZ6YmAjrzjZs2ICF9+/f79ix45EjR+ghsS3AzMzs8uXLO3fu7N+///Hjx0E4bdo0X19fiqLgW+L69evpyVRTU7OysuKbZ9SmTZtkZGR+/vxJzwEREZHQ0FAsASeh6urqr1+/9vT0hI+rNjY2oMO8efMuX77s6OhoY2MD+4yMjY2vXLni5+dHUdTTp08h2Pbt29++fTt37tzExESKoubMmQNyQUFBExOTqVOnDhw4UFBQ0Nvbm7/CzcjD0tJSWNtiaWnJMHIACrdDhw63b98ODAycOXNmXV1dQUEB1JBVq1bRAx84cABSFB0dDZKKigpwj4gQ6tSp09ixYydPnqympiYrK4vD8IdhJh88eBCCRUREcEfCs/gaAp8of+3aNbocDvoRExMrKyvDwrKyMrCoOjk5YWF1dTVs3hw+fHhVVRUI4+Pje/ToYWtrW1tbi0PevHkTzuJRVlY+ffp0WFjYzZs3hw8fDtvtpaWlFy1aFBkZSVFUREQEPvGte/fu1tbWFhYWsrKyhoaGJSUlOEL8CX3nzp105WFjXZcuXWpqahjkOg9+/vyJz1G6f/9+Q8HAHKOjo0MXvnv3Dm48ffo0Fja1n+EDXs3RsWPH7Oxs7gB8eryTJ0/CvSYmJpcvX962bdvw4cPBIKWoqHj58uUzZ85ASLDWmZqa0m/Ha82KiopAwqQ/ef78OdzF0U6pplQePnW+rq4OFkAtXryYLr9y5QrcEhQURJcPHjwYIaSvr48lDOsbLJZRUFBgXq/Kysq6du0qISFBTwsATpA5ml5KSoqOjo6dnV1lZSU98N69eyHwp0+fsBD7vlBUVDQ0NDQzM7O2tp41a9aqVavc3NwqKiq49SksLBQSEurWrRuTJMC8tE+fPnThjx8/4KFbt26ly+HkAQEBgcLCQix8+PBh586dhYSE6I1o48aNsrKyHIWycuVKiNbU1NTLy+vDhw8nTpzARkwNDY2VK1eWlpZSjF8lc+fOhR4M1ys+8Oz5qYZL/MuXL9Ccu3fvfu7cuaNHjw4fPhyeiBA6duzYtm3bICTPgqMYty+G7xTmXSsfhg0bhhCSlZWtrq7muLRz505BQUFFRcWkpCSQlJeXm5mZ6erq/vjxAwfjP9ZqCLzlX0tL6+vXrwUFBTU1NTU1Nfn5+UlJSbdv34YVoOLi4lgx2LfRsWNHqBKAj48PxANjJAB2YIiKimppaZmYmIwfP37atGnz5s3bs2fPx48f6WqwWCzYiTly5Ei6nOegDjZQP3z4EEvggDl6F8f87cCH4uJicMs7ceLEsLCwv//++/bt21RTek7+4PqppKTEXe4c4C7LxcWF+yqThol9++IGAoCfej09PSxJT0/HxwUKCgpqa2sbGxtLSEhMnz6dzWbjYC3PZIZDbiYlziQHvn//Dm+rDh067NixIzg4ODAw0NLSEuqeoKCgnZ3d3bt3G1WbQCC0KcTYRyD8FoCxb968eUZGRkOGDLG1te3evfvQoUPxeJSiqOPHjxsYGMALWEBAYOrUqdzDlMDAQPpXUGVl5cuXL9PHE3RSU1MlJSXz8/M55MnJyVu2bIGPhAghKSkpJycn+nC8oqJixYoVMjIy/fv3X7VqlYWFhYGBwbt377gfsXPnTmxoMDY2/vbtW3h4uLCwsLm5+blz57KysiiKqq6ulpeX19TUHDdu3MmTJ8+dO2dqajp37tzi4mL+mbZp0yY9PT1VVdWNGzdevnzZ2dm5b9++9BEMRVHPnj0DY5mAgMC8efNgCJiSkgL7FGA+4OLiQt/4ZmlpCVYhNpttYWGBh48vX76EONls9pEjR6SkpHA+GxkZBQcH89eWg0bzMC8vb+fOnQMGDKCX+P79++nzTz5kZGTgs8aNjIxSU1M9PT3xhBNMM3FxcRkZGatXr8YbITU1NS9fvgwxlJWVzZ07Fy8iEBAQmDlzZnJyMsMENprJHz9+nD9/Poz+oa7C4iN6JDyLjxvwpYAfp6Ki8vfff+fk5Hz58mX58uU4CUOHDgUD0MWLF/HuJ0lJybVr18bExODYzp49q6ysrKSktGjRohkzZvTt2xfWYHLw+vVrvNtaQEBg8ODBMTExioqKWlpae/bsSUhIwCETExNNTEzwak0JCYlt27bhqV1hYeG2bdtwVJKSkqtXrw4JCYGrMEU3MjJimO10nj59am9vD8NxoEOHDpMnT+boN968eYMtswihiRMnPnnyhKKov//+G3cC0tLSdMN0k/oZnpw5c2bSpEl0vwpdu3Z1cHAICAjAYfj3eLW1tZMmTcL1efny5cXFxS9evIB87t279/fv39++fYv35QkICCxYsODdu3d5eXmrVq3CPgSNjY3v3LlDNdaffPr0acOGDfh4uE6dOi1duvTs2bMc6eJfeb59+8anznt6euIOp2PHjmvXrg0PDy8oKNi6dSs+ul5FRWXnzp3FxcXPnj3Dq5CgbQYGBkI8/Ovbo0ePrKyscLYPGTJk165dBQUFTErt8OHDCKE3b95wX7p+/frAgQMFBARUVFRsbGxGjhzZt29fLy8vephXr16tXLkSnz/bp0+f7du3435+586dDbluUFFRyczM5HjizZs3ES+rKwfBwcFLlizBvuZnzpx57949iqKOHTuGe1dxcXF4xyUmJq5cuRL3h0OHDr148SKO6sePHxYWFp06dRo5cuTq1auHDBliZWWVkZHB/dDz58/jdicqKmpnZxcWFiYoKDhy5MgLFy7gd26jr5L79+/b29tj5U1NTfft28e/oXH3/I2W+P79+/Ejxo0b9/3794KCAliHJSEh8eDBg4YKrknti2L8TmHetXJz7Ngx3C1AYv/++++oqCh6mFevXunr60tJSU2dOnXJkiXa2tpr166lv1yYjLU48PDwGDduHIc/2YawsLCgKCotLW3lypU4o8zNzWF/8b59+3A/o6Kisn//fnhEbGwsrrEcCAgI7NmzB4KdOHFi1KhR+NKsWbO8vb35DOqePHmirq7es2fPBQsWXLhw4dChQ/369Ttw4ADE1oy3Ax/wAlhxcXGOLGX42m0o8ydMmEDfD6Gqqrpw4cKUlBTuwGVlZX/99Rc+bkJCQmLp0qUcn8EabZheXl54NCUtLb1+/fqMjIzw8HD6NvAZM2aEh4dD+IKCgqlTp+Ky7tKly+nTp+vr61s9k5kMufmXOMMcAFJTU83NzXHOKysr379/f8aMGQoKCitXrnz79m2TRgUEAqEtEKDIpnoC4TdAWVk5PT197969W7ZsSU5OTk9P79WrF37NN5Xc3Nzv37+D9zH+o8/Y2Fj8sb0ZJCcnp6Wlqaio8PFhV1RUFBsbq6KiAgbN8vLympoajr1+lZWV4uLicBhNdXW1lpYW3ULREBUVFRISEhRF/fjx4+fPnwoKChoaGnjSgsnPz4+JiVFVVaWfPl5fXx8dHV1TUzNgwAA8AgsPD5eSkurTpw8Oxmazo6Ojq6urBwwYwOHwkaKo5OTknJwcDQ0N7K6xGTDJw+ZRVVUVEREhKSnZVJ96dOrq6mJiYurr6/v27Us/HI0JDDOZPzyL7xeQl5cXGxsrIyOjpaVFn0VwB0tISOjXr1+XLl0QQomJibB6kZvy8vLo6GhpaenevXszdx7KYrE+ffo0YMAAbi+H7Q7zfqbtSEhIyM7O7t+/P+Q/QigpKam4uFhPT6+pOcawP2ECw8rTpjSvvvGntrZ22LBhWlpaHh4ePAMUFBT8+PGjtLS0W7dujXqQ5+DixYvp6en29vbp6emFhYXl5eUVFRUVFRXp6em+vr6TJ08+c+YMPfzIkSMrKipCQ0Pb1BUvN/X19TExMeXl5Y32/MnJyfn5+Xp6enAEbXFxMc8TLVvrVYJpRs+fk5Pz/fv3Xr164SVI0LPp6ek1tdtvFIbvFOZda/OoqKiIjo4WFhbW0tJqFQ8/bU1mZubKlSv//vvv6urqnJycsrKyioqK8vLy0tLSjx8/PnnyJCsrqxl+UWtqaoSFhYWEhHJzcxMSEjp06KCtrY0/SLQ6KSkp6enpOjo60tLS3Fd/h54T0+oNs6amJioqSlJSUkNDo+1S1+iQm3mJM8wBaErdu3eHQVpKSoqKikrzXp0EAqHVIcY+AuG3ABv7tm7d2t66EAgEAoHAg9TU1CFDhgQGBrau39K///7bz8/vy5cvPOfASUlJTk5O9DMir169unr16rCwMHC5QCD82fz8+XP48OHHjx/HrmA4mD17tpOT08CBA3+xYgQCgUD4nSEOOggEAoFAIBAIjaOiouLj47NkyRJwmNAqvHv3bvfu3cbGxg2tdvn+/Tvd5W5SUtLu3bsDAwOJpY/wH2HJkiWpqakmJiYNBcjKympd+zuBQCAQ/gCIsY9A+C0Ax15N8vtGIBAIBMIvZujQoYcOHbK1tcVeNVsIOHz08fEJDQ3luFRXV3fnzh1XV9c1a9aABM49vHXr1pAhQ1rl6QTC7w+0ka1bt4JDMzppaWkzZ85ctWrVL97PTiAQCITfH7KNl0Bof8rLy7t27VpTU+Pg4ODl5dXe6hAIBAKBwI+MjIwjR46cOHGi5cc1lpSUjBgxIjIyEiGkq6s7YMAAGRmZ0tLS7OzsyMhIBweHTZs2gWvUysrKHTt2bN++nX5yPIHwx+Pu7u7o6MhisSQlJUeNGtWjRw8REZHc3Nz4+HgREZH9+/fTnXIQCAQCgQAQYx+B0M4cOXLk+vXrERERCCFBQUEbGxstLa3du3e3t14EAoFAIPwKKisrL126dOPGjYSEhMLCQklJSS0trTFjxqxataoZPgcIhD+PDx8+nD9//tWrV+AAulu3bsbGxrNnz6a7WiYQCAQCgQ4x9hEIBAKBQCAQfgvYbHa7O+IkEH5bYOLWXg7QCQQCgfAvghj7CAQCgUAgEAgEAoFAIBAIhD8E8u2UQCAQCAQCgUAgEAgEAoFA+EMgxj4CgUAgEAgEAoFAIBAIBALhD4EY+wgEAoFAIBAIBAKBQCAQCIQ/BOH2VoBAIPCGxWKFhYX5+fkVFxdfvHixvdUh/A81NTWDBg3Ky8vDEkFBQREREQEBAYqi6urqevbsqaGhYWtra2lp2Y56/iZ8+vTp27dvQkJCpqamPXv2bF4kqamp2dnZgwcPbokmCQkJCxYs0NPTO3PmTEvi+a0oLy9/+fLlvXv3Bg0atHz58taNPC0t7fDhw58+fcrJydHS0tqyZYuxsTGf8CUlJa9evcrLy+vVq9eIESNERESa9Ljq6uqIiIj4+HhJScm+ffv269evZeoTCAQCgUAgEAj/RcjKPgLhd+T169cTJkwwNTU9cuRISkpKe6tD4KRDhw5RUVEZGRljxozJzs5WVFRMTk7+8eNHSkrKjx8/EhMTz5w5U19fb2VlNX78+JqamvbWt91ITEwcNGiQp6dn7969u3btumLFihkzZpSUlDQpksrKSg8PD319/ZZb6G7cuBESEnL27NkfP360MKrfBDc3t6FDh1pbW1+5cqWoqKh1I09ISBg9evTKlSuDg4N1dXUfPXo0duzYurq6hsLv379/1KhR5eXlenp60dHRurq6Dx48YP44Nzc3BweHly9fdunSJScnZ/z48SYmJu/fv2+NpBAIBAKBQCAQCP8hyMo+AuF3ZPjw4cOHD7exsfHz82tvXQgNIigomJycjBCaNGlShw4dsLxjx44DBgzw8vJKT08PDAx0dnY+efJk+6nZbmRkZAwZMmTt2rXbtm0DyZgxY6ZNmzZmzJjQ0FBh4UZeQDU1Nfb29tnZ2bBksqioqOXu421tbf39/fX09Hr16tXCqH4TFi1aNH/+fAUFhYKCglaP3MbGpmfPnpqamgghLy8vKysrBQWFhhbrbdq06dq1ax8+fOjWrRtCaNCgQUOHDh0xYsStW7cmTZrU6LMuXbqUn59/9+5dLBk5cqS+vv6YMWNevnxpaGjYSmkiEAgEAoFAIBD+fMjKPgLh90VBQaG9VWgT0tLSzp49+2ueVV9fv3Xr1jaKvLKy8sOHDwihkSNHcl8VEBAAuZeXV8utVP9GVq9eLSoqumnTJiwRFBTctWvXx48fDx482OjtIiIi27Zt8/Pze/ny5dSpU1tFpX79+oWFhbXpvvg2rXI8ERIS6tq1a6tHGxIS8u3bNzk5OfjbqVOnV69e3bp1i2fgiIiIo0ePbty4ESx9wJAhQywtLRcvXlxYWNjo4/bv33/w4MFr165hSd++ffX09MrLy48ePdqypBAIBAKBQCAQCP8tiLGPQPh9ERAQaG8VWp+amppp06ZlZ2f/msetW7fu9evXbRR5aGhoXV1dhw4djIyMeAYoLS1FCBUVFcGP/xS5ubm+vr6jRo3iWMGno6Ojrq4O25z5xyAoKKivr98WZqw2pU2rXEO0RV/x7NkzhJCgIKNxwqVLl1gs1tixYznkNjY2eXl5169f5397XV1dbm5uaWkpPBQDCzC/fPnCXG0CgUAgEAgEAoFAjH0EAuGXsnz5clgN9wvw8vI6depU28X/6tUrhJCRkZGYmBjPAGFhYQihXr16SUlJtZ0avydPnz6lKIqnOw4VFZWcnJzQ0NBfr1Vb09ZV7leSlZXFPHBgYCBCSEVFhUMOknv37vG/XURE5MyZMzNmzFi3bh1dnpmZiRDS0NBgrgmBQCAQCAQCgUAgZ/YRCL8RwcHBd+7cyczMVFdXX7RoEc8wP3788PDwmDdvnpKSkouLS0RExJw5cywsLHCAjx8/hoSEfPnyRUxMzMDAYPTo0X369OGIJCYmxtPTc/Xq1fLy8t7e3s+fPwdPqbNmzZKQkOAIXFpaGhAQ8Pnz55SUlH79+g0YMMDKygov18rMzLxx4wb8VlBQcHBwQAjFx8c/fPgQx+Dk5CQkJFRfX798+fLLly8jhN69e3fs2DGE0KRJk3r37t1QhpSUlNy4cUNZWdnS0vLWrVv37t0bOnTomjVr4GpaWpq3t3dSUlJlZWX//v2tra379++P73Vzc1u2bBlCKD09HZ41YMCAUaNG4QAPHjwICQlJSUnR0tIaM2aMqalpQ2o0BBj7eO7hRQglJyeDb4HNmzc3NeY/gKdPnyKEeK7LU1RURAh9+PBh+PDhv1othOLi4m7cuDFnzhx1dXUsTEtLu337tpqamo2Nzffv38+fP5+Wlqarq+vk5MRhqK2urr506VJ0dLSoqGifPn20tbW7d+8OTmMbrXLh4eEBAQHfvn3r3r27kZGRra0tx4o8Npv94MGD4ODgrKwsBQWFAQMG2Nvbcx9uWF9ff/v27aCgoIqKCj09vZUrVzYpB/i3aITQ+fPnKysrP336hBCKiYmBtCCEVq5cST+bEpOcnJyUlNSpUyduqzeUdXh4OEVR/Jcfzp8/f/78+XRJfn4+tKDFixc3KYEEAoFAIBAIBMJ/HLKyj0D4LWCxWOvXr581a9aIESNOnTo1ZswYe3v7oKAgeoBTp04NGzZMTU1t9+7dBQUFkydP3rRp082bNxcuXAhhysvLly5damtrq6amtn///rFjx7q6uvbv33///v0sFgvC7N+/38DAQFtb+/Dhw4mJicbGxkuWLLlx48bVq1eXLFliamqalpZGV+zFixc6OjqhoaGzZ8/esWNHfX39lClTjIyMvn37BgGUlJTs7e09PDzWr1+P3VD07t172rRp7969W79+/fr162HDZmpqKlhGEEKCgoLCwsLCwsINbRJ88ODBlClTFBUVly1blpiYuH///hkzZty+fdvJySk+Ph4hdPLkyT59+igqKp44ceLs2bNhYWGDBw/29fWF22tqaioqKkxMTBBCAgIC8CwhISG4mpKSMnbs2Ddv3gwdOnT06NGenp7Dhw93dnZms9nMi6yysvLjx4+oYWMfRLhu3bolS5Ywj/aPITExESHUuXNn7ktgPktPT/+V+lRUVBw9enTAgAH9+vXbvXt3cXExyAMDA4cNG6aiorJ+/fr4+PhLly4NGTLk6dOnDx8+3Llz55AhQ6qrq3EkRUVFurq6hYWF586dO3HihIiIyNixY8Egxb/KVVVVLV269MiRI5qampMnT/706ZOdnd348ePpXjWys7P19fVPnjy5cOHC69evT5gwYcmSJRYWFhUVFfSEFBQUjBs37vz587Nnzz5+/Hi3bt1MTU2Z74tvtEUjhEB5+M1isYT/oSFrXaNlXVZW1lQXzAghDw+P+vr6OXPmWFtbN/VeAoFAIBAIBALhPw1FIBB+AxwcHMTExBISErAkMzNTWloaITR69GiKolgsVkJCAj74f+rUqX5+fq9fv9bW1p45cybcMmrUKDExsdTUVBxJbm4ueNJctWoVSNLS0g4cOACR6Orq3rp1q6amJj8/f8+ePSA0MzPDt798+VJAQGDFihV0VcGi16lTp58/f2Lh2rVrEUKGhob0kNiVcHV1NRbq6+sjhLZt28Y/Q1JSUvDawPHjx+/ZsycuLs7S0rJfv36lpaWfP39GCCkrK+Pw0dHRIGGz2Vjo5OSEEDI1NaXHXFhYqK6ufu7cOSz58uULWDE8PDz4a0UHDhfr0KFDVVUVx6Xc3NwZM2aIiYlt27aNrs/vSXl5eVzTKS8v5x+ttrY2Qsjd3Z370ooVKxBC8+fPZ67kmTNnEEIODg5NSxuNysrK5ORkXM9hrRlQWFgoIyODENLQ0Pjrr78qKiooioqIiBAVFUUInT9/HodcvXq1lJQUPdq5c+e6urrivzyrHEVRNjY206dPpysDVu+5c+diIbispecYuCW5dOkS/UYNDQ0dHR1QEnj8+DEkau/evfwzgXmLpigKVulOmzaNf5wURd25c4ejPWLy8vJAN47I+ZOenn769GkZGZmdO3f+/i2IQCAQCAQCgUD43SDbeAmE9ic0NNTb23vBggX0Da3dunUbPXr03bt34a+goGDv3r1nzJixb98+hNCQIUPANPD161cI4OXl9fLly8WLFysrK+NI5OTk9uzZM3369LNnz9rb2w8ZMqRHjx5jxoyBjaWHDx8eN24cQkhWVnbbtm1FRUXHjx9/8eLF69evhw8fXltbu2zZMoqiOHahrl69+uLFi9++fVu2bJm/vz8IeW7ua+gkOyb06tWrV69esrKyBQUFgoKC27ZtQwg9evQIrsbFxXGE79GjB0IoLS0tPz8f+w/lycGDB3NycugbA/X19fv37x8dHX3w4MF58+Yx1BD28MrIyOD1jGw2OzMzMzExMT4+3tzcPCEhAbTi5vXr11lZWcXFxfPmzeOZdb+Sw4cPHz9+vKl3rVu3bufOnXwCgE8Snis3Yb1bM5Z6tYSOHTuqqqrOmDFj+/btHJe6dOmioqJSWFhoYmKCt6zq6emZm5s/fvz448ePS5cuBeH79+/LyspycnKwp+zly5fDyj4+PH782NfX9927d3RlpkyZcvr0aS8vr3379oHhLzY2luNGqD909xQnTpyIj4+/evWquLg4Fo4fP15OTg6b1RqiSS26STRa1ohxcX/79m3v3r3l5eXh4eHjx4+3srL6I/0UEQgEAoFAIBAIbQox9hEI7Q9YsiwtLTnk3OediYiIwA9zc3OOS/iMMA755MmT5eXlc3Nzz507N2TIEIQQ3qAHni4xa9euPXnyJIvFev78+fDhwwMCAuLi4hQUFMASQWfx4sVOTk4BAQEpKSmqqqpNSGoTgfRyJ3bKlClnzpyB5AAURcGPwsJCPsY+iqI8PDwkJCQ2bNhAl9fU1CCEvn//np+fz9D9Kxj7TExMDA0NsbBjx45aWlqwJLMhWCzW+/fvL168mJOTg7dgtyO7du3atWtXq0cLe7dxudABYV1dXas/tFFwC+IAGoWsrCxdCDWfvt1YTk6OzWZPmDDB19cXXE8MHjy40QoD51R6e3vfunULC2HbLJvNfvfu3bRp0xBC165dCwsLmzVrFg4DGVVYWAh/y8vLDx06hHj1FbKyso0a+9quRTda1ohxcWtra+MzQHfu3GloaOjo6Hjq1ClYZUkgEAgEAoFAIBCYQIx9BEI7Q1EUrAzidmTJB7xeBqivr4dlQdx2LhEREUNDw4CAgKioKP5x9ujRQ0tLKzo6OiEhASEEG2N5Gs6GDh0KP6KiotrU2AdwJBYh1KFDB9gKihDKyspyc3MDZwLoH7tDQ8TFxeXl5enp6RkZGdHl+C+3MwSeVFVVgU9hOzs7blskf4SEhJydna9du2Zqasrwcf9GOnfunJWVhQ+LpAPC399DMZQO/STHv/76C1xb9OvXb+vWrevWrevQoYOamhr/eF6/fi0iIjJs2DC60MjICA5z1NXVBYmhoSEYjimKCgwMvHbtGtQxXKW/fv1aWloqISEBm46bStu1aDitj09Zo2YV986dOwMDAy9cuFBZWXn16tWm3k4gEAgEAoFAIPxn+WPnmQTCv4UfP35UVVUhhLjd4DInOTm5trYWIZSTk8N9FTb2ggmPP8rKytHR0ZWVleifTYV8ImQYZ9sRFxd34MCBqqqqTZs2rV69mv96OiAzMxMhJCwsbGdn15JHv3v3DjK8ef5kCwsLo6Oj6Wu4/jzAuMPT9gpCnv4cfnPMzMy8vLwWLVpUWVm5detWDw+PS5cuNeShBairq8vLy6MoytLSslOnTvzjr6+vv3z58pUrV+zt7V1dXbdt2wbuaABoks3uKNquRTda1qi5xe3g4PD+/XsvL69ly5YZGxs3IwYCgUAgEAgEAuE/CPHGSyC0M9iNZkuck+KNhDwjgbU8TBbsgDGib9++OM68vDy6N1J6hAzjbCP279+vq6s7YsSI27dvc29ebgg4ViwlJYXnKiTmwB5eLS0teXn5Ztz+5s0biqL4G4n+7UAlwVtQ6YAL2m7duv1qnVoDe3v7qKioUaNGIYQSExPNzc3PnTvHJ7yAgADUOnBZy4fExMSBAwdevHjxwYMHK1as4DbqQV+Rl5cHu86bStu1aLi9qKiI+xKUtZiYGH9bfH19/Zs3b7hrC141Cf5wCAQCgUAgEAgEAhPIyj4CoZ1RV1eHH9+/fx8zZkzzIpGRkVFSUsrMzIyMjOS+Cod54d2CfABbYb9+/RBCOjo6IIyOjh40aBB3hAzjbAvc3Ny2bt06derUBQsWNOlGcIFSWFgYHBxsZmbGcZXNZj9//pxJKYCxr0nWuuzs7BcvXuTl5dnY2Lx69apTp04DBw6ES5mZmTdu3NDQ0Jg4cWJFRUVAQEBycrKRkdGIESM4Ivnx40doaOiPHz90dXUtLS2x74IvX748e/bMzMxs4MCBxcXFgYGBqampkyZN0tDQaFSxc+fOeXl5MU8IMHv27OXLl/MJYGho6O/vn5uby30pKysL8TqK8fcnOztbUVGxT58+L1688PPzW7ZsWXZ29po1ayZMmMBxAiZGWFi4Z8+eycnJ9+7dA1fUHAQGBo4bN66qqsrS0jI+Pj46OrqhQwChr6AoKiEhoX///k1Vvu1atI6OjqioaG1tbXFxMYdRD8p6+PDhDZ2WCCxevPjKlSsyMjLp6ekdO3bEchxboycSEggEAoFAIBAIBAwx9hEI7Yy8vDz4nHVzc1u5ciV3AJ7H3nMzZswYT0/Pp0+f5uXlcRzLlZycjBCaPHky/xhqa2vj4uKkpaUnTpyIEBo5cqSwsHB9ff21a9c4TAMQYf/+/bGlEqxOHGfww3ZgnvA/Wa9RTp06hRDS09NjEiH9Uo8ePSQlJcvKynbv3j1q1CgOR5979+6le9toiKqqqrCwMNQUY9/Fixc9PDxOnDgxatSobdu2vXz5ctiwYXAkHIvF2rRp04EDB4yNjV+/fl1YWLh8+fKBAwcOHTrUw8PDwsICYigvL9+wYUNpaemaNWssLS0PHz586NChoKAgMTGxpKQkLy8v8La8b9++jIyMpUuXamhoWFpaurq6NmpTs7CwwL5lmdPoakoLC4tdu3ZlZGRwX/r586e0tPTgwYOb+tB2Z/PmzR4eHvB78uTJAwYMGDhwYH5+flhYGN3Yx1Eb+/Xrl5ycfP78+ZUrV3IsBX316tWrV6/GjRv3+PHj+Ph4YWFhbW3thuLR0tKCH25ubi4uLtzq8e8rmtSim0SHDh3MzMyePHmSkZHBYez7+fMnQmjs2LH8Y4Dtw4WFhfHx8fR2nZ+fDz84FCYQCAQCgUAgEAj8oAgEQnuzb98+aI+urq50OXiNMDQ0xBI4dQsh9OnTJ45IMjMz4VSsY8eO0eXp6enCwsLm5uZYEhERwTMSb29vhNChQ4ewBFzWysvLV1ZW0kMuW7YMIfT69WsscXV1RQh17NgxNzcXJLW1tfhcvPz8fBwS1tPNmTMH/tbV1fHJGTBCcaSIoqguXboghGxsbLDEz88PnvX582cs3L17N0JIRUWF/qzt27dDyAULFlRUVMClmpqanTt3jho1io8ymOfPn0MM2dnZTMIfOXJETk4OBwY/CTiffXx8PDw8KIrq3r27iopKQUEBJKdz586xsbEQpqqqSl9fH2caRVFsNlteXn7v3r0URa1duzYxMRFWZc6dOxeHWb169cCBA5lo2EYYGhpKS0vX1NTQheCFdtGiRXRhXFyckZGRvb09i8XiGdXp06cRQvb29jyvBgYGjhkzxtHRsaSkhL9K+Ai8d+/e0eUGBgYIofXr19OFsG6U3nYMDAwSExPpYezt7RFCYWFh8JdnlcMVxtDQMC0tDd977949VVVVaB0nT56EMJGRkXC1pqYG7KHW1tb4Flh22qlTp/DwcCzMzc2FDfgc+nPDvEVTFGVra4sQmjhxIv84gYcPHyKEDh8+zCG3tbUVFRVNSkqiC3fu3KmrqxsUFIQlmzdv7tmz5549ezhud3Z2Rgh16dKF3tYeP348Y8YMDm0JBAKBQCAQCAQChhj7CIT2p6qqCpbzCAgIrFq16unTpy9evJg2bRqsshEQELCzs7t37x5FsxrcuHGDO55bt26Ji4t37NgxNDQUJNXV1dbW1rCwCAfDxr4xY8aAaYmiqOjoaDk5ucmTJ1dVVeGQ5eXlsJPUxsYGW2GeP3/eqVOnffv20R+dkpIiJiaGENLV1b106dKBAwf09PTw4qMpU6bcv38fQjo5OSGEpKSk/P397927t2zZsoaypaKiokOHDgghR0dHjkvjx4+HmFetWuXm5mZvb7948WJQwN7e/tSpU1+/fqVoFkAXF5dXr17NmDGjtLS0pKQEHwQmIyNjaWlpZWWloKCgra2Nc4MPLBZr3rx5CKFu3bqx2exGw8fHx4uJidGzy9/fn24eunv3bnFxMbhNOHLkCP1B+PeKFSuEhYXpdiKKorS0tMaNG0dRlLu7O0VRt2/fRghFRUXhAMuXL+/QoQMTJduIz58/i4qKnjlzhi5csGCBgoIC3f5LUdSWLVugRD5+/MgdD4vFAse1gwcPptdPDF4Nx11VOHjy5AmEvHXrFhay2WxFRUWoqPTA4H2lf//+WKKvrz9+/Pja2lp84+DBg01MTOrr60HCs8pRFDVt2jSQi4qKjho1asqUKZqampKSkji97969gwB9+vS5ePHivn37TE1NbWxsEEKysrJubm6nTp2iKCo6Ohps+p07dz548GBoaKivr++IESNkZWWhWS1atCg6Orqh5DNv0RRFwQq7vn378s9SjK2trbq6Ot3eGhcXJy4uDiZpTGVlJaxptbS0xMLs7GwDAwO6BZOiqKysLDBi+vj4YCGLxYLVkX369GGoGIFAIBAIBAKB8F+DGPsIhN+CsrKyuXPnioqKwoRfXl7+6tWrS5cu7dq16/Lly9++fVtaWrp27VowSYCVysnJiXttS2xs7IgRIyQlJSdMmLB8+XJdXd2lS5fi9WsANvbt2bNHWVl54sSJ5ubmMjIyf//9N7dhiMVi7d+/X1FRUU1Nbfny5dbW1rq6ui9fvuROwo0bNyQlJSFmbW3t9+/fJyYmCgkJjRo16uzZs3hhTkpKSvfu3SGYpaVlcXExzww5fvw43s0nLCw8e/ZsLy8vfPX79+9w+h5CqEePHteuXaMoCi8k3LRpEySkvr4eH3unqakJFkCKooqKimxtbfEhYqKiok5OTo1a+k6ePKmurq6goCAlJSUrK9ulSxdlZWUNDQ3wttEQW7duRQjFxcVhyYYNGyQlJbF5CPDx8UG8FmxSFFVdXd2hQwcOO1R1dbWoqCh9pdvy5ctlZWXpJWhiYtLuBpGHDx9269bNxcWlsLAwPT197dq1ffr04bZGRUREaGhoWFhYcKz0NDc319bWVlZWlpGRkZWVlZGR6datm5aWlpaWFri4BVasWAFFyccylZycvHnz5h49ekBIRUXFTZs2paenh4SETJo0CYQiIiLLly+PiYmJi4uDJXuAnZ0drBg1MTHR0dEZPHjw4cOHL168aGFhMXnyZPqis4aqHIvF2rlzJ90b78SJE/HKTWDbtm2wr1xQUNDOzg6O4IQ+oVevXjExMRAsMTHRxMQE70DX1tb++PFj3759NTU1d+3alZCQwL9EmLToY8eOTZgwAas6ZsyYPXv2pKen84+5tLTU1tbW1NT006dP5eXlgYGBvXv33rx5M/dqTScnpx49evj5+dGFCQkJFhYWc+fO9fHxCQ8Pv3HjhpqaWq9evQICAjhuByMsR4sgEAgEAoFAIBAIGAGK2XFgBALhF1BbWxsdHS0lJQWWrJ8/f3bv3h1WwTSJmpqa6OhoFoulpaWFDXCYyMhIcBQQFxenrKwcExPDYrG0tbXplghuMjIyEhMTlZSU+vTp01CYioqKr1+/du3aFdYkVldXl5aWcvurraioiIyM7Nq1KxP3EQ1RW1sbFRWFEDIwMBASEkII1dfXh4WFqamp0d281tfXw2I3AwMDcIqKgVwSFRXV1NSEJYRtwdixYyMiIuh+KgYPHiwnJwfr+zBr1qy5cuVKYWEhpIXO27dvTU1Nz549S3eIcePGjVmzZl24cAGWvCGEtLW1+/btC0ZDhFBOTk737t3t7e09PT3bJGGMKS4u9vHxiYqKEhUVHTJkyMSJE9sit8H/iYeHR1BQUKtHjqmoqJCQkKipqYmLiysuLu7bty/3cYd8qhybzY6Pjy8pKenbt6+UlBR3/JmZmYmJiX369MF1+OfPn7m5ufr6+hwOLkpLS79+/dq7d29oX0lJSU09bo9Ji24GYWFh/v7+BQUFysrKU6dObWrk4eHh8J1AUVFRX19/+PDh4uLiHGHq6uoiIiIGDhzIkb0EAoFAIBAIBAIBIMY+AuE/B93Yp6mp2d7q/OGMHj26vLz8/fv38DcvL69bt2779+93dnZevHixq6srWPd0dXV79uwJB59xEBUVpaend/fu3alTp2LhqFGjiouL3717B5uXc3NzFRQUXFxc1qxZAwFOnjy5efPmqKgovATyj2f9+vVDhgyBk+YIBAKBQCAQCAQC4T8L+SpOIBAIbYiFhQVe1ldXV7dmzRoWi9WvX7+Kigo2mw2Wvvz8/K9fvzbk21dbW3vAgAEfP37Eki1btiQnJ9++fRssfQih4OBghFBeXh78/f79++7du8+cOfPfsfRdvHixuLiYWPoIBAKBQCAQCAQCocnbAwkEwr8dNpsNP+rr69tXk/8Cq1evjoiImDVrlqGhYWJi4t9//11VVeXl5fXo0SNwjYoQ+vr1q6CgIPY6woGQkJCPj8/SpUs3b97crVu39+/fy8jIfP36lb5B+9WrV5KSkh07dty/f7+YmFhQUJCvry8cbfZf4M2bN6WlpRcvXmxvRQgEAoFAIBAIBAKh/SHbeAmE/xz379+fPHkyQujBgwcTJ05sb3X+ExQVFdXX18vJycHfrKysLl264HV5FEVlZWUpKSnxj6SkpKSkpERFRYX7kra2ds+ePQMCAioqKqqqqrp27dq6+hMIBAKBQCAQCAQC4d8CMfYRCP8hysrKtm3bdvPmTdhYKicnZ2tra2FhYWVl1d6qEZoPHNh38ODBjRs3trcuBAKBQCAQCAQCgUBoZ4ixj0AgEP7deHh4LFiw4PXr18OGDWtvXQgEAoFAIBAIBAKB0M4QYx+BQCD8izl27NizZ8+EhYXr6+sXLFhAPFQQCAQCgUAgEAgEwn8cYuwjEAgEAoFAIBAIBAKBQCAQ/hAE21sBAoFAIBAIBAKBQCAQCAQCgdA6EGMfgUAgEAgEAoFAIBAIBAKB8IdAjH0EAoFAIBAIBAKBQCAQCATCHwIx9hEIBAKBQCAQCAQCgUAgEAh/CMTYRyAQCAQCgUAgEAgEAoFAIPwhEGMfgUAgEAgEAoFAIBAIBAKB8IdAjH0EAoFAIBAIBAKBQCAQCATCHwIx9hEIBAKBQCAQCAQCgUAgEAh/CMTYRyAQCAQCgUAgEAgEAoFAIPwhEGMfgUAgEAgEAoFAIBAIBAKB8IdAjH0EAoFAIBAIBAKBQCAQCATCHwIx9hEIBAKBQCAQCAQCgUAgEAh/CMTYRyAQCAQCgUAgEAgEAoFAIPwhEGMfgUAgEAgEAoFAIBAIBAKB8IdAjH0EAoFAIBAIBAKBQCAQCATCHwIx9hEIBAKBQCAQCAQCgUAgEAh/CMTYRyAQCAQCgUAgEAgEAoFAIPwhEGMfgUAgEAgEAoFAIBAIBAKB8Icg3N4KEAj/dcLDwzMyMhgGHjRokJKSUpvqw5Py8vKgoKB79+6NGzfOwcGhjZ5SUFAQGBjo4+Pj6Og4duzYNnrKH0Npaenz5899fX1NTEwcHR2bdG96evqzZ8/8/f1tbW2nT5/eRhr+l2lJ6bQuJ06cuH79+p49e8aPH9+mD6Io6tu3b0+fPn306JGHh0fPnj3b9HG/hrq6utDQ0Pv371dVVZ0/f75J95aUlAQHBwcEBEhLSx88eLC1VEpOTn706NGdO3cCAgIkJSVbK1puUlNT582bp6SkdOXKFWHhXzpcZJ7t1dXVwcHBvr6+PXv23Lx58y/T8I/k2bNnmzZtsrOz27BhQ3vr8j/ExMTcv3//1atXgYGBbfeUNmqwLaG8vPz58+d0CUVRoaGhs2bN0tfXb3nkv2BcxwGLxfr48WNAQMD79++fPn36ax5Kh81mz5s3Lz093c3NTU1N7dcr0NawWKzQ0FBfX18BAYFjx47xD9ySTj4rK8vf3//OnTunT5/W0NBogcoEAqEtoQgEQrvy7du38+fPKysrQ5MUFhY2NjYe/Q+jRo0yNDRUVFSEqzdv3vz1Gl6+fBm/yM+cOdNGT9m+fXv37t3hKX5+fm30lD+Go0eP4kI5ePAg8xszMjKWLFnSpUsXuNfNza3tlPzP0uzSaXXYbLaUlBRCaPTo0W36oDdv3kybNk1ERARSnZiY2KaP+zU8fPhw+PDhkKLx48c36d6NGzf26dMH7nVwcGgVffLy8hYsWCAhIQHRFhcXt0q0DYEniiEhIW36IA6YZ/v169cHDBgAIZ2cnH6Zhn8qEydORAh17Nixtra2vXX5PzIyMmbNmgUdS9euXdvuQW3RYFvOoUOHuCduxsbGLY/514zrOHj69Om4ceMEBATaujT58OnTJ0j13r1720WBNiUkJGTkyJGQwGnTpjUavtmd/Nq1a2VkZODer1+/NldfAoHQ5hBjH4HwW3D16lV4a44bN477KpvN9vX1FRYW9vLy+vW6URRVUVEBH/3adFCYmJhIjH3MqaqqEhMTa545CS8mJca+NqIlpdO6HDx4UFdX9+HDh7/gWdeuXfuTjH0AzJ2aauyjKIrNZsN0unVtB25ubr/G2JecnGxiYmJra1tXV9emD+IJw2xns9nwqYwY+1pOQECArq7uvn372lsRTmDNZlubh9qowTab6upqRUVFVVXV3jT69Onz4MGDVon/14zruFm7dm07Gvvq6+unT59uYmKSkJDQLgr8AoyNjRka+1rSyeNltsTYRyD8zpAz+wiE3wJVVVX4wXNbloCAwOTJk2fPnl1XV/cLlAkJCXn48CFdIi4ujpeTtB0KCgpt/Yg/CTExMVi01QyUlJQEBUn/34a0pHRal40bN0ZGRlpZWf2CZ+EVyn8Sze6XBAQEevTo0brKoKbrc/DgwaKiomY8SFVVNSQk5Pbt2794Dy/AMJkCAgLy8vJtrcx/BAsLi8jIyC1btrSjDhRFbdq0iUP4a4q4jRosnSY1Rg8PDxUVleTk5AQa8fHxsACz5fyacR037fuaEBISunXrVkhISO/evZsdCc9a+vvAvL20pJMnHS+B8K+ATPYIhN8CvP2ND/Pnz+/Vq1dba5KZmTl9+vTKykoOOey8aFN+wSP+MFqSY8TY19b8B+vzH1mpfrdW1iR9nj17tm3bttra2lZXo61hnsz/YEP7g9m1a9f9+/c5hL+siNu0B2tSY2SxWEeOHNm1a1fb6YPaqe38Aa8JnrX09+HXFCvpeAmEfwX/+g6XQPjvMGzYsFGjRrXpI6qqqqZMmZKVldWmTyEQCARCW5OYmGhnZ8disdpbEQKBET4+Prt3725vLdqEpjbGW7dupaSkeHh4HD169OPHjxRFtal6BOb8wbWUQCD8eRBvvATCv4Do6OjXr1+vWLGCLszKynr69GlERERmZmb//v0HDBhgYWHB8cmUxWIFBgZGRUVt2rQpISEBzuLduHEj3jVMJzs7287OLiwsDCH08OHD9PR0hJCDgwP3dqqwsLBr166lp6erqaktWLBAS0uLO7agoKDg4ODv37+rqamZmJhYW1s3Kcn19fX37t17+vRpVVXV0KFDp0yZ0q1bN44wOTk53t7e8fHxJSUlmpqa5ubm+Fh3TH5+/sWLF5OTkyUkJDQ1NdXV1TU0NDiSn5+ff+PGjaioqJqamgEDBkybNo3JXh6Koj5+/Hjr1q01a9aoqKjExcVdunQpKSlJVVXVyckJXJGWlJRcv3797du3AgICY8aMmTNnDnc8VVVV/v7+kZGRcXFxampq+vr6EyZM4LkDtLy83MvLKzQ0tL6+fuDAgcuXL29It7q6ups3b3769CkzM1NXV3f8+PGGhoaNpgjw9/eHbO/evbuBgYGIiIiFhQXDezGurq4VFRX47/z588XExDi8avbv33/cuHEIoa9fv+LDX5YtWyYuLo7DNLUWubi44NnU4sWLO3fujBA6c+ZMTU0NCI2MjExMTJqaXiZqMC8dnjCpzDypq6vz8PD4/PmzoKAg1B9xcfGhQ4fiADU1NYGBgRERETt27KDfGBUVdefOnUmTJhkaGoaHh7u7u+fn5+vr669Zs6ZTp07cD3ry5Mnz589//Pihqampp6c3evRo7OaFoZ7Nq5PJycl37tzR0dGZMGFCYWGhq6treHi4tLT0woULoShZLNb9+/cDAgJKSkoGDhy4Zs2ajh07csfz/Pnz8PDwyMjILl266Ovrjx49mmc3SFHU/fv3/f39CwsLNTU1ly5d2uop4g9DPYGSkhJPT8/379937tzZ1NTU1ta2Q4cOcOnDhw9Tp04tLCxECJ07d65z584iIiKrV6/G90ZHR9+5cyc1NZXFYunq6trY2HDva0tKSrp9+7a5ufngwYOxMDMz8+7du9LS0nPmzMnIyDh37lxsbGzv3r0dHR3V1dW5lfz27duDBw+io6OlpaUHDhw4a9Ys7gJinu38CQ0NvXnzZlZWloGBgZWVla6uLsjj4uL8/f3pIc3MzAwMDBBCxcXF7u7uIOzbt6+lpSX/R/DJN19f3+TkZBxSXl5+9uzZ8NvPzy8pKQl+29vbY6dbjb59ysrK7ty5IyEhYWdnFxQUdPny5d69e2/ZsgXykKKop0+fBgUFZWRkSEtL6+joODg4QL/Hrbavr+/Xr1+VlJR0dHTMzc15bhR4//79rVu39u3bR++Hm1riDJ/FwZUrVxwdHSmKKigogLGKmpqajY0NR7Dy8nIPD493794JCQmZmJgsWbKEe6VY897pfKiurr5582Z4eHheXp6ysrKhoaGdnR3HyiY+XXGjjZEDiqLgsNfbt2/fvn0bIdS9e3cnJ6eVK1fCUbAMYbFY9+7d+/DhQ3JysrKy8ujRoy0tLRtakMVkXMekLSOEXr9+/fLly69fv8rIyBgaGtrb29OrEzcXL14sKyvDf3v06GFnZ4cQqqmpefToUWJi4saNG1NTU93d3b9+/dqrVy8zMzOe7ZThaKqwsNDX11dUVBQ3T8ir4ODgO3funDhxQkxM7N69e/fu3UMImZubz5s3D2ca/1oaHR19/fr1vLw8GRkZHR0dCQkJa2vrpu6QjYqKCgoKwn81NTWtrKyePXsWGRlJD7ZkyRI49gfnHryXOWJLTU11c3P79u2bvLy8tbU19/CGZycPFBUV3b59+8uXL4WFherq6nZ2dg35gKYo6t69e48ePSovL9fR0Vm9erW0tHSTUk0gENqK9jkqkEAg/C/v37+HJsnzSN19+/Zt27aNLrly5YqsrOyBAwfi4+PDwsJWrVqFEDI2No6Li4MAERERy5YtAzudqanp58+f8at32bJlPHUIDg4+cuQIhJkzZ46Li4uLi0tOTg5chdtPnz69detWUVFRfX19mFGIi4t/+PCBHk9aWpqVldXy5ct9fHyuXLkCgwMbG5uSkhL+mVBeXg5P9/T05LB0KCgovH79mh740qVLoqKiJ06cKCgoqKioWLFihYCAwIEDB+hhIiIiZGVlb9y4wWazq6qqtm3bhhC6f/8+RzxGRkaurq5+fn7Ozs4iIiJdunThCMPNxo0bse/gz58/b9q0qWvXrsbGxmD+UFJSysvLe/z4cffu3XV0dAwMDGCk+Pfff3PE8+7dO01NzTlz5nz8+DE+Pv748ePi4uI9evTw9/fnCBkXF9e3b9/p06eHhob+/Pnz5MmTOjo6MIDmcAEREhJiZGS0b9+++/fvnzhxQk5OTkhIaOfOnRwRwgCUw0HH/PnzR44cmZ2dTVHUly9f+vTpY2BgwD8rePL161cYYYuJiUVHR4MwIiKib9++kGl+fn719fUgZ7FYFy5cQAgdPnwYx9C8WlRVVTV//nx4xI8fP0BYXFx89+5dmBBytKNG08tQDealwxMmlZknpaWlurq6Tk5OVVVVLBbLx8dHXFx87dq1cDUkJGTevHlQEPr6+viuM2fOYPPEgwcPtm3bJikp2bdvXyEhIYSQjo4Ohy/OqqqqyZMn6+vrf/78OTY2Fudwz5499fX19fX1cVGGhITAJQ4HHQzrJAeXL1/G5rPTp097eXl169Zt4MCBYDgQFhYODg5OSEgwNDTs2bOnsbExTDtHjBjBEU96erqFhYWhoeGzZ89SU1O9vb179+7dsWPHI0eOsFgsjvycNGmSrq5uQEBARkaGr6+vjo4OGO45PEUwTBFMvRie989Qz0ePHkGevH79msOEMWjQoLS0NAh27do1R0dHkO/evdvFxeXs2bNwicViLVq0qEuXLo8ePaqqqsrIyDAxMZGSknr69CkEKC8vP3r06KBBg3BrBfmTJ09GjBgBvdn69et9fHxkZWX79esHVV1KSgq/gICamhonJycrKytvb+/bt29PmzYNIaStrR0TE9O8bOcJVJI1a9Y4OTnRc0NERAR7HmCz2Q8fPhQVFUUIdenSBecSkJOTIy8vb25uXlRUxOdBjeZbbW0tNlNOmjSpvLwc31teXr5gwQJBQUFfX18s5P/2efDgwbRp0yBvt23b5u7uji0Ot27doigqPz9/8ODBAwYMCA8PZ7FYHz9+lJOT69evH+76MHv27JGXl3/48OHPnz+PHz8OnWHXrl319PT09fXfv3+fkpLi7OwMeY5ovl+aWuJMnsUzb6urq11cXMzNzRFC8vLyMPzAWeHi4gKRxMXF9e7dW0lJCb9KbG1tOaJq3jsd4NlgP378KC8vP3fu3OTk5Pr6ei8vL4TQ5MmTq6urcRj+XTGfxsiToqKi2bNnm5mZcRiqBg4cCG8rJoSGhurq6i5btuz169cxMTHQ+oYOHUp37MN8XMewLaelpdnY2JiZmYGRbu/evQghRUXFyMhIHAaXJv1Ga2trUVHRHTt2pKenUxT16dOn2bNnY1fyt2/f7tSpE/6SgRCaN29eTU0NPQYmo6lr165ZWlrCsTnYpU9tbe3KlSuxb9mMjAwrKyt5eXn8lQWse1RjtdTd3b1Hjx5fvnyhKConJwfMkY2Oe7mpqalZtmwZPHrx4sWVlZUURZWXl0NmIoTMzMxyc3Nx+KysLD09PVVVVTxcnzx5MkJo2rRpvr6+8HJXUlKCe3HFa6iTx8CRkUeOHImMjAwLC9PU1EQIrVixgs1mQ4CIiAi49/379+PGjZOUlNTX14e8VVNTKysra2rCCQRCW0CMfQTCbwE29o0ZM+brP0RHR7979+7UqVOdOnWiGymOHj2KEHJ1daXHANsKOnfuDNOYrKysiIiIPn36wIDM3t4+MTHx8OHDCgoKnp6eDalRXV0Naty8eZPjEgwK+/Xr5+zsDG/xvLw8MBlYW1vjYKWlpf369du/fz+W5OXlwfBx+fLl/DMBG/t69+69Z8+enz9/gt0ERg/i4uKZmZkQMjY2VkBAoGPHjvjeyspKISEhISEhHIaiqCFDhgwbNoz+iLFjx96+fRv/vXTpkoKCAn2CB17/xMTEfv78yV/bgIAA0NbAwMDT0xMGQFlZWTBkHDVqlJmZWX5+PgQ+e/YsQkhSUpI+/QsLCxMSEpoxYwY92qCgIJiR0icn8fHxnTt3trGxwcMsiub5lG5O+vLli6SkZEhICJbgRXP0hFO8jH2PHz9GCD1//hxL3r59q6WlxT8fGgLWkYmKisJQFfDx8QFl8KgUePbsWY8ePbA/uJbUoidPnsAjOGa8YCGit6NG08tQDealwxOGlZknGzduFBYWps959u/fj3VLTU39+vUrLNCgG/soivrw4QOop62tffz4cbDuvXnzBmb17u7u9MCwAgVb21ksFjj7U1dXf/bs2bNnz3DCeRr7mNdJbnJzc2GZoaqq6tatWysqKiiKqq2thY8Burq6BgYGMLmiKCoqKgosC/QPAyUlJd27d1dVVaXXw9TUVLBr/PXXX1hYW1sLU6bCwkIsTEhIABsi3erEPEXMjX3M9cTGviFDhnh7e+fl5UVGRuLJ4YABA3Bx4LbAYSBwdXVFCFlYWHDo369fP/hbWVmZlJQE61Y45oHFxcUwb+zdu/e8efMgr7KyslRUVBBCs2fPpj/I3t5+7NixdAnUHB0dHWwgZp7tDQHGvl69es2cOTMqKiovL8/Hxwd/jKF3pGBwERER4XBkXFhYKCoqGhERwf9BjeYbRVEVFRWQP9yab926debMmfhvo2+flJSUp0+fggl+zJgxa9euTU5Onj9/vrKyclRUFEVRM2bMQAgdOnSIIwaO73mwQGn37t1YsnHjRoSQgIDAvXv3nj17VlBQkJ+fn5SUBMup0P86em5SiTN5Fp8cBqcHffv25ZCDeUhCQsLMzOzdu3cgPHHiBGj7+fNn5rnKH+4Gy2azIflhYWFYCNUYTK44mXy6YqrhxsgfFov15s2bmTNn4mOdDQ0N6UbGhnjz5o2YmNjRo0ex5MePHxCDs7MzFjIc11HM2nJOTo66uvqkSZPwq5zNZsPaz8GDB+MbuY19N27c6NevH90gWFJSkpCQAAYmOTm5SZMmwVfD2NjYsWPHQkLobqMZjqbi4+NfvXoFQg7/3YcPH4Zohw0b9vjxYxDu27cPISQoKBgfH49D8qylRUVFYmJi27dvx5Li4mI5OTm6VY45xcXFsGqPHiFFUbBUmfuDvamp6fnz5/FfMPapqalZW1unpqZSFFVfXz916lTITKilfDp5iqKOHj0qJCREN817enpCyICAAJBgY5+hoaGXlxcU+sePH2Fsefz48WYknEAgtDrE2Ecg/BZgY5+QkFCnf5CQkMBf8rGRIiUlRVxcXFFRkWP1DYvFAnOGpaUlFs6dOxeGyBy2lYZo1NhHn3NSFHXw4EGEkIKCApbs2LFDRESEbtKiKMrW1hYhJCwsnJeXx+fp2Ni3ZcsWuvzWrVsgX7RoEUh8fX0RQpKSknhMSVEULGPEA5Ha2lphYeHu3bvjkShFUX5+flevXoXfhYWFUlJSmzdvpj/r27dv8Kw1a9bwUZWiqOzsbAh548YNutzBwQEhpKioSJ+04y0qL168AEldXZ2enh5C6OvXrxwxw9YSJSUlPOOaNGkSQggbNXAMMB+mm5OGDx9ubGzMEaGcnBxCSE9Pjy7kNvaBeY7DiDx16lT++dAQGRkZ8Ihr167RdQYPbidPnqQHXr169a5du+iaNLsWvXr1CrKaw9gHK0Hoxr5G08tQDealwxMmlbkhwOZFX1yTn5+PlyEAYODgMPbV19dDLtFngxRFGRkZIYSWLFmCJeXl5WBuoM//YdwvICDAMZPhaexjXid5AgXHkSg3Nzd4EEe2Dxw4ECG0Y8cOLIH91HiFFwZ2bgoKCuL5DNgOXFxcOELC/J9uu2GeIubGPuZ6YmMfXlAGYHuft7c3SBqyL6xZswb9r9EqNjYWQtJ7LXx4K8c80NTUFCFkZWVFF8IEWENDA0tevHiBEAoMDKQHg88eCCG8wI15tjcEGPtMTEzo1vbExEQwE/ft2xe/ApKSkqAyHzt2jB6Dq6uroaFhow9imG/wNU5YWJhurAerBy5E5m8fsPYOHjyYnjoA3iB0Yx8clUA3rFAUNWTIEI5CTElJgaHF3bt36SFv3LgBCnAYQxmWeJOexRP+xj5xcfGsrCwsrKurA4MINnO08J1O8Wqw+fn5UGfoxj6witKtZo12xc0z9mFiY2M1NDQgBo7ay01NTY26urq6ujr9nUJRFCzH4zb2NTquY9iWHRwchIWFExIS6MHOnDmD+Br7Dh48OHbsWI4qB4A5W0NDg760ubq6un///gghaWlpuKtJoymKosCGyGHse/bsGSQnPDwcC6uqqqD2Xr9+HQt51lLIIg5ro5OTE9jamgH05xzt69SpUwgheXl5+vg/Nze3S5cu9JV0YOwbMmQIvQLgWUZsbCwW8uzk4+LihIWF586dS390RUUFuFHmNvY9ePCAHnL8+PEIITs7u+YlnEAgtC7kzD4C4ffCxsbmzp07+C+Lxfr8+TPYF4Bz585VVlaamppyOPAVFBRcvHjx1q1b/f39k5KS4GsqhNHV1QULS8tRU1Oj/4VtDrm5ubW1tfCx1N3dXVJSEjbMYlJTUxFC9fX179+/t7KyavQpeGcBMH369J07d8bGxt67d+/SpUsIIUtLy3Pnzuno6NAPQ6EoCiEEx+IghERERKSkpDIyMqZNm+bh4QGDWgsLC2yku3v3bklJSWRk5Nq1a3EkbDZbQECAopktGgI/GuYbGFhR0qlTJ/pZNp06dRIXF6+srMRPf/r0aWRkZIcOHbhPxlm6dKmXlxcck7Rw4cKwsLD79+8rKytznJYiLCwsJSVVVVWFJQkJCa9fv9bV1aWnCCEERRMVFVVWVsahLR2wVmzYsKF79+64mA4cOMA/HxpCSUnJysrKz8/P3d191qxZWOdBgwb5+/tfvnwZn1hUVVV148aNL1++4HtbpRY1SqPpZaIG89JpCCaVmX8SJk2adP/+fZjAyMrKrlu3jh6Gp6dvmL4ihLp27UqXw7ZQOK8TiIiIgGMQS0tL8Z4y8BREUVRmZibo0BAtrJPon4bGs5Whf3IAA0ZS3MrKy8thNdaAAQM4op0xY8a6deuKi4vPnDkzZMiQ6upqWBw9ceJEjpAcWdTyFHHDXE/6JY5+cuvWrZcuXYLTTu3t7fk8bvPmzSoqKjAhBKh/jv8vLCzEeduQj3gokUZrDhhkfXx8YAktkJaWBj9CQkJgIyTDbG8UQ0ND+mFk6urqDg4Orq6ucXFxMTExOjo6CCE1NTU7O7vr16+fOHFi1apVOIHe3t4LFixo9BEM823JkiW7d+8uLS29fPny1q1bQfjq1StJSUlcgszfPqCkubk591Frnp6ewcHBeFs94tVvsFgsOOqrtLQUC3v16qWqqpqcnEwvL9TiEm/Ss5oBfOaka9WjR4/Y2FhcqVr4TueJrKzsvXv3SktL6Seaceczk664JfTt2zcsLKx///4ZGRmPHz/+66+/+AT29fVNSkpavXo1x2lx169f37JlC/exa42O65i05aysrGvXruno6HAc/blixQpTU1PIEw7q6+uXL19eV1fn7+/P82A7EKqoqNCPZezQoYOTk9OiRYuKi4vDw8PNzc2Zj6ZAyLOeYwXo9VxMTExWVjY/P7/R2gsV4ObNm1paWlu2bIE37IYNG5p9dJ2jo+P58+fj4+PfvHkzbNgwEMJqytzc3ICAAPjKiBByd3efMWMG90m7ysrK9FzFu5LT0tLwLnieWeHi4lJfX4/jB8TFxaOionJycriLkmf9wdWDQCC0L8TYRyD81ggJCQ0aNGjbtm0/f/4ESVRUFEKIp/EOex6IjIykH5uNJ/atDhyhQlFUXV2dqKhoQkJCRkaGpqYmrA/C4L/9+vVjEi33rMbW1nb37t2FhYU5OTkKCgoiIiJ4GUtRUZGnp2dISAisnsNLlhBCzs7OGzdu9PPzCwkJ2bdv36JFi0RFRWHzEUIIloAZGBjAPBADJ2o36RhsOg3Nl2BDK54eQDnKyclxJ3bIkCHCwsL19fUwcXr79i1CCKvNB0iRurp6Q/nPn9mzZ+/fvz8rK2vixImTJk06duyYuro67ARvHo6Ojn5+fi9fvkxOTobhYGpq6qtXr0RERCIjI798+QIH5F+/ft3U1BRPlVurFjUK//QyVIN56TQEw8rMk7/++svPz+/79+86Ojpr1qzZvn17586dW1JkMDdgs9l09eDH9+/f4cM+Qqh79+5CQkJsNpuP7wighXWyIfi0MkSbhH/9+hXSwt1hiouL6+vrv3r1ClpZTExMUVGRgIBAowf5t0WKmOtJh6Pr6N69+9ChQ+F8Lv6PU1BQwMaCxMRENzc3vEaj0SrXENw1BzJq+PDh9AmnkZERfLuCJsM82xuFuyOdPn06mFCxsQ8htGnTphs3bqSnp1+/fh1Wvv/48ePTp08PHz5s9BEM801SUnLu3LmnT5++ePHi5s2bwU7h5uaGmzlq+tuH50tcT08PFjQhhEJCQjw9PeGTCV0ZISEhUOD79+/0e3v27JmcnMzTvQZDuEu87Z7VEDACqa2thb9t9E7HHpnq6up8fX19fX1hkRQ9n1u9K+ZGWlp6//79c+fO/fr1K/+QsIqQ29+OiIgIvHP5wzGuQ8zaMiw05n4oQgjXUjq1tbUTJ0589uxZREREU11YwNoxhND379/Nzc2Zj6aaAXc950n//v0tLCweP368Y8eOO3fuuLi4mJmZ4ZPymoGent6QIUPCwsLc3d2xse/ixYsSEhIVFRWXL18GYxybzXZ1db1//36jEeITD3F7aYiG6o+0tDQT2yVHqyQQCO0LMfYRCP8Cxo4d+/nzZ/gNEzm8dIUOtjgkJCT8Mt3oZGZmIoSofza5tCL44PDs7GzsHTglJeXw4cOpqanOzs5OTk7dunXjWEjl7OxcWlq6f/9+2FBz7tw5d3d32OiHtdXW1m51bZkA5Qi7IDlGqEJCQkpKSqmpqVCOEFJCQqLROCFFcnJyzUuRlJTUy5cvLS0tk5KS7t+///jx4w0bNuzYsQOG+81g7NixvXr1+vHjx+XLl+Fs6WPHjs2aNauwsNDHx8fDwwMmHufOnaOvp2u7WsQB//QyVIN56fCn0crMk6FDh969e3f27Nlw2PbVq1dPnDiB11G2CgYGBioqKqmpqR4eHtjTX15eHovFGj58OE+/n3RaWCdbCDZ7ZWdnc9saoMOktzJRUdFGa3tbpIi5nvyBfpLn24GbkJCQw4cPd+vWbd26ddQ/26VbCzabDWqYmZlxO1LHMM/2ZkB/a2Chjo6OlZXVw4cPjxw5MmfOHAEBAW9v7ylTpjBfgMMk31auXHnmzJnU1NTHjx9bWloWFRUFBQWBGyKgtd4+bDb7+vXrFy5cmDBhwsGDB+/evfvp0yeOMNbW1jdv3rx69equXbuw0RCOl22hzZ2bX/ksbtrunV5WVnbq1KmAgIDly5d7enrOnTsXlnhjfkFXjBCC4+qg++XzERe+DTN5gzCBYVtu6kOFhYXZbHZ9fb2Njc2HDx+atAKuW7duYMKrrKxETRlNtSm3b9+2tbV98uRJdHS0ubn51KlTz549i8erzcDR0TEsLOzOnTunTp3q3Llzdnb2jRs3bt68OXHixICAAPjy7e/vr6SkxNOc2jzYbDYsymut+kMgENoXTnf1BALhN4S+dQg2C/BcIY9XhTS63KaNgNFnWloaPvuvtcADJvzj5MmT/fr1U1VV9ff3HzFiREM37t27982bN/CdPzIy0sjI6Pr163Rt4+PjW1dVhkA51tbW5uTkcF+FooRyhHE2kz0RLU+RpqZmVFSUs7OzqKhobW3tvn37RowYQd+T1SRgazlC6MqVKywWq6Cg4MqVK87OziC8du1aTU3N+/fvi4uLx4wZw5GKtqhF3PBJL0M1mJcOHxhWZp5Mnjw5JiYG+ofc3Fx7e/sVK1a0RBkORERE7t69C16t4XxDFou1detWFRUV7ISED79DK0N8O0x6K6upqcnNzeUfZ1ukiLme/IHusdHpZXV19YIFC8aOHfvXX39duHABnwXWusCKGP4ZxTzbmwH3WwMAjw3fvn3z9/dHCHl7e+P9ffxhnm8aGhpgl4Glhd7e3tOmTaPvs2uVWpSammpsbLx79+7r169v3rxZVlaWZ7DTp08PGDAgLS1t4cKF0JtduXIlNTX12rVrTd0r3Si/8lnctFFv8+rVK01Nzc+fPwcFBdnb2zdkmG7rrhghpKio2LFjR3V1df7bNeBqK5q3mLTlpj5UUFDw2rVrPXr0SEhImDlzZqNL5zjuhWNSYN8u89FUm9KpU6fHjx9fvnwZ9PHx8dHV1W10GSYf7OzspKSkKisr4TBNFxeXqVOnWllZGRkZYa/QZ8+exR7AWwso7vZaNEAgEFoXYuwjEP5lgDeulJQUbhMMni9BmF8PLPuvqqrCR8hzgM+obiowhlNXV4cje27cuOHk5GRkZOTs7NzQLRRFwV0mJiYRERFubm6SkpL19fWLFy/Oy8vD2tJPSKSTmZkZHR3dPG2ZgMuI5+4SKEoIAyt9fvz40ajxC1L05s0bnkNeFouFj6DmCUy8xcXFDx06FBsbO2HCBITQ+/fv9+zZwyBBvFmwYIGIiEhGRkZgYOCZM2cmTJjQu3fvMWPG9OzZs7Cw8P79+2fPnnV0dKR/jW+7WsQB//QyVIN56TQEk8rMPwnKysq+vr4vX76Ew3TOnTsHVozWYtCgQdHR0XJycleuXDExMZkxY4aqqmpsbCyTrZctrJMtpKmtDCEUFxfHP862SBFzPfkDKoE7BT6sWbPGw8PD2dl55MiRTVWVIYKCgjC7bqiDDQ0NLSsrY57tzQAXED7gAjA2NgaT+qFDh8LDw2traxnmQ5PybdWqVQihgICA1NRUNzc38JODafnbp6amxsrK6sOHDxcvXsT763nStWvXsLCw4cOHf/nyZejQoVOnTv306VNcXBzeC9mK/MpncdMW7/TExEQrK6uSkpKrV6+Ki4s3FOzXdMVFRUVVVVXcJ9NxAKtNnzx5wvOtlJmZCcewMoRhW4aHJiUl8czk8vLyoqIiDmHXrl1v374tIiLy5MmTLVu2MFeptLQUTrqAwzRaq/9sCaWlpbDMcP78+YmJievXrwcHVvQjNZuKuLj4nDlzEELu7u6lpaUXL14E39bwufTy5csJCQnh4eHTp09vpUQghJCgoCBsP/fz8+MZgGNNK4FA+M0hxj4C4bcAn/PdKLBeoLa29u7duxyXkpKSEEL9+/dv+UqN5p3cpKioKCMjgxDau3dvXV0dx9XAwMDQ0NDm6QOflPF5SeDNjWPnAofOtbW1f//9N/wWFBRcuHBhaGhohw4dKisrYU80jJi/fft2+/ZtjsfV1dUtXbq0JeetNMqIESNggQD38qiqqqrMzMyOHTvCBElbWxuEDS2kwpUHUsRisXbt2sUdDJzP8uHkyZPYNZuamtrDhw/Bk13zzjUHFBUV4WSZ06dPnz17FtzYQXEghI4fP37//n2O0XALaxE+yZvjXhiI0+GfXoZqMC+dhmBSmRsC8hMYOXLk+/fvQZ+WFBk3VVVV69at8/f3f//+fWho6J07d7Zu3cpn3kunhXWyhfTs2RNm3eBLkeMqdJhTpkxB/5QjQghcAHHTWq2shXryJz4+XlhYGMxMdOjVqby8HI7bp1e5Zh/VxwfIqCtXrqSkpHBcysjIOHr0qKSkJPNsbwbw1rC1teW2hUHDCQkJcXJyWrBgAfdRX9w0Nd8sLCzU1dXZbLajo6OEhARHA2/52+fFixdgVWGiz549e+bMmRMZGfn582cfH5/Tp0/ztw+2hJY/q9m1sS3e6ZcuXaqoqOjduzf9rAZuDZl3xS1pa2FhYQghDs8J3ICfn/T09JMnT3JcKikpcXJyovu7YAKTtqyvrw+OUDZt2sTdbFeuXMn9CkYIGRsbHz58GCF06NChmzdvMtQHzsocPnw4WCGZj6ZaEY5yfPfu3YMHD+B3586djxw5cvHiRYRQeHh4S/YowEeCjx8/rlixYtSoUWDctLOzk5SUjI2NXbRo0Zw5c5p9GGVDQP3x8fHBDnwxnz9/xl6YCQTCvwJi7CMQfgvKy8vhB8/xEJ2ZM2fCqgR3d3eOEZWnp6eAgICrqyseyUEA5oPLDh06wIg2IyMDJPhesHdwWD3wEbw4GLifi4yMnD59enFxMVbj2rVry5cvb55zuurqak9PTyMjI7xbATa74XMMEUIxMTH5+fnof08FfvDgQU1NDf7bv39/WPMCziLmzZsH+54WLVpEP509KyvLysrK1NS0oV1RQEO7TkDObR4COS6yXr16wW6ye/fucXzxvn79en19/fbt23v16oUQmjVrFhyUs3fvXvouv9TUVHBEUFJSAhJdXV3YD3v+/Pl9+/bhQqmqqtq4ceOXL1/wmWtYH45U0D/dCwoKLlmyBNFcrdXX169bt27EiBFM9m9iYLT65MmTIUOG4HnpggULhISEwsLCrKysuN25tqQW4YO6Xr9+jYUvXryAKo0bGpP0MlGDeek0BMPKzJPv37/D2eSAtLT0jBkz0P96x4NIOCpkQ7UXAnN0LNOmTRMRETE0NOSvDEIILxjB8TepTvKEp6pYyDNddP3Pnz+PEEpNTQ0KCqKHTExMDA0NHTNmDCyL0NDQAIewt27devHiBT1FkMPNa2WQIUx2qDHUkw8RERFhYWHr1q3DrQz3YPT+PDMzE/ShVzncWOhVDv/myGSey4K4aw40kIqKCgsLC7rHhvDw8JEjR65fvx41Jdubgaurq5SUFLe9AyE0fvx4eBe8e/cO3HQ0CvN8AwQFBWEX55MnT7j32TF/+zT0Ese9DV0fMC1xKHP48OHr16/DEiH+tLDEm/QsnkCqc3Jy4IkURdHfp9xvVdABZ04L3+mIV4OFfE5OTsZVsaysDIxN9HxutCvm2RgbUiMmJubVq1cc6aUo6u+//x45ciR8lOKDvb09rJndvHnz7t27sZ7x8fHm5uarV6/G1u0mjev4t2Xwf40QCggIsLGxKSgogDBFRUUODg6amprYBxdHnXFycpo6dSpCaOHChdwnTsJzOSTnz58XEBA4duwY/GU+mqInkEklR7zqeUO1lMPEPHfuXDExMSUlJWyMu3jx4ogRI7Zv3858TK6trQ0Lk729vSGNCCEJCQk4DvL169ccS4YBhsWKGmjyW7ZsERERoShq3Lhxvr6+OO2BgYFz5szBdm18C88HtcUHJAKB0BwoAoHwGwDuCxBCSkpK1dXV/AMnJibC2r0NGzZg4f379zt27HjkyBF6SPiYqaCgUFNTw1AT8Funrq7++vVrT09P+EhbUFAAo8NVq1bRA2O/CtHR0SCpqKjAzlI7deo0duzYyZMnq6mpycrK4jANUVNTA+f9L126lMVigbCystLOzs7AwCApKQmHtLGxgUfMmzfv8uXLjo6ONjY2sIXH2Nj4ypUrfn5+8DV1/fr19PjV1NSsrKywxM3NDY96tbW1p06dOnLkSFFRUUdHx0YzCo9Kr127RpfPmzcPISQmJlZWVoaFZWVlYIF1cnLCwurqatg6Onz48KqqKhDGx8f36NHD1ta2trYWh7x58yYciKOsrHz69OmwsLCbN28OHz4cDoGSlpZetGhRZGQkRVERERHYZ0L37t2tra0tLCxkZWUNDQ1LSkpwhPj7/M6dO7Fw06ZNMjIyP3/+pOePiIhIaGgo/MUbWgUEBMLDwxvNIoDNZsPE4+3bt3S5lZUVQig4OJj7lpbUIjabDWYpCQmJ3bt3u7q62tnZTZ8+HZbEdu3a9dChQxkZGUzSy1AN5qXDEyaVuaF7jYyMTE1NceWhKMrBwUFFRaWgoABLwPYkIyNTV1eHhdi797Zt2+gRwumWenp6WIKdGygqKhoaGpqZmVlbW8+aNWvVqlVubm4VFRX02z08PCDwq1evsJBhneRJXV0drK9cvHgxXX7lyhWIMCgoiC4fPHgwQkhfX58u3Llzp6CgoKKiIu5DysvLzczMdHV1f/z4gYN9//4dntWhQ4cdO3YEBwcHBgZaWlrC4hFBQUE7O7u7d+8yTxGLxYJ7R44cyT+ZTdITr7ag9zwpKSn/j73zjIsieR5+k0RBBERAQZEkoGQTAooBE4oRERVUzBHFiGfO5xm5wywiKJgQAXPADCiKAqJkEEmC5Lik3Xle1P/6md/usgwswbvr7ws/0tvTU9Npqmu6qwwNDR0cHKqqqnBiSUkJeLaaNGlSRETErl27bt68WVdXB+Z1SUnJPXv2nD17dsqUKdi52KJFi9zd3ePj4ymKwpuSjh07RpcT7BdDhw6lJ+LthMXFxTgRG31ERUUtLS3t7OwGDBggKirq6+vbjGpvCHjNDRw4sKSkBCcePXpUSUnp0aNHDV315MkThNC4ceMElEyHeb1hiouLpaWl5eXl6Y2CYfL2we/EqVOncl0eFRUFl6uqqp4+ffro0aNWVlbYcHns2DE8tOF8ZZcuXYyNjYcPHz5x4kQHB4clS5YcP348MzOTXibWQz5+/EhPZ97izO/FF2gUhNCOHTvCwsLmz5+fkpJCUdTatWvhvZOfn48zV1dXQ+XY2dk1qVYbgu+AxcZiS0tLLy+v7du3W1lZjRs3DqZELy+vkydPUgymYr6Dka8YLBYL4pn26dPn3r17kFhZWTlr1ixra2t6bQuuSbgd9FgzMzMTExNRUdFTp07hPMz1OorZWM7KysJh4kRFRfX19c3NzaWlpWfOnMnhcHC2bdu2IYQkJCTws5SWlkIfU1RUfP/+Pb0Ooahz587hEs6cOSMuLn706FG6zMy1KRaLJSMjAw1BL8HHxwckDwkJwYn5+fmgua1du5Zet7y9FNx63L17F2cDn3dYJy8uLsZB5LlmVMFcvnwZITRmzBh6YmRkJEJo5MiRfC+B7z2Ghob0xLdv38LdPTw8cGJDkzzdeYucnNywYcNgcyu9dQIDAyFDQEAA/VoYHUpKSliNJxAI7Qgx9hEI7cylS5cmTJhAP1WhoaGxaNGib9++CbiqsrJy1apVXbt2NTAwcHFxsbGxMTU1ffv2Lc5w7949MKYAZmZme/bsoZsAGiIkJAQO6ImIiDg7O8OuOrypB5TmhISE7OzsNWvW4IMturq6Xl5eUEJ5efn8+fNBW4VyZs+enZaWxqQ23r9/b2NjIykpKS8vP3bsWFtbW21t7S1btnAZK799+4bd1igoKLi7u9NP1U2cOLG8vLy6ulpJSUlXV3fcuHF//vnn6dOnhw4dOn/+fPqCkKKox48f0z/59urVy8vLi66Y8gKRHLAAampqu3btysvLi4qKWrlyJX5wCwsLWAacP3/e3NwcEmVkZNatWxcXF4dLO3XqVK9evVRUVBYvXjxr1iw9PT3Ys8nF69ev8elsERGRwYMHx8XFde/evV+/fvv27UtOTsY5U1JSLC0tcY+Slpbevn17WVkZ/FpUVLR9+3ZclIyMzJo1a0JDQymK2rJli7GxsYaGhpubm5eX1+bNm/X09OjKa0ZGBizLEUJcqrZgjh8/bm1tzZV4584dAwODhi4RphfFx8cbGBjAhZ06dTp06FB9fb2trW3Pnj1dXFxevnxZX1/P5HmZi8G8dXhh0pkbunbkyJFGRkYmJiYHDhzw9PR0cnKysLDAvcvb29vBwQEvfUeOHPnXX39RFHXlyhU8ouXk5DZu3JidnR0ZGUk/KDpr1ixsz929e3dDB4XU1NRycnIoioqJiVmzZo28vDyexLZv356RkQElCO6TDeHj42NjY4Pbcd26dZGRkYWFhdu2bQPfnSDA7t27S0pKQkJC8N43aKbHjx/jol6+fGliYiIrK2tnZ7d06VJ9ff1169bxflbJyMiwtrbGcvbq1Ss4OHjWrFnKysqrV68OCwvDM0OjT3TixImRI0fiipozZw59VdwQDOW8evXqgAEDRERE1NTUpk2bNmLECD09vStXrvAWePDgQRBASkrK3d0dEh89eoRbqm/fvmARw9Lu3LkzLS1t69ateJOsrKysq6trbGxsWFgY7iQiIiILFy58+/Ztfn6+i4sLjlpgbm7u7+8PN+JwOEeOHJGVlcX1MGTIEF77PvNq50t2dvbixYvl5eU7duxobm5ub2+vp6dnZ2eXm5sr4Kq6ujoxMbGGDC58abTeeC9ZsmTJ5s2bGypQ8Nvn+PHjEK8catvOzg63IHDw4EEcqGHcuHGJiYmFhYWw7UhaWvrOnTuQLSgoqKEgqlJSUk+ePKEo6uXLl6tXr8YhRPr06bNjx46SkpKmtjiTewmAw+HgIa+iovLixYvMzMwVK1bg+cfExATeqr6+vvBhEiEkKirq7OyMjYnNeKdTDQ/Y2tpafGy2Q4cOK1euLCkpef78OXRXbW3txMREqrGpGLcXrgqupuSCHsZ30KBBY8aMGTBgwNmzZ+HNxZDPnz/DLAHl6Onp0afEpup1DMdyYWGhnZ0d7hvy8vIeHh5Y7OfPn8+fPx+7gDA3Nz9w4EBNTc2jR4+wMBISEuPGjYPPRWDsMzMzW7Bggba29syZMw0MDHr37k1/EDqCtana2tp9+/bRu42TkxO88Xfu3In7ra6u7r59+yiKunnzJpZKSkrK1dUVbM28vZSiqEePHmlpafXu3XvhwoXnzp37448/+vbt+/vvv+O7czgcMzMzuIr+yblRWCyWkpLS69evudL79++Pxx3mzZs3+PMhQmjSpEkwTe3atQvP53Jycps2bWpoksdF+fv7093ywgyDf926dSs+FK+srLxly5bKysrw8HC6ymFtbc0rNoFAaGNEKCH8oRAIhHYnLS0tMzNTTU2tBWONFRQUxMXFaWhoCOPWp66uLi4urr6+Xk9Pj+7shglVVVWpqakFBQXy8vL9+vXjG/yuvr4+Nja2pqamf//+OENkZKSsrCx4F4ZypKSkKioqEhISqqur+/Xrh21VXPz8+TMxMVFNTU1NTY2J/6YWJz8/Pz4+vmvXrv369RPgTyc/Pz85Oblv376w5kxJSYEtYLxUVFTExsbKyclpa2vj78mCqayslJaWpigqPT39+/fvysrKOjo6XFH/2Gx2ZmbmkiVLli5dam9vz/Dpqqur8/PzuboTh8PBe1Qbotm9iM1mJyUlVVRUGBkZgakuMzOzZ8+e9MZl8rxNEoN563DBpDPzBR6hrq4uKSkpPz9fS0urNVxxnT9/Pisry9HRMSsrq6ioqKKiorKysrKyMisrKzAwcOrUqSdPnmRSTjP6ZMtSWVkZGxsrLi7er18/AT4HIZuqqipU5rdv39TU1Ph2jFZ6IoZyFhYWpqenl5WV9ejRA3ZU8eXbt29ZWVmGhoZycnJ0yT9//iwjIwN7ORFC1dXVkZGROjo6OKp7S0FRVFpaWl5eno6OjoCQrMyrnS9sNjslJSU3N7djx466urr0h+XL27dvJ0+enJWVhU35TGhqvYE8goUR5u2Tl5eXmJiorq6Od1TBLGRsbIxnqvDwcA8Pj4MHD2ZlZeXn58P4raioKCgoePr0aW1trTABQ7kQ/l4cDic2Nra6urp///7CjKmWfacnJyfn5uYaGBhga29qampJSYmxsTGELmU4FfMdjHxJTU39/PlzeXk5aAV0zwxNoqSk5OvXr71792YST6lRGI7lmpoaGCY6OjpN9Q9IZ+7cub6+vqNHj3769Glubm5SUlLXrl319fUFNyhDbUoYeHtpTU2NuLi4mJjYz58/k5OTJSUl9fX18eZKTEZGhpeXV35+fpM83yUlJfFqSklJSdra2q30gJisrKyMjIx+/fo12mMJBMKvCTH2EQgEAqEJlJSUWFtbh4aG8iqyhH8fu3btCgoKioqK4ruoSE1NdXV1pbvHIhD+ETg5OWlpafGNsvJv4tGjRzNnzkxISOAbm4LD4RgZGbWUsa8t70X4L0A39rW3LC3GhAkTDh48aGJi0t6CEAiE/wTi7S0AgUAgEP4xlJSUzJo1y93dnVj6/gu8fft27969y5Yta2j7QGJiIgQ6IBD+QcTFxQUFBUGY438xFRUVTk5OKioqDUWhLSkpEbx3+Ne8F4HwT4TD4axbt87a2ppY+ggEQptBovESCAQCgSl//PHHoUOHhg0b1t6CENoCCLwYEBAQHh7O9VNdXZ2/v//Zs2fBdz6B8IsTEBAwYcKETZs2Xbx4cfLkyWvWrFFWVm5voVqXgoKCwsLCxMTECxcu8J7j+fDhw8yZM3FI03/QvQj/ESDK7b8mrqu3t/fQoUMhtDGBQCC0DeQYL4FAIBAIBD6UlpYOHz48JiYGIWRkZNS/f/+uXbuWlZXl5ubGxMQ4OTlt2bIFx6UlEH5lDA0N8RlSAwODyMjIJnnr+4fi7OwMYUZ79uw5dOhQZWXlurq6vLy86OjoQYMG7du3j6Ff0V/tXoT/AkOHDg0LC+vdu3dqaipz950EAoFAwBBjH4FAIBAIBP5UVVVduHDh2rVrycnJRUVFMjIy/fr1GzNmjIuLS4tHciAQWg9/f/99+/alp6fb2NicPXsWR1r41+Pv73/p0qXo6Oi8vLwOHTqoq6tbWVmtXr0axxj5h96L8C8mKCjowoULDx48gD8tLCyGDRu2atWq1ohARSAQCP9iiLGPQCAQCARC43A4nNaO/UcgEFqJthy/ZK4gEAgEAqHdIcY+AoFAIBAIBAKBQCAQCAQC4V8C+exGIBAIBAKBQCAQCAQCgUAg/Esgxj4CgUAgEAgEAoFAIBAIBALhXwIx9hEIBAKBQCAQCAQCgUAgEAj/EsTbWwACgUDgw4kTJ65evbpv377x48e3tyztTHl5+cmTJ2fPnq2urt5QnuLi4mvXrn39+rWoqEhfX9/Z2blnz55tKCPicDjOzs5ZWVmenp6amppteesWISMjw9nZWUVFxdvbW1z8X/tmrK2tff/+/cOHD6OionCgwxYnPT09JiaGYWYNDQ0jI6NWkkQAbDY7PDw8MDBQRETk2LFjrXSXwsLCx48fBwQELFu2bOzYsYIz/1s7YVlZ2bNnzwIDAy0tLZctW9aka7OyskJCQu7fv29vbz9z5sxWkpDwS5GVlfXw4cPbt28fPHjQ1NS0GSXExcUFBwe/fPny8ePHLS4eprS09NWrVw8ePJCTkzt06FDr3Yg5UVFRz58/j4qKUlRUNDU1dXBwkJSU5JuzVXWGioqKp0+f3r59e9y4cU5OTi1VrGCqqqpevnx5//59SUnJ48ePt81N6fxTtKCQkJDKykquxIkTJ1ZXVz979ow3/4ABA3Df+Pnz59u3b+m/mpmZde/evZVEJRAILQBFIBAIvxgcDkdWVhYhNHr06PaWpT0pLS3dv39/165dEULR0dENZQsODlZWVp47d+63b99yc3M3bdrUsWPHS5cutaGk1MePH+Gdsn///ra8b0uBzT2hoaHtLUtr4eXlNWzYMHjMnj17tt6Nfvz4ce3aNQsLC6xpGBgYjP4ba2vrIUOGaGtrw0/Lly9vPUkaIjQ0dMSIESDAjBkzWukuO3bsUFVVhbsEBQU1mv9f2QmPHj2qo6MDz3Xo0CHmF2ZnZy9dulReXh6u9fT0bD0hCb8IVVVVy5cvV1BQgEZ/9+5dU0vIzs6eM2eOhIQEQqhbt26tISTg5ubWp08fkNPJyan1bsQQFou1YcMGrgjIffr0CQ8P583cqjqDl5cXHvInT55skTIb5dSpU4aGhq09pQvmn6IFPXv2bMyYMbiT2NnZhYSE1NfXs1isJ0+e2Nra4p8cHBwePXpUUFCAry0uLr5+/bqenh5CSFlZ2cPDg/4rgUD4BSHGPgKB8Cty6NAhIyOju3fvtrcg7UNtbe3JkycXLlyIvw83ZOwLDw+XkJAYMWIEh8PBiY6OjiIiIrdu3Worean6+vqZM2daWlomJye32U1bkLS0NEtLS3t7+7q6uvaWpXVZsmRJaxv7gIyMDOi6UlJSbDabN0NcXJyqquqiRYtaW5KGMDc3b+2VYUpKCnNj37+1E7JYrI4dOzbV2AdkZ2cTY99/jZcvXzbb2Af89ttvrW3soyiKw+GAVetXMPbZ29srKyvv2LHj4sWLO3fuxNulu3Xr9uPHD3rONtAZKisrYW9ymxn7gP79+7ejse8fpAVVV1fr6upCD/H396f/xGKxNDQ04KeQkBC+l58+fRoh9PTp0zYRlkAgCAXx2UcgEH5F3NzcYmJi6N8YAYqitmzZwpu/ofRfikOHDhUXFzPMvHjx4osXLwo+YMjhcObNm1dXV7d9+3YRERGcvnPnToqiXFxc6urqhJKYMWJiYjdu3AgNDcU7tlqVFm9uDQ2N0NDQmzdvCnN8MjQ09O7duy0oVWvQq1evNrsRVKaUlBTXfhOgb9++Bw8ebJsuWlhYePjwYa5EJSWl1r6vsrIy88wt0gl/QTp27Ag7tZuBiooK385D+BfTpFGD+L1Y22BoI4RERERa210GQ53h1q1bnz9/TkhI2Lt378KFC/fs2fPx48c1a9YghAoKCvbs2YNzto3OICUlJS0tLXw5TaWNvZdw0SJaUNuospKSkjt37oT/cx3d7dix46ZNm+D/AQEBfC+Pi4vT19cfPXp0qwpJIBBaBKJCEQiEfxJ79uwJDg5mnv7rEBISsn379traWiaZJSQkwNWOoqKigGyBgYEpKSny8vL4TCKgo6Ojr6//48ePhnS1fzq/YHPn5OTMnDmzqqqqvQVphLY0nTRqtJowYYKZmVlri8Fmsx0cHL59+8aVTl/rthJtcIt/BMLUAzH2/ddoUm/h+2Jts3HXqp2Tuc5w+vTp69evy8nJ4RRxcXF8gj48PBynt5nO0C5T379grmgz3cbe3h587V2/fr26upr+07x58+DzjK+vb0VFBe+1L1++XLBgQRsISSAQhOcfPy0SCIT/DgEBAXv37mWe/uuQkpLi4ODAZrNbttgrV64ghAYMGCAmJsb10+DBgxFCHh4eLXvHX4FfsLlZLNb06dN//PjR3oL8w+jWrdvKlStb+y7r1q3j63ecQCD8o2mlF+uvAPNHq6mp0dDQMDEx4UqXkJCwtrZGCOXk5ODE/6bO8E+hLXUbCQmJOXPmIIRKSkq4LLzS0tLz589HCJWXl1+/fp3rQthD2mZxVwgEgpD8q46KEAj/OD5//vz06VP8p66urq2tbUhICFcsy6VLl8rIyCCEzp8/X15ejhAyNjamb6H/+vXrnTt3YmNj5eTkBgwYMGfOnE6dOtFLyMzMvHLlysSJEw0NDc+dO/fixYuxY8cuXryYeQktLjmbzX78+PHnz5+3bNmSnJwM51Xd3NzAXUhNTc3jx4+jo6PxWQNvb+9ly5ZRFFVYWAiZNTU1p02b1lA680f7/Pmzv7//lClTBg4cGBkZefHixYKCAhMTk7Vr13bu3FlAJWCqq6uvX78eGRmZn5/fq1evgQMHOjg44C/b79+/t7OzKyoqQgidPn26S5cuEhIScMRGGCiKev36NUIIO8OmAw5ZwsPDWSyW4KaEoj58+HDjxo21a9eqqaklJCRcuHAhNTVVQ0PD1dW1d+/eCKHS0tKrV6+GhYWJiIiMGTNm3rx5XIUUFRUFBgZ26NBh7ty5OLGiouLevXvx8fF79uxhsVjnz59/8+aNoqLizJkzR44cCXlycnKuXbsG/+/SpQs4lcvPz798+TIux97eXk1NDTXcDXDOpvZkTGpq6s2bN62trWHNg2W7deuWnJzcvHnzsrOzT58+HR8fr62tvWzZMi0tLciTm5vr4OAQERGBELp7925WVhZCyMnJiX4Y7enTp69evUpMTNTU1LS0tJw8eTLX3T99+uTt7e3u7l5eXn7kyJFv374tWbKkuLg4LS0N51FSUsJ1GxQUlJqaCv93dHSET/SC+6Fg6urqLl269OnTJ1FRUU1NTRMTEykpKXqQjZaFw+GsXr0aXP9gWCzW/fv3Y2JiEhISQIYJEybwngBtdDbD1NfXr127Fu4SHR0NHWb48OEDBw7kypmRkeHp6fn161clJaXJkyfb2NjwltbsroWFuX379pMnT1gsloWFxfTp03v06MGVh28nBGJjYwMDA798+aKiomJoaGhtbS0gPDedFhndiHHrIIQqKiquXLkSHh5eX18/YMAAAfbcurq669evf/z4MScnx8jIaPz48bxN0xD379+HylRVVTU1NZWQkODbaoI5e/YsPTDlggULOnbseObMGXoeAwODcePGIYS+fPmCo7uuWLFCSkpKyKegKOrJkydPnz7Nzs6Wk5MzNDR0cnLq0qUL/PrXX3/Rz1QOGDAA78by8PCADV/i4uJr167FeYTUBJhPII8ePXr27Fl6erquri6803EoFToMayYzM9Pb2/vz58+ysrKjRo0Cz2uNwvDFWlFRcenSpbdv34qJiVlaWi5dupR3/5cw/ZAvTGpSwJTbJJ1BUlLy4sWLfH+CdxB2wdaCOgObzb59+/b79+/T0tJ69eo1evToiRMnNvSuiYiI8PPzy8rK0tTUXLhwYb9+/XjzNPqKbOpNEUIFBQVXr16lDyJzc3Oo4Zqamnv37qWkpLi5uWVkZFy8ePHLly/q6uqjRo2aOHEib1E/fvx48uRJdHR0Tk6OgYFB//79bWxseDtSs7Ug1JhuExsbe/Xq1fz8/K5duxoaGkpLS0+ePFlIhw/z5s2DsMWenp6Ojo70n+rr6+E/Fy5c4Hq3ent729jYcJ21F/LlSCAQWpF28BNIIBD+pqamZsWKFTAYlyxZUlVVRVFURUXF/v37IXHUqFE/f/7E+X/8+GFsbKyhoZGXl4dLcHV1tbW19fX1vXnz5owZMxBC+vr6cXFxFEXV19d7eHgMHz4clJKnT5/C9zqEkKioKLhtFlxCa0geHR29YsUK0BWGDh366dMnfPxkxYoVoaGhzs7OsIY0MTGBy6urq93d3eEztZKSkru7u7u7e3BwcEPpTCqHoqiTJ09i7yp37tzZvn27jIyMnp4efPQ2NDSsra1ttBE/fPigpKQ0f/78tLS0+vp6+HI+derU6upqyODn57ds2TK4y969e93d3U+dOsWke1AUFRoaChfyBuhISkqCn/bt28d7oY+PD/yakJAg+BZubm44ZuinT5+2bNnSrVs3c3NzWLypqKjk5+c/fPhQVVXV0NDQ1NQUdOtdu3bhEvz8/CZOnAgBEF1dXbF4Dg4OcBh54MCBcXFxurq6mpqa3bp1g+7n5+eHS8jNzYWNCerq6pDC4XByc3Ox8xpwFC1kc/OloqLi6NGjgwYNghvhQAqPHj0aPnw4POzGjRsDAgIUFBT69u0LK3xZWVlcsa9evTpy5AhcPm/ePJAKj9DMzExbW9uVK1cGBAR4e3vDY06bNq20tJSiqLy8vJ07d/bt2xcuz8/PNzY2hv/37du3trZ2+fLl8OeUKVMqKiroYi9cuFBUVDQwMBBSGu2HAIxQrgAdZWVlRkZGrq6uLBaLzWYHBARISUmtW7dOcM9pFAjLwNdNflhY2NChQ+kpb9++1dXVnTdv3ocPH5KSko4fPy4lJdWzZ8/79+9DBiazGRepqanu7u5gz7KwsICm+fjxI/w6depUhNCMGTMCAwNh4KuoqECBXCO0eV2Loih8BsrHx8fKyoqufSkrK79+/Rpn49sJMfv27VNSUrp79+7379+PHz8ONdCtWzdjY2MTExMBcQyEH90MWweTkJCgp6c3c+bM8PDw79+///nnn4aGhjBquAJ0hIaGDhky5MCBA8HBwSdOnFBUVBQTE9u9ezdXgbCg5QrQsWDBghEjRuTm5lIUFRUV1adPH1NTU8FtwZcvX77Au6Zjx46xsbGQGB0dDeEmoS3q6+shnc1mnzt3DiF0+PDhpj4FLwUFBYMHD+7fv39kZCSbzf7w4YOiomLfvn3T09PpGUCMQ4cOYTEoisrNzTUyMlJVVcU9UHhNgOEEwmKxpk6damJi8unTp/j4eHyar3fv3iYmJiYmJlhOhjVz584dOTm5Xbt2xcXFxcXFrVq1ysDAAMoUHKBDwIvV3d0dBkhCQoK2traKigpuUHt7e65ymt2CFEXBl0uuAB1MalLwlCuMzkAHTE44OGxL6Qzh4eFGRkYrVqx4/fp1XFwcdDYLC4uSkhKcB5Q6Dw+Pbdu2dejQwcTEBKzYUlJS79+/p5cm+BXZpJviKR2n1NTUKCgodO/e3dvbG3J+/Phx7ty5MOpHjx598+bNzp07g6ICODs719TU0G/t7e2toKDw+++/JyUlRUREuLi4IITMzc3pFSWkFiRYt7l48WLPnj2joqIoisrLywNzJFf9NA8cwjgpKQknVlVVycnJ4a8OMTEx+Ke6ujolJaWAgAB6DTfv5UggENoGYuwjENqZkpIS2Pu2Y8cOejoEU1uxYgVX/qFDh545cwb/6ejoOHbsWHoGCDFpaGhYX1/PZrOTk5OxAW7KlCkXL16Mjo4eNmxY//79wZIluITWkPzHjx/R0dF9+vQBncDR0TElJeXw4cPKyso+Pj4ZGRlfvnyBb7/Y2AeA6UdPT4+r5IbSmTza+/fvoXL09fWPHz8OdfLmzRtY9F68eFFADVAUxeFwwEAQERHBdZcbN27glEePHsFdYHXKHAHGPvzTX3/9xXthUFAQ/PrkyZNG7/LgwQPIbGpq6uPjA0H6fvz40bVrV4TQyJEjR40aVVBQAJlPnTqFEJKRkcG2p6SkpJcvX3bo0IGu5lIUVVdXB9/nlZSUxo4dGx8fT1FUTU0NqKpqamr0aIDQiNjYB6Snp4Ng9KhwwjQ3L1VVVampqTgQCt3OUlJSAo2rra3t7OxcVFQE1QJ7DOfOnYtzYpc3169fpxdeVlYGYShwSn5+PujQK1eupCiqtLQ0KSkJligIoUWLFoWFhd25c0dDQwMyVFZWggzjx4/nknzbtm2zZ8+G/zPsh1QDxj43NzdxcXH6CufgwYMggDCAsU9OTu4LjQ8fPly/fl1XV5du7IuIiBATE5s1axb98qdPn0KngjUPk9mML0OGDEEILV++nCsdql1TU3Py5MkZGRkURdXX19vZ2SGEFBUV6bXR7EkSG/u0tbX37dv3/ft3sH/BmlBKSionJ4cS2Akpirp9+zZCaO/evTjFzc0NISQiInL79u2QkJDCwkIBMgg5uilmrQMkJSV16dJl2rRp9KHt5+cHAtCNfVFRUTIyMqGhoTgFb5q7efMm/Ua8xr6HDx8ihJ49e4ZTwsLC+vXrJ6ASBACbxzt06ACfrAB8rg1b7YGQkJCePXvicMnMn4KXWbNmIYT++OMPnAIxZOmvzqioKHgT8QYytrS0PHfuHP5TSE2A+QQC+8uwnZrNZkM2LS2tkJCQkJAQaHqGNXPt2jURERF3d3f6LWBzN2IQjbehFysY+6SlpUeNGvX27VtIPHHiBGT+9OkTvYab3YIUP2Mfw5psdMptts6Aqaur69GjR+fOnWGSoVpIZ3jz5k3Hjh2PHj2KU/BrevPmzTgRjH19+/bdvHlzeXk5RVH5+fnwbXXy5Mk4W6OvyCbdlMvYx2azXVxcbGxs8PxGUVRpaWlycjJsY1RUVJwyZQpY+ePj48eOHQtlHjhwAOc/evQoQujs2bP0SoDDtl26dMnMzISUFtSCuHSb4uLijh070tXskpISRUVF+tf0ZoPjVrm5ueHEK1euKCoq4n33Li4u+Kfg4OCuXbu2yMuRQCC0DcTYRyC0P7BFTkdHh574119/gX5AX8T+/PlTXl4eNCeKop4/f44Qevz4Mf1CWK0hhPCWH+yZfuPGjVy3ZlhCi0tOURTsLJCWluZaTQHwWVsYYx/DR8OnFeh6JPW3gWDp0qWCa6CgoAC2AdI1ewcHBy4dtDWMfdiLM33Jx3tHrh0xfMnNzYXM165do6eDW5bu3bvTl8FwHBsh9Pz5c3pm0J7pai5FUdu3b4fOQC8By4YXIRRF7d69G/EY+7BgjRr7hOzJ2N0el51l6NChCCFbW1t6IghA7/YNGft27twpISFBt5tQFGVvb48QEhcXz8/Ph5RLly7B5ffu3eOVDRYb4uLi9OricDhaWlp4McywH1INGPtg0xl9n0JBQQEcKRIGMPYhhDrTwCePsLGvrq4O9jN++fKFqwTYmaKiooK3bwiYzRpCsLHPzMwM224oinr37h2UD6sySriuhY19W7dupaffuHED0hcvXowTG+qEEMOEnvjt2zewAd26davRxxdydDepdaZMmYIQgh0omLq6OjjSRbdYWVlZmZubcxUI8YiMjY3pibzGPjDPcS2/7ezsGq0KvmRnZ8Mt6HuNYQMLQujPP/+kZ16zZs2ePXua8RS8QK3SjX1wfHjw4MH0bBCS3sDAgJ6Ynp6uoKCAG054TYDhBFJRUQHZ6NupYEeYiIgI3frApGYqKipUVFSUlZW5Ng/it56Qxj4pKSn6ht+6ujr4PEn/XCpMC1L8jH0Ma7LRKVd4Y9+tW7fQ/25SFl5nqKmp0dLS0tLSos+ZFEXBfi5eY9/69evp2Q4dOoQQUlZWxilMXpHMb0o39lVVVU2fPt3NzY1uTcOAqV1HR4fNZuPE6upq2FUqJycHPfzbt29SUlLdu3fn+pjEZrPBi8LEiRPp6UJqQQJ0G65vLa6urvCBSkiys7Nht6+ysjKu3hEjRri6upaVlUFIZXl5eRaLBT9NmzaNboQVUu8iEAhtAPHZRyC0P8uWLTtz5kxSUtKbN2+GDRsGifBx7OfPnw8ePIAVFELo4sWLs2bNwo7kPD09EUIBAQGw0wHIzMyE/4SGhoLqA7tIEEJjxozhujXDElpcciyVkZERrKm4wDI3G4aPht1Uw8EKTM+ePRFC4H9NAAoKCrdv3y4rK6P72KIoCiEEDndaD+wSBW7HBU5k4rIN219gLYSBA4CdO3emu1/p3LmzlJRUVVUVNiIAfJsMSpaSkqKXAHWLEMrKyuJ1W9Y8hOzJDfU3kL95fQMhdPHiRRkZGdD1MRkZGQih+vr6d+/ewUpewAhFCC1dunTv3r1lZWVeXl7btm2DxJcvX8rIyOBQtkL2Q1jcTpkyJTg4GJYrCgoKGzZsaPRCJnTr1i0/P58u1ffv31esWIENYU+ePImJiZGUlOT15bR8+fIrV66A58RFixahxuqqGfTq1Yvu+Qj7t8rMzISjf0J2LQAf0QVmzpy5e/fu+Pj427dvX7hwARL5dkI2mw2OUMvKynCiurq6hoZGWloak04o5Ohm3joRERHBwcG9evXiihUgLi4uKyvLYrFwSnJy8uvXr42MjNatW0fPCftiPn/+XF5eziUtHeiumzZtUlVVhRGEEPr9998brQq+qKio2NraBgUFXbx4ETzWg8yDBg26f/++l5cX9pXGYrGuXbsWFRXVIk/h4+Pz6tUrelBLvgN248aN9+7d+/LlS1hYmKWlJSR6eXnNmzcPN5zwmgDDCSQ6OhpCRpSVlWF3jeB3jKKonJwcaBqGNXPy5MmcnJy5c+fSD1Einvm22YCZBv8pLi7es2fP+Ph4XDNCtiBfGNZkq065CKHq6uotW7ZMnz4du1tBLaEzBAYGpqamrlmzhstb3NWrV7du3cobJERTU5P+J8yuP3/+rK2thUpm8ops6k0RQvn5+VOnTp03bx4+Dc0FFKWmpkb3uycpKenq6rp48eKSkpLIyEhra+vTp09XVVUNHTqUa3IWFRVdsmTJtm3b7t+/n5qaih34toYWBF3l+vXr/fr127p1K6ismzZtogdfbjYqKirW1tZPnz7Ny8u7d+/e1KlTU1JSXr16deLECRkZmZkzZ166dKm4uPjWrVtOTk6FhYX37t178+YNvrxFXo4EAqFVIcY+AqH9MTY2NjMzi4iIuHjxIjaZnT9/XlpaurKy0svLC0xmHA7n7Nmz+NssQujly5cIISsrK7oONGTIEPguCocN6fDGX2tqCS0luQCRWgohHw2u4nA4jd4Ie5Kuq6sLDAwMDAyEzUF4z2ArAafwEEI1NTW8v+JEWDhlZWXxxvVTU1MTbApsyAQGR96abc3ELcKkehkiZHM3CYZ9Izk5OTs7W1dXF3aW0aWC/2BXfRi+w0FGRmb+/PkeHh7nz5//7bffYHHi6elJX8Uh4frh+vXrg4KCEhMTDQ0N165du2PHji5dusBB+xZHREREXV3dw8MDmzk+f/6MEFJUVOTtjWZmZuLi4vX19VyRf1CrTR3Y7gABEFALdS3eR7O3t9+7d29RUVFeXh6Xs3M6YmJi0OKJiYn09N69e6elpeFFZjNgOLqZt05YWBhiViFQpVpaWg0NDcHMnTv34MGDP378mDRp0pQpU44dO6alpSVMd122bFlQUNCLFy/S0tLAPJGRkfHy5UsJCYmYmJioqChTU1OE0NWrV4cOHYp9IAr5FMbGxthBZ2hoqI+PD5gRuQbs8OHDjYyMPn/+fPbsWTD2cTgcb29vfNoUtYQmgJhNILjPJCYm9urVC/6vqqoqJibG4XCwoZxhzTDvMC0FjG6uod3sFmwIJjXZ2lPunj17lJWV/fz86MO2SToDX2A/GvZ0jJGQkIAxIhiof4qi6urqOnTowPAV2dSbpqenDxkyRFRUtBkRY8ePHw//SUxMtLa2htmP7wdpbHmPiYlpxjzMXAsyMDCwsbF5+PDhzp07/f393d3dR40ahd3LCs/cuXMh2p6np+fUqVO9vLwMDQ3BhLpw4UI4dnDhwgUnJ6erV6+qqanhT4yobfUuAoHQPIixj0D4JVi2bFlERIS/v/9ff/3VpUuX3Nzca9euXb9+fdKkSQ8ePIDV4P3791VUVPDyACIYIIRGjRrVvO1RwpfQPMnbgBZ5NOaUl5f/9ddfDx48WLlypY+Pz/z58+HTdKuira0tIiJCUVRJSQnvr8XFxfAf0LfMzc15NwFVVFTAMY1/Om3c3AzJyclBCFF/n+EShtWrV588eTIjI+Phw4cTJ04sLi5++vQpxAqg0+x+aGFhcevWrblz50KkiMuXL584cQLvcmoNtLW1YT2AEIqLi0MIwRlALouSmJiYiopKRkZGcnJy6wkjgNbrWhA2BCGUm5srwNiHEJo8efL169cvX768Z88ebKYBp3tCWiWYwLx1ICeTKQWGhqKiYvOGhqys7IsXLyZOnJiamhocHPzw4cNNmzbt3LkT9go1g7Fjx6qrq6enp3t5ecE592PHjs2ZM6eoqCggIODSpUtgUzh9+jR9/6CQT4EQ4nA4V69ePXfu3IQJEw4dOnTr1q2PHz/yZlu9evXSpUthna+goPD48WMtLS0ccaIFu2ijE4ipqamamlpGRsalS5fgBCtCKD8/n81mW1lZYY/+DGuGeYdpJYRvwYZotCZbdcq9efPmkydPQkJCsCMFoEk6A1++f/+OEKLv0hUGhq/Ipt4UfKHm5OQsWLDg5s2bTRKpR48e8AGjqqoK/d1FuQ4xALiW2uDddPPmTXt7+0ePHsXGxlpbW9vZ2Z06dUrwW4M5sP2zsrLy0aNH6enp3t7eGzduhJ+GDh2qq6ubmJj4+vXrxMREb29vehf9NfUuAoHABXfUcAKB0C44ODjIyspWVVVdu3YNIeTu7m5nZ2draztkyBAcze3UqVM4NCcAH9NwhLVmIHwJzZO8DRD+0Rjy8uVLXV3dT58+PX361NHRsdkLzqYiKysL673s7GzeXyGxS5cuEG3N3t7emQeuQzH/aNqsuZkDdpnMzEzs0a/Z6OjogO/ws2fPIoR8fX1nzJhBPxSPhO6HU6dOjYuLg0M3P3/+dHR0XLVqlZBiCwafjoRjSrW1tXl5ebzZYFcF3jTU9rRS18JLtUbXbB4eHv3798/MzFy0aBH0JW9v74yMDD8/v5Y68CgA5q0DCz98hksAMDSEqVJdXd3Pnz9v3ry5Q4cOtbW1Bw4cGD58OP2kc5OAQ3kIIW9vbzabXVhY6O3tvXnzZkj08/Orqal59+5dSUkJ/QCskE+RkZFhbm6+d+/eq1ev/vbbbwoKCg3ldHR0lJeXr6mp8fb2Rgh5enq2uCaAmE0gEhISt27dUlBQuHbtGvhMZLPZ27ZtU1NTw2FYEOOaYd5hWgnh+yFfGE7FrTTlhoWFHT169PHjxxBxm06TdAa+QI21lHmL4SuyqTdVUlK6ceOGuLi4v7//wYMHmySSqKgoHLYFrwUw+/Htoni7Xxu8mzp37vzw4UMvLy+QJyAgwMjI6MuXLy1SuLS09PTp0xFCbDbb0dERuiL+deHChfAfV1fXT58+zZ49m37tL6h3EQgELoixj0D4JZCSkpo3bx5C6OLFi2VlZefPn4d4i7DY8PLySk5OjoyMnDlzJr5EVFQUlAx/f3++ZYaHh2Nv63wRvoTmSd4GtMijMSElJcXW1ra0tPTy5ctSUlLCF9gkwAAEH5+5+Pr1K0Jo+PDhoCgfP378Eg9cnpL+ubRZczcJOHbEYrHu3bvHNwN20c0EFxcXhNCDBw8yMjI8PT25XBEJ2Q9h1d2rV6/AwMAXL16AD6nTp0/fv3+/qUU1A4jfjRDiPauLEPr58yc9TxvTel0LbGdaWlp0t2J86datW0REhJWVVVRUlIWFhZ2d3cePHxMSEvCJs1aFeevAWbb09PRGrdswNN68ecPXgMhms0NCQgRcDt1VSkrqjz/+iI+PnzBhAkLo3bt3+/btY/BA/Fm4cKGEhER2dvbjx49Pnjw5YcIEbW3tMWPG9O7du6ioKDg4+NSpU8uWLaPvbRTmKWpqamxtbd+/f3/+/Hl8HrYhpKSkwGHluXPncnNzIyIi6J6wWqSLMp9ABg0aFBsbq6io6O3tbWlpOWvWLA0Njfj4eOyDDDGuGegwCQkJgmVrPYTsh3xhWJOtNOVGRkauX7/+7t27DX0GYK4z8AVshY8ePeI7xnNycni9hQiA4SuyGTcdOnQobMLdsWNHk6q0rKwMBgucIIaZ7du3b7wfEmDqQ63/biorK4NthgsWLEhJSdm4cSMEw6G7+xQSCLWEEAoPDx8/fjz9+9O8efPAovfo0SMjIyO675FfU+8iEAhcEGMfgfCrAKv3Dx8+rFq1auTIkfBOdXBwkJGRiY+PX7x48bx587gOZcC3R29vbxxlD5OdnX306NFGfUsLX0LzJBeGhnyQcaW3yKM1yoULFyorK7W1telnkQR4SWtZR37Ozs4IoU+fPvGecAF3SFxu3X5lwDFZXV0dPRF0XL60S3M3Cl2q7t27g4+k/fv3cz0XQujx48fh4eHMS7axsdHS0uJwOMuWLZOWluY6FN/UfsgFBAEERowY8e7dO319fYQQDovZPPi6gedl+PDhsPmFvjkIYLFYOTk5nTp1Et6w1eyh10pdC3ZDrF+/nknmffv2zZs3LyYm5tOnTwEBAR4eHo1aiFoK5q0DfYbFYvHmBHB/gCpls9l79uzhzQbBdgXw559/4sjFmpqad+/ehcWqMN21e/fu4GHWw8Pj1KlTMCJERUXBynb8+PHg4GCu1bUwT/H8+fPY2FiEEH0gC+iiK1euFBUVTU5OXrBgwbx587g2iwnfRZlPICwWa8OGDffv33/37l14eLi/v/+2bdu4rFoMawY6zMuXLxvaGcRwAmlI1EYRsh/yhWFNMp9ymT9aTEzMihUrbt++zbtZ+NmzZ+A3UEidoX///gihrKysP//8k+un0tJSV1dXeryLRmH4imzeTTdu3Dh16lQOh+Po6Mjl8FQA0dHRCCErKyswY4FttLa2FkIb00lNTUUIGRgY6OjoMCycIVwt/vbt2zt37sD/u3TpcuTIkfPnzyOEIiMjhT80AFhbW2MngPPnz6f/1L1794kTJ8L/IYQxnV9E7yIQCAIgxj4C4VdBX18fPP76+vr+9ttvkCgtLQ0+Ml6/fs0bVgxit1VWVtrY2NC1mcjIyBEjRmC/G1hj5tUaGZbQ4pJjqRpSZMGFNpf+Bwed8vLy4EMuRVHg25hvOsNHa8g7MgjQ6GIDznekpaWVlpZCSnl5OeiL2As4lhDRjs8w1OCxz2y+kpiYmEyaNKmqqiowMJCe/ubNm4yMjP79+zM0kTRUCZDOq4VDOpdIfJuM73d+XDP0EsCFWU5OTkpKCk6EY+AIIRy5FQnX3A2BRWIuP114SUlJWNpxtS9IFRMTM3PmTOwmiaIoPz+/lStX4tiLAkYoRlRUFA55PXr0iPdQPMN+iJ+Iq8UTExPBEzkgJycHaj09luLZs2fnzp3Ld0sIX+rr66H3CrDYAurq6jBv3L59G/uNAq5evVpfX79jxw51dXVIYVJXXECH4R160NZcLY6rC2drkUmSi+rqah8fnyFDhtCbsqFOePjw4atXr8IG6mYg5Ohm3jpz5swBz0379++nH3zLyMiAcB+4cxoZGcF52DNnzhw4cABXNYvFcnNzi4qKwv7gsDxcT0HfSyIqKrp06VJE66719fUbNmwYPnx4Q2ZHvsB76tGjR2ZmZtgGt3DhQjExsYiICFtbWzhDh2nSU3CB6+fTp084EQw9XAMW0NDQgLjDT548gV3zdITXBJhPIDNmzJCQkBg4cGBDj4YY18z69evBhdy6devoISNwvGMsTEM09GLlO7Txs2B5hGlBgHc6ZViTjU65TdUZ4uPjZ8+effLkSXFx8TwaqampV65c2bJlC0QHFlJncHR0hP2Yv/322969e/ETJSUlWVtbr1mzBm99bdLsKvgVyfymXDqJt7e3pqZmaWnplClTuOYuoLKykivlzJkzIiIix44dgz9nz549fPhwhNDFixe5Cvfx8RERETl79izd1CikFtSQisvleXD+/PkdO3ZUUVHBH9FTU1OdnZ2PHz/OeyMmiIqKwtFdOTk5HF4Gg0/y8h7QaY2XI4FAaGEoAoHwy3D58mWE0JgxY+iJkZGRCKGRI0fyvQSvAEVFRS0tLe3s7AYMGCAqKurr64vzwBddhJC7u3vzSmgNyUGnVFZWrqmp4f0VlOyuXbvW1dXhxCdPnoCoO3bsCAsLmz9/fkpKioB0Jo8Gvp8RQtu3b6cLAG5rjI2NBT84/tRsaWnp5eW1fft2KyurcePGIYS6d+/u5eV18uRJiqJKSkrAEcykSZMiIiJ27dp18+ZNwSUDR48ehfIfP37MN0NmZqaGhoaWllZRURGksFisQYMGycrKJicnM7kFRVHYK7yfnx89HXYBdOzYsby8HCeWl5eDduvq6ooTWSwWfMKdNGkSvQTYC9OpU6eysjKcGBAQALcLDAzEiTk5OVCCpqbm6dOnjx07ZmFhceLECfBMZ2pq6uHhweFwKOGauyHwfopjx47R02HpNXToUHoinKhFCBUXF+NECwsLhJCWltbr1699fHy2bNlCUVRlZSU+9tK5c+exY8dOnTpVU1NTQUEhNjYWX3vo0CHIEx0dLUDI4uJiaWlpeXn5qqoqrp8Y9kOKouC7fceOHVksFr58yJAhQ4cOpac4OTmpqakVFhbCn9hf0oQJExqtTIC+ReXz58+CM1dXV8NhTCsrKyxGUlJSz5497e3ta2trcU7Bsxlftm7dihCSlJS8efPm48ePZ8+eDbMKGHQMDQ3pmd++fQvle3h44MRmd62amhqIWrB8+XI2mw2JVVVVDg4Opqamqamp9MwNdUI4xdalSxdjY+Phw4dPnDjRwcFhyZIlx48fz8zMbPTxhR/dzFvn+vXrcAawV69eHh4eERER169ft7KyglEsJye3ePHimJgYiqKio6NxPAdVVdXJkyfb2NgoKCgMHDiwtLQUF4g3jOzevRsnbtmypWvXrt+/f8cpnp6eEhIS4eHh8Cc+FSgiIhIZGdloFQEcDgdsCmFhYfR0sLK9evWK9xKGT8FLVFQUWChUVVVPnz599OhRKysrvKfm2LFjXO8j6u95b/z48XwLFFITYDiB4EgF3bt3Hzhw4KhRoyZPnjxnzhwXFxdPT8/Kysqm1szq1ashz9ChQ69cufL+/fsTJ05gS6KOjs7q1avp7w4uGnqxrl27Flo/Pz8fZ66urgaR7OzsmionX9hsNuz/GjFiRFNrstEpt0k6Q3JysuAICYcOHcKZhdQZnjx5AoLBpGpmZmZiYiIqKnrq1Cmcp7CwELq3i4sL/Voc3wa//hi+IpnclKIo+Oo8cOBAnBIVFQWnUM3MzHJzc+m1DYPl3LlzoFdQFHXmzBlxcfGjR4/Sy0xJSYG9e5s2bcKJwcHBnTp1OnLkCD2n8FoQX90GzjLfvXsXZ4PXMf3uOO7w+/fvqWYBpufly5fz/lRXV9e9e/f+/fvzvbBFVhAEAqH1IMY+AuEXgsViKSkpvX79miu9f//+/v7+fC/hcDhHjhyRlZXFWt2QIUPwsqS8vHz9+vV4f760tPTy5cuDg4OZl9Aakt+7dw+WT4CZmdmePXuwjuvt7e3g4IA/1Y4cOfKvv/7CotrY2EC6iorKixcvGk0X/GhXrlzB6wo5ObmNGzdmZ2dHRkaCu2Jg1qxZApaLtbW1cPgLIdShQ4eVK1eWlJQ8f/4cFsza2tqJiYmQEzuKlpKSYmKnOHny5OrVq/FRIDU1NTc3N09PT96cxcXF06dP19HROXHixKlTp0xNTUeNGgX2r0YB3/ZwFgPusmvXrry8vKioqJUrV2KnfhYWFrBEOX/+vLm5OSTKyMisW7fu8+fP+/btA1MXKHxOTk53797NzMxcvXo1Pm5mbW199epViqIOHDiAlyVqamoHDx7EwgQHB+PtDD179rxz5w5FUZ07dzYyMtqzZ8+XL19wszavufmSlpa2detWHBpVVlbW1dU1NjY2LCwMdwMREZGFCxe+ffs2Pz/fxcUFP5S5uTnu3iEhIXCWTURExNnZubq6GtLLy8vnz5+Pa1JERGT27NlpaWnw69evXxcsWICXMb169YIdJQ1Ju2TJks2bN/Ntx0b7YXBwsKOjI/bHNHTo0AMHDsA6Z+TIkUZGRiYmJgcOHPD09HRycrKwsIiLi8Pll5eXgxMoMGIKJjw8fPbs2fT4IV26dJk9e/aTJ08EX3jq1KlevXqpqKgsXrx41qxZenp6sJ8Cy9DobMaX7Oxs7EN9yJAhGRkZb968mTZtGhZv0qRJjx49oihq165duCfIycnh1Z0wk+T79+9tbGwkJSXl5eXHjh1ra2urra29ZcsW+keOhjoh/BoUFNTQSl5KSkpArQo/uul9QHDrYF6/fo3PtYmIiAwePDguLq579+79+vXbt28f3ZqQkpJiaWmJ98VIS0tv374dr4eLioq2b9+Oi5KRkVmzZk1oaChFUVu2bDE2NtbQ0HBzc/Py8tq8ebOenh59MZyRkQHHAxFCXEt3wRw/ftza2por8c6dOwYGBg1dIvgpBHDw4EE8GMeNG5eYmFhYWAgToLS0NMx+XOjo6Dx48IBvaUJqAsxfZLt3727IKYeamlpOTk5Ta+bMmTO4sTp06ODg4BARESEqKjpixIhz584VFBQ0Wo14LLi7u2dmZq5YsQJLaGJiAn3b19eX/pJydnbGhvLmteCJEydGjhyJn33OnDlg3WBYk41OubyP1pAkxcXFAuLnghh5eXlclzRbZ6Ao6vPnzwMGDMBKmp6eHv1jpI+PD1arOnTosGzZsoSEhOzs7DVr1mB9RldX18vLC/ILfkUyvOmFCxfwaVOEkL29/eXLlymKOnfuHFYqZGVl58yZk5GRQf1tHTMzM1uwYIG2tvbMmTMNDAx69+7N96tqZWXlqlWrunbtamBg4OLiYmNjY2pq+vbtW5yhtra2RbQgvrrNo0ePtLS0evfuvXDhwnPnzv3xxx99+/b9/fff6RLC98IuXbpA2PTmYWxsTH8oOm5ublx3xLTICoJAILQeIhRjjxgEAqENSEpK4vUAkpSUpK2tLcAZCkVRaWlpeXl5Ojo6zYvPKHwJzZO8qXA4nNjY2Orq6v79+0tISDSajlri0RolOTk5NzfXwMAAx79LTU0tKSkxNjamR7z99u1bVlaWoaGhnJxci8tQUlLy4cOHqqoqIyOjdgxdKiQsFuvr16/S0tK6urrQbTIyMnhXMu3b3A1RUFAQFxenoaHB60+trq4uLi6uvr5eT0+P7supqeTm5nbs2LGh/sOwH/JSWVkpLS1dV1eXlJSUn5+vpaXF+wjl5eWpqakmJibNFp4h+fn58fHxXbt27devX0tNHSwWKzo6WkZGxsDAoNmFCNO1qqqqUlNTCwoK5OXl+/Xr16RYyeHh4R4eHgcPHszKysrPz6+oqKisrKyoqCgoKHj69GltbW1LhWVkAsPWyc/PT05O7tu3L3TFlJQUcMbPS0VFRWxsrJycnLa2NtdYbgjorhRFpaenf//+XVlZWUdHhyuqAJvNzszMXLJkydKlS+3t7Rk+XXV1dX5+Plfn53A4eHdPQzTjKRBCeXl5iYmJ6urqeIqDejM2NuY7SyQkJOjq6tKDhHAh5OzHZAI5f/58VlaWo6NjVlZWUVER9MbKysqsrKzAwMCpU6eePHmSXibDmklLSysoKDA2NpaUlKyqqiopKcGmyUZpkRdr81qwIRqtSSZTLvq1dYaSkpKvX7/27t2bHpul2TB8RbbUTefOnevr6zt69OinT5/m5uYmJSV17dpVX19fwOBCCKWlpWVmZqqpqbWeisWr29TU1IiLi4uJif38+TM5OVlSUlJfXx9/IMR8/vxZXV0db1NtBl+/fgXHkbz8/PlTVFRUwJTSjnoXgUAQDDH2EQgEAoFAIPyKPHr0aObMmQkJCXxtHxwOx8jIqC2Nff8gSkpKrK2tQ0NDeRfGhOaxa9euoKCgqKgovnbe1NRUV1fXu3fvtr1gBEKToBv72lsWAoFAaEVIgA4CgUAgEAiEX46KigonJycVFZWGdjmVlJT06dOnjaX6R1BSUjJr1ix3d3di6Wsp3r59u3fvXnNz84Z2dCYmJg4dOrSNpSIQCAQCgdAQxNhHIBAIBAKB8MtRUFBQWFiYmJh44cIF3nMYHz58mDlzJg4cSaDzxx9/HDp0aNiwYe0tyL8HiLYZEBAQHh7O9VNdXZ2/v//Zs2chMgaB8IsDUW6Zh3QnEAiEfyjkGC+BQCAQCATCr4izs7OPjw9CqGfPnkOHDlVWVq6rq8vLy4uOjh40aNC+ffsa8oVHILQspaWlw4cPj4mJQQgZGRn179+/a9euZWVlubm5MTExTk5OW7ZsEcZlGIHQZgwdOjQsLKx3796pqalc7j4JBALh3wQx9hEIBAKBQCD8ovj7+1+6dCk6OjovL69Dhw7q6upWVlarV682NDRsb9EI/y2qqqouXLhw7dq15OTkoqIiGRmZfv36jRkzxsXFRUlJqb2lIxAaJygo6MKFCw8ePIA/LSwshg0btmrVKr4BUggEAuGfDjH2EQgEAoFAIPzqcDicFoxsTiAIA+mNBAKBQCD84hBjH4FAIBAIBAKBQCAQCAQCgfAvQby9BSAQCAQCgUAgEAiE/0EEibTZvShEdj8QCAQC4V8F2YFPIBAIBAKBQCAQCAQCgUAg/Esgxj4CgUAgEAgEAoFAIBAIBALhXwI5xksgEP6PsrKyZ8+eBQYGWlpaLlu2TEDOwsLCx48fBwQELFu2bOzYsW0mIQEh9OPHj/v37/v7+3t4eOjo6LTx3TkcjrOzc1ZWlqenp6ampuDMbDY7PDw8MDBQRETk2LFjbSMhJi0t7d69e/7+/g8ePJCRkWnju7c9TWqadicgIMDf3z8qKkpcXHzYsGF79+5lHs2zoqJi9erV3t7erSlgE6irqwsPDw8ODmaxWGfOnBGcOSsrKyQk5P79+/b29jNnzmwbCZtB+84zbUBpaemrV68ePHggJyd36NCh9hbnV6S0tPTly5f5+fnq6urDhw+XkJBoXiFv3rz5+fOnurr6sGHDmBSSn58fHh7+f39MQQihiPcRgwcPpue5e/duWFhYenq6urq6jY3N8OHD+RZVW1t78+bNmJiYjIyM3r17m5ubT506VUTk/x8N/vr1q4G+Afw/ODgYIWRkZKShodGMJ0UIZWZmfvr0iXl+DQ0NIyOj9n1RNpWKioqnT5/evn173LhxTk5O7S0OgUAgEARCEQgEAkUdPXoUr+gOHTokIOeOHTtUVVUhZ1BQUJtJSKAoat26dV27doXK//LlS9sL8PHjR7j7/v37BecMDQ0dMWIEZJ4xY0bbiAfk5+cvXLhQWloa7l5SUtKWd28vmDdNu7NixYpx48bV1dWlpKSAjW/KlCnML1+1ahVCqL6+vtUEbAJ37961srKCmh8/fryAnNnZ2UuXLpWXl4fMnp6ebSZkU2n3eaa1cXNz69OnDzygk5NTe4vzK3LgwAFTU1NfX9/3798fP35cT08vODi4SSVUVFSsXbvW2Nj4yJEj9+7d+/PPP8ePH5+SktLohSEhIdA0y5cvRxRCFHr58iX+NTMzc+TIkQghMTExvJZZtGgRbzlv3rzR1NRcvHjxhw8fUlNT/fz8evXqNXDgwPz8fJzn48ePcAvsr+/kyZNNekw6np6eUIioqKiCgoK2traenl6HDh0gUUdHR0dHp3v37jhlyZIl7fiibAZeXl5YURSmoggEAoHQNhBjH4FA+D9YLFbHjh0bNfZRFJWSkkKMfe3F48eP23ERXl9fP3PmTEtLy+TkZCb5zc3N22sNg9dd/xFjX1Obpr149uwZQujGjRvwZ1hYmLa29pkzZxhe/vr1a9iY84sY+wBYrgs29gHZ2dm/vrGPau95pg3gcDhgtiDGPl7c3Nx69uyZk5ODU969eycpKcn8jZ+fn29kZGRra8tisSAlIyPD0dFx4sSJjV4Lxj4FBQUOhwNmOPxTZWWlqanp+vXr09PTORxOamqqg4MDdFRfX196Ienp6d26dZs9ezY98cuXLxISEmPHjqUnYmNf3759hbRh/fXXXwihNWvW/Pz5EycaGPzfzsGKigpIYbFYFy9eFBcXd3R0hJR2fFE2lcrKSnFxcWLsIxAIhH8ExGcfgUD4Pzp27CgrK8skp7KycmsLQ2gI5qcdWwMxMbEbN26EhoZqa2tz/URR1JYtW7gS21Ha/1ovFdA0vxRw0FVRURH+tLCwSE5OXr58OZNrq6urf//9dykpqVaUr1kw72wqKiqior+c6nXo0KHi4mJ6SvvOM22AiIhIz549+f7Edyr77xAdHX306FE3N7cePXrgRDMzs4kTJy5ZsqSoqKjREths9pQpU4qLi/38/OALIkLo3Llzfn5+zE+5ysjI0M/bAn/99dfSpUuPHTvWu3dvERERTU1NPz+//v37I4QePnxIz3nhwoWCgoLp06fTE/X19U1MTJ4+fVpYWMh7R3xkodmwWCwLCwt3d3c8v/GlY8eOCxcuXLduHYvFgpR/0HCTkpLCu+YJ/zhCQ0Pv3r3b3lIQCIS245fTOAkEQjvCq1sLk43QGvyylb9nzx7weUSnHaX9ZSvqP87Tp08RQs0zeO3atcvNza15vsNalSZ1tl/N2BcSErJ9+/ba2lp64n9h+DTUEHynsv8OFy5cYLPZvN54p02blp+ff/Xq1UZL8PDwCA8PX7t2bZcuXXCivb39oEGDGJr1G6KmpoarBDExsUmTJiGEuKzVHz58QAiVlZVxldC1a1eKtsG2ZamqqnJycmI4dubMmVNVVQX//2cNt3+WtARMTk7OzJkzca8jEAj/BX4tjZNAIBAI/0QCAgL27t3b3lIQfnVYLFZpaWnzrv3w4QOLxWrIEz+heaSkpDg4OLDZ7PYW5FeBTGVwgltNTY0rHVJu374t+PK6urqjR48ihLjizxgbG79//37nzp3CyLZr1y7eRDBejBkzhp4IOwqxMwegvr7+8+fPnTt3bqXtz5MnT7a3t2eY2cTEZP369a0hBoHAC4vFmj59+o8fP9pbEAKB0KaQaLwEQjsTEBCQnp4O/x8/fry+vj5CKDg4GPvFU1VVnTVrFv2S+/fvP3nyhMViqaqqmpqaSkhI2NjYcBX79OnTV69eJSYmampqWlpaTp48mffWFRUVV65cCQ8Pr6+vHzBgwMqVK5shf319/e3bt0EeCwuL6dOn08/+YL5+/Xrnzp3Y2Fg5ObkBAwbMmTOnU6dOXHliY2P9/f0zMjLYbLaRkdG0adO4NPLa2tonT56EhIS4u7sjhEJCQq5du1ZcXDxw4MC1a9fC0ZJv375duXIlNjZWUVFx9uzZw4YNY/IUdXV1ly5d+vTpk6ioqKampomJiZSUlIWFBVee69evf/z4MScnx8jIaPz48QMHDuQqp7q6+vr165GRkfn5+eAL3MHBgeszeGZm5pUrVyZOnGhoaHju3LkXL16MHTt28eLFOENSUtLdu3ejoqJERUX79es3b948FRUVvmJTFHX79u179+5VVFQYGhquWbNGTk5OwGMWFxd7eXnhP6WkpFasWJGYmHjv3j16Nhsbm379+iGEHj58GBcXhxDq2rXrggUL4NeioqLAwMAOHTrMnTsXUry9vZctW0ZRVGFhIQQT1NTUnDZtGtfdMzIyPD09v379qqSkNHnyZN5O2xCNdoxGKS0t9fHxeffuXZcuXYYOHWpvby8pKcmbrdFRQ1HUkydPnj59mp2dLScnZ2ho6OTkRN+9ghAqLi4OCgoqLS11dXWtr6+/cuUKLJ4nTJgwb948yPPu3bsbN25kZGT06dNn+fLl6urqTJ6ioKDg/PnzaWlp0tLSurq6WlpaOjo69MCRvE1TUFDg4+PTUIH6+vrjx4/HfzLp4QJ49uxZZGRkTEyMvLy8iYnJ6NGj6bLFxMSEhITgPQU3btyIjIxECKmrq9vZ2TVaeG1t7e7du69fv85cHkxeXp6vr29SUlJpaamurq61tTUOqQGw2exXr175+/ufOHGiY8eOt2/fBouGtbW1s7Mz7zYWiqKCg4Pv379fVFSkq6sr5GYlOo32wM+fP/v7+0+ZMmXgwIGRkZEXL14sKCgwMTFZu3Zt586deQt89OjRs2fP0tPTdXV1jY2NR48ejSOEIITev39vZ2cHBzNPnz7dpUsXCQmJNWvW8D5vk+YZoKam5t69eykpKW5ubhkZGRcvXvzy5Yu6uvqoUaMmTpzIm7+goODatWufP3+uqanp37//jBkzuI7Z5uTk3Lp1S05Obt68ednZ2adPn46Pj9fW1l62bJmWlhZXaY02Ol/4TmV9+/a9f/8+PduoUaNMTU0RQiUlJRcvXoREPT09vs/FFw6Hk5mZWVJSoqOjw/sqbEfS0tJSU1M7d+6Mj99iunfvjhCKjIykKErA3q6QkJDs7GwVFZVevXq1rqwIIYTYbPbjx4+trKxWrFhBT7ezs7tz587bt2/XrVt34sQJSDx79uyPHz+OHTvWSq4A4EAxc7gMlACTF2WjgwW1nKIomIiICD8/v6ysLE1NzYULF4LmACV//vyZnnPevHlwujk2NvbJkye4BoyMjOCJbty40b17dzs7u/fv3/v6+mZnZxsZGU2ePBnGWlMlF6BoNelewit1jc5FFEV9+PDhxo0ba9euVVNTS0hIuHDhQmpqqoaGhqura+/evRFCpaWlV69eDQsLExERGTNmDNYl6AjoFbm5uQ4ODhEREQihu3fvZmVlIYScnJzoDigardJPnz55e3u7u7uXl5cfOXLk27dvS5YsweFlmPQ3AoHQDrSTr0ACgfB/sNnsCxcuwHj09vaGxKqqqg8fPoCuPHToUHr+BQsWjBgxIjc3l6KoqKioPn36mJqa0jNkZmba2tquXLkyICDA29vbxMQEITRt2rTS0lJ6toSEBD09vZkzZ4aHh3///v3PP/80NDQEDbjRAB0VFRUgsI+PD5fWoqys/Pr1a3rmmpoaV1dXW1tbX1/fmzdvzpgxAyGkr68fFxdHr4TFixfLy8vfu3ePxWJlZ2dbWlrKyso+efIEMmRlZS1YsABWs926dcvNzZ00aZKqqqq5uTlYbcDl9h9//CErK2tmZqapqYkQkpCQePz4caNNUFZWZmRk5OrqymKx2Gx2QECAlJTUunXr6HlCQ0OHDBly4MCB4ODgEydOKCoqiomJ7d69m57nw4cPSkpK8+fPT0tLAysPQmjq1KnV1dUURdXX13t4eAwfPhzOjj19+nT+/PlQaaKioj9+/KAoisVibdmypU+fPqAa3rlzp0OHDhISEnTX49HR0XDVu3fvxo0bJyMjY2JiAgcbNTU1y8vLBT+sr68vXG5qagpOxNls9p07d2Bpp6ioGB8fT2/oBQsWiImJhYeHUxTl5+c3ceJEuJerqyvkqa6udnd3t7a2RggpKSm5u7u7u7vjuI1Tp05FCM2YMSMwMFBGRkZPTw8bLk+dOtVo0zTaMQSALZivX7/mWggNGjQoMzOTnpnJqCkoKBg8eHD//v0jIyPZbPaHDx8UFRX79u2bnp4OGd68eWNrawvOy2fMmBEZGWlgYKCrq4vXDwcPHqysrHR0dOzatevQoUNh5dOjRw+6I/yGiI6OVlBQuHbtGofDYbFY27dvRwjheubbNBRFffr0SVJScv369RcuXPD+G3g6ERGRZ8+e4ZxMenhDZGVl2djYDBw4MCQkJCMjw9fXV1tbu1OnTkeOHGGz2ZDn8+fP7u7u+/fvh6pYtmwZdJXAwEAmt9i5c+ejR4/g/2BpYhig48KFCx06dDhx4kRhYWFlZeWqVatERER+//13+LW2tnb16tU48mx2dratra2SkhI2U4Lph05ZWdmUKVOMjIwePHiQnZ0dGBhoaGgI6zEmATooioIewhWgo9EeePLkSWzjvnPnzvbt22FAQUxSQ0PD2tpaeoEsFmvq1KkmJiafPn2Kj4/HxvrevXubmJiYmJjU19f7+fktW7YM0vfu3evu7o5HpTDzzMePH+fOnQseYEePHn3z5s3OnTvTzevOzs41NTVczTRkyJCzZ88GBQVt3rxZQkJCXl4ed+9Hjx4NHz4cFtgbN24MCAhQUFDo27cvvLBkZWUTEhKYNzpm9OjRiBago6GpjMPh3L17FyKoysvLc00deXl5SkpK1tbWxcXFAuoEw2azDx06ZGZmtnXrVg8Pj7Fjx65cuZLr7dyOwJcJFRUV3p9yc3Oh+QQ/6ebNm7He4u/vP2fOnAULFuzcufPz588MZYAAHerq6tTf0TMEZN6xY8esWbMqKyu50tlsNrZWODg4lJSUXLlyRVpa+ujRo1w5cYAO6A8tHneCN0AHL016UQoeLEBLKYp8gRnYw8Nj27ZtHTp0MDExgS9eUlJS79+/hzzV1dW7d++GRxgxYgTXs798+RIh5ObmVldXd+vWrfHjx8Pcsm/fvs2bN4uIiODpQlxcnKsGBEsuWNHy9PRs0r2EV+oanYvc3Nyws8hPnz5t2bKlW7du5ubm8FVGRUUlPz//4cOHqqqqhoaGpqamMA3u2rWrSb3i1atXR44cgbvMmzcPJre8vDwmVZqXl7dz504IX4MQys/PNzY2hv/37duXYX8jEAjtBTH2EQjtT3V1Nbw4sbEPcHZ2Rv9r7AMX1PQlelhYWL9+/fCfZWVlffv2PXjwIE7Jz88HPWzlypU4MSkpqUuXLtOmTeNwODjRz88PxGBu7NPW1t63b9/379/BXAgqlJSUFN144ejoyBX8DgLPGRoa4uX62bNnEUI2NjY4Dyw5sCYBuLm5IYQkJSVHjx4N5ieKFjVyzJgxrq6usIbkcDgQpG/UqFGCnwWKFRcXpy8+Dx48SK+uqKgoGRmZ0NBQLvEQQjdv3oQUDocD2nlERATXk0LgUTabnZycjI0dU6ZMuXjxYnR09LBhw/r3719bWwtOzdXV1fPz83EJsESXkJDAIWXxInzgwIFXrlypq6ujKOrDhw9gQTh+/Hijzwv22WHDhtETXVxceCucoqjt27c7ODjA/5OSkl6+fAmLXrpFifrbn72enh7X5bCG0dTUnDx5ckZGBkVR9fX1sJNLUVGRa8HPC8OOwRds7DMzM/P19c3Pz4+JicG7P/r37487P8NRAxts//jjD5zy22+/IYRWrFiBU2pqamAzaffu3R0cHOCRcc4uXboMGzbMz88Pbl1SUgI7kvbu3dvo45iZmXE12dixY3H3a6hpYGML/aqXL1/CamH16tU4kUkPb4jS0lJVVVUNDY2qqiqcmJGRAfav9evX0zMXFBRAsdhyx4SYmJilS5fiP5kb++Lj40VERDp16oRTqqqqxMTExMTE6HPU4cOHQaphw4Y9fPgQEg8cOABrtqSkJJyztrbW2NhYQ0OjqKgIJyYnJ8PmrGYb+xj2wPfv34Oc+vr6x48fB+vemzdvoEEvXrxIvwts0MOfXthsNkxHWlpaISEhISEh0AkfPXoEZcI6DSPMPFNaWpqcnKyrqwvDfMqUKbGxsRRFxcfHY09wBw4cwPkvXLigrKxMNyHBeOnYseP3798hpaSkBCZYbW1tZ2dnqP8fP37A2dK5c+fiaxk2OsVj7AMamsrAKkqfioGioqIOHTpER0cLqBBMTk7OyJEjbWxscCFsNtvQ0HDMmDE4z4sXLx48eNBoUenp6QlNBFemAPz9/RFCvXr14v0pPz8f2k5wORMmTEAI2dranj9//sKFC2AWef36dbdu3f78889GBaAYG/syMjLgVdK9e/dz587xZqioqADTLUJIQUFBR0fn69evvNl+HWMfkxclk8HSUopiQ8AM3Ldv382bN4PdPz8/Hz5FTJ48GWfjcDhgIRo8eDBXCffv31dSUoLnys/PDwsLg48WKioqmzZtys3Nraure/XqFbxEEEL43dSo5IIVrZycHOb3El6pYzgXPXjwAEowNTX18fGBmfnHjx/wFWrkyJGjRo0qKCiAzKdOnUIIycjI0LsTk16BFxrXr1+nt0WjVVpaWpqUlARdFCG0aNGisLCwO3fuaGhoQIZG+xuBQGhHiLGPQPglgJcol7EPTofRjX3g7+bs2bP0bHZ2dvQMEhISXDolOJERFxfHVqQpU6YghKKioujZ6urqYMnK3Ni3detWevqNGzcgffHixZDy/PlzhBDX9jpQVhBCeFPP2rVr0f/adOLj4yEP3YKAD/HBJ1MMbIGcMmUKPREijomKisIyVQBg/KLvDSkoKKDv6LGysjI3N+e6CrZlGRsb40tAg6TrhWBw3Lx5M0759u0bPMLGjRu5CgTvQpcuXaInRkdHi4mJSUtL47bDi/A7d+7Qc8J5TGyYEwA0k6ioKDZFURSFT9zExMTQMxsZGb169YqeAmv4Jhn7zMzM6K3w7t07uBd9FyFfGHYMvmBjH9c2QGzvw/slGY4a+JpNN/ZBYFmulQwMWz09PbolHZ/KP3HiBD3zhg0bEEJWVlaCn6W2tlZcXFxVVZVu4QoKCrp8+TI9G2/TfPz4MSQkBP9ZUVEBm161tbXpe2GY9PCGgOP/vCtkON4oKir67t07nNgMY199fb2trS19GcPc2BcYGAjrInr3g4NLdHsKGBfQ30cUARaLBUa0q1ev4kQ4D+ju7s51IzATNNvYx7AH1tfXg5xcu5OGDBmCEKLbQysqKmA6olum4EC3iIgIbOkFGjX2NXueAeO4jo4O3t1JUVR1dTXYPuTk5EC2oqIiWVnZ3377jX7t169f4e5r167FiUOHDkUI2dra0nPCzKOjo4NTGDY61URjX2pqKlTpsWPH6Olnz54dOHBgo7VBUdTPnz979OhhaGjI1dDwXrt27Rr86ejomJWVJbio4uJiJSWlzk1ESUmp0e2HMGx79+7N+xOOwyt4j97gwYMRQubm5ly9FN77fn5+ggWgGBj7OByOi4sLl3eLHTt28BZVU1ODzxKKiorSTcyYX8fY1+iLkuFgaSlFsSFgBub6kHPo0CGEkLKyMj3R29ubd2qlKGrWrFlcKhAcEl+0aBE9MS0tDb5gjRgxokmSC1C0mN9LeKWO4VyE98ziSQBwcnJCCHXv3p2u7ZSXl0Pm58+fQwrDXtGQsY9hlV66dAkuv3fvHtdjNtrfCARCO0J89hEI/yRg+b1p0yZVVVVbW1tI/P3333GGixcvysjIwBE/TEZGBkKovr7+3bt3tra2ERERwcHBvXr1gr36GHFxcVlZWRaLxVyeQYMG0f+cOXPm7t274+Pjb9++DWeTwYAVEBAAn/6AzMxM+E9oaCjouL/99puamhr+cogQov42gBYVFeEzDrBIRgjJyMjQ76uiopKZmQmVgwGNisPh5Ofn83UjiIELp0yZEhwcDOYSBQUFsMIghJKTk1+/fm1kZLRu3Tr6VaAXfv78uby8XEZGRkFB4fbt22VlZbDUoT8FXiMhhHAgUV5nPeDUHOywGGNj46SkpM6dO3fr1o0rP1htMHDwENetAKZNm6akpPTz509vb+8dO3ZAYr9+/WRlZUtLSy9duoQ9HL17966uro7rpHYzYqH26tULtx0WFaTV09MTcCHDjiEYrl66bdu2CxcugK9JR0dHxGzUIIR8fHxevXqFj0Mifu2L/u6lnTt3prv1waLy7aVY128ICQkJWVnZ7OzsGTNmXLp0CdZaNjY2XBfyNg2XDyk3N7e0tDRRUVFvb2/stYphD+crWEVFBey+5HVWNWvWrA0bNpSUlJw8edLMzEzwAwrgyJEjixYtYuIkjpeJEyeePn3a0NCQ3v14Ww3/Sh9lHTt2VFBQKCgoAPdGCKHq6moI3QDRP+nwDs8mwbAHwsqT93ZwSh3LiRCKjo6GmBtlZWVwnBYhNHLkSIQQRVE5OTlc/VAAzZ5noFbV1NToQW8lJSVdXV0XL15cUlISGRlpbW1969at0tLSmJgYevfjcDgiIiIURYWGhnIV2OizM2z0pqKpqeng4HD16tUTJ064uLjgsebr67tw4UImJaxaterHjx9+fn7gXhYDY+fKlSuzZs0qLS1lMrPJycnl5eU16zkaAQzKeJqlgxPr6uoElAB+Od+/f88VymPKlCmrVq3asGGDra0tl5/TpiIiIvLXX3/99ddf+fn5586dO3DgQHV19R9//DFv3jwud64vX778/PnzihUrYM7ftm1bXFzc5cuXf7WI2ECjL0qGg6VFFMVGpeU7M/z8+bO2thbeHQghR0fHXbt2ff/+/ciRI/hjbXl5+Z07dyBcMgYenO7mFf6cOXOmr6/v69eva2pqJCUlGUouQNFifi/hlbqmvoC43rMwD3Tu3Jnu1rNz585SUlJVVVX47c98CuWL8FXaaH8jEAjtCDH2EQj/JObOnXvw4MEfP35MmjRpypQpx44d09LS6tOnD/yanJycnZ2tq6sLGz0w+E84UhEWFob4xdprBrxeuu3t7ffu3VtUVJSXl6esrAyeWaysrOi6zpAhQ+CzIZZBWVkZh6VLSUnx9PTE+0rwZhYB8DU/YXWzqKhIsLFv/fr1QUFBiYmJhoaGa9eu3bFjR5cuXXCtwiNoaWk1VKsY7CGorq4uMDAwMDAQvszzfQS8bge+f/+ekJAgLy9Pd58PcKnUDQF+Z2praxvNKSEhsWDBgj/++MPLy2v79u3QiDdu3AA5fX19//jjD6i9U6dOtWDwAS5RmUgrZMcAuHqpqqqqhYXF69evIfYIw1GDEDI2NsauakJDQ318fKKiohhK0pCFFOqZiQ1i8+bNbm5uQUFBoaGhBw4cWLx4cYcOHZo0ip8/f3769GmE0Pr16y0tLXE68x7Oy5cvXzgcDkJISUmJ6ycpKSkTE5OXL1/GxMQwF5KLxMTExMRE2GzVDCQkJPBGzuLiYh8fn9DQUNgZwaTVYNaCB0QIxcXFFRcXi4iI8PrCFwbmPZChnIjW3xITE3GcBFVVVTExMQ6Hw7XKbRLM55mGwDFhEhMTra2tofuZmpoaGhrSs0F8JN4wEVzwfXZhGl0AW7ZsuXbtWlZW1tWrV8E5V3p6+sePH2EXuWACAwP9/f319fXB5EoHmgOmI29v71WrVgkjpJCAGY5vdGaciM3HfAGDhZ6eHuyfwqiqqiopKeXm5r548YLrm1azUVRU3L59u6mp6aRJk2pra2/fvg0eA4FLly4tW7bs4cOH1tbWTk5ODg4OWVlZfn5+nTt3hk8Uvzi8L0qGg6VFFMXmSUtRVF1dHda+xMXFN27c6OLicuvWrdTUVHBbcfv2bWNjYxzKQzDjx4/39fXlcDipqakSEhJNlZxL0WJ+LxBPGKUOCT0XCVAbqqqqsNogzBTajM7A+5iC+xuBQGhfiLGPQPgnISsr++LFi4kTJ6ampgYHBz98+HDTpk07d+4E1SonJwchRP190KAhYFHBtbmgpcB+T3JzcxUVFeHb46hRowSb24DQ0NDDhw/36NFjw4YNFEUJ3vPVglhYWNy6dWvu3LkVFRVHjx69fPnyiRMn5syZA79CrSoqKgquVaC8vPyvv/568ODBypUrfXx85s+fDx9IG+X79+8IIXBhIyDQYUuxdOnSw4cPp6enP3v2DM4uHT58+NSpU1u2bMnNzb1379706dMLCgru3bvn4eHR2sI0Sot3DOil0DkZjhqAw+FcvXr13LlzEyZMOHTo0K1btz5+/CikMAzZvHlzWVnZwYMH4Yz56dOnL168OGDAAIaXV1RUwKmlvn377tu3j/5Tk3o4FzCZIIRyc3N5I6KCLTI5ObmpxQIcDsfNzQ3HL2o23759O3z4cEZGxubNm11dXXv06NGk/csYeNgOHTrgpWyL0KQeyBBTU1M1NbWMjIxLly7BAEcI5efns9lsKysrIfdVCUmPHj3ExcXr6+thCxg8vr6+fgs+Pmq5RqdjaGhoa2t79+7dI0eOzJs3T0RExNfXd/r06Uy2ncJEOn36dN6fxMXFxcTEioqKfv78effu3adPnwoppzCAIY+vJQInCu4/UALfNz5oHbGxsS1l7AMmTpw4efLk4OBgPB0hhJ4/f75kyZIFCxaA2z4LC4vIyMgpU6ZEREScO3du9uzZw4cPb0EZ2gaGg6VFFMWWYtGiRfv27fv58+fx48fhKPeVK1cYboZFtE/CVVVVlZWVqDUlp98LJzZbqcO0xlxER5gptEU6g+D+RiAQ2pdfcR87gUAQgK6u7ufPnzdv3tyhQ4fa2toDBw4MHz68rKwM/f3BLTMzE/vm4AvYOJicw2oGcCwR/we2XSQlJQm+qrq6euHChWPHjl2/fv25c+d0dHRaQzYBTJ06NS4uDo6L/vz509HREW+vgFpt9BEQQi9fvtTV1f306dPTp08dHR2bpOjAXaqqqkD3am00NTXBBAAemh49elReXu7k5AQxYby8vBBCnp6e06ZNa97xyZailToGdE74l+GoQQhlZGSYm5vv3bv36tWrv/32m4KCQosIw5z9+/e/efMGvt7HxMQMGTLk6tWrDK/duHFjenq6uLi4j48P16d+5j2cF3walO98Atv9mr2P7PLly2w2++bNm6f+l5qaGoQQ/P/WrVuCC/nzzz/79u2roaFx//59IZf3MHPW1NT8/PlTmHK4YN4DmSMhIXHr1i0I3wyulNhs9rZt29TU1HAgpvZCVFQUTqXB3hlhul9DtGCjcwFu779+/Xr//n2EkK+v76JFixq9is1mv3nzBv19kpoXcXFxDoezbdu2o0ePtsHHHgHAiC4uLub9qbCwECHUsWNHwS8FcIVB38uPgcTs7OyWkPR/gO2i9PPF+/fvZ7PZM2bMwCnKysoPHjyAbbngYOQfB/PBIryi2FJ06tQJjpdeunQpPz8/Ozv7/fv3zE1LYCAWERHBkcdbT3L6vSBFGKUOaL25CCPMFNpSVSqgvxEIhPaFGPsIhH8SsNqUkpL6448/4uPjIezdu3fvYKsOeKthsVg4NAEX4IsdNuCkp6e3hsIEjoS0tLS6d+8uKioK63wI8MdLeHg4nGhYu3btpUuXNm/ePGLEiBYXqVGgVnv16hUYGPjixQtYq5w+fRqWc1Crb9684esjic1mgzfxlJR5HNSpAAEAAElEQVQUW1vb0tLSy5cvY29ozMHKZVBQEN8MLW6chfO5gYGBRUVFf/zxx+bNm8XExBYvXiwiIvLo0aOsrKyzZ8+2xhneJtFKHQOaEvz9Mxw1NTU1tra279+/P3/+PD4X2WZQFAUyW1paRkdHe3p6ysjI1NfXL1myBIfIFEBISMi5c+cQQlu2bOHyYIgY93C+GBkZwX/4ntUFoxjO01QMDAzwkU9McXEx820R165dc3V1HTJkCP1wX7PBWxcTEhKELw3DsAc2lUGDBsXGxioqKnp7e1taWs6aNUtDQyM+Pr5lzyA3g7KyMpj24YAYPH5D74icnJzY2Ngmld+yjc6Fubk5rNj/+OOPyMjI2tpaJlNTQUEBbIszNTXlm0FUVLSyslJVVZXLkW5DlJaWjho1yryJWFtbl5aWCi7Z0NAQluslJSVcP/348QMhZGVlJdhtK2w35uuaAMz0DM9vNgnYTgjvbgACFHAdJe7atSsEFMJBFf5ZMBwsLaIotiArV66UkZFhsVh//fXXtWvX7OzsGnICywu44+zVq1fnzp1bW3L6vZDQSh1q5bkII8wU2iJVKri/EQiE9oUY+wiEXwJwF83l95p+lAD4888/QeFGCGlqat69e3fu3LkIIXDB2717965duyKE9u/fz+tC+/Hjx+Hh4QghfX19hBCLxWpoiwdf59wMga+L2M8aqPXe3t68unV2dvbRo0dlZGQqKirgMzt2iIaEdq7UJOgewUaMGPHu3TuoIqhVeAQ2m71nzx7eayEMGULowoULlZWV2tra9PPRzJ9CQUEBjpDs37+fdz12+vTpyMhI5k/EhMmTJ/fo0aOmpmbt2rWJiYmwp09LS2vkyJFsNtvR0VFeXp7ul5oJLdtqrdcxkpKSxMXFXVxcEONR8/z5c9CY26WX1tbW7tq1C/4vKiq6aNGi8PBwSUnJqqqqT58+Cb62rKwMNh8ZGxvj7gr8+eefHA6HYQ/nS+/evWGBDSFruX5NTU1FDRxdZMLAgQNX8YADpMCf9J07vLi7u6P/bTIkRKvBtIAQauhkcfNmToY9sKmwWKwNGzbcv3//3bt34eHh/v7+27ZtE7BkbbPODG43rays4FMQdL+vX7/evHmTK2ddXd3y5cu5gq42Sos0uoD88L4IDQ11dXVduHAhk414ioqKHTt2FLAnTlxcXFxcfOvWrQzFk5WV3dh0NmzYINjdHkJIUlJy1KhRiN/+O/A1MXbsWMElTJgwoUOHDl+/fqU7UkQIcTgc2DDI1TQtQnx8vISEBPgCBiC4Ae9TgHWjRXwWM0QYhYoLhoOlRRTFFqRLly5gYz116pSXlxeTzbAYmC5g2m9tyen3QkIrdailX0AN0dQplC5Ai1Sp4P5GIBDaF2LsIxB+CcCJ2OvXr3FKdnb28+fPEUIVFRX0nPTPd6KiokuXLkW0GA4QQzYmJmbmzJn4yzxFUX5+fitXroRf58yZA/509u/fT98vlpGRAV/jG/343xDV1dU+Pj5DhgzBO8LgjpWVlTY2NomJiThnZGTkiBEjNm7ciBDKycmBVQHdbIGrgu4JnmvxwJXOpangzI2q2omJiZ8/f8Z/ysnJzZo1C/1dq0ZGRhB97MyZMwcOHMB6EovFcnNzi4qKgvOwUJNpaWm49srLy0F3pD8CFoZX4QNrTm5u7pAhQ758+QKJdXV1x44du3fv3rRp03AK138AuAtzPVJcXBz85vj6+m7YsAH7Al+yZAlC6PXr19ixNBdwI667w5nWvLw8cOJOURS9UfiK2qi0zDtGk4iOjo6IiNiwYQNWwZmMGjxS6MKALsslCd9eihP59lImq8E7d+7AvhjAwMAAdibS47fwbZoNGzZkZGRISEj4+Phwbcl59OiRqKgowx7eEGfOnEEIZWRkcPkaS0lJCQ8PHzNmzMyZM3EifgT6szQJXFdMKg1ajd5kcXFxBQUF6H9bjW84ApwH30hHRwfi8N64cQMmZ3w5TCAMZ05odHo/YdIDUcMTIJecwIwZMyQkJAYOHChYGHwaHZtFoAO01DwDbrbonDlzRkRE5NixY/Cns7MzyLB48WJ6pIsfP37Y2toOHToUS8i3mXifnWGj4wK5arWhqQwzfvx4GHpv376FMB2NIioqOmTIkOrqat7aQH8PbVFR0SYdEpwwYYJdE4EdN40CLiwePHjAlf7kyZMOHTrgNxFCqKamZvr06VZWVnQvZgoKCvb29pWVlVznCiGu95AhQ6D2mgeLxeI9G1hVVeXt7f3777/TgwkMGzYMIcTrADElJQUhRI9QJJizZ8/OnTuX7g2wqWAVjvfzLYbhi5L5YBFeURRAM17rmzdvlpWVLS4urq+vF9ABuAZIfX39hQsXunfvDroic8kFKFrM7yW8UsdwLmqSZot41AaGvUJSUhKsllxTfYtUqeD+RiAQ2hOKQCD8AuAX6tKlS728vNauXTto0CDYHyciIuLm5vbhwweKorZs2dK1a9fv37/jCz09PSUkJMLDw+HPyspKrO927tx57NixU6dO1dTUVFBQiI2NxVddv34dXHX06tXLw8MjIiLi+vXrVlZWcHhBTk5u8eLFMTExDUlbU1MDLrqXL1/OZrMhsaqqysHBwdTUNDU1lZ553rx5II+oqKilpaWdnd2AAQNERUV9fX0hQ11dHfgJkpSU3LNnz9mzZ6dMmYJd5i1atMjd3T0+Pp6iKLw+zM7Opt8CdohYWlrSE/FaJSgoSHDlw/KDxWLhFCcnJzU1tcLCQvgzOjoauyRXVVWdPHmyjY2NgoLCwIEDS0tLIc+ff/4JGSwtLSHKrZWV1bhx4xBC3bt39/LyOnnyJEVREMoNIeTu7s4lBpvNBpsL0KtXLysrKyUlJT09PSwJRVGBgYGQISAggH453EtJSQm3SKOkp6eLiorKy8uXl5fjxOrqagUFBRkZGXoihsViwQGcSZMm0dOfPHkCUu3YsSMsLGz+/PkpKSkURYFBzdDQkJ757du3kNnDw0OAeMw7Bl9wVfv5+eHEb9++GRoaOjg4VFVV4UQmoyYqKgq28Kiqqp4+ffro0aNWVlZ4qX/s2LHt27dDTrAH9enTh6uqIee2bdvo6XC6R0REpKioSEBVwIn7jRs34pSamhpNTU1bW1ucwrdp8Bmc/fv30wusqak5cOCAiYkJ/Mmkhwtg9+7doqKi3bt3x2O/oqJi1KhRRkZG6enp9JzPnj2Duxw5cqTRYvny4sULKCEhIaHRzNgw4ezs7OXltWzZsmnTpsHWHnNzc29vb5gcfHx8IFtISAi+Nj8/HzZcr127FicmJibCPghJScmdO3e+evXq8ePHEydOhClIVFTUwcHh1q1bAkTCe5x3796NExnO27C1CiGEOxsAbhyNjY1xCpyrgsln4MCBo0aNmjx58pw5c1xcXDw9PSsrK3HOkpIS8KA3adKkiIiIXbt23bx5k2qJecbJyQnq5Ny5cxwOBxLPnDkjLi5+9OhRek5PT0+8P05fX9/Ozm7EiBEdOnRYtmwZPRssHYcOHUpPhP25CKHi4mJIYdjobDYbWm3EiBH0AhuaynjzjBs3TnAN0Pnw4YOoqOiFCxfoiTk5OWvWrDl06BB8dykoKKAoaunSpbi62gt7e3stLS362E9ISJCSkuKaRnBdcQ3nnJwcBQWFOXPm0BPnzJnToUMHAXoFBvwGqKurUxSFKISo/1uqsNlsFRUVcXHxxYsXZ2VlQWJpaenUqVPnz5/PVWnFxcUaGhodOnQIDQ3Fibm5ud27d7ewsKirq8OJcAtEIfiqAW9qAAcXmjBhQqNi8+X79+/YfWFwcHBD2Zi/KJkMlpZSFPlSWFgIAri4uNDTf//9dyiwoRJg4+rvv//O91dwL6CoqBgWFgYp9fX1Tk5O8vLyz549w9kYSi5A0WJ+L+GVOoZzEQ7zRddVKIqC8xYdO3akK2Pl5eXwYnJ1dcWJDKdQiM+rpaX1+vVrHx+fLVu2MK/SQ4cOQZ7o6Giux2y0vxEIhHaEGPsIhF+C8vJyrBaIiIjMnz+/qKgITrnOmjXL39+/oqKCoqgtW7YYGxtraGi4ubl5eXlt3rxZT0/v7t27XEXNnz8fb9QSERGZPXt2Wloa1x1fv36Nwx2IiIgMHjw4Li6ue/fu/fr127dvX3JysmCB379/b2NjIykpKS8vP3bsWFtbW21t7S1btkA8WTocDufIkSP000NDhgx59eoVPc+jR4/k5eXh1759+z569IiiKOzLfOfOnWlpab/99hsss0FPOnbsGEVRgYGBEFUDmD179v3792tqarZv347P6fTu3Xvv3r1lZWUNPcvIkSONjIxMTEwOHDjg6enp5ORkYWERFxdHz5OSkmJpaQk6FkJIWlp6+/bt9DJra2txhMEOHTqsXLmypKTk+fPncIm2tnZUVNT69evxeQppaenly5dzaf/19fXbt2/HZhdxcfHVq1fjRSxFUVu3bsUlKCsrb9mypbKyMjw83MHBAat61tbWr1+/Ftx8mMmTJ+/bt48rcf369atWreJKrK2t3bdvHyiLCCFRUVEnJyfc9zgcjo2NDfykoqLy4sWLN2/e0PeATJo0CZp1165dOGSznJzcpk2bBIjXaMcQ/HRXr14dMGCAiIiImpratGnTRowYoaend+XKFd6cTEbNwYMHwUSOEBo3blxiYmJhYSF8M5eWlr5z586rV6+WLl2K88yePfv27dsURR07dqx///6QKCUl5erqGhsbm5KSsnr1anw+yMLC4vz58w09SHV1tZKSkq6u7rhx4/7888/Tp08PHTp0/vz5JSUlgpsGO2jT09MzMDAwMDDQ19fv1asXrD8nTpyIb9FoDxfMy5cvTUxMZGVl7ezsli5dqq+vv27duurqapzh48ePmzZtwjE6O3fuvHz58lOnTjEsn6Ko6dOna2hoyMjIyMvLy8nJycrKampqLl26VMAl3759ww7CFBQU3N3d6aeVJ06cWF5evnPnTiyVrq4uDIebN2/iPXHQZPn5+VBmRkaGtbU1rqhevXoFBwfPmjVLWVl59erVYWFhDVlqioqKtm/fjmddGRmZNWvWYEtEoz3wypUrWCQ5ObmNGzdmZ2dHRkbST0nPmjUrMjIS8u/evZsrEgtGTU0tJycHl3zw4EH8pLBebZF5Box9ZmZmCxYs0NbWnjlzpoGBQe/evR8/fsyb+fHjx+rq6ljCXr16eXl54ZoMCwvDjykiIrJw4cK3b9/m5+e7uLjg3XDm5ub+/v4MG/3EiRP0WBlz5szBH594pzJeaevq6sTExMAqypxnz55paWlt2bLl+vXrHh4eixYtWrVqFbxqs7Ky1NTUli9f7uLi4uPj06RiW4OysjJ7e/uhQ4d+/PixoqLi8ePH2trav/32G5eFt6qqavjw4fr6+rwKw6dPn3r37m1nZ/f48eMHDx7Mnj1bV1f348ePTO7ekLGPoqiJEydC03Tq1AmC8NrZ2d27d49vOd+/f581a5aEhMTcuXOPHz8O84+LiwsYVTECjH3l5eXdunVDCIFZpEk8efLE0dERKy0IIUlJyalTp3KZhJrxohQ8WKgWVRS58PHxwbMQ2JISEhKys7PXrFmD32W6urpeXl6817548UJMTIw+89CBV5Wzs/OQIUPMzMzs7e1VVVUtLCy4Ph43Knl5eXmjihbDewmv1DU6FxUVFR04cADnUVNT27VrV15eXlRU1MqVK/EzWlhYQLc8f/68ubk5JMrIyKxbtw5rqo32CoqiQkJCwJODiIiIs7MzfkELrtKvX78uWLAAvglBybDlHxfLpL8RCIT2QoRqOV8SBAJBSDIzMzMyMvT19cGzT15enpycHH4BI4QqKyulpaUpikpPT//+/buysrKOjg42LtCpq6uLi4urr6/X09OjOxzhIj8/Pzk5uW/fvmBSSUlJga+ODKmqqkpNTS0oKJCXl+/Xr5+AU0gURaWlpeXl5eno6ID2zEVFRcXnz59lZGRglwpCqLq6OjIyUkdHB2J6th5Qq3V1dUlJSfn5+VpaWg1FYKioqIiNjZWTk9PW1ubrpDw5OTk3N9fAwACbqFJTU0tKSoyNjflGJ+QLRVEJCQlVVVX6+voNLddbiszMTPAnRU+EYyZ8m0kAHA4nNja2urq6f//+gj24NwnhO0ZhYWF6enpZWVmPHj1wIBS+NDpq8vLyEhMT1dXVsSkZRpCxsbGAUdYiVFVVSUlJVVRUJCQkVFdX9+vXj76MbCka7eGCqaysjI2NFRcX79evX/M8mrc49fX1sbGxNTU1/fv3xxNUZGSkrKxsnz59ml0sPKmqqirMFd++fVNTU+M7FTcJhvM2E86fP5+VleXo6JiVlVVUVFRRUVFZWVlZWZmVlQXfSE6ePIkzf/v2LSsry9DQsAWjb8+dO9fX13f06NFPnz7Nzc1NSkrq2rWrvr6+ACd3P3/+TExMVFNTU1NTEyYorZCN3uhU9vbt28mTJ2dlZdHfzkygKCo1NTUlJUVRUdHIyIheeGVl5fv37/v06dPuEVQwERER9+/fLyws7NWrl52dXVPHC4vFCgkJef/+vYSExODBg4cPH47tBYJ59uzZ6NGj1dXVv337JoJEEEIU+v9LlaioqMTERDabraysPHDgwEZ7bG5ubnx8/I8fPzQ0NPT09PCrGQO3QAiNHjM6JCTk5MmTeP84Qqi8vDw1NZVh4JS2RMBgaXFFsUX4/fff3717FxwczPfXXr16ZWVl7d+/f+vWrWlpaVlZWerq6tjcyYswkjfpXkIqda30AmqIRqfQgoKCuLg4DQ0NXi232VXKvL8RCIS2hxj7CAQCgUAgEP497Nq1KygoKCoqCu9ApJOamurq6kr37tQa0I19rXqjtsfJyUlLS4tvQBuC8Ag29rU4go19hBahvr5eW1v70qVL9B21dLABbtu2ba0tTFvei0AgENoXpjtNCAQCgUAgEAi/OG/fvt27d++yZcv4WvoQQomJicIESfiPExcXFxQUBJGmCa1HQUFBTU0NkkSIZo9rVZodmozQKF5eXnJycg1Z+ggEAoHQShBjH4FAIBAIBMK/BIh7HhAQMG/ePOzJEairqwsKCrpy5crNmzdbWwwIGck8bu+vTEBAwMWLF/X19fX09H7//fc1a9YoKyu3t1D/cioqKhwdHdGttrsjjpNAaBF2794dFRU1cOBAGRmZbdu23b59W0Dmtpwu/k1TE4FAIAiGHOMlEAgEAoFA+JdQWlo6fPjwmJgYhJCRkVH//v27du1aVlaWm5sbExPj5OS0ZcsWHAWo9Rg6dGhYWFjv3r1TU1P/6f6bDA0Nv3z5Av83MDCIjIxsqrc+AnN+/vz55s0b+P8Muxltdt9bAbcQQiYmJlpaWm12038rhYWFdLe/S5YsOX/+fEOZKyoqunXrVlNT4+TkdOXKlVYVrC3vRSAQCO0OMfYRCAQCgUAg/Huoqqq6cOHCtWvXkpOTi4qKZGRk+vXrN2bMGBcXl9aOd4QQCgoKunDhwoMHD+BPCwuLYcOGrVq1qqHAR78+/v7++/btS09Pt7GxOXv2LG+QBwKBwMXOnTu9vLzYbPbixYv37NnTkFeBI0eOXL16NTo6GiEkKio6bdq0fv367d27tzVEast7EQgEwq8AMfYRCAQCgUAg/DvhcDgNLbMJBAKBQCAQCP9WiLGPQCAQCAQCgUAgEAgEAoFA+JdAPvYSCAQCgUAgEAgEAoFAIBAI/xKIsY9AIBAIBAKBQCAQCAQCgUD4l0CMfQQCgUAgEAgEAoFAIBAIBMK/BGLsIxAIBAKBQCAQCAQCgUAgEP4liLe3AAQC4V9IXV3dq1evbt68aWRktHr16vYWpx149OiRj49PZGRkp06dbGxs9uzZ07Fjx/YW6tfl69evS5YsGT169N69e9tblrZj27Ztr169OnfunL6+fnvL0ka018yQmpoaHBx869at8PDwNrspncLCwmfPngUHB48ZM8bZ2bldZCAIICsr6+PHj1yJ5ubmioqKd+7c4c1vZWUlLy8P/4+JiUlPT8c/ycvLW1lZtZqkrUtGRkZQUNCNGzfCwsLaW5a24D84Cf/Tqa6uDg0NvXv3LkLozz//bG9x+NDig4jFYr18+TIgIMDY2NjFxaVFygT+m6oXgfDfgiIQCIQW5dWrVxYWFjDD7N69u73FaQcuXbo0evTourq62NjYLl26IIR27NjR3kL90mzZsgUhJCoqWl5e3t6ytBGVlZViYmIIITc3t/aWpY1ol5mhtLTU0dFRQkKiHXWeDRs2qKmpgQDu7u7tIgNBMHl5eYcOHZKUlIRm6tOnz507d0pKSiiKev78+Zw5c7DavGDBgqdPn1ZUVOBrExMTjx49KiIighCaMWPGmzdv2u85hMLFxQVeWP+R1cF/cBL+p+Pp6Tl48GDoolOnTm1vcfjQ4oPo1q1bOjo6rfTe/A+qXgTCfw1yjJdAILQwVlZWYWFhWDv5rxEXF7dw4cI5c+aIi4sbGBj4+fn16dPH0tKyveX6pZk1a9bAgQM3bdrUuXPn9paljZCSknJ1dR00aJCjo2N7y9JGtMvM0KVLF19fX09Pz7a8KRdHjx5NSUmRlZVtRxkIglFSUnJzc8PbWywtLSdNmgRNNnLkyMuXLxsZGcFPK1asGD16tLS0NL5WR0dnw4YN/fv3NzU1vXHjxtChQ9te/hbhr7/+Onv2bHtL0Xb8ByfhfzqLFi2KiIiYMGFCewvSIC0+iOzs7BITEzU0NFqwTMx/UPUiEP5rEGMfgUBoFRQVFdtbhPbB09OToqhu3brBn7a2tklJSePGjUMIJSQk+Pn5tat0vyjGxsYfPnw4dOhQewsiiOrq6gMHDjTvWh8fn5SUFK7Eo0ePvn//3tDQUGjRfl22b9/OldIuM4OSklLb35SOhISEsrJy+8rQLvDt+a0Hb39rEi4uLl27dkUIvX79mp4uJia2adMm+D/fU70URSUnJ69YsUJUtP31auZ1zvtK+q+9uP8Lk7DwCDmsGML8DduzZ8/WFoY5bTOIWukV9o9QvQgEgjC0v1JCIBD+lcCZpv8gz58/RwjxLvkqKyvt7e1LSkraQSZCS7By5cro6OhmXBgXF7dq1aqampqWluhX5/Lly+fOneNKbJeZ4VeYjn4FM1Ab08Y9n29/axKdOnVasGABQigtLY3L35a9vb2CggJCyNvbm8PhcF34+fPnmpoae3t7Ye7eIjCvc76vpF9hpBB+KYQfVgxh/ob9debSNhtEZGASCITm8atMlwQCgfDv4MePH7yJFEUtXrz4y5cvbS8PoUU4e/bspUuXmnFhaWnp9OnTKysrW1ykX5yoqKjly5e3txSEdqONe35L9TcnJyf4j7e3Nz1dUlJy7ty5CKGsrKwnT55wXXX9+vVJkybJyckJL4AwMK9z8koiMKHNpvFmv2HbETKICATCrw+JxksgtCfR0dHPnj3Df44YMWLAgAHnz58vLy/HiUZGRmPGjEEIhYWFvXv3DiHUrVu3+fPn4wypqam3bt36/PmzvLy8mZnZ7NmzxcX/Z2i/fv362bNne/bsSU5OPnz4cKdOnXbs2IEPGjR6eUOkp6cHBgZ+/PhRXFzc2NjY2dkZRyfkIiIi4vLly7m5ubq6ui4uLj169ODKUFdXd+/evU+fPiUkJPTp06d///6TJk3CvtIxb968uX37dmVlpaqq6oABAzp16mRtbU3PwOFw/P39IyIisrOzjY2Np0yZwhViLy0t7fr160OGDBk1atT79+89PT1LS0uHDh26cuVKcNQtmJKSksuXL3/9+rW0tLRfv34TJkwYOHAg/FRYWAiLw9LSUoTQ7du3ExISEEJ9+/YdM2bMkiVLrl+/jhAKCQmprq5GCDk7O8M+EYRQWVmZr69vdHR0dXX1oEGDZs2axXUMJCkpydPTc+vWrSIiIocPH87Kylq7dm3//v0FiNpQs6akpAQHB9Nzzpw58/v37xEREfCnmpoafX9KdHS0t7f3wYMHORyOl5dXWFiYgoLCyJEjZ8yYwfup+c2bN0+fPk1ISNDS0ho1ahT0W0xNTU1QUFBVVRV4uL948aKRkdHWrVvh18+fP/v5+W3evBlXC0Lo69ev165dmzVrloGBwZcvX86fP5+ZmWlgYLBhwwZYVyclJfn4+CQkJGhqai5fvlxLS4tLpBbpEmfOnIEQeImJiceOHUMIWVtbm5iY4Kr28/P79u0bm802NjZ2cHDAh4yys7OnTZuWmJiIELp06VKPHj06deq0cuVK+LWysjIoKKi6unrRokVcYicmJj579uzTp0/19fUDBgwYNWoUb7DIiIiIq1evbt++XVFR0d/fPzAwUFJSctq0aZMnT0bMePPmzdu3b6Oiorp16zZgwAAbGxuuo6a1tbWPHz9+/fr1kSNHWCzW6dOnw8PDe/bsuXDhQmNjYwElh4WFTZs2jcViiYiIQI3p6OhMmjSJ9xEEzwyNDo2GyMvL8/LyioqKkpWVnTBhQqdOnfhma6h7/Pjx4+rVq/Scq1atgoDaz58/j4qKgsR58+YJP5EiBg2BGI/Eqqqqu3fvpqambt26ta6u7tKlS0+fPpWUlJw3b97YsWMRQnV1df7+/k+ePKmtrR0zZgxsZOOVR8BALi4uvn37dm1t7YoVK7Kysk6ePJmcnNyvXz9XV1c8eAX3fF4oirp69WpoaChCSFNT09TUVEFBwdTUlJ5HQGdg2N+YYGJi0rdv3/j4+Bs3bvz5559SUlL4JzabDf+5dOnS+PHj6cL7+fmdPHmSobQCSE9P9/T0zM3NVVBQMDIy6ty585QpU+gZBExozOu8rq5O8CsJqKiouHjx4tu3b6WkpGxsbPjuW2xGt8/Ly7t586aysvLMmTPz8/NPnz4dHR3do0ePtWvX6urqIoSKioq8vb0/fPggIyMze/bskSNHcpUg4EXMYrHOnj1L33ppbm6uqqp669YtnLJhwwb8f76TcLMHUVZW1o0bN+D/mpqa06ZNQwi9e/cO7xJVVFScN29eC1YFXwQrVHR5hg8fDlXn7++fkZEBif3794cbCRhWlZWV/v7+oqKi8+bNi4iIuHLlSl5e3sCBA+3t7TU1NaEceplOTk4wpx0/fpyiKEicMmWKtrY2auwN2ySEn7swHA7n2rVrYWFhJSUlJiYmFhYWQ4YMwd27BQcRXx4+fBgUFFRYWNivXz+uyPUURbm7u0Mnl5CQWLNmDUIoOzsbhAFWr16NW7wZqleTtIvk5OQbN27ExsaqqqqamJiMHTu2e/fuDB+TQCC0Ou0WGoRAIFBUdXU1bBZACB07dqy+vp6iqPT09FGjRkHitWvXIJGiqJqaGldXV1FR0fDwcFzCgQMHxowZc/ny5Zs3b8Jr2NLSMicnh6KovLy8rVu3gvlDT0/vy5cv4I0IIXTo0KFGLxcAh8M5dOiQpqamt7d3YmIi6HxdunShRyEEJ+W7d+/ev3+/pKSksbExeDTv0aNHQUEBvbT4+PgBAwbY2dm9f/8+ISHh4MGDENoiKiqKnu3gwYPGxsbfv3+nKOrjx4+9e/eeMGECVzkjRozYuXNncHDwsWPH5OXlJSUlz58/D78+ePDA3NwcHv/SpUt79+7t0qWLrq4urJNnzpzZaGM9efJERkZm165d+fn5FRUVCxYskJCQuHLlCvxaUFDg7u7u7u7eoUMHhNCiRYvgz0ePHiUnJ7u7u4MXPzs7O0jHlXD37t0hQ4acPHkyKCho/fr1YmJiqqqq0MT19fXu7u5Y7OTk5AEDBsD/x48fL0BUwc0aFxeHnc0/efIEbrRw4UJJScmLFy9CUDYOh7Nv3z4DAwPI9uHDhz59+sCjAfb29iwWC9+xtLTUycnJ2dn51q1bly5dgvXnsmXLamtrKYqKjo5esmQJdL/ly5f7+fnhYzjPnz8/dOgQvlFWVhYUeOHCBexH6enTp9u2bVNUVDQ3N5eRkUEImZubczicgwcPysnJWVhYqKioIIS6d+/O1XVbpEuwWCx3d/chQ4YghIyNjaH5oqOj4df9+/dLS0v7+/tXVVVlZmYOGDCgR48e+NcXL17s3r0bbuHm5ubu7n727FmKol6/fj1nzhywIyxevJguM5vNPnTokKKioo+Pz/fv3+/du2dpaSkmJrZjxw6oTIqi3N3dcXTXr1+/TpkypXv37r169YKU06dPN9qZS0pKHB0d9fX1Hz9+/P37d29vbxUVla5du16/fh0yVFdXL1++HKIKqqurJycn9+3bV0tLC2ysUlJSkZGRAsq/cuUK7JCSlpaGGrt37x78xHxmEDA0BPP27VtlZeXNmzd//fr169ev8+bN69evH6/OI7h7REREgHVPXl4+OzsbX1VdXb1+/XoREZGAgACcyGQi1dPTQzzReBttCOYjMSMjY+7cuWDWHDFiRHR0tKGhYb9+/WAwioiIPHjwICkpydjYWFtbe/DgwbBq3bVrF10ewQM5ISFh+vTpcKGzs/O9e/e6devWt29fWFVqaGjgGLUN9Xy+sNlsGxsbR0fHiooKNpt948aNDh06HD58mJ5HcGcQ0N+aAQ7Tgad3iqJqa2sVFBRgREhKShYVFeGfXrx40bVrVzw8G5W2IZ4/f66oqPj69WuKonJzc0ePHi0lJUXPILjHMq9zAa+kkJAQKCEtLU1HR0dVVRXMMQihAwcOcJXTVP3h5cuXY8eOhcl/9+7d169fV1FRGTRoEEzgCgoK2dnZ9+/f7969e//+/fv27YsQEhcXh5cURvCLmKKovLw8HHBj+/btbDaboijYNbZu3bofP35ANr6TsPCDKCsrC2rM1tYWUmpra0HJQQiZmJi0YFXwhYlCFRYWBpIfPXoUUiorK1+/fg2vV1dXV0jkO6w+fPgwa9YsmLTXrl27a9cuERERPCPJycmFhITA5fX19UePHoV0/L4oKio6c+YMJAYFBVGNvWH5ApsNuaLxttTcBeTn51tYWEybNi0tLS00NBS0kY4dOxoYGJiYmJw9e7alBhEvtbW1ixcv7tOnz927d8F8bGhoCD77cDTeyspKOzs7hFDnzp0hhcPhZGdnY7szRBJvhurVVO3C09NTUVExICDg+/fv2OViz549TUxMcG8nEAjtCDH2EQjtTHZ2toSEBELoxo0bOBE7LklLS6NnPn78+JQpU/CfBw4cMDQ0rKurgz/ZbDa8ue3s7CiKKi0tTUhIALuhmpra3Llzk5OT9+/f36NHj8ePHzd6uQA8PDzk5eUzMjJwCnzs1dLSwimwpNfV1V2zZg3Yj9LT0+HL4b59+3C2nJycLl26WFtbczgcnOjr64sQ6tSpU2pqKqRkZGSIiYl5eXnhPA8fPhw9ejT+My8vr2fPnvQ6vHnzJmjnWGvMz8+HiGO6urqHDh0CFRCyIYRiYmIEPHJ1dTXowaWlpbg0UMrpazyKouAWoMXSAW3v5MmT9MSXL1926dLl27dvOAU2YqiqqrJYLDabnZycjB3DOzo6vn79+vbt22pqavv3729IVCbNmpSUBOsZWFGzWCwdHZ2bN2/Sy0lPT8e3trCwePToUX19fUZGBt5Vilc4HA5n7NixK1aswNdmZGSADu3h4UFR1I8fPyIjI2HflpWVlYuLS2pqqqOjo5aWVnp6elpaGv7IjDVOiqLS0tLA7mZgYHDp0iXoIXFxcbDhbtSoURs3boTKZ7FYsM/x4MGD+PKW7RKLFy9GCM2YMYNeRbA/wtTUFKf4+/sjhCZPnoxTsrKyoLQvX77Q6ycqKgo2AHIZ+6DOX716hVPKysrg6ZYuXYoTHz16BMWamZndvXsXEletWoUQUlJSwq3PFw6HY2Zm1rVr1+LiYpz45csXMORdvXoVJ27ZsgUh1KVLlzFjxsTFxVEUVVFRMWjQIITQ9OnTBdyC+nsUd+vWjSud4cwgeGgIuG9ERISkpOSWLVvoiRMnToTqwilMugc8focOHSorK+mlHT9+nL7OZDiR8hr7GDZEk0biwoULodpnzJiBp+g5c+YghPr16zdgwIDY2FhIhC1IsrKyVVVV+HLBA5miqJqaGisrK4SQhoaGo6NjYWEhRVEpKSkwPf7111/4Wr49ny/g3p7+snNzc6NPcUw6Q0P9rRkkJyeD5NbW1jjx9u3bSkpK7u7u8BN9Jl+0aNHy5cubJC1f+vTpM2/ePPxnXl6evLw8/c9GeyzzOqcaeCVhO8WYMWNCQ0MhcePGjdBV6AaR5ukPVVVVYHlXU1Nzc3ODkVVeXg5Buq2srMaMGYOHw4oVKxBCY8eOxZczfBFXV1eDrQd/EZw7d+6qVavokjQ0CQs5iCiKWrJkCaIZ+wCww9LNH0JWBV8YKlQURfXp0wfRjH3AiBEjEM3YR/EbViUlJREREfgD25o1a3Jzc2trax88eABnO2RlZbHY2dnZ0J3oH4fYbDa8xOlqEt83bEPwGvtadu6iKGrGjBliYmL4+9O3b99A5k2bNoWEhODJSshBxJepU6d27dqVbjTHBy+wsY/6e9rExj7g5cuXkBOMfc1TvZhrF58+fZKQkKB3GBg7ysrKISEh2OxLIBDaEWLsIxDan+nTpyOE7O3t6YlwhAFvwQNGjRp1//59+P+PHz86duzI9el+37598JLGusiRI0cQQmJiYunp6fScDC/nJTExUUpKas+ePfTEp0+fIoT69OmDVUxY0s+fP5+eDRRWuilk9uzZCCEwPtKxsLBACI0bNw7+vH37NkJo27Zt9Dxz587F/1+7dm2PHj3ov9bW1sLGHLoMsN7+7bff6DlBbaVbEnlJS0tDCImLi9M3H4EqmZKSQs/ZJGOfiYnJrFmz6Clv3ryBJrh06RKkfPjwAVLomxcagnmzQseQlZXNy8tbu3btnDlzeEuDU3Xof81ebDYbzvjIysqCQhkYGIgQSkhIoF8LJ6x79+6NU8Dgoqury2uKwuci6RonRVFg9Thz5gw9EbbjTZw4kZ64f/9+hBDd/tuyXYLvUsTLy4tr/fb27VuEUM+ePXGKgOU3HNCjrzO/fPkiISFhbm7OlRN8hImIiISFhUFKbm4uFEsfO0lJSY2OX4qiwNs61yNTFAV6v7KyMl6qwbEgSUlJvLSmKOr8+fOwQBVwC6oxY1+jMwOTocGXYcOGdezYkWuTII6WiFOYdI8fP37AGOcaeubm5niLDfMRx2vsY94QDEciRVFgjVJRUcFbwimKevz4MVz+4cMHnFhfXw9Ph5ejDAcyrLQtLCzoBgUwhdBtVcwNT3AM7enTpzglLS1tx44d+E8mnaEFjX0URQ0ePBhGHOwlpyhq8uTJLi4uhYWFsIlpwIABkM5isWRlZem72pvXdYuKihBClpaW9MR58+bBxjSKWY9tQWMfXWHIy8uDxE+fPkFKs/UHiqKmTp2KEHJwcKAn/vbbbwghRUVFutUMur2EhATuacxfxO/evYNNTI8fPw4MDNTW1uYy2QO8kzAl3CCiKAoOXXIZ+yDgKddeJ2Gqgi8MFSqKosAyy2Xsg8Ppgo19wOjRoxFC06ZNoye+ePECqgh/figoKIAUrp3g0G9b0NjXsnPX9+/f0f++xymKghPcXG8uYQYRX+DajRs3cqWDcZZu7IOT6VzGPnD1g/429gFNVb2YaxewkdDT0xOnhIeHQzb61w4CgdCOEJ99BEL7s2jRotu3b9+5c6e4uBi7vTMzM4uOjr5y5YqbmxukfPv2LTk5edy4cfDnjRs3qqurnz9/Dr7hALwl4e3btxoaGggh2DaoqanZu3dv+k0ZXs7LgQMHqqqq8E4ZYPTo0XFxcT179uTy48ZViLq6OkIIL0jy8vKuXbuGEOJ1P7d48eLw8PDHjx+npqZqaWnBxp8jR4707dsXn9DZtWsX/IfD4Vy5cqVz587r1q2jFyItLV1dXY2VD4QQHOLgcqqioqKSk5ODpeKLhoaGn5+fnJwcvpb62+9MswPsxsTEREdHS0lJ0cVmsVjwn/DwcGdnZ/R3CyKE8OFuATBv1nXr1l2/fv3jx4+TJ0/Oysr6/Pkzb2nYPQ2984iKiq5du/bFixelpaUfP34cNWqUj4+PmJjYmTNn6K0PWv73799zcnLAdgYPYmVlxevUCT8jXwHgwzsGSuNygAWHXLA+3QZdAiHk4OBQXl5ObxfoFQy7BO9Tnzx5sq6ujnc4jB49Wl1dPT09/cyZM7Bsw3VIlxxqBiGUlZXV0PhFCJ04cQLxG3SLFi36/fff8/LyAgICwIMV3EVCQgJOL9Lv0mjlCEbwzMBwaPASEhLy5s0bKysrrgbl+pNh9+jevfusWbN8fHw8PT1x3IakpCQ4Ygl/NnsiRU1vCNTYSMQ5paSk6E5IccegjxoxMTEFBYWcnBw8ahgOZLhF165d6dmE6RXQOs7Oznfv3gU/fRoaGnjfYrM7gzDMmTPn/fv3FEVdvnx5+/bt+fn5Dx8+DA0N7dq1q62t7e3btz9+/BgbG2toaHj37l1ZWVlLS0shpe3cubOkpGRYWNi6desOHz4Mk8OePXvgV+YTWktB72lKSkqdOnVisVhZWVnQQMJ0ewGzurS0NN29JszqdXV1xcXFcBSR+YvYzMxszZo17u7uy5cvr6mpuXnzJt39Iobvq0eYQdQkhKkKXpgrVM0Qla/kXFrliBEjTExMoqOjnz9/jk+Utw0tO3d9+vQJIVRWVka/hZWV1ZMnT/BeRSYIHkR8AbV2woQJXOkKCgp4fDWVpqpezLUL3ooyMzPr0KFDbW1tdnY2vNYJBEL7Qox9BEL7M27cOFVV1ezs7Bs3bsCHR3B+LCUl9fXr16ioKNAMLly44OzsjLVP+NI7aNAg7E0DITRkyBBwAsjlPp83+kSTLqcDHx6xD2YMuJURDHwDr62thT+/fv2KEBIVFeV1jYydqcXGxmppaQ0bNmzw4MHv3793cnLy8/Pz8PDQ0tLCOmt8fHxRUVGfPn3A7Qv9cfg+Oxeg2dBdevMFPv8ihEpKSry9vUNDQ+vr6xFC8G8zgCbQ1dXlEht26/AGK2ASQoR5s4qJiV28eHHgwIERERGHDx9uaP3AF/jEjRBKTk4eNWpUaGiolJQUbjJ8U/gPfMNv0lMIhq96CtttiouL4c+26RJSUlKwKQkhlJaWdu7cucjISCREl4ARwevIX0RExMzMLD09PTY2tlGxBUteW1ubkpLC9y6amppKSkp5eXlM7tJo5TQJrpmhqUMDAzsrscuhhmDePVasWOHj4/Pq1avk5GTYW+Ht7b1w4UK8Vmz2RCp8QyCekdhQtoaM6VyjpkkDmQthesWSJUuOHTuWnZ09aNAgFxeXPXv2dOnSBU/vze4MwuDg4LB+/XoOh+Pt7b1t2zY/Pz91dXXY7jd//nzYaX7p0qXjx49fuXLFwcGBqz80Q1oJCQkXF5ejR4+Cp9dTp06NGjUKr5aFnNCER1JSksVicY3QZnT7hhAwqyOE6BYu5i/i/fv3BwUFffv2bcKECdga22wYDiLhYV4VXDBXqFpKVF7Gjh0bHR3dbLNUs2nZuQtqu6ys7OfPn2BmRX+/VgRYsRuFaxDxwmaz4SRHo6+wZiDkLMFXu4CKoje3qKhoz54909LShKkoAoHQghBjH4HQ/oiJic2fP//gwYM+Pj5g7Lt06dKQIUN69ep17ty5y5cvm5qa1tfXe3t74xhqCKEfP34ghAYPHgxeSJpK8y6vr6+HCyH0mJDApgAOh5Ofn481KgDHM4X1sIiIyL179yZMmBAZGfnw4UMDA4Nt27b99ttvoL6ASFJSUg4ODsJLJYC8vLxDhw7Fx8dv2bLF1dW1U6dOOD5jMwCx1dXVW1DsJjWrsbGxo6Ojj4+Ph4fHihUr4AAyEzp16tS1a9eioqKqqqq6urqCggIxMTF7e3vs+7ndabMugRCKjo4+cOCAjIzMli1bpk+fzrUabxIwIvhuEoERAcNBGFJSUmBV3NBd8vLyhL+LkDR7aMTHxyOE+G7h4S2fSfcwMzMzNTWNioq6ePHioUOHOBwODhpLL6oZ83CLNAR9JDbp7ry040Du0aPH06dPbW1t8/Pz3d3db9y4cerUKYhkilpnnmyU7t27jxo1KiQkJDU1NTQ01MfHB+8ot7GxUVRUzM/P9/X13bRp06NHj3bu3IkvFEbaQ4cOFRUVeXl5JSQkWFtbz549++TJk2DZacsJjQlCqh9CwvBFLC0tvWvXrgULFjx+/PjTp0+C49f/C2CuULUecCPhp6Mm0eJz17Bhw2RlZUtLS69cuYJjN8PhVnoY7hYnNTUVTIGNvsJ+ESZNmvThwwd/f/8TJ07AFzs2m11QUGBoaIh3AhIIhPblV1mYEQj/cWCryLt375KSkjgcjru7+6ZNm8AdxtWrV+vr6+/cuaOvr0//VgZ2rtTU1ObdsdmXw4UtojLi78+8JyM6d+4MH2Px1gZFRcW3b9/u37+/U6dO1dXVO3bsmDRpUl1dHRYJvPm0HkFBQbq6uh06dHjw4AH4sRYSIVtQ+DIzMjJevnwpJyeXmZm5bdu2Jt0ILIN6enoiIiJiYmJsNhvc3PwitE2XoChqx44dZmZmTk5OXl5e4FhdGGBE8D0oBPu/hD8XI2DQteBdhKTZQwMMZ40eJm1S9wB/gt7e3vX19SEhIQYGBnjlLIyoLdUQeCQ2VQAu2ncgDx48OC4uDs5K//jxY/r06dgBXGvMk0wA92cIoc2bN0dHR2Njn4SEBGwuy8/Pd3Z2VlVVHThwIL5KGGlht3VAQACsk69duzZgwIDMzEzUVhMac9qrUVBTXsT19fWnT59WV1dns9lLliwR5svcP4ImKVStREtNR02ixecuGRmZa9eugXNqcLj548ePs2fPLl++HFxstxL4w4+QXjLajM2bN9vY2BQUFCxYsAB2ABw9elRCQgKOkxMIhF8BYuwjEH4JtLS0hg8fjhC6fPlycHCwvLz8iBEjzMzM+vbt+/Pnz8ePH587dw4cGNMvQQiBW2JeIGKG4Ds243JxcXE4wHv37l3eX2tra3/+/Cn4vnTARTRCKCYmhuuniooKUB0MDQ0RQsXFxbW1teLi4tu2bYuPj7exsUEIPXz48MKFC/hZvn//Dg5EuCgsLMROiJtNdHT0zJkzlZWVDx061FLbXkDse/fu8T31+fHjx2acDGLerBRFLVy4cPv27YcPH0YInTx5Ekd8axQOhwM7O/T09MTFxeHICd+bUjRn1W1JG3QJhNCZM2f2798/f/588PIuPDAi+PpPhKCTMByEQVlZuVu3bojfoGvBuwhJs4cGzE6JiYlMymfYPebMmdOlS5e8vLx79+55e3u31DzcIg1BH4mCczZKOw5kWOJ269btypUrb968AaP5rl274Fhia8yTTJg+fTrsVXn37p2ZmRl44gewP8EnT57Y29vTr2q2tPX19YWFhXDfxMREiN2Znp4OG4vaZkJjjpDqR7Np0ov44MGDOjo69+7dk5CQ+PTp0/Hjx1tJql8E5gpV6wFWqjY29rXG3GVjY/P69WsJCYn169ePHTt2/fr1J06cOHPmTAuI2zDYPU6jr7BfBElJyfv370+fPv3z58+WlpZ2dnb5+fmJiYkQC5tAIPwKEGMfgfCrAI7Yr1y5cvTo0c2bN0MibO47ePDgp0+fIHAbBtSpBw8e8Gr/79+/h/CdAmj25RAm+NSpU7xfjzdt2sTl0lgwurq6sEeG9zPgt2/fEEL9+vUDP1lPnjzBAQ179+599+5d+L4Kjsl79uwJ35MhJCsdiqJcXFy6d+/OXCq+nDt3rq6uDjay4cRm7BSgrwChCYqKik6fPs2Vrbi4eP/+/RCLtkkwb9ZTp07V1tYuWrRo8eLFw4YN43A4ixcvhp2SjZKQkFBXV2dhYQFbTXGY0YqKCq6cfBPbgFbqElwLeA8PD/S/3ioFeOtj4sgPogemp6eD7zk6MCJaxKoIdwkMDOQ6jM/hcL5//96xY0ccBUh4mue+sNlDA9oiNTX19evXAspvUveQlpYGT2THjx9/+/atra0tr6jNm4eFbwiukSgkrTSQG+0DBw8exCEshg4dGh4erqmpSf0d27pJnaHZ7jJ5kZOTw27ycXgWwNTUFBtNZsyYQf+p2V23sLAQgsAihDp37nzkyBGI39K8dxzzehBmhDZb/Wg2zF/EUVFRp0+fPnHihL6+PkQ527VrV9tsjQTZuF6muIe3HswVqqYKybyTgJtRPF5wM9FvxGazG9I0mj1+W3zuys/PP3z48KdPnz58+PDkyZNr165Nnjy5ocwtNe2oqqpCLCwvLy++GXA4GvR33XLdug26GRfnzp0zNzf/8uXLx48fAwICjh49yusykkAgtCPE2Ecg/CrY2dnJyspmZGTk5ubikwJOTk5iYmLh4eFOTk6wywCzcOFCGRkZNps9bdo0+lagp0+fzp49G/YFoL+VA15tmOHlvLi5uYmIiFRVVVlaWmJFn8Vibdy4UVxcHG9/AGWOS6UDdyRYOxEXF4f1zLNnzzIyMug5fX19EUIeHh746z34RAfExMTADApLXFFR0dWrVyOEAgMD3dzcsP/j0tLSuXPnqqurY7fofFcFkJ+uRfEC36vfvHmDnygiIgL+z+VuuaamBv9LB1wv5eTkwJ/19f+PvbMOq2r5Hv4cGgQRAwRFDKQREEXsDsRCxBZRLLCwu1tRMRADxUYM7MACERVUEBEQ6Ua6+5yz3z/W7867v6fYlHC983nucx9ZZ/bsNTNrau0JtpmZGZzvtmHDBi8vLxwyLi5u2LBhixcvhsEcVoyJb5FhscbExGzfvv38+fMsFovFYp0/f15KSio8PPzQoUMCo+U5ggcWVOLAK1euRAilpKRYWVnB4hRI4LFjx7y9vbGTWpgpIloe8hiMwPP+QcgzxgUhzqsGNwn+4kP/WIWvry8OBqe5VVdX42fxYeo8z+K30JO8cOHCHj16IIQ8PDzo+uTl5T179mzgwIHTp0+vUW1UkzEfOnRITk6uqKiIXqcQQj4+Pjk5OevWrcNbvUS/RTSQ8KKiotLSUogKtGLSMjCsGvzY2dmBM2Xjxo30CV5oaCj8o7CwENXGPADYyfv+/ftp06bxXGjIvCGFzKSbNPOCwIiuiUhklUE11RqGFblWVQYJsnz+Z588eYL/bNOmDRzYB807Q2MQZm8/fvyYNWvWnTt3BL5aNLBdV0JCgv+kPFjc17Fjx969e9PldTZdhND9+/fpGWhvb89isWrVxzHPcySkTcMVk+dBnhpa5/EDqn2rjmh2xbAjrqystLW1PXDgAGyH37p1q5aWVnl5+eLFi3ney98Ii9ZQmJL0goM7WENDQ3FtLSoqAgvn8TrVJyv4qdWACpSkHwMdFRUF60PpSgqrVgBPc5SVlfXw4cPhw4ePHDkSPw5OavqLPD09ITn8L2Jit0hQW9qwbReHw7GwsOjRowfPdcP81KcSCQQS4uvrS+8USkpKYMs89F8A6FZRUREcHAwSLpd769Yt/AgOWduhF/PRxf379zdu3Lh06VIRKSIQCE0MRSAQmg0wpXR1daULLS0tEUI/fvzgD3/kyBGoyGJiYrCE3tDQUFZW9v379zgMDLtbtGhRXFxch8cFsnz5ctyGqKurDxw4sHXr1oMHD66srMRh4AvzzJkz6Q/C9SNKSkpsNhsLFy1ahBDq1atXSUkJSAIDAxUVFbdt24bD3Lp1S1JSMjg4GEuOHTsmLy8fExMDf+bl5eFrAVVVVSdOnDh69OiWLVtOmjQJv4vNZsPIzNHREcdTXV0Nm+kcHBxEJHn37t0QuY2NzaVLlxYtWjRt2jRYTDFlypRTp06Fh4dTFJWYmAjB9u7dyxMDrNxUU1Pz9fW9fPnynj17KIp6//49vidOT0/P2tp64MCB4uLizs7O+EE8Dfb19RWhIabGYq2srDQzM1u9ejX9KdgsJikp+eHDBywMDAyEqKZPn45L59GjR1JSUtu3b6c/DlYKljZ69OgJEyaoq6trampmZmbiMHCy1eDBg/l1vnv3LjxOt73i4mKYmfDUiIEDByKE+vbtSxfu378fISQhIYH1bFiTuHbtGmTptWvXfHx85s6dy+Vy4f5TcXHxTZs2nTt3bvz48WDMCKGdO3du3boVnoUtb8OHDw8MDNyyZcvTp09BDouDhg8fTk9IcHCwqqoqi8Xy9PTESs6cObN79+6/fv3CwbCf/fbt21iIt/J5eXnxZzIdDw8PKSkpJSWlnz9/giQrK8vAwMDCwqKsrAwHg41vYmJiv3//xkKYTyKE6IXLT1JSEhTf6tWrP3z4MHfu3LS0NIpxy8CkagjE1dUVnurdu/fVq1eDgoL27duHN7gNHDjw6NGjFDPzoANWRy8CDJOGtLy8XEFBASG0bt06+rMMC4J5TYRV4crKynThmzdv4HEfHx8srKqqgnn4pk2bsJBJRYYVLnp6evRXwJpTXV1dulCY5fOwZMmSbt265efn019hZGRUUVEBfzIxBmH2BqsjZWVli4qKBL5dBFBqY8eO5f8pIyNDXFx8+fLl/D/VzXTh+P+TJ09iCdRx3A4wtFiGeU4J6ZLAfYwQio6OpicWhFB3gDqPH+BSYxsbG7pw27ZtCCFpaWl6McGqRrrdMuyIV65caWpqyuVycVT44JHDhw/T3yuwEa5nJYqIiIBPAgMHDrx48eL27dt79Oixc+dOeHzt2rUhISH1zwphMBlQURR17tw5iHDevHmXLl1ydHQcPHgwLGFWUlI6ePAgtPnCqhXcU9GyZcu3b99ChGVlZcOGDevWrRselQFw9qWsrOzOnTvPnj07adKkpUuXwjZPQ0ND3LkL7GGFpXHEiBHQwtOFDdh24WvQO3fu3KdPn5EjR06ePNnW1nb79u30ARJV70rET1FREVRhCQmJzZs3v3v37unTpyNGjABvfsuWLRcsWBAVFUVRVGVlJXwNUldXd3V1dXZ27t2798mTJ+EtkyZNevToEcRZ26EX89GFtbU1ZLiRkdHgwYMtLS2nT5/u4ODg5uZWh/aWQCA0BsTZRyA0I75//66iokKf41EUde/evX79+gl75MaNG/DtGhg6dOi3b9/gp4iICFtbWykpKfipZ8+eO3fuLCgoYPi4aE6cOKGkpARPSUlJbd68uby8HH569+4d3v0kLi4+f/78qKiohISExYsXS0pKgnzYsGEfP36kq9GlSxc1NbWFCxdOmTJFX1//2bNn9NfdunVLW1tbQ0Nj1apVFy9eXLdunYGBgZ+fHz1Mdna2paUlHByOEFJUVDx06BD2P/r7+2Ot5OXlV69enZaWFhAQMGrUKBDKyso6OTnl5eUJTG9JSQkcqghD4RMnTnC5XOz0XLdu3e/fv7dt24aPqmnRosWSJUvc3d1xDJGRkeBCYrFY8+fPr6qqAnlISAj9fBNNTc179+7BT8XFxatWrcIFpKqqun79eoFuXx5EFKuPjw+M/CwsLHBUgYGBffv2hcAKCgrLli3DchAePHiwY8eOcNVs+/btb9y4wfPG6urqtWvXysrK4nJfuHAhHmQ/fvyYviFx4sSJp0+fhp9iY2M3bNiA96B169Zt+/btJSUlHh4e+PB1VVXV7du3Z2Zmfvr0ad68eXhpzOzZs1+/fp2Tk7NmzRq8OW7IkCG3bt1qcJMoLy/Hx/Cbm5unp6dTFBUeHo6//Gtra798+ZLL5cLSPBkZmTt37mDrBR1kZWVhbnPx4kW8IZfFYs2aNev58+c4M7OysqytrRUUFIYOHbp8+XITExNbW1v60Pny5cvYe9WlS5fdu3dTFHXlyhW8oVhdXZ1nTstPcHCwmZmZoqKijY2Nvb29rq4u3DYLv3K53K1bt+L9OAMHDrx58yZFUZs3b8ZZbW5uDmuRhIE/+KupqQUEBNS2ZRBRNUTj7e2Nl+ZJS0vPnz//wYMH0tLSkydP9vb2xgYg2jx4uH79+ogRI4S9UXRD6uzsjO9obtGixfLlywMDA/GvogsCYFIT4+Li1q1bh+/Utra2huzau3cv3runoaGxa9eu8vLye/fuwSZiSPj69euTk5OpmipyUlLSkiVLcI7Nnj3706dPKSkpCxYswCuGpk2bFhoaCuH5LV8gjo6ORkZGurq6u3fvdnd3nzFjxrBhwxITE+lhmBgDj72BcPXq1QghPT09Eb4DEdja2oLl8zN27Fjs6eChDqb7+/fvDh06dO3adfr06WfPnj106JCuri5PpjGxWIZ5TvF1SZWVlZs2bQIJ5Bi00u7u7kZGRviNdK9WbccPvr6+dnZ2EFhKSmrlypVfv35NS0tbt26doqIiyAcMGAD95qFDh/CVR5qamvv37+dyuTV2xDExMbAxQkdH5/Hjx/DejIwMWKQJiZ0yZUpqaqrARrhBKhFFUefOncMO30GDBsXFxd29e1dWVnbKlCm3b98uLS2tf1aIyOcaB1QURXE4HHz8KIvFWrx4cXFx8bJly5SVlR0cHPz8/GARHyWkWoGzz9raetCgQX369LG2tlZWVh43blxubi7PizIyMvr16wcxtGjRAorMwMBAV1cXzuWEYAJ7WH7u379P3zg/ffp03N03YNvF5XKXLVuGA/OwY8cOrE/9KxE/+fn51tbWeAl5165dfX19zc3NNTU1d+7cSfelfvz4sUOHDtgsYTgkJiY2aNAgV1fX379/12HoVavRRVBQEP0wUzodOnQAvzCBQGhaWJTIzT4EAuEPExsby9N3cjicxMRE+NYnjMTExPT0dG1t7bodllG3xymKio6OLikpMTAw4NliXDeys7N//vzZrl07+iFoQHl5uYyMDIvFSk5OTkhIUFZW1tbWFng+d1lZWVhYWKtWrTQ1NXk23NUTiqIiIyMLCwt79uwJ43gulxsUFNSpUyc83hJNYWHhjx8/1NXV+feG5OTkREVFaWho4LUb9aeeVoEQCgoKAidFQUGBmJhYeHi4tLS0oaEhds3wwOFwIiIi2Gy2jo6OnJxc3VVvUBrKJKqqqkJDQ2VkZMCdB1RUVISFhUlJSRkZGYEXsri4OCwszNDQEA7fAdLS0uLj4/X19fE+uxphs9kRERFlZWW6urp1OL2RIWVlZd+/f5eWljYwMMAfBhoQqDKmpqZ1jrzOVSMpKSkzM9PIyEhaWhpuRcDfJ+gwNI+qqqrU1FR8gLpA6lPjRBdEbWtiPWnAiszE8ktLS1u0aMHhcKKjozMzM7t16yasrGs0BoH2FhwcXOdOKjExUUVFBXsQ6MTExHTt2lWYR4CJtnS4XG5VVZWMjExeXl5UVJSUlJSBgQF2GNGp0WKZtzYiuiTm1L+jqRX174j/DIWFhREREZ06dYK1zHl5edLS0i1atPhjCogYUGHS09MTEhL09fWhi0lPT1dRUeE3af5qZWFh8eLFCycnp2PHjsXFxaWnp3fr1k1Y/lMUFRMTU1hY2KNHD6iGycnJcKUGHYE9bG1pkLaLoqh9+/apqKiYm5unpaUVFBSUlJSUlJQUFxcHBwc/efLk9+/f2KPXIJWIn/Ly8rCwsA4dOoD9xMfHC+x9qqqqwsPDZWRkdHR0xMTE2Gx2dnY2zzEUjUdMTMymTZtgHWhWVlbJP6Snpz98+HD8+PF4pSGBQGgqiLOPQCAQCIKhuxjwcgMCgfCHITWRQCA0H7CzD5/q8DexfPnyhIQE+kGidBYuXLhs2TK8Uu8/S1RU1KBBg549e4aXZNJJS0tbtmyZsAu7CQTCH4Nc0EEgEAgEAoFAIBAIhP80b968OX36ND7YhJ/MzEz6Jv3/LAsWLCgqKjIxMRH4a0xMjIg8JBAIfwzi7CMQCASCYITdP0ggEP4kpCYSCITmg8Arg/8OoqOjEUKXLl369esXz0+ZmZkLFiywtbVt2CNi/qXExMRUVlbu3r2b51JpLpf77Nmz48eP06/yIxAITQVprQgEAoEgmOTkZPhHfHz8nzmPiUAg8ENqIoFAaD5AixQfH9/UijQ8U6ZMcXFxiY6O1tPT69evn46OjoKCQk5OTkJCQnFx8YEDBywsLJpax2bB5s2bV61atXv37nPnzg0ZMqR9+/ZVVVUZGRmhoaETJkzw8PAQeNQpgUD4w5Az+wgEAoHAS3Fx8ZYtW27evJmbm4sQ6tSpk5WV1bRp08i+DALhT0JqIoFAaD48ePDA1dX19evXCCEWizVp0iRjY+Pt27c3tV4NSUFBwZkzZ27fvh0XF1daWtq2bdvevXtPnz591qxZAu+F+8/i6+vr6uoaGBiYkZEhLS3dtWvX4cOHOzk5denSpalVIxAI/wdx9hEIBAKBQCAQCAQCgfD/4XK5xMFXIySXCIRmC3H2EQgEAoFAIBAI/0VYiPXH3kUhMukgEAgEAuEPQdzwBAKBQCAQCAQCgUAgEAgEwl8CcfYRCAQCgUAgEAgEAoFAIBAIfwnkNl4CgUAgEAgEAuE/TePtsf2TO4UJBAKBQCAAZGUfgdAsePz4ca9evS5evFjPeFJSUpYvX96vX79u3bqNHz/+06dPDaLef5yYmJiBAwcuW7asqRWpCxRFff/+/dChQ/3796+qqmpqdQTDZrN9fX2XL1++a9euJlQjJiYmODiYR8hms1++fMnhcPjDFxYWPnz40N3d/fXr19XV1XV+b3l5+dOnT+fPn3/mzJk6R/L3UV1dbWNjY2FhkZOT09S6NC+aYaUOCwvbvXv3lClTmlqRZopAY65VxU9KSrpw4cKECRPevXvXICqFh4c/fPjw4cOHWJKVlUUPkJ6efvz48fnz59vb27u4uBQVFQmLKiIi4tChQ3Z2dvPnzz98+HBqair9Vzabjf8Nb/T392+QJBAIBAKBQBAFRSAQmgFDhw5FCGloaNQnkujoaC0traioqKqqqkmTJiGE5OXlq6qqGkjH/y7YA5WQkNDUutSO9+/fW1lZSUj83yLuioqKptZIAAEBAQMHDgQNlyxZ0oSaODs7I4Q6duxobW29dOlSW1vbgQMHKikpTZs2jT/wvn37TExMrl+//vnz52PHjuno6Dx8+LAOL71586axsTEkf+/evfVOxN8Ddmp4eHg0tS7NiOZWqdPS0qZNmwb66OjoNK0yzRZ+Y2Ze8dPS0hYuXKioqAiBX7x40SAqOTk5IYTatm2LKAT/paen41/d3NwUFBRYLBa+ZFNZWTk4OJgnEjabvXjx4i5duty4cSM+Pj4oKMje3l5WVvbMmTM4TFVVFX6FhoYGQsjc3LxBkkAgEAgEAkEExNlHIDQL7t2716NHDzc3t/pEoq+vP3LkSPh3cXHx4MGDp06d2hDa/deJjIw0MzNbuHBhUytSR86dO9dM/AIi6NGjRzNx9tGRkpJas2ZNdXU1T8gNGzZ07NiRPjcODAyUlpZ+8OBBHd7L4XBUVFSIs4+HioqK8ePHDx8+PDMzs6l1aXY0t0q9YsUK4uwTgUBjrlXF//XrV2M4+xYtWoQ9cfinmzdv6urqPnnypKysrKSk5Pbt2+3atUMIde/encvl0iNZu3atuLh4REQEXThjxgxxcfH3799jCX7FiRMniLOPQCAQCIQ/A9nGSyA0CyZPnvz9+/clS5bUOYaAgICIiAgYkSOE5OXl/fz8vLy8GkjBfxObNm3icrkNGKGurm5QUND58+cbMM76w2azt2zZwiSkurp6YytTW/jLCJtu02JlZTVnzpzhw4fPmTPn8OHDUVFRzs7OeBUVEBoa6uzsvGHDBlVVVSzs06ePpaXlwoUL8/LyavtSMTGxtm3bNoD2/0tKSoqrqyvDwI8ePfrw4UOD61AfpKWlHz169Pr1a2Vl5UZ9EfOq1HxobpW6mdTfZotAY65VxW+kEm/ZsiWPpLS09OzZs+/fv7e0tJSVlW3RooWNjQ10fzExMXFxcThkeXn5iRMndHR09PT06DHMnj2bw+HcuXOH/3UdOnRohEQQCAQCgUAQAHH2EQh/Ca9fv0YI4R03/1m8vb0PHTpEUY110HjzYc2aNQxPPmpuViGwjFisZnGC++jRo69evfr69eurV6+uW7euS5cu/GEuXLjA4XBGjRrFI7eyssrOzr5582Yd3tvgya+srJwyZcrv37+ZBI6Pj7ezs6uoqGhYHf4tMK9KzYfmVqmbSf3918E83/5Yifv6+h4/frxNmzZ04dixYyUlJRFC+fn5WBgaGlpdXc1/ll/r1q0RQikpKY2vLIFAIBAIBKE0r8EigUCoMxkZGU2tQtMTGRlpZ2f3X/D0Xbt27eTJk02tRV34A2WUlpbGYrHGjx/fSPH7+PgghDp16sQjB4m3t3cjvbdWODo6fv78mUnIkpISKysr+hz+P8W/tyoRCI3BuHHjevbsySOsqqqCTceGhoZYKCMjgxBKSUl58eIFPXBISAhCyMTEpPGVJRAIBAKBIBSJmoMQCITGh6Ko9+/fP3jw4NixY1jIZrPfvHnz8OHDM2fOcDicK1euvHjxQlFR0draesyYMTiYm5tbWVkZ3CIaGRl59OhRkC9btkxaWhr+nZaW5uXl9f37dykpKVNT05kzZ/Js3iksLPT09FRXV7e0tPTy8vL29u7Xr9/KlStxgEePHgUEBCQkJOjp6Y0cOXLAgAH0xxmqivn9+/e9e/dCQkIqKip0dHRsbW3h3G46NerMQ2BgoJWVVXFxMULo6NGj4uLi7dq1s7W1xQGKioqePXsWEhKSkJCgq6vbs2fPcePG8ezQFEZUVJSnp6etrW23bt2wMCUl5fbt2127drWysvr165ebm1tKSkqPHj2cnJzwYepAZGTklStXVqxYoaysfP369Tdv3oiLiw8YMGDmzJktWrRACKWnp3t6ekJgFRWV2bNnI4Sio6MfP36MI3FychIXF0cIubu7Ozg4IIRSU1OhuHv27Al3vNSW8vLyGzdufPv2LT8/v2fPnlZWVvQE1iqNwP3799+9e5eRkWFgYNCrV68hQ4bIysriX2ssI5xdly5dio+P19DQmDt3Lj7GvjkQHx8fFxcnLy8PE1067du3Rwh9/fqVoqj6LHTy8fHx9vYuKioyNze3trbu2LEjqo2FsNlsR0fHS5cuIYQ+ffoEFjJx4kRNTU3+d2VkZFhbW4eFhSGEbt++HRoaimrZdCQmJnp4eNjZ2ampqbm4uISGhtra2lpYWCCEcnJy7t27Jy4uvmDBgtLS0vPnzwcEBMjLy8+bN2/IkCEIITab7e3t/eLFi9LS0v79+y9duhQsHJOVlXXnzh0NDY1x48ZhYVFR0cOHD9PT0zds2JCfn3/mzJng4ODOnTsvXLhQV1eXJ4ElJSU3btwIDQ3Nz8/v1q3b4MGD6Usya6xKots9EXz9+vXZs2cREREdOnQwNze3sbHhMQkul/vo0SOoLCoqKj179pw1axZPc1Rjs0zn0qVLdI+tsrLynDlz4N+XL1/Ozc1FCCkoKCxatEjg42fPni0tLcV/Ll++PCcnB5scYGlpqaOjgxB68OABbOeEloEnqpycnAsXLoSEhLRq1WrcuHETJ07kf12NdhUSEuLl5TV//nxtbe03b95cv369srJy8ODBCxcurHGNG0VRfn5+z549O3LkSE5Ojru7e3BwsKqq6tChQ/m1RQglJia+evXq27dvBQUFJiYm/fr169+/P3+we/fu+fr6VldXa2homJqaysrKDho0CP9aUVFx4cKFHz9+SElJde/eXV9fv0OHDnSDFGjMdARWfCbU2IzXn+fPn7NYLHd3d3q7Z2RkpKmpGRsbO2/ePF9fX7CNoqKio0ePqqurL1++vGF1IBAIBAKBUDua8LxAAoFAUVR8fPzatWthWC8vLw/CqqoqR0fHVq1aIYRkZGR+//7dr1+/Tp06gTeBxWJdunQJx3D+/HkXFxdTU1OEkJGRkcs/VFZWQoCTJ08OHDjw4sWL3t7e9vb2CKFu3bqFhobCrw8fPrSysoIRvIuLy759+3D78OvXL9Bw5MiRa9euvX///tmzZzU0NFgs1rp16zgcTm1VBU6fPt2pUydXV9fIyMi3b98qKipKSEicP3+eHka0zgK5ceMGhEQIHT161MXF5caNG/jXN2/edOrUafny5WFhYWFhYZs2bWKxWKampuHh4SLiLCkpOXLkCF6hAH4ciqJevHiBp/0HDx48f/68oqKirq4ubHTS1tYuLy+HkPv27cO+Kj8/P1NTUwkJCTxZNTY2Tk5OhpAZGRn6+voIoV69eoGEw+EkJiZaW1tDYDiJv6KiwsXFZfDgwQihLl26QFm/e/dORCrwsgues/zfvHnTp0+fo0ePPnz4cPfu3XJycvLy8p6enrVNI1BYWDhy5MjBgwf//PkzJCQEXCri4uJaWlrGxsYLFiwQXUYjRoxACC1ZsuTChQsyMjKGhoZKSkpgVB8+fBCROn5SU1MRQuPGjavVUxRFOTs7nz17tri4+OrVq2vXrt2zZ4+vry+bzaaHgWV9ampq/I/jPbP5+fm1fbWBgQFCaNeuXdOnT6f30W3atHn79i2EYWghcXFxLi4ucDbWyJEjwULi4uIEvvfdu3cHDhyAxx0cHJg3HWw2+8SJEwMGDAAf1pcvX7BbX1VV1d/f38LCAjx3S5Ysef36dZcuXYyMjLp37w5W8fr165iYGBMTky5dupibm0tJSSGEbG1tsWLXr18fPXo0xODs7AzC6OhoGxsbCDxmzJhPnz517NhRW1tbQUEB2s+wsDB66t6/f6+kpLRixYrU1NSqqqrdu3cjhDZs2AC/iq5Kots9EZSVlS1evHjq1Km3bt26efMm+INGjRqVk5ODw2RkZBgaGg4ZMuTHjx8cDufFixdSUlIjRowoKSmBADU2y/yVOjs7G/sxjx49StezvLzcysqqXbt2sbGxwtQODQ2FDw9KSkr42nFfX19o25WUlCIjI3HgwsLCCRMmtGzZMiYmBiR79+5FCOno6AQFBbVv375bt2546evRo0d53iW6eT916hTePv/+/fvFixcrKSlhV/XcuXNFZH5mZqaTkxP0p+3bt//w4YOKigr2XCOEZs6cSW8GuVzuqVOnWrduffr06fj4+Ldv306ePBmC5ebm0mOeMmXK+PHj8/LyuFzu27dv27ZtSzfXvLy87t2779y5s7q6uqqqytXVld73CTRmgEnFB/Aue54LOkQ34yKACzrWrl3Lf0EHD8nJyfr6+m/evOH/KSAgALK3bdu2jx8/zs7OHjRoEL1fA/Ar7t69i8gFHQQCgUAg/BGIs49AaGJycnJiY2Nh7QN29gE7d+5ECElISAwfPvzjx48URbHZ7JkzZ4KvgWfOCSt9pkyZwhP/iRMnOnfuXFZWhiWwtEFTU7OqqoqiqISEBLw4aMyYMXv27ImKirK0tNTV1S0qKsrLy+vWrduZM2fw49++fYPpvYeHRx1UvXz5soyMDP3yvk2bNiGExMTEsrKyGOosjAcPHkBCeBw0vr6+LBZr6dKlPDkDeZ6UlCQswrKysvj4+D179kC02NlHUVReXh6cTKSlpbV69erS0lKKokJDQ8ETgS9WTklJwc6UHj16eHl5VVZW5uTk4DiHDRuG41y1ahXdlcOTKPocFeZpAwYMEJEbGIHOvqCgIB7nCGSIpKRkdHR0rdIIzJs3DyEUFRUFf5aUlMD9FePGjXv9+nVwcDBPcnjKCJx9Wlpas2fPBrdIYWGhkZERQsjCwoJJMjH1cfbNmjVr0aJF/v7+xcXF8fHxY8aMGTFiRHZ2Ng4Dp86rq6vzP56dnQ1JE2FRwoA5v4aGhqOjY3R0dHp6+oULF+Tk5BBCsrKyaWlpEIy5hYCLeevWrTW+uqCgAB5//fo1XS66GnI4nJiYGHyvhbW19YMHD/z9/fX19WfMmEFRVHl5OXyB6NChw+LFi8F1wuFwYFmTgYFBz549v3//DjH7+vpCI4CzLiYm5uXLl+AWp/tHqqqqRo4ciRDq2LHjhAkTUlJSKIrKy8uDVUXTpk3DIaurq+HcMezo5HK5cB8CdlFRQqoSw3ZPIFZWVvRr0MvKysDxSvdSQYN/8eJFLAGP7YULF+BP0c0yJaRSR0dHQ44dP36cR6uBAwceOXJEtOarV68GNxO9xd6xYwdCqGvXrjyBFy5cuGbNGvwnOPvatm07YsQIaN65XO78+fMRQq1bt6ZbEZPm3c/PD1JnZGR05coV0MfNzQ0hxGKx6G5HHioqKpKSkiBCWVlZ6I+4XG5cXBz2ie/evRuHh57rzp07WMLhcKZMmYIQMjExwddww978kJAQHMzDw2P69On4zxUrVigqKtI1mTt37tmzZ+HfwoyZYlzxKSHOPibNuDCYOPs4HM7NmzehF5g0aVJiYiJ/mAcPHmB3auvWrXfs2MF/fTlx9hEIBAKB8Ochzj4CoVlw4cIFxOfsw3N4uu8gKCgIhDxfzgU6+7KysuTl5Q8cOEAX3rt3D2Kgf/yHWfHYsWN5FFu/fr28vDzP2B1O7dHW1q6tqvHx8QoKCitXrqTHlpiYKCMjIy0tDRMb5jrzI9CRVFlZCY6A1NRUnvCwToo/1TzExMRAtHRnH/WPP2XevHl0IWxgnD9/PpZ8/fqVf5JG/TO1RgjhxUQbN25EfK4cgbP6+jv7+vTpM3r0aHqwnJwcCLZ48eLaphE2CUpKSnK5XCxcu3YtzOHpz4p29o0ZM4Yew9mzZ2ECySSZmDo7+44fP25nZ0dXoKCgQFVVVV9fH1eBixcvwuSc/3F8Dy/P+jImwJx/0qRJdOGzZ88gwoULF4KEuYXU09nHsBr++PEDJIcPH+aPGRqlwYMH04Uw20cIffnyhS6H1VhXrlyhC8FNxuMfAePv3r07vV06dOgQ+l+fFBzdiBCiL2cD9/Ht27d5YuOpSgzbPX6gyD59+kQXwn5GMTEx3ARpaWmh/3X2weZcR0dH+oPCmmVK+HJdcGnp6+vTzTg5OVlKSiozM1OE5hRFxcTEQI69evUKCxMTE0FIb/04HI66ujqsMQTA2aeiolJQUICFUVFRPDWCoV1hm6TbA4fDkZeXRwhdvXpVdEKcnZ0RQjIyMvQ1ttXV1X369EEIKSoqFhYWUhQVHR0tLS2tpaVFzyuKotLT02E/Nbbq9evX85hNZWUlvVU0MzMTExP7/fs3lgQFBZ04cYIerUBjZljxKSHOPobNuEBqdPa9e/euf//+9HMYlJWVMzIy+KN6+/Yt3oGup6dHd6YDxNlHIBAIBMKfh5zZRyA0C2BrJA949Ey/Fw8mDAih1NRUdXV10dHevn27pKQkMDAQFgQB2CUREBCAtw6BAsOHD6c/TlGUh4dHixYt1q1bR5dXVlYihH79+pWTkwMrZRiqumfPnuLiYp4TizQ0NKKjoyUkJGAhGHOdGfLs2bOoqCgVFRWsD2bhwoVOTk7Pnj1LSEgQeO8qILB00D+p5rm1EN4C/iZ6MIRQ586d6SFXrVp14sQJDofz5s0b+tlPf4CIiIigoCBTU1N6JiOE5OTkysrKAgICsIRhGr99+4YQqq6uLi8vh2UpCKFBgwY5OzvTg9VI586d6UebQaHk5eWVl5fT55yNxKJFi2RkZOgKKCoqTpw48ezZs25ubuCyYbPZCCFK0AUjWFhdXV03BWApHMbCwsLU1DQ4OPj+/fvnz5+vW5x1g2E1xFWDp+kAwHhgjy0GV8N27drR5crKyqmpqTzXB4toGGH7P0+0dGNTU1O7ffs2Qoh+fhmUEU6IQJi3e/zAUYnXr1/38vLCwoiICIQQl8v99OkTrBq7ceNGUFAQrH0WoZjAZlk0GzZsuHfvXkRExPPnz8eOHQtCT0/PMWPGKCsri35WU1NzyJAhvr6+Hh4e4HxHCGloaHTo0CE1NfXq1avYPl+8eKGpqQkuSzpKSkr0ozxxo5qamgquUoZ2JbBDERMTa9++fWxsbI1NCjzeqlUr2IOMhStXrpw5c2ZhYWFwcPDQoUNdXV0rKytNTEx4jlNUVVUdN27cgwcPTp8+DTYAturg4KCsrAz7vqWkpPCyVgjA5XLHjh17//592L9sZmbGYyTC+hFU14rPvBmvG4MGDQoICOBwOB8+fNiwYUNgYGBWVtbmzZvByDFlZWVubm4jRoz4/ft3aGhoZGSkmZnZ06dP+/btW08FCAQCgUAg1Afi7CMQ/mXgWRCXy60xsL+/P0KoV69ecFQWBiaB/M4vntPxo6KisrOzjYyMzM3N6XL8p+jbLfhVffnyJfrfuTdA91rWVucagcVHPJ4FoF+/fvCPsLAwEc6+WgGpZlI6HTt21NPT+/HjB145+MeATNbS0hJYsiImpQB/GvEjsbGxPXr0gH/DpSv1OSoe7w6rqqoS5uyLiooaOHAgXcLhcBBCPj4+PIVuamrKc2skD9hNScfMzOzs2bOurq7g7INrBOAVPGChwKtLmMB/rYe1tXVwcHBOTk5ubi6Py7VRqWfTIQJh1gV7w+t8KbDAegeeNYRQWVmZp6fnixcvUlJS0D8eW2HUp93z9/eXlJTkMUhzc3O4FgNXjV69evXq1QshRFGUj4/PjRs34OpkgYoxz1uEUO/evYcOHerr6+vs7IydfTdv3oT9qjWyaNEiX19fb2/vwsJCMOMXL15Aody4cePIkSN4C7+wiz7oQGCEUFVVFfyjns0789ZVIKNHj4Z/xMTEDB06VHTX8ODBg+Tk5IKCglatWtnZ2R08eDA3N3fo0KG2traHDh1SUVGht2yrV6+G2590dXW3bNmyZs0aaWnprl27MlSsbhW/ns04Q8TFxQcNGuTn5zdq1Ch/f38vL6+LFy9ihQsKCoYPH966detnz55xudxVq1a5ubnl5+dbWloGBATo6ek1iA4EAoFAIBDqAHH2EQh/M+np6QghY2NjYdf/MXlcQkJi2rRp9VeGzWZDhHg7koiX1llnfn7+/IkQyszM5P8JOxn/vLsNK/Djx4+ysrI//F7IZDU1tQYpWYSQmZmZiopKZmbmlStX8H3QkOew5/ffC3j3YmNjKyoqZGRkwAMi0CmDhaKvja4V+JbqP+zsa/Bq2FTk5+c7OzsHBASsXbvWy8vL1NS0Rn9indu96upqON7R0tISNpyKgM1mX7p06fLly7NmzTp79uzWrVujo6Nr9TphrF+/3tfX19fXNzg42NTUNDIyMj093dLSksmzVlZWbdq0yc3NvXXr1uLFixFCzs7Ohw8fPnz4cFJS0tOnT62srJKSkj5//oy3Y9eKprWr1q1bw6o3aHKZdA2xsbG9evVq27bt27dvx44dm5aWduXKlfv37x84cMDR0RGHHzZs2LVr1xYsWFBWVrZlyxYPD48LFy7AldN1g0nFb/BmXATS0tInTpwwMTEpKytLSkrCq9QnT54cGhqakJAAvsUzZ8706tVr4cKF+fn5y5Yte/v2bWMrRiAQCAQCQRhiTa0AgUBoROBE8NjY2Po8npCQIHAdUx2AJSqiPWv11Jkf2EuVnZ3N72TEazoaallfbQGPABwp+Cdp8EyWkZG5ffu2oqLi6dOn4TD70tLS/fv39+vXDw7zajx0dHSy/xdYsDN69GgeuehlfW5ubgYGBtevX+eRw2IiDoeTlpaG/rEZgQ4jOLhQRkaGvnmwnqioqECc9VkgWQca3EKahOfPn3fv3j0zM/P169fjx4/Ht2CLps7tHovFYphvsbGxpqam58+ff/To0dKlS+Ea3IZizJgxsITwyJEjCKEbN27MmjWL4TovaWnpuXPnIoQ8PDwQQt++fQsPD58/f76dnR1C6MqVKwih8+fPz5kzh37FLXOa3K7oTS50DQI3BfN3DT169IiIiFiyZImYmFhRUdHSpUtnzpxJX2M4a9assLCwoUOHIoRiY2OHDx9+5syZOuvJpOL/4cw0NjYGrfAxBf7+/r6+vmZmZvjmZYTQ/PnzDx48iBDy9fWNi4v7M7oRCAQCgUDghzj7CIS/GU1NTYQQOF/4CQoKKiwsrPHxvLy8d+/e8f/K5XJfvXrFXBkJCQmYtzx69EhgANhhV0+d+YGzotA/+3np4OtT8fa6PwzMM3V1df/weyGTX758WVpayv9rZmbm9+/faxvnoEGDvn79Ki8vf/jw4SFDhsybN2/GjBnv37+vm1Pgz3P+/PmIiAj++TlkkZycHCxmMTQ0lJKSqqqqwncIYDIyMhBCgwYNaqgNdAihrKwshJC+vn6t9nLWnwavhn+eiIgIKysrSUlJNze3WpVInds9CQkJWJAlLN98fHwQQuXl5ZaWlmFhYZcvXxZ29l89gZPm7t69Gx8f7+npCa46hixcuBAhFBQUFBkZeeTIkRUrVsjIyMybN09MTOzp06dpaWmXLl1asGBB3RRrWruqrKyENh+aXOgafv78iXcZYyCYmpoarKqDoyQVFRXd3Ny+ffsGF314enp6enpCeAjQvXv3t2/f3r9/v3379lwud+XKlYmJiXVTlUnFb4xmXDSKioqysrJ42SMcRtm+fXueYCtWrICTOhMSEhpWAQKBQCAQCMwhzj4C4W8GpjTv379/8+YNz0+ZmZn79+8XfbhYx44dYci+e/du/hsJ9u7dW9uLCExMTBBCV65cgf1TdK5cufLp06f66wzQd1kOGTIEFmfduHGDJ1h8fDxCyMDA4A8vmwKqqqqioqJatWo1fvx4kMBBSDy5KmKTr+ijx0QAmVxeXn748GGenzgcjoODA1yWUiuKioq2bt0aEBAQGBjo5+d3+/btRYsWiVhLVWflG4muXbsOHz589+7dPPJfv34hhExMTGDWLS0tPWzYMIQQLPSjk5SUhBAaNWpUA2oFy2DnzZsHfzaqhdADN0g1bFrOnTtXWVmpp6dH9/QJyxC6vD7tHuSbm5sbOGvo+Pn5+fn5IYSeP38OVxLBbeCiFasb06ZN69y5M4fDmTNnjpKSEtxBzBAdHZ0BAwYghHbt2vXixQvYrKqhoTF8+HA2mz19+nRtbe06L0ZuWrv68eMHRVGDBg0Cn+zIkSMRQgUFBfj2Wwx0DZMmTYI/jxw5ghfz9ujRw8/PD65UwpdgbNq0CT87adKkoKCgtm3bstlsfCV9beGp+AJpjGZcBFVVVfHx8dbW1jIyMiCBMxb5W0JpaWlwCNJX/BEIBAKBQPjDEGcfgdAsgJUFPHNIgZvI8BoEnlko3BQJ/8fMnz8f5k7Tp0+n380XHh4+ZMgQJycnLIHYeN7IYrEgzLt37+A0IqzDrl27/Pz88AHwDFXdvHmzmJgYm80ePHiwr68vCLlc7tmzZ69du2ZjY1Mrnflp3bo1/AMOM0IIsdns7t27w2WFnp6e5eXl9PCwZ/PMmTOi9/fhhDAvIIG3tfKsv7hz505eXt6mTZuw2jAFjY6OxksOq6ur8bWeJSUlPCmlJ1OE/lhPvOmsV69ecIfAvn373NzcsLY5OTmTJ082NjbGF3cyTCNFUePGjevYsWONqxQFlhH6J295chjn/B/wDFpYWMyePRtfQop5+vQpQmjr1q1YsnTpUoQQv4Pg5cuXUlJSVlZWWHLq1KnBgwcfOnRIoD3UCBzr1rt3bwcHB5A0hoW0bNkSvOH0wAyrIU6XQDsReJECFvKUNch5Mqq2DSP9cVg2GxkZiVvFtLQ0WGpEX8nFn1HM2z1+Vq9ejRDKycmxtLSk7w+9f//+/Pnz165dixVjs9l4rXFVVRVc0MGzxExgs8yTCQIzWVJSctu2bQihjx8/1mpZHwCXb9y+fdve3h7vSZ8/fz5CKCAgAJb+8SCw/uI/sfkxtCthG6hFtK78VFRU8GTOuXPnEELYNWZrawtr9Hiul0UIXb9+vW3btvgIAi6Xe//+ffyrjIzMnDlzEG2T7/fv3+lbVjt16gSXgdAPiBBozALhr/iofs14bcnKyuLP5PPnz6upqZ04cQJL+vTpIyUlFR4eznOJNpvNTkpKUlZW5rmGRRhRUVFz5sy5ePFi3bQlEAgEAoEgGIpAIDQDNm7cCFUyMzMTC/GOwrCwMCzEJ17fvXuXHgOs3dDR0eGJ+dSpU7i+m5qaWltbm5ubi4mJnT17FocpLS2FvZaLFy/mebywsBBfKdi6dWtLS8tx48apqKjo6+vn5ubWQdUNGzZgfZSVlQcNGqSmpqahoZGRkVErnQWSlZUFq3hsbW0DAwMdHR0/f/5MUVRJScngwYMRQlZWVhwOBwK/efNGXl5+3759ouOkKAqf9ebl5YWFXC4Xti9NnjyZHhhWfBgYGGBJaGgoPD5y5EicaT9+/GjXrt2kSZPKy8txyISEBFg00aNHjwsXLhw4cMDIyMjFxQUenzx58sOHDyHkgwcPQOji4uLn5zd9+vSioiJh+uP8jI2NxcKgoCB8UljXrl2trKyGDx8uJycHB1HVNo343KiOHTv27t17xIgRkyZNmj179rp16+7fv48jpISXEUwLhw4dSn+Rm5sbRBsaGiosdfyAM2XcuHHMH6EoisPhjBgx4vv37/wKLFmyhCewjY1Nt27dCgsLsSQqKkpOTm7v3r1Y8vv3b+xE9vT0FP12sM8RI0Zge6iqqpo/f76+vv6vX79wMOYWAn4TRUXFp0+fent7Ozg4iHi7sbExQsjExOTjx48uLi6nT5+mmFVDvD5LYAJhCeTAgQPpQmy6L168oMvBj2ljY4MlRUVFcJ0rj/JTp06F1oPNZmPh9u3bIdqcnByQwMFhCKHRo0d7eHisXr169OjRPXv2hORcvnzZ29ubElKVGLZ7AsFXAEtJSQ0dOnTy5Mna2toKCgpfvnyBALCEGSHUvXv38+fP79u3b8CAAeAjbtOmjbu7+8mTJymRzTIlpFLTqa6u7tixo5SUFM4Q5pSVlbVq1UpKSio1NRULKyoqlJSUWrduTW+yMLCxt0WLFmVlZViIV3C7uLjway7CriIjIyEAZAVQUlICV3IvW7ZMtP64RmzYsKGqqgqEN2/elJSU3L9/Pz1keHg4rD47fvw4Fu7fv79Vq1bPnz/HEicnJzU1td+/f2PJunXr2rRpk5KSAn8aGxuPGTMGv4vL5ZqZmfXv3x+bqDBjZljxKYrCrmF3d3csZNKMCwOaiLVr1yIKwX8gh0ZPS0vr1q1bOJLHjx9ramoGBgbyRAK+v8mTJ1dUVGDhrl27WCzWkydPsAS/Au51MTc3p0dibW2NEBITE0tOThatM4FAIBAIBOYQZx+B0MT4+fk5OjrCHAYh1LNnzz179lAUtW3bNvxZ3sTExNXVlaKo06dP41vw1NXVDx48SFHU0aNH6StNRo4cuWfPHvok7f79+7DdBjA0NKRPY44dO4Y3eUlISMyZM+fatWt0DfPz821sbPBWOCkpKScnJ/qMl7mqwKVLl3B4MTGxuXPnwjoCOqJ1FsGWLVvgkVatWt25cwfLORzO/v3727dv37VrV0dHxwkTJvTo0cPX11d0bPHx8Zs2berYsSPE2b59+40bN6ampgYEBEycOBGEkpKSjo6OkZGRUVFRs2bNwjpPmzYtJCSEojn79uzZo66uPn78+OHDh7du3XrHjh388zFPT0/YQogQ0tfXDwwMjI2NFRcXHzp0qKurK55twupICKatrR0eHi5Q/+/fvy9btgxfDmtoaLhr1y7sFoyIiDA1NYWdoQihdu3anTp1qrq6Gn6tVRopilq3bp2wk9FGjx6Nfaz8ZfT+/fsJEybgwNOnTw8MDCwoKFi7di1eVaSlpXX58mUmBkDV1dlHUVRmZua4cePWrVv38uXLp0+fLl68WF5e/uTJk/zFVFRUZGNjM2DAgODg4JKSEh8fH01NzU2bNtGTyeFw8FmQ/O5CHpKSkmxtbVu2bNmiRYvBgwdbWVlpamquWLGC37HC0EISEhJwDbK0tCwoKBDx9idPnoBfSUJCYuPGjTi9IqphcXHxqlWr8HFdrVu3dnJy8vf3h1/9/Pzs7e3BtFgs1sKFC9+8eVNeXr5jxw58zWinTp127txZUlLy5MkTfDertLS0g4NDUFDQnj17zMzMsHDx4sUfPnxISUlxdHTEZjZx4sTnz5+XlpauXbtWTk4OhH379r137x5FUeXl5XhLdYsWLbZt21ZRUYHdxyNHjgRfrbCqVGO7JwwOh7Nz5076bbzjx4//+fMnPczWrVshc8TExKZNm5aenv79+3dwBnXu3DkyMlJEsyy6UtOZNWvWlClTalRYIE5OTosWLeIRrlixYvXq1TzClJSUJUuWgPIIoX79+t28eZOiqKNHj2pra4NQUVFxw4YN+BHRzfu1a9fA+4wQat++/ZYtW4qLi58/fw6bixFCsrKyTk5OIpyY4Oxr27bt1q1bO3XqZGNj07Nnzw4dOty/f58/cEFBAVQ9MzOzFStWDBw4cPjw4YmJifQwq1atMjIy0tTU3LZt28WLFx0cHExMTLD3lqKo/v37GxoampmZHT58+Pz58xYWFpMmTYLKWFVVJdCY4UEmFT8vL2/Lli3Y+9yuXbt169ZFRETAr6KbcREIc/b5+fnh2qSrqzt9+vRRo0Zt2LABbprm5/Lly2pqanp6eps3bz5y5MiYMWP09PTonj6qJmcfeOrV1NQE+pEJBAKBQCDUDRZVp71FBALhXwdsYevWrVvdzvGprKz88eOHlJSUtrZ2g1y5kJCQkJWVZWBgIOImyrrpHBMTk5WVZWxsLDDmtLS02NhYNTU1hjuM6s/3799h7hoVFaWurh4ZGcnhcPT19enuADqlpaXh4eFt27aFkwQrKiqKior4N2Sx2WxYR2liYsLwmlGBFBQUREREqKiodO3atT7xHDt2jMvljh07NjU1NT8/v7S0tKSkpLS0NDo6+s6dO9evX8cHYKGayqgJ4XK5/v7+4G00NTUdOHAg//HzmKCgoKdPn+bm5qqrq1tbWwu0qKSkpKNHj6qoqGAXpwjYbHZ0dHRWVpaCgoKenh7+BsADQwspLS39/v1727ZttbS0anx1Zmbmr1+/tLW14cJNOvVsOpqWqKionJwcY2NjXN1CQ0NlZGTop86JqEp1bve4XG50dHRhYaGOjo7Ac+jS09NjY2O7d++OczUpKQkqRUNd8KKlpXXixAkLC4s6PJubm8tisfCmeyArK0tCQoJHWGcaz65OnDjh5OTUvn37jIyM3Nzcnz9/ysvLGxoairjsAsorOzu7e/fu/FW+tLS0RYsWXC43Li4uLS1NXV29a9eu2L+GA1RWVkZFRRUUFOjo6PDXIxEwrPgiqEMzvmrVKhcXl7Vr1zofcQYJhf5vRlBYWBgQEJCbm6ugoKCpqWlgYEBPLD9VVVW/fv2KioqSlZXV0tLS1NTk0YGF/u/xu/fuTpkyxdzcHK9vBb59+6atrY2djAQCgUAgEOoPcfYRCARC40J39uGlLn8Z69ev//jxI/0QLjpXrlwpKChYuXLlH9aqOcDlcocOHXr9+nV8hSWB8Ad48+bNkiVLoqOjRbtp/krozr6m1qX5IsLZ1+DU6OwjEAgEAoHQ4JALOggEAoFQL/z9/Y8cOWJubi4sQHR0dL9+/f6kSs0ENpu9YMGCefPmEU8f4U9CUdTevXtXrFjxH/T0EWoFvpoGIcRCrEb6D7+isLCwKVJJIBAIBMJ/EYmmVoBAIBD+cvDliX/gStkm4devXwihmzdvzpo1y8TEhP5TSUnJmTNnqqqqevfu3UTaNSXnz5+fMmWKiMtbCYSGoqCgYOnSpRISEubm5oGBgSkpKXBpxn8QaHL/1va2YXn48OGffN3Ro0f/5OsIBAKBQPgvQ5x9BAKB0LgkJyfDP+Lj4/X19ZtWmcbA2traxcUlMjKyZ8+evXv3NjQ0bNWqVX5+fmpqamxs7Jo1a9auXdvUOjYNjo6OTa0C4b/Cixcvbt68iRC6evWqhISEj49PHY5++zuAJjc/Pz8/P19JSamp1Wmm2NnZwZ0nU9CUP/bS3bt3I4Qa6thHAoFAIBAIIiBn9hEIBEJjUVxcvHXr1lu3bmVlZSGE2rVrZ2NjY2Fhge8e/WsoLi4+d+7crVu3YmNjCwsLlZSUjIyMxo0b5+DgQM5cJxD+AIWFhQsXLnz+/Hnnzp2PHz8+YsSIptaoCfjx44ezs/OtW7eqqqoQQvr6+mPGjJk3b95f+ZWFQCAQCAQCQQTE2UcgEAiEhoTL5dbnVl8CgUAgEAgEAoFAINQH4uwjEAgEAoFAIBAIBAKBQCAQ/hLI4gsCgUAgEAgEAoFAIBAIBALhL4E4+wgEAoFAIBAIBAKBQCAQCIS/BOLsIxAIBAKBQCAQCAQCgUAgEP4SiLOPQCAQCAQCgUAgEAgEAoFA+Esgzj4CgUAgEAgEAoFAIBAIBALhL4E4+wgEAoFAIBAIBAKBQCAQCIS/BOLsIxAIBAKBQCAQCAQCgUAgEP4SiLOPQCAQCAQCgUAgEAgEAoFA+Esgzj4CgUAgEAgEAoFAIBAIBALhL4E4+wgEAoFAIBAIBAKBQCAQCIS/BOLsIxAIBAKBQCAQCAQCgUAgEP4SiLOPQCAQCAQCgUAgEAgEAoFA+Esgzj4CgUAgEAgEAoFAIBAIBALhL4E4+wgEAoFAIBAIBAKBQCAQCIS/BOLsIxAIBAKBQCAQCAQCgUAgEP4SJJpaAQKB8O+ExapvDBTVEHoQCAQCgUAgEAgEAoFA+P+QlX0EAoFAIBAIBAKBQCAQCATCXwJx9hEIBAKBQCAQCAQCgUAgEAh/CWQbL4HQlCQnJ3/79o1hYElJybFjx4oOw+VyAwMD7927JyUldeDAAdEhv3379uzZs3fv3r1+/Zqpxvz8727cyMhIHx+fkJAQZWXl3r1729jYiIuL/0/4+u///XvZsmXLu3fvzp07p6+v39S6EP4/ubm5b968efjw4ciRI+3s7JpanboQFxf38OHDu3fvfvz4sal1IRBQRUVFQEDA48ePEUInTpxokDjv3Llz6NChTZs2WVtbN0iEzRxh7dJfkw85OTnPnz+/c+fO5s2bzc3N/7wC9eyRSYf+byc5OfnBgwdeXl4fPnxoal3+KHWuegkJCS9evHj48OGxY8f09PQaT0MCgcAUikAgNB1nz56FmighIaGioqKlpdWtWzeQKCgo6OjodOnSRVFREUtEx/b58+cxY8ZA4NmzZ4sI+e7du4kTJ4IbrlWrVnVRHaH/++8fOBzOoUOHJCUl6S1Mr169oqOjRT9IAEpLS6FENmzY0NS6EP4/a9as6dSpE9izi4tLU6tTawoLC2fNmoUrZlOrQyBQ7u7uZmZmYJCTJk1qqGhhUtqnT5+GirA5I6Jd+jvyYdeuXUpKSpBAPz+/P69APXtk0qH/21m+fHnLli3/g/1m3apeSkqKvb29vLw8PBgaGtqoShIIBIaQbbwEQlNSVlaGENq1a1deXt7v379//fr17t07+MnS0vLnz5/x8fEFBQWhoaG9evWCwCLo3bv38+fPLSwsanzvoEGDHjx4IHrpX23ZtWvX/v3716xZc/Hixe3bt8M3va9fv9rY2FRVVTXgi/5W5OTknJycevfuPWvWrKbWhfD/cXZ2jo2NxT73fx0tW7a8fv26u7t7UytCIPwf9vb2QUFBNS5Ury3Lly/v0aPHqlWrGjba5omIdunvyIft27eHhoY2oQL17JFJh/5v5+TJk/h7/H+KulW9jh07uru7v337thE0IhAIdYc4+wiEpqS8vNzS0nL79u0KCgoighkZGV2/fp3L5VZXV9cYp7KyMsO3d+zYkWHIGomIiDh//nxoaOiBAwfmz5+/a9eub9++zZ49GyH0/fv3q1evNtSL/m6cnZ0/f/5saGjY1IoQ/gdJSUkVFRWBP0VFRd24ceMP61MHmDcLhL+JK1euxMbGNrUWgqlPB1RRUbFv3z4e4cyZM79//z5t2rT66fWvQVi7JCwfBGZac6ZWrVZjmHo9e+T/TofegP1gEzZZ/Klo165dk2jS5NR5wNCA0woCgdAgEGcfgdCUlJWVzZw5k0lIbW1tY2PjGhf3IYRYjA/FExNrsBbg0qVLx44d69y5M5ZISUm5urq2adMGIRQUFNRQLyIQmgSBlaW0tNTGxqagoOCPq1NrmDcLhL+GyMjIpUuXVlZWNrUigqlPB+To6Ni0y76aCbXKw39dpjFvtZq5qf/dNGA/2ITlKDAV/9l+s84Jb8BpBYFAaBBInSQQmhIrKytLS0uGgU+dOiUtLd2o+tSZ4uJi/nUELVu27NOnD0IoIyOjKZQiEBoRiqIWLFgQHh7e1IoQCAIoLCycPHlyaWlpUyvS8Jw9e9bDw6OptfiX8Rdn2l9s6s2fBuwHm7AcSW9OIBD+VshtvARCU2Jqaso8cP/+/RFCBQUFV69ejYiIKCws1NPTGzt2bK9evYQ98unTpxs3bmRnZ5uZmU2ePLlLly4M3xUXF3f37t2wsDAlJaU+ffrMmDFDQkJUc3H+/HmBctgLwOS9iYmJ7u7uv3//btOmTY8ePeTl5SdOnIgQunPnTnJyMoSZPXs27Fo6duwY9c8twBMnTtTU1IR/c7lcf3//W7duwUkrL1688PT0LCsrGzZs2OLFi+GTo5+f3507dzIzM42NjVevXi0nJ0dXIyIiwtPTc/r06QYGBuHh4efPn09JSTEwMFizZk2rVq0QQtHR0VeuXImKiurateuSJUvwhSqYgICAR48epaSkKCkpGRsbz507l8dFGx0d7e7uvnnzZhaLdfjw4dTU1JUrV/bs2RMhVFpa+uDBg4qKCnt7ewiclpZ269YtYZlmZ2cHayeBGktNWCaLBu5lCwkJycjIMDIyMjc3Hzp0KP41MDAQX1Q3ePBgsEZ6qfXs2ZMeXkTyBeLv7//mzZtdu3bFxMQcPnxYVlZ227ZteHNNnZNcK7vip7q6euHChVA0r1+/rqioQLTiKCkpOXfuXFRUlJycnI6OjoaGRq9evWrcF1Oj5cTHx9+6dcvc3HzYsGGfP392d3cvLCwcMGCAo6Mj753XCGVmZl66dOnbt2+Kiopjx46VlZUV/XY6FEU9ePDg48eP8fHxnTp1mjRp0uDBg/lz4MmTJyEhIVFRUd27d+/Zs+f48eN5FC4rK3v8+HFcXNzmzZurq6s9PDxevXolLS1ta2s7atQoiOTOnTsvX76sqqoaOXLkvHnz6I9zudyXL18GBgbu3LkzOTn5woULkZGR2tra48aN69evH48+cXFxN27cSEhI4HA4RkZG06ZN49lPlJube+3atV69evXv39/Nzc3f39/GxgZfV8rlcu/cuRMUFJSWlmZkZDRx4kSeCzQpirp582ZAQABCqGvXriYmJm3atDExMakxM3/9+vXmzZuQkBA2m21qajps2DAcM0VRLi4uXC4XISQpKblixQrEV+WXLVtGz1XR1QGTlpZmZWX169cvhJCHh4eqqqqsrKyjoyOTSGq0w6CgoJs3b27durVdu3Z37ty5f/++tLS0lZXVhAkTeNR4//69t7d3aWlphw4dTE1NZWVlhw8fXmOOie7j3Nzcli9fDhl79OhRhNDw4cONjY0RQhwO5+3bt/7+/nv27OGJU3QjBtSqfgnk58+fly9fjoyMlJOT69Wr18KFC8vKyu7evbtixYo6lHWt+no6/PkgMNMyMzPp3g0pKSkIgxDy9PRMT09HCCkqKi5YsEDEu2pUMj8/39vbu6qqysHBITU19fTp0zExMXp6ek5OTvTOC6isrLx+/fq7d+/YbLa5uTlPgyCQOps6m82G28kzMzM1NDT69+/P3xvy98i1SlE9H0cIcblcT0/PDx8+FBQUGBsb9+vXz9zcXPRgjE5QUJCPj8+PHz/atm07YMCAqVOn8lyhhhB6//79p0+fvn371rZtW1NTUwsLC/694cJ6tAbsB0WXI2JWhYUhurcSnQp6Jly8ePHTp09ycnIWFhY2Njb8L3r//v2rV6+ioqK6des2bNiwkSNH8gRg2IADmZmZt2/fVlFRmTp1anZ29pkzZ0JDQ1VVVVeuXKmtrY0QysvLu3z58pcvXxQUFGbMmCEwQ0R0QHSYV70a00ggEJoXTXY1CIFAEERqairUzenTp/P/+vLlSwUFhR07dmRnZ5eUlMybN09SUvLatWv0MHZ2dgih2bNnb968mV7ZFRUVX7x4QQ8Jgxv+23j37ds3cuTIq1ev3r59G+Zv/fv3T09P/59AzC7VBR+Bj4+P6Affvn3brl07f39/iqJ+//49YsQIOTk5+InNZjs7O0MSvn79CsK8vDw3NzcQPnjwAIRr1qzBY6acnJwJEyaoq6ubmZnBPG3jxo0lJSU2Njbt2rXr378/XLI2ePBgrMOFCxfw2TqvXr3asmVLu3bt+vbtC8cp9u3bl8vl7t+/v1WrVv369VNTU0MItW/fnp4tFRUVEydO7N69+6dPnzgczqdPnxQVFfv165efnw8JcXFx6du3L7wiJiYGu3rHjBnj7+8/c+ZM8DwuWLCAnjMKCgpbt269ePHi5cuXL1++7ObmJiMjgxAyNTVls9nMS01EJovg8ePH7du33759+69fvz59+gTWZWVllZWVhcN8+PABph/Ozs4gKS0t9ff3h6xzcnKqMfn8783MzNy8eTP4UnV0dMLDw1u3bg3hDx48WP8kM7crQEdHB9FuvYyJiXFxcWnbti1CyNra2sXFxcXFJScnBzTv1KnTiRMnOBxOeXn5unXrEEKfP38WkcmiLYeiqGfPnuGs8/Dw2L17d8uWLbW1tWGvzdSpU3ki/PTpk4qKyvr16yMiIiIiImxtbeHCHCadfmRkZN++fefOnfvu3buIiAi48Gfq1KnV1dU4zM+fP01NTa2trT9//hwVFbV//34JCQkDA4Nv375BgOTk5Dlz5oCHcciQIaGhoYaGhnp6ejDNYLFYz549i46ONjIy0tTUNDMzA/vZsWMHPJ6RkbF8+fL27dsjhIyNjR8/fqygoIDnZmJiYocPH6brvHfv3hYtWty5c6esrCwlJcXU1FRVVRXfBvjw4cOJEydKSUkhhDw9PRcvXgzxKCgoQKJ+/vw5ZMiQ7du3P3z48OjRo0pKStLS0ufPn8fxczgcCwuLWbNmlZSUcDgcLy8vKSkpHh344XA4Bw8ebNeu3ZUrV5KSkp48edK/f39xcfFt27ZVVVVBmNLSUnA4ysvLg4TL5aalpeG5VkFBAcWsOtDx9fXduXMnBNiwYYOLi8vZs2drjKRGO3RxccE3wEZEREycOLF9+/bq6uogOXPmDF2H/fv3GxkZJSUlURQVHBysoaExduxYeoAlS5Ygvtt4Rfdx5eXlLi4ucNuskZER1LvQ0NC4uLhVq1aBwWhqavLkRo2NWG3rl0AuXLigqKh46tSp+Pj49PT0AwcOtGzZUl5e3tzcvFZlzSQfMDztksB8EJZpZWVl+BKPJUuW0Ct4RUXFjBkz2rZtGxcXJyLJopWMioqaPHkyVG07O7snT560bdtWV1cXKnKXLl1KSkrosWVkZPTp02fcuHEBAQGJiYkHDx4EHy4SeSVo3Uw9KSlJS0tr4sSJ0dHRbDYbvlY6ODjgaAX2yMxTVM/Hgezs7H79+llZWcXHxwcEBMD4REZGxsDAwNjY+OzZsyKKJj8/387Orm/fvo8ePYqJidm4cSNCSE9PLyMjA4cpKCiYNWuWvr6+j49PUlLS5cuX1dTUWrdufevWLXpUInq0BuwHBZYj/pXJOEQYNfZWIlLx+vVrUCk+Pl5LS6tDhw74E+C+ffvobyksLJw9e7adnd3du3c9PDygp1u8eDE09bVtwP38/EaNGgXfp3fu3Hnr1i01NbXevXvDyLNNmzZpaWlPnz5t3759z549dXV1EUISEhIvX76kR8KkAwIYVj3RaQR+//4ND5LbeAmEZgJx9hEIzQsRzr6KigrwnhQWFoIkOzsbOn56XwvDIA0NjcmTJ3///j0zM/PGjRtwZ5+0tHRycjIOKdDZt2/fPkNDQzzu53A4BgYGMAb6H20YOPsKCgpkZGS0tLQ4HI7oB7t3725ra4v/zMzMVFJSwn+mpaVBnmCnDCgGXjy6U+by5csQcuTIkR8+fAAhjONlZWUHDx7s5eUFwsTERHCZffz4ET8eHx8PszsDAwMPDw8ul0tRVGRkJLxo2LBha9euhawuLy+HxWj79++nZx1CaNWqVViydOlS9M96MQ6HExMTA0NehNCsWbP8/f29vb07deq0d+/e5OTkb9++wVokurPv+fPn9FdQFLVp0yYY2NHHUkxKTXQmC+TJkycIoV27dtGFCxcuhCyiW1337t0RzdkHDBkyBP3j7BOdfP5XFxYWRkVFDRs2DCHUqVOnOXPmxMTE7N27V1VVFXzH9U8yc7ui+CbVAIz7T58+zZM/nTp1wn9yuVxzc/OAgAD+NGJEWw6QnZ0tLy+PENLW1j548CBk/u3btyEJ379/xyGDgoKkpaU3btxIfwU+LkCEGhRFFRUVde7cec2aNVgSHBwMD+I5fHp6esuWLYcPHw4VBLh+/TrUMuwd4HK58+fPRwi1bdt2ypQpuOWBU0r19PRMTU1//PgBQi8vL4SQoqJiWVkZRVHl5eVRUVEjRoyABmrcuHE/fvzgcrnfv3/Hi0B9fX3hWVhYamJigpW5c+cOQmjChAnwZ0JCwr179+ApCwsLNze3kJCQPn36gJc5MzOzY8eOuGXAucpisXAVg1Pb4+PjcZgNGzYItFs6YO3v3r2jZy/ov2jRIiyEyLEDCPDz8wOFwQFUY3XgB3cl4eHhIGFSp2q0wxcvXkC0ffr0efz4MT0YrPQBSXJysri4+KVLl/CDz58/HzFiBF1Dfmcfwz4O1ppNmTIFS7Kzs2NiYmC5KI+zj2Ejxrx+CeTw4cMIoefPn9OFYMDY2UcxK2vm+UDxtUsi8oE/0+BFsIxryZIlPCkaNWrUgQMHRCSZiZKVlZWDBg1CCHXp0mXWrFm5ubkURcXGxsKDJ0+exLEVFRV16tRp4MCB9ARCriKRzj6qTqYOy5EePnyIIwHPRUhICPwprEdmmKJ6Pg5MmTJFXFwcvE4URSUkJEDftG7dutevX9ObI36sra379+9fXl6O3wvOLHt7e5Bwudw+ffq0bt0au/IpigoPD4cdDDdv3sTCGnu0huoH+csRYD4O4YdhbyUsFdjZN3LkSKz82rVrobfCzlkulztq1Ci6szg5ORl8uKdOnaLq1ICXlZXBJ7pOnTpt2LChtLSUoqji4mItLS2E0KBBg0aOHInLzsHBASE0atQoegwMOyCGVa/GNALE2UcgNDeIs49AaF6IcPbFx8cjhCQkJPDgj6Io6GtjY2OxBJx9BgYGdBfbx48fIdr58+djIb+zLyMjQ0ZGhueLMd4K9D+DSwbOvjNnziCE3rx58z9Svgfz8vIQQv3796eHsrW1xfrn5OSAAnSnDEVR4K2jO2Xw6eM/f/7EwvLycnDhHTlyhP44zMR4Zuww0nVzc6MLYdGHpaUlXbh3716EEH36amtri/7xbQEHDhxACM2ePRtLvnz5Ahryr9GgKAq2EdHnBn5+fvRUh4aGwrqArVu3YiGTUqsxk/kpLy/v2rWrjIwMnssBBQUFsGKLPhUERxuPs2/MmDE8GSI6+fwcOXIEISQuLp6YmEiXN0iSmdsVVRtnX48ePaSlpYuLi7Hk0qVLvLXgf2FiOViHTZs20YXwqZ/uVRk4cKCMjAy9laD+8TKgmpx99vb2rVq1oq8woihqwIABCKHr16/DnzNmzEA8y3UpiqIo2Fo7evRoLHFxcUEIqamp0Zeg+vj4gCZfvnzBQjabDU0ZfTYIVUxFRYU+CcnNzYU1EXhZ7qVLlxBCxsbGOMynT58QQh07dqSr16JFC/pcF7Ny5UpVVVW6pKqqCmxg7ty5IIFNl69evcJh4uPjt23bRgknPDxcUlKyb9++PPKXL18ihFgsFv4acffuXcTnAAoMDIRcopeFsOogEGEzZxGRMLFDPJejG0B0dDRPH+Ht7Y0Q2rJlCz3+OXPm0P/kd/Yx7OME+q0oijp+/Dj6XydXrRoxhvWLn/T09BYtWvA0NdQ/k226s49hWTPMB0pIu8SfD5TwTINa1qpVK3qTlZWVJSUllZqaKiLVDJWEUu7Xrx/d2wIef/qXmK1btyKEnjx5Qn8F/h5TW2cfIMLUYYEqvZEfPXo0Qsjd3Z0ejL9HZp6iej6elJTE34iBGxe3S8K4du0a+l8vD0VRW7ZsoStz7tw5fmunKAp2hKioqGBHUo09WkP1gwLLsVZVmB/mvZVoZx/dhDIzM0GIXcP3799HCEVFRdGfhSMLNDQ0sKRWDThFUZMmTUIITZs2jS6Ez73t2rWDD2MAnC8hKSmJjYp5B8Sw6jFMI3H2EQjNDXJmH4Hwr6FLly43btxo1aoVPkmE+ud8Mf570IyMjOi3YvXt23fo0KG+vr4PHjy4ePGisFd4eXlVVFS8ffs2KioKC2NiYuAfnz59Yn7qX3Fx8Z49e9asWQMfM0UgLy8vLS394cOHVatWHT58GM6U2bVrF8MX0cFn2cB3ckBGRqZ169a5ubk8B6PA2TF43EaPgf44QgjmezU+vmfPHmNj49mzZ2MJFBC9dPCJOQKzhf88HfpZaRwOx97ens1m6+rqwvgMYFJqdcjkhw8fxsfH6+vrw5ZnjKKioo2NzdWrV11dXWFzEHNEJ19Y+K5du2poaNDljZTkBqFNmzaVlZXjx4+/ffs22IyNjY3oe7SZWA76xzh5DhJSU1NLT0/H06TXr1+/f/9+0KBBPMH4T4PiJz4+/uLFi+PGjYOFwBgfH5/4+Hjw52ZmZnp6eiKE+I9ZXLBgwcePH318fOLi4mC/EigsJydHP/IMahP63wolLi7epk2b9PR0eoWCx1VVVen1onXr1ra2ti4uLgEBAVVVVVJSUtOmTSsuLqZblMDcg0h4DI/L5V67dk1eXh7vZwRatGhRUVGBv5FA7tnZ2T1+/BjO6evSpcvcuXOF5SRC6PTp09XV1fy5NGLEiM6dOycmJrq5ufGfPCgaYdWhoSJhYoe4jaVbFC7T1NRU6CPg1yNHjujq6s6aNQt+3bFjh2jdatXHCUsanVo1Ykzql0AOHDhQWlo6efJkHjn4l+tAg+eDCBwcHA4cOFBQUODh4YHP7Lt79+7gwYM7dOhQfyUhV1u3bk2/3xMMBudqYWHh8ePHxcXFweOGYdJqiUCEqd+6dSssLIx+PZqIRoMHJimq/+MhISEIoaKiIvqzgwYNevnyJXbECGPLli3y8vKwhBCzZ88ea2vrHj16wJ/gEeZvoOzt7Q8cOJCZmXnv3j04arAOPVqdn+KnPuOQWvVWoqGbkLKysqysbHl5eWpqKnQHV65cERcXd3Nzo5cpfE1MSkpKT0+H8q1tAy5iONqiRQv6ObwwHK2urs7Pz4ePYQw7IOZVj2EaCQRCc4M4+wiEfxPw+RchVFBQcPny5YCAADabjRCC/9Oh98fA5MmTfX198/LycnJyYEMHP/B5sHfv3vgYJoSQubn5nDlzEEJGRkbMVd2yZUvv3r3xXgARSEpKLl++3NnZ2cXF5cWLF66ursOGDevcuTPzdzF5Bb8QzvDKz89vqMc7deqEXQYfPnzw8PD4/PkzElQ6CCGGJ77TOXr0aHBwsJiY2MWLF+nnuDMptTpkckREBOJzcQJ9+/a9evVqampqQUEBrIWsLbVKPn/gRkpyg7B69WpfX18/Pz9tbe39+/cvWrRIXl4edggKo1aWwwPMB+Dgf4QQLGrDB6vVijdv3iCEunbtyiOXk5MDTx/6xyrExMT4JwP41LMfP36ImD4J80Qwr4+jRo1ycXHhcDgJCQna2tpycnKw8g4hFB8ff+7cua9fvyJm9e7nz595eXndu3eHE80w8CcOvHDhwqNHj6alpfXu3Xv58uW7du1q2bKl6CmisOrDYrH69OmTmJj448ePGlMqkDo0HQwjqbMdYg8gtsOBAweamZl9/vx59uzZN27cOHXqVLdu3ZhMqpn3cUyofyPGU78EApWuPh5Yfho2H0TQunVre3v7kydPuri4LF26FL4R3rp1a9GiRY2nJE+uhoWFlZaWduzYkfnVE8wRaOp9+/aF9orNZt+/f//evXtQH+ucvUzspFaPQ3tYVFSUlZWFL7WAhl30N9eYmJjk5GTs1MOwWCx8oVBVVVVsbCwSVDW6du2qrKycmZmJG6g69Gh1foqf+lTh+vdWwpCWli4vL6+qqoI/AwIC5OTkcJwA7lNgnTimng24iOEoQgg7+xh2QMyrXq3SSCAQmg/E2Ucg/MvIzMw8ePDgz58/N27c6OTkJCsry+FwmDyIpyJ5eXnCnH0ZGRkIITMzM55vwrXF09MzODj41atX9NWFIjh48GBeXt6lS5eioqKGDx8+Y8aM06dP4wOM/108ePDAxcVlxIgRhw8fvnDhQm3XvgkjNjYWjq9esWIFz3iLYanVNpNh0RzPykcA33MaGxvL8ILIhqWRktwgjBs37ty5c8uWLcvPz3dwcICbVQTef8dDg1jOz58/EUI8d0wzJCUlBSEEdxEKA6yCy+VmZ2fz3KtIt4o6vJ05+EX0dSKhoaH79u1TUFDYuHHj5MmTeZx3wgBDkpOTmzZtmohgqqqqr169GjduXHZ2touLi5eXl6urq5WVlYhHaqw+jZ1LdaZB7JDFYj158mTs2LFfv359/vy5gYHBli1bNm3axGSiW+c+jp8/0IhRFAWVjmcBTv1pwHwQzapVq1xdXePj4+/fv29tbZ2WlhYaGiravBtWyfq0WnWmsrLSzc3tzp07Dg4OV69enThxIrQGzYSBAwcqKioWFhZeu3ZtzZo1IIQ9knA+hjCYNOOxsbHg1hRWNTIzM3EDVbcerc79IA/1qcJ/preqrq7OyckRFxe3sbFhOOL9AzDsgBhWveaZRgKBwARSYwmEfxMPHjzQ1taWkpJ69uwZ3H7AHBjrSEtL86/cwcBMLC4urj5KBgUFnThx4unTp8zH7uLi4hcvXrx37x5sBPD09DQ1NYUx67+IoqKi8ePHOzo6uru7b926tQGdShRFLVy4sLy8vEuXLnDEEh2GpVbbTIaP4QL3r+HPxX9goZxAGinJDcWiRYu+fv1qZmaGEPr69WuvXr1evXolInwDWg4M7kXvOhQGDOJFT37wEgn+rWTy8vLweb+xrQKWh7BYLDiqnKKobdu29enTZ/bs2ZcuXQIhQ8CQ4PQx0ZiZmUVGRsIW14yMjMmTJ+MzIgUCGSVwwx1Un6aqOyJo2BasXbt2nz592rt3r6ysbEVFxbZt28aPH19dXS36qfr0cfz8gUasuLi4tLQUMbMi5jRsPoimc+fONjY2CKFjx44hhG7fvm1tbc2k+24oJevTatWNnz9/GhkZPX78+Pnz57Nnz8YLo5oPCgoKnp6ecnJyu3btev/+PUIoIyPj7NmzS5Ys4d8wTgea8YSEBBFeVxHNOBLUQNW2R6vPUwJVrVsV/jO9FYvFEhcX53A4cMxiM4FhB8Sw6jXPNBIIBCYQZx+B8K8hNDR06tSpKioqBw8erMO3NbgpT19fX8RyfdjOAAfx8sNkoBYZGblixYoHDx4w393JZrNzc3MRQpMnT/716xfcdJaYmIi/Zv9bWLBgwZMnT5ydneGk5wbkwoULcGPjhQsX+M+BYlJqdchk2LlZXFycmJjI8xPYUvv27YUtEW1sGinJDQKMnnv06BEYGHjp0iV5efmKigo7OzsRG8Qa0HLAlf/r1686PKutrY0QCgoKgvLlAeYDeD/v9+/feQKUlJTAchJDQ8M6vJ05oIm6ujrUBTc3t717986dOxeOw68VYEhJSUlwQhYPubm53759Q/+Uadu2ba9du/b+/XvwJ+7YsQO2SgkEMiosLIz/J8jexs6lOtCAdpifn19VVSUhIbFly5afP39aWFgghJ4/f37hwgURT9Wzj+PnDzRiLVu2hDMu6eeH1pMGz4cagVbx48ePgYGBt27dEn0eJdCASkKrVVZW9mf8fSUlJZaWlr9+/bp69SrPSXDNCgsLC39/f0lJydWrV48aNWr16tXHjx93c3MT/RQ049XV1fgqJDqZmZnV1dUqKipg9vzNOOJroOrQo9X5KX7qU4X/TG8lISEB26sFDkgo2i0ffxKGHRDDqtc800ggEJhAnH0EQvMCH3HNz7lz56qrq3V0dOjn8THfMgPXF4gexMPVfs+ePeOf+n7+/Bmu8RJBUlKSnZ3drVu32rdvz/PT8ePHha3pyM3NhVs7EULy8vJHjhyBo6Px6fg4vfQYOBxOjYtE/iSZmZl37txBCOnq6mJhg5yvlJ6evn79eoSQvb093H2GI4dxP5NSqzGT+RkyZAhM4eCIazoJCQkIIbpvBcqIp0TKy8trlVLmNEiSG8queEoZ353CYrHmzZv37t07MTGx9PR0/rkK0LCWA5HExcX5+/vX9lk467C0tHTfvn08P8XGxsJNgtra2rAJSJhV6Onpde/evU66MwUOG8L3SJw6dQrVNfc6duwI6wT5F8xSFLV8+XJoyvbv34+NecCAAR8/fuzatSv1z7W/AoGqmpiYyB+Gp/qAHfLo3IB1h2FuNKwdvnz5EpYjIYQ0NDQeP34My5FENDioln0cE91q1YjVGTD4J0+e8BzZxt+SMCzrevb1IhCWab169YKleatWrcrKymJyjkcDKolNDm7W5kfEuIgOQ3N9+vRpQkKCoqIi/QaSBj8Msf5kZ2cfPnw4JCTky5cvL1++9PT0nDBhQo1PqaqqwjaO7du38ySqsrJyxYoVUF7QQN2/f59nwy+Xy01KSpKRkcE3NjDs0erZDwqLqj5VuA69Vd3MAF+KXVJSwvOTQOEfgGEHxLzqNcM0EggEJhBnH4HQvMBdJv+1ZfDl7f3793gKERQUBP/G5wQLg6Iod3d3IyOjpUuXYiGMy+nzk/nz5ysoKHA4HCsrK/onwVevXs2YMQPWRgkjKytr0qRJBw4ckJOTy6QRGRl56NChnz9/irgi8P79+/TRvL29PYvFwqdQt27dGibkHz58wGE8PT1Bc/ogQ9jx2CDnGcaBkGcWITAGJo/j76K+vr44AMxs6aWDwwucFEFInimio6NjYWGhqqqqs7MzXf7p0ycwEoalJjqT+dHX13dwcEAIXb16lUfb69evKykp0f0jcCIkvYCioqJgVRS9gEQnnx8Izx+4QZLM3K6QoMoCMSCE0tPT4U+wkMDAQPrZTz179jQxMZGUlBR2tSVDyxGYDzgMTqadnR0sqt24cSM9CaGhofCPwsJCgWoghPT09GACcOLEifXr11dWVoL869evkyZNAo+zhIQE+EzfvHmTnJxMf/z69esIoVOnTuFlPiJqE2JWH5GglvDChQutW7cGfdA/GYhzD/1zf0t1dTU9NoG2JCYmtmzZMoTQ/fv3N2zYgDO8sLBwzpw5nTt3VlVVRQhVVVU9efIEP9WmTRs40UxE9Vm4cCGcke/h4UGX5+XlPXv2bODAgdOnTwcJ1J2Kiorg4GCcFbdu3YJ/81cfhnUHb8LlsU9hkTC0QxFGiP63+Ly9vfG/xcXF582bh/43x/jrFMM+TmC9Q4Laz1o1Ykzql0Dmz5+PEIqLi6N7ySsqKrC7E8OwrJn39QLbJYH9iLBMw8BdyYGBgXPmzOG/4IsfhkoyydWePXsOHDgQIXTq1Kno6GgcjEmrhepq6oWFhfhbUX5+fmRkJOLLXoE5ydxO6vM4h8OxsLDo0aNHHS5+2bRpE0IoODh4zJgxWVlZIPz9+/fYsWNnz54NezsOHTokJydXVFREr6cIIR8fn5ycnHXr1uH9rTX2aA3SDyIh5VirKsxDrXorganAZcdTX6C8sHDlypUIoZSUFCsrK9hPAL8eO3bM29t70qRJIKlVA45qPxxFNBNi2AExr3oM04hTV+fLaggEQgNDEQiE5gTumDt16lRZWUn/affu3fCTjY3NpUuXFi1aNG3aNFh4MmXKlFOnToWHh1MUBftxdHR0cnJy4EEOh7Nq1SotLa2IiAh6hDBIYrFYxcXFWAjrdxBCYmJi/fv3t7a2NjQ0lJWVff/+/f8oitD//UdRFEUVFBQYGxuLaGo+fvwo8EGKouDM6ZMnT2IJDME9PT2xZMaMGQghWVnZnTt3nj17dtKkSUuXLoXDng0NDV1dXSHYs2fP4HX0lJaXl8OJPJs2baKnYNSoUQihvn37YklxcTGM/HCEAAyG6CEpitq/fz9CSEJCoqSkhKKokpIS2BDUpk2bU6dOOTs74+FUy5Ytz58/f+rUKYqisMvA19eX4gM2VgwfPhxLvLy8IPyDBw/oIRMSEszNzW/evAl/1lhqTDKZn/z8/D59+iCEHBwcsNDNzU1BQeHhw4f0kOfOnQMF5s2bd+nSJUdHx8GDB8PduEpKSgcPHvz9+3eNyecH3HYtWrSgm2gDJpmhXZWXl8Pp++vWraPrYG9vjxBSU1Pz9fW9fPnynj17KIrS0dGxsbHhcDgQprS0tH379kuWLBGWRoaWw2azYTbi6OiIn62uroYdTPQCcnV1hZzp3bv31atXg4KC9u3bhzc0DRw48OjRo8KUiYqKwluipKWl+/TpY2hoKCEh8fTpU3owuKmzV69eYPwURQUGBioqKm7bto0eDPxxysrKdCHc+YsQ8vHxwcKqqipwvNIr6cGDByHk8ePHuVwuCDdt2tSiRYvHjx/jYMOGDUMIiYuLb9q06dy5c+PHj8cXie7cuXPr1q0URRUWFkLV3rFjB0+S8/Ly8J3OqqqqEydOHD16dMuWLSdNmsRmsyHMkiVLunXrlp+fj5+aMGGCkZFRRUWFsJykKCo4OFhVVZXFYmGTY7PZM2fO7N69+69fv3CwyspKmFerq6u7uro6Ozv37t375MmToNKkSZMePXoEIUVUB4HAJuXhw4cHBgZu2bIFClFYJAztEPtHbt++jZ8Ftz5CyMvLCyS3bt2SlJQMDg7GYY4dOyYvLx8TE4MlI0aMACvFEoZ93LVr16DiX7t2zcfHZ+7cuWAe4Ldt0aIFvVwYNmLM6xc/5eXleOPzqFGjDh06tH79+v79+1taWiKEzM3NcUiGZc0wH4S1SwLzQVim0YGMio6OFpHY2hYWLEbT09OjP4tXFWHJly9fYFe+iorKiRMngoKCrl69OnjwYHiFpqbm8uXLRVS3Wpk6XvVsYmJy8eLFrVu3DhkyBBYzGhoaXr58Gb4SUYJ6ZOYpqufj+DLczp079+nTZ+TIkZMnT7a1td2+ffuHDx+E5QNQWVmJL6xgsVj6+vrm5ubS0tI8QyAPDw8pKSklJaWfP3+CJCsry8DAwMLCoqysDAersUdrkH4QEFiOzMchAmHYWwlMBT52gF4psBOT3pNCZQd7Gz169IQJE9TV1TU1NTMzM3GY2jbgcNyhjY0NXbht2zbonYuKirAQr5im96oMOyDmVY9JGuFLG0KIZ8xAIBCaCuLsIxCaC5cvX540aRJ9+VvXrl3t7e3xIKOkpAR3wEpKSidOnOByucuXLwfJunXrYOxeWFi4Zs0aZWVlKSkpc3Nza2trLS2tBQsW0IcX3759c3R0xKe/9e3bd//+/fjXGzdu4JOPEUJDhw799u0br7r/67OD+bYw/mdcK8jZ16FDh65du06fPv3s2bOHDh3S1dXlcbdlZGT069cPjzMg7QYGBrq6unByVmVl5Z49e/DdI7169YJxmKenJ9atRYsWa9asiYmJCQkJmTt3Lkz+xcTE5s6d+/HjRw8PD3zEuKqq6vbt2zMzMz99+jRv3jy8zGH27NmvX7/OyclZs2YNPpRwyJAht27doijKy8sLH2o+cuTImJiY5ORkyOROnTp9+fJl1apVOGNVVVXXr1//48cPSODFixfp2/pmzZr1/PlzNputoqKCEBIXFzf4Bz09PeyOobvMRJcak0wWSHV19aZNm9q0aWNgYLB8+fLhw4cPGDCAfzbI4XAWLFiA9V+8eHFxcfGyZcuUlZUdHBz8/PwKCwtFJJ+fiIgIW1tbfG56z549d+7cWVBQQA9T/yTXaFcURTk7O+OrXVu0aLF8+fLAwEB4PDIyEsqCxWLNnz+/qqqKoqjevXvr6+sPHDjw2LFjZ86c6du3r4ODQ3l5uYhMFm05ERER/v7+Y8eOhQDy8vKrV69OS0sLCAgAhzVCSFZW1snJKS8vDyL09vaGJWkIIWlp6fnz5z948EBaWnry5Mne3t48XxF4SEpKGjRoEF7vYGxsjNPLk/ldunRRU1NbuHDhlClT9PX1nz17hn+Ni4tbt24d+O8QQtbW1vfu3aMoau/evXjblIaGxq5du8rLy+/du4f3pysqKq5fvz45OZn6x9mno6Pj4OCgqak5derU7t276+nphYaG0jUJDw/Hi1+0tbVfvnzJ5XJhUYOMjMydO3eOHTuGfZ0yMjKLFi168uQJPYbs7GxLS0t8UayiouKhQ4foueTo6GhkZKSrq7t79253d/cZM2YMGzYsMTFRRDYCWVlZ1tbWCgoKQ4cOXb58uYmJia2tLX2SBnz8+BEveNHQ0IBGRkxMbNCgQa6urr9//2ZSHfi5desWJEpWVtbV1bXGSGq0w8uXL+Oc7NKly+7duymKunLlCt4Lpq6ufvjwYXi1tra2hobGqlWrLl68uG7dOgMDAz8/P3jR/fv3p0yZgmvu9OnToQll2MeVl5djX4a5uXl6evrbt28XL14sLS0NwsGDB9Nreo2NWG3rFz+/f//GLQmLxbK0tPz9+zfcn0539jEpa4b5ILBdEpEP/JnGn4qNGzf269dPtFFhalQyMTFxyZIluFrNnj3706dPKSkpCxYswM3LtGnTcHUODw83MTHBeTho0CA4e7R///5ubm65ubkilKmtqeOTWyUlJR0cHAoLC+/evQuSYcOGFRYWCuyRk5KSGKaono9TFMXlcpctWybs9mr+jxY8lJeX29vbwx0UCCFlZeWrV6/yBwsODjYzM1NUVLSxsbG3t9fV1T148CCPF7jGHq2h+kH+csRyhuMQYYjurQSmorKyctOmTXispaend/r0aeqfLTIgVFRUxP7T6urqtWvXysrKwk/i4uILFy7EXrDaNuC+vr52dnYQWEpKauXKlV+/fk1LS1u3bh2cEIoQGjBggLu7O0VRhw4dwtdSaWpq7t+/H5cgww6IYdUTncacnJxNmzbB0X4IIQ0Nja1bt6alpTEsIwKB0EiwKGYHYRAIhOYARVGRkZGFhYU9e/aEYRyXyw0KCurUqRPP5giKomJjY9PT02VlZXV1deH7f61ITExMT0/X1tbGN5r9D3ibT23bEL4HuVxuVVWVjIxMXl5eVFSUlJSUgYEBHqRiKIqKiYkpLCzs0aMHTGaSk5PxwKKZkJ+fHx4e3rFjR7xbLS0tLS0tDTaw/AEFhJUaw0wWQXx8fGpqateuXeEQHIGkp6cnJCTo6+uDJzQ9PV1FRUXYdKWhqGeS62lXhYWFP378UFdXx/6m0tLSFi1alJeX//z5s6ysTF9fX0lJqcZ4GsNykpKSMjMzjYyMpKWl8/PzEUJMNAGKiorAiSZi1xVCKDs7++fPn+3ataMf9NZQHDp0aOPGjcbGxt++ffv9+3d0dLSysjKcHMRDRUVFWFiYlJSUkZERuOaLi4vDwsIMDQ2ZH8BfVlYWFhbWqlUrTU1NnluMoEw5HE50dHRmZma3bt3wYkAmsNnsiIiIsrIyXV1dYTcXVVVVhYeHy8jI6OjoiImJsdns7Oxs7LGtM2lpafHx8fr6+gyv1m0oOywvL5eRkWGxWMnJyQkJCcrKytra2kyucWDYx1VVVYWGhsrIyIBXlyFMGrH6kJGRERsbq6mpCQW3a9eunTt3mpub8xybxaSsmff1zKkx00aMGDFt2rSFCxcyjLAxlMzNzf3165eenl6rVq0oikpMTBSxWZ5ObU09OTk5MTGR/vEM7tvR09Njsou5saEoat++fSoqKubm5mlpaQUFBSUlJSUlJcXFxcHBwU+ePPn9+3eNd8tUV1d///5dQUGhe/fuImpfWVnZ9+/fpaWlDQwM+C8mZtKjNVQ/iGoqx/pU4Rp7K/5U1BYOhxMREcFms3V0dJjcZ/1nYNIBIcZVr3mmkUAgCIM4+wgEQp1oOGcfgUAg8EN39jW1LgRCrRHm7GuexMXFmZmZ4bWchKZl+fLlCQkJ9KNC6SxcuHDZsmV4iRmBQCAQCAIhF3QQCAQCgUAgEAj/Xfbv329nZ0c8fc2BN2/enD59um/fvsICZGZmwtmyBAKBQCCIQKLmIAQCgUAgEAh/FoHXDhII/xbgKs/mvIFm/fr18fHx5ubmubm53t7eP3/+bGqNCAghBPeiXrp0acqUKdra2vSfMjMzt2zZYmtry3PUAIFAIBAI/JCugkAgEAgEQrMjOTkZIZSenl5ZWYnvHCAQ/i3AzcW5ublNrYhgUlNT4U7ze/fuIYTc3d3hLl1CkzNlyhQXF5fo6Gg9Pb1+/frp6OgoKCjk5OQkJCQUFxcfOHDAwsKiqXUkEAgEwr8AcmYfgUCoE+TMPgKB0Dj8+PHj8OHDXl5esDbKzMxs6NChK1eurP+FFQTCH2Dfvn0vX7709/eHPy0tLbdv325mZta0WvFAUdT69esvX76soKCwbdu2efPmNbVGhP9PQUHBmTNnbt++HRcXV1pa2rZt2969e0+fPn3WrFlMLrohEAgEAgERZx+BQKgjxNlHIBAIBAKB0JhwuVzi4CMQCARCHSCdB4FAIBAIBAKBQCA0O4inj0AgEAh1g5zZRyAQ6gdeqUcgEAgEAoFAIBAIBAKhqSEfiwgEAoFAIBAIBAKBQCAQCIS/BOLsIxAIBAKBQCAQCAQCgUAgEP4SyDZeAoFQJ8j1GgQCgUAgEAgEAoFAIDQ/iLOPQPivEBYW9uDBg4iICC8vL9Eho6Oj7e3te/bseeLEiT+j219ARUXFoUOHAgICYmJiOnXq5ODgMGPGjKZWqnYUFRX5+PjcuXNn3rx5FhYWjfEKiqLCwsKeP3/+6NGj9+/fi4uLg7yqqmrmzJllZWXXr19v3bp1Y7z6L+b169elpaV0iYyMzOjRo1NTU4ODg/nDT5w4Ef/7w4cPOTk5+E9lZeW+ffs2nqp1Zu/evU+ePDl58qSZmVlT69IoMG+f/xq4XO7nz5/v379fXFx85syZplaHEcnJyYcOHQoJCcnKyjIwMNi6dWvv3r1FhM/IyHB2dg4PDy8pKTE3N+/fv39BQcHNmzevX7/evn37P6Y28JdVItJr1BmSdaJhs9mBgYH379+vrKw8ffp0U6vTYJCxPYHwX4QiEAh/O2lpadOmTZOQkEAIGRgY1Bh+165d0D6kpKT8AfX+AoqLi4cOHXr9+nWKovbt2we5FxIS0tR61YKLFy9269YNNL97925jvOL9+/dWVlZghwghNpuNf3r37h0Ir1271hiv/rt58+bNkCFDcLfu6Oj46dMniqKys7O9vb11dXVBLiYmduTIEX9/f/qzwcHB69atgwDz5s0LCgpqokSIoqqqSlpaGiFkb2/f1Lo0PLVtn/8OPn/+PHbsWBaLhRAaM2ZMU6vDiKioKC0trejo6MrKSktLS4SQkpISh8MRFj4iIqJTp063bt2iKMrZ2Zk+9k5NTf2DilPU31iJSK9RZ0jWieDt27fDhw+H/Bk3blxTq9OQkLE9gfAfhDj7CIT/CsuXL2c4mYyMjDQzM1uyZMkf0OrvYNGiRS1atMCzviVLlpiamubn5zepUrUmNze3UZ19gKurK7+zr6KiYvz48SNGjMjOzm68V//FlJSUdOrUCTL2y5cv9J8iIiLExMQQQoqKilwuV+Djbdq0GT169B/RtI5s3bq1Z8+eHz9+bGpFGgvm7fPfxKRJk/5Fzj5tbW0LCwv4d2FhYf/+/W1tbUWEHzRokJqaGv7zwIEDxsbGTeXso/66SkR6jTpDsq5GxowZ8/c5+8jYnkD4D0Iu6CAQ/isoKyszDKmrqxsUFOTm5sb/08OHD4OCghpUr/8hOTlZ4HubM8XFxTdv3mzTpg24VBBCbm5uX79+bdWqVZPqVQP8Wd26dWu8r7bxUFdX5xdKS0s/evTo1atXbdu25fmpCU2CzWZv3769SV5dW1q0aIEX6L19+5b+k56eHrhUCgsL8YIOOtnZ2bm5ucuWLWt8NRkhsJHZs2dPcHBwU20xbvB2b9OmTTwS5u3z34SKikpTq8AUPz+/X79+tWvXDv5s2bJlQEDAlStXhIX/+vWrv79/z549sWTjxo1nz55tdEURQs2yEjU4InoN5vDXxL8P/jQ2SNb93dSqaWoMK2qMkY+IsT2BQPhbIc4+AuG/AmyYqg/x8fHz5s2rqKhoEH34qaystLGxycrKaqT4G4mgoKCSkhLs6ftXICyr628kNVKrjGpak1izZs379++b5NV1wM7OTl5eHiHE74BYunQp/OP8+fP8D/r7+ysrK8NChiansRuZOtDgKnl7ex86dIhH+AeqXjPkX5Tq169fo9o0X76+vgihJvnk0wwrUfNEYE38y/gvpLExYN40NUYO/0sHwwQCoRnyb5qdEgiEJqSkpMTKyio/P7/xXuHo6Pj58+fGi7+RyMjIaGoVas2/JaubUM9r166dPHmySV5dN+Tl5WEFX2RkJE+mDR06VFNTEyHk7e3NX4U9PT1nzJiBz1JsQv5AI1NbGlylyMhIOzs7itxm/m+jtu18UlISQgiOyfuTNMNK1Dz5L9TE/0Iam5ZGyuF/ywiNQCA0f5p+cE8gEICUlJRr167Fx8eXlZUZGhpOnDhRT0+PHqC6uvrJkydpaWnLli378uXLyZMnO3TosHXrVljOgxDKyMi4d+9ecHBwVVWVrq6unZ1dx44dBb4rKyvL3d09JCSkTZs2EyZMgLPG6URGRnp6es6fP79Lly4Qs7W1dVhYGELIy8vr69evCCEnJye867OsrOzGjRvfvn0rKCgwNTWdPHkyPMhDaGjo8+fPQ0NDFRQUTE1N58yZA8qz2WxHR8dLly4hhD5+/Hj06FGEkLW19YMHDzgcDjwLryspKTl37hyOcNq0afQ0xsfHX758ecGCBcrKysePHw8LC5s3b96oUaOYK3n37l1fX182m925c2dTU1M5ObkBAwYIKTH0+PHj6OjowMBAhFBhYSGoDZp37twZIQQXL3bt2nXMmDE3b958+PDhoEGD8DIrhNDnz58DAgK+ffsmJydnYmIycuRIfEsGJjs7+969e1JSUvPnz4fkf/jwQVFR0d7eHnSrrq6+c+fOy5cvKysrBw0atGTJEhEfpYVlNShMx8fH5+7du/n5+QYGBitWrOC/s49hoTMkMzPzzp07Xbt2HTt2LEM9Hzx48PHjx4SEBH19/VGjRvXr148eoej6UlJScv369e/fv+fn52tqag4ZMmTEiBH4WXd3dwcHB4RQSkoKvNrMzExaWhov9DMzMxs4cCBC6OXLlz9+/AChurr61KlTcSQ1lv6XL1+ePXsWERGhrq7et29fa2vrei50mj179vXr1xFCly5d4rlwE+pRZWXljRs36Dt28/Pznzx58unTJ3rg+pSs6EJBCH3+/Pn27duFhYXt27c3NjaWlZWFEhfdyJSVlT158uT3798rVqzAUVVUVDx79iwkJGTv3r0URXl5eT169IjD4VhYWNjZ2UEYX1/fu3fv/v7929DQ0MnJiX+l1ZcvX7y9vVNSUmRkZIyMjGbNmoVNvaHaPUxgYKCVlVVxcTFCCOxKVVV15syZPMFqbJ9RnYynurr6zZs3jx8/dnV1ZbPZHh4eL1++bN26tY2NDTb+z58/18rIG7B1CgsLu3LlSnJyspGR0bhx4/DxdrVKdWBgoLe39+HDh9PT0w8dOlRaWrp+/XotLS0R2cLlcp8/fx4cHBweHq6urt6zZ09LS0u6nZw5c6a8vPzbt28IofDwcNzO0y2BzosXLyIiIqBO/fjxA4dH/3gAhcGkO0D1sFiBlaiwsPDhw4dZWVlr167Nzc11dXUNDQ3t2rXr4sWLu3fvzv/2wMDAJ0+e/Pr1q1u3bsbGxqNGjWJ4nWtERMSVK1dWr17dunXra9euvX37VkpKasCAATNnzpSVleUJXFhYCFU7KSlJT0/P1NR07Nix/LnN02sAycnJt2/f7t69+8SJE3/+/Hn27Nm0tDQjIyMnJycFBQWcCiY1sQ5alZeX379/v6CgwNHRMSIi4tKlS4mJiQYGBhMmTDA1NeWPsz69GJfLffjwob+/f0ZGRvv27U1NTWfOnIn1EZ1GgVkHMLHD4OBgLy+vRYsWaWpqvnz58ubNm9XV1UOHDrW3t+epkuXl5e7u7j9+/JCWlu7evbuurq6GhoawKlnb9gcxGLMxbKX9/Pzu3r2bnp6uqam5aNEigerxUKMVMTRjOqJHPqKHuDVOIhDf2B5gUmVEINoOCQRCE9OkJwYSCIT/4/jx49LS0pcvXy4uLs7JyRkzZkyLFi2ePHkCv4aGhi5atAhOV5kyZYqPjw9eL3D58mUIc+LECQ0NDTc3t8jIyNevX8vLy0tISFy5cgW/Yu/evQghAwODT58+KSsra2pq4tPTXF1dIUxRUdGhQ4fwLCs0NBTk7969g8cRQsuWLXNxcXFxccEXLLx8+bJPnz7Hjh17+PDhzp07ZWVlFRQUbt++TU9gcXGxo6OjkZHR3bt3Y2NjT506hRBSVlb+9u0bRVFxcXEuLi7t27dHCI0ZMwbiT0xMLCsrmzFjBry3oqICosrKysJ3ivn5+VEUxWazT5w40b9/fxhlhoSEjBw5EgJ07tyZuZLW1taTJk3Kz8/ncrmvXr1q3br1woULRZTakydPXFxcbGxsEEIKCgou/5CYmPjw4UMrKysZGRmE0OnTp3fu3Ilb3cTERMiQRYsWdenS5dGjRykpKbdv3zYwMJCWlj58+DC+6MPf39/CwgI2ji1btszHx6dz587GxsYw8paQkHj//n1kZKShoaGmpqaZmZmkpCRCyNHRUYTOwrIafoXlXV5eXnZ2drKyssbGxmBpXbp0KSkpocfDJD+FZRrkA7afq1evjho1CoaGLi4uTPSMjY0dMWLE+vXr79+/f+bMmY4dO7JYrE2bNsEFFDXWF39/fyUlpVWrVqWlpVVWVm7ZsgUhtGPHDoi8oqLCxcUFJgyamprw6oCAAIqiPn/+LCUlhRDauHEjDvzt2zcokSFDhoCwxtIvKSlZuHDhtGnTvLy8bty40b9/f4TQ2LFj8/LyasxAEbDZbDhpqGXLlmVlZVju6+srJycHhWtsbEx/xM3NTV9fny6pc8mKLhTAxcVFW1s7Ojqaoqjo6GgTE5Pu3bvDT8IaGX9//9mzZ8PkdtKkSRA4NTV17ty5cnJy0KbFxMT07dtXU1MTT6cPHDhQVFRkZWXVrl27/v37KyoqIoSMjIyqq6uxMtXV1TNmzOjQoYOfn191dXVERISGhka3bt1iY2NFq1TnjLpx4wb2QkJscFUrxax9BupgPFVVVY6OjpAJ8vLy6enpZmZmGhoaYC0sFuvGjRs4MEMjb5DWacmSJVDBjx8/Tt8hKyEhUatUZ2Zmbt26FZxTbdu2TU5OxvfVwLobYcTFxQ0YMGDkyJEBAQHx8fFnzpxRVFRUU1PDnS9FUWfPnnVxcTExMUEImZiY4HaefsUQnZcvX7q4uOjo6CCEzM3NXWg4OTmBVjwXdDDpDqh6WKzAShQdHT1lyhQol3HjxgUEBKipqWlra0MwRUXFqKgonqStXr1aQ0Pj3bt3sbGx2GOooaFhbGxsZmYmLJP37dvXo0cPCOzv729iYiIpKYldQr169UpPT6eHf/36tbq6upOT048fP0JDQzds2IAQ6tOnD10fgb3GixcvwDAQQs7OzmfOnGnZsqWuri40fYaGhlVVVRBSRE0URo1aBQcHz5w5E3Jv9uzZp06dkpSUxF2PhITEmTNn6BHWsxfLyMgwNDQcNmxYeHg4h8N5/PixhISEhYUFbvmFpVFg1gFM7PDUqVPYT/Tx40d7e3slJSVYOY4QWrx4MT3C3NxcTU3NvXv3VldXV1VVnThxAiFEb234Ydj+ADWO2Zi00mw2e/Xq1R06dLhz505KSsrz589NTEwgjaIv6BBtRUzMmB+BI5+4uLgah7iiJxHCxvbMq4wwarRDAoHQtBBnH4HQ9ISEhCCEunTpgiVfvnxBCGlqasKfGRkZwcHBMHvp06ePvb19fHz8ihUrOnTo8PXrV4qiLl68KCsrSx9DrFq1CiEkKSmJ74SFOUC7du1GjhwZGRlJURSXy509ezZCSEVFBXr0srKy+Pj43bt38wwIKIrKyckBIfjXMJ8+fZKXl4+IiMCSw4cPI4SkpKTi4+OxcM6cOUZGRkVFRVgCc6devXphiYGBAUJo586d9Pjv3bsH78XOPoqifv/+TVeGw+HExMSAywYhZGNj8+jRI19fXx0dHZjpMVESXhQWFobDnDt3TvRliwCcud6+fXu6MCEh4fHjx6DPmDFj9u/fHxkZOWbMGAMDAxgDDRkyRE5OLi0tjZ4oGM6uW7cOC8vLy8F/0aFDBwcHB5jZstlsGOoZGxubmppCaVIUBW+UlJTMzMwUrbPArKb+cfbp6+sfO3YMMhw+yCOETpw4gYMxLHSB8Dv7oqOjfXx8YKrPM/cQqGdubm6XLl3Onz+PJfgoephFiK4v1dXVbdq0QQglJSXB42w2u2XLliwWKzk5GccJy994phYURRkaGtLnIQD4LHDgGkt/4sSJM2fOxI+XlJTA/QyinctMWLlyJbz32rVrWGhraztr1qzx48fDT8HBwfinfv36HTp0CP9Z55KtsVAoivr9+7ekpOTJkydxmMjISHq7J7CRSU5O/v79O1QN7KcAoIwUFRXHjRv38+dPEELz1apVq/79+3t7e4MwLS0NVig8ePAAP378+HGE0Lx587DkyJEjCCF7e3vRKtUno+7evQsR8siZtM9AnY1nx44d0D4MHz48KCiIoig2mz1lyhT6jBFgYuRUQ7ROEKe6uvro0aM/f/6cm5v76NEjvHr32bNnDFNdWFj469cvOHeyVatWdnZ24eHhZ8+eVVVVPXfunLAMyc7ObtOmjampKd0F/P79e3DTP336lB54+vTpCKHp06eLzmQMKLNgwQK6EJaBIz5nH8PuoM4WK6wSVVVVwaJOdXX1SZMmgQI5OTngu5k7dy5dyYsXLyKE3N3dsWT06NEIoW7dur1+/frt27fCsiIpKQn3zsbGxvfu3auqqsrOzt62bRsIx44diwO/ffuWxWKtXLmSHoOzszPUdJxFwnqN3NxcJSUlhJCWlta6deugvf369St0bR4eHjiksJooECZaFRYWfvnyRUNDA8YDdnZ2iYmJHA7n06dP2Bf2+fNnrGd9ejGKoiZOnIgQon/QhSXA9JZfYBpFdLgM7RDfAWVsbHz9+nXwA4IjT0xMLC4uDodcvnx569at6a+YMWPGpUuXROc2w/anxjEbw1Z61qxZsrKydLWTk5Ohv6jxNl5hVsTQjIXBM/KpcYhb4yRCxNieeZURCBM7JBAITQhx9hEITc/Nmzd5+ml8PBDdO2ZtbY0QUlVVLS0tpT8eHx+voKCwZs0aujA6OlpSUlJOTi4rKwskMJlUVVUtLCzEwUJDQ+FFsNYGiImJ4R8QCJv0mpmZWVpa0iVpaWkQEg907ty5gxB6/PgxPdjly5cRQr1798YSgZ4d7BuiO/sKCgr4lcEbPY4dO0b9L0yUXL9+PULo7t27OExZWdn8+fOpmhDo7APAqTRhwgQeOVyh4ODgwCO/du0aQkhcXDwkJAQLYcLP43WC3ZoIoR8/ftDlMGi7d++eaJ1FO/uOHDlCF8JJcPSZNpP8FAa/sw/o0KED/9xDoJ7r169XVFTkeRwW9RgZGWGJsPqSlpbGYrFYLFZCQgIWamtrI4QePXqEJcKcfb169eKfh8CCHZ7Awkr/2bNnCKEvX77QhQsXLoSiz87OpuoBPuhn6NChICkqKpKTk3vx4oW3tzf8tHTpUvgpJiZGTEwsJSUFP17nkmVSKJDwZcuW0cPQvSfCGhmKombNmoX4nH3u7u4IIRkZmfLycixMTU2FSE6dOkUPDGa8fPlyLIG9WnTXiaenJ0KoZ8+eNapU54wS7eyrsX2uj/E8ePAAYqNPNf39/UGIewqqNkZez9YJJvAdO3akN++JiYktW7ZECPXo0QNWOTFMNSwYRwjR208RzJs3DyHk5eXFI7e3t0cIderUqbi4GAsbz9nHvDuoj8VSQioRFKuuri7d4wlOAT09PXpI2BIYGBiIJeBtYbFYubm5onMDNhQjhF6/fk2XOzo6ghyirays1NHRYbFYGRkZ9GBcLhf8ZZMnT6bLBfYasHyJx/c9bNgw9L8LS5k7+2ql1bhx4xBCAwcOpIdMTk4GD7KFhQVI6tmLURQFO2HpTpbFixcjhFavXs0kjfxZx9wO8QCMvkavuroaVuTR182ZmZmJiYnR/fv+/v48Kxz5Ydj+1DhmY9JKBwQEIIQWLVrEowOUY92cfbU1Y34EjnxEDHEZTiIEju0pxlVGIEzskEAgNCHkzD4CoemZPHnyqVOn+vbtiyXUP8f95uXl4SMzYLuNubk5bF7D7Nmzp7i4GIYmmO7du8fExMjIyLRr144ub9OmDcyjALwdIzU1FR/QAy9iQkRExOfPn3v37g0LCTFSUlJVVVUfP36EPzds2CAhIcFz3efcuXN79uyJv3jXH6z28OHD66AkZNSSJUtUVFRgC6esrOymTZvqrxKPPuif41169uzJI58yZcqKFSvy8/PPnj2LjyYEBxw+mRGAkTpWG9OuXbv8/Hy88rFu8JxoA39iHwrD/KwtDK2OoigPDw85Obm1a9fS5XAmXVhYWGlpaYsWLZDw+qKmpnb79m0xMTH68X9Q4/Ly8uqmvECElT4cx3PlypUbN25g4a9fvyAVnz9/5j9EiTm9e/fW0tKKjo728/NLSEjo0qXL7du3W7ZsOWLECC6X27Zt25ycnBs3bjg7O8vIyFy7dm3IkCH41Ms6lyzDQgFbdXNz09XVxZP8PXv2MEmXQPOAqiEhIQETaUBVVZXFYlEUxVM1YCEYvWrs2LFDW1sbnDg4IYiBGTRSFUAM2uf6GA++gwXc0ICamhp+BU+OMaFBWid9fX36RRYaGhpz5sxxdXUNCwuLj4/v1q0bw1SDkbRq1QqWjYsmLy8PPjjxt8MLFy68ePFicnLy48eP8TkSjQfz7qDOFguIqESKior0K3rAKnCbjxAqKyuLiopCCBUVFWHh4MGDQYeMjAzRJ/fhyHkOh129erWbmxtFUW/fvu3Tp8/Tp0+joqI6dOgA2xgxLBZrwYIFGzdu9Pb2zsjIUFVVrTFFdCMXmCLm1EoreDus78PASXNXr1719fXlcrksFquevRhC6MaNG0FBQdOmTcOSehoDczsU2JJISEgoKyunpqbSM7ldu3ZcLtfS0vL+/fvQ0fTv3x+3D/VE9JiNYSsNy0v5z0WFDdR1o7ZmzBBhQ1xUy0kEP/WpMvW0QwKB0NgQZx+B0PRIS0vjI/PT09PPnz8fHBwMf7LZbJ7A/Kfevnz5EiHEf4Iyz3BT2KvhH1VVVbXUGiGEYFWIlpaWubk5XQ5/wqnbsbGx8fHxXbt25b/uEzZrNDg8WcRESYSQnZ3dwYMHc3JyBg0aBP+Gk7MaXB82mw2zJv6ptYyMjImJydu3b+GEdREIG7TBp/WGvYoRjARbCMP8bCSioqKys7NNTEwEvh3RxriAwFOiYesiQqi0tPTmzZsvX74EBwR/das//Ar4+/tLS0vzHCJubm4O/i846qs+zJo1C84fvHz58q5duzw8POC0bHFx8ZkzZ548ebKgoMDb23vGjBnXrl3bvHkzXTFUp5JlWCi9evUaPHjwu3fvli5devPmTVdXVyMjowZ09wNiYmLi4uL8RclfNdTU1FavXg3//vbtG9yJgRiYwR+rAvztc4MbD26WuVxuvXSlUdvWif/KDmtra1dXV4RQVFRUt27dapVqhvdKw+Y+JKgd7t27t6SkZHV1dVhYWGM7+2rVHdTZYmsL5CHdJHCZxsTE4PPC2rRpIycnV15ezmSwIZBu3bppamrGxMTAmqPw8HAkKCsQQvjmirCwsNp6SZCgFDGnQbQaPXr01atXKyoqUlNTS0tL69+L9erVC1bAwQ0zt27dgjth6mYMDTIs4c/k1atXP3v27OvXrzo6Olu3bl29erWUlFTXrl3roCE/osdsTFpp6p/FtviIzwah8cwY4LeHWk0iGMKwyjSsHRIIhAaHOPsIhObCz58/9+/fX11dvXHjRgcHB55PgsJgs9np6ekIoYqKikZWUADw6o4dO9I/6/GQnJyMmkg9gImSCKG2bdu+fft27NixaWlpHh4e3t7ehw4dgv0IDUt8fDxM3TMzM/l/hVP58W6LZgjD/GzUt0tJSdXz7Xl5eUeOHAkMDFy7dq2Xl5e+vj59xUrjUV1dDbsOJ0yY0EiOUXD2IYQuX748Y8aMDx8+nD59Gn6aN2/eyZMnEUIXL17s1KlTamrq5MmT8YN1LlnmhfLgwYOJEyf6+/t/+PDB1NR06dKlBw8ebGwHsWh8fHycnZ179uy5detWf39/+rIpYTRVFfgDxtNMwM6j3NzcRkr1z58/4R+ZmZlwdQlGTExMTU0tKSnpD7TDdegO6mCx9UdSUnLMmDHPnj27cuUKXpZbVFRUVlY2aNAgnqWdtUJdXT0mJqasrAz9UygisgIhFBMTA2cF/jEaRCscsqysrKF6MTab7e7ufvXqVVtb23Pnzq1ZsyYuLq5uUTXSsGTYsGHXrl1bsGBBaWnppk2bPDw8Lly4MGjQoLopyYPoMRuTVjoxMbG8vBwhBOsoG4qmMuO6TSLqTwPaIYFAaHDEag5CIBAan3379hkZGY0YMeLWrVv4wiyGwFe+JnEPwRnPol8NYdLT00tLS/+QWoIUYJI/PXr0iIiIWLJkiZiYWGFh4ZIlS+CE8obVB28PEbg/Ar4G82ykbVYwz8/Ge3t8fHx9FiI9f/5cS0srLy/v1atXlpaW9GtAGxsWiwWvi42NbaRXdOvWDRYvJCcnz58/38DAADcpxsbGRkZGCCFfX9/du3ePHDmSvvmuziXLvFBatWrl6+vr4uLSsmVLDodz8uTJvn37NtV+n5KSkqlTp86YMcPZ2fnQoUPM11k0VRX4A8bTTIBrghFCBgYGjZTqZtIO10qNOltsg3DhwgUdHZ3Pnz9v27YNDpvbtm1bhw4drl69Wp9owVEIyzMhNzIzM6urq3mC4XVSf75zbBCtIJkSEhLdunVrkF4sNja2Z8+ely5devz48ZIlS/j3+daKxqsOs2bNCgsLGzp0KEIoOjp66NChFy5cqIem/4OIMRuTVhofKVC3/d3CaBIzrs8koj40rB0SCIQGhzj7CISmx93dfevWrRMnTpw7d25tn4WxI0Lo0aNHAgM07CCGB9gx8eLFC4GOvLS0tIiICLzHCt9PSqe0tLRh95zWTUn0z2dYRUVFNze3kJAQ2Jhw9epVfABzQ9G6dWs4DOX79+/8v2ZnZyOEevTo0bAvbUAY5mejvj07OxtfLECHw+G8efNGdAwRERFWVlZSUlKnT59muOOvAZGQkIBVS/iaaR58fHzq/xY4iR8h9OnTpzlz5tB/srOzQwhRFPXq1aupU6fSf6pzyTIslIKCgsrKSjExsZUrV0ZHR4OS379/x5dy/mGWLFly586dPXv2gAOUOU1VBf6M8TQHsrKyEELi4uJ6enqNlGp8iETTtsO16g7qbLENgpqa2rdv3wwMDJ4/f96/f/+pU6cqKSn9/Pmzznt4ARii6Orqon8KhcvlwkZIOpAVqCk6xwbRCpKpqakpKSlZ/16svLzc0tLyx48fHh4ePEet1Y1GGpbAsKp79+5v37719vZWVlbmcrnLli3DF2XUB9FjNiatND79BrYwNxR/3ozrM4moDw1uhwQCocEhzj4CoemBjXX04XutTruAj3iXLl2Kjo7m+encuXNwpk8DQtcNBuhlZWVHjhzhD7Z06VJVVdX27dvDboKdO3dWVlbyBFu5ciXPDl+etOPjnOjfSGHLD0OYKIkQOnLkSGFhIciNjIz8/f3hdJUPHz4wfxdD4MyjFy9e8K9pio+PRwjBzaGNTd0OVWGYnw0IXc+OHTvCadO7du3iX3S5c+dO/vO/eDh37lxlZaWenh79ZDFhWcEvh/h5vtjXwSBdXV3xvZmYV69e1ed6B8y0adPAjykmJjZz5kz6T7NmzYKES0lJTZw4kV+xOpQsw0J59+7dq1evQKiionL9+nU4Op2/iv2B437y8vLgtgeGDW9t270aFYDD+GvLHzAe1BBGXk9gPc6MGTPg6pXGSHW3bt1gcQ390g+goqIiPT1dXl6e/yD8xoBhd1Afi20oNmzYsG/fvq9fvwYFBd25c2fnzp347P+6UV5eHhMT07ZtWwsLC4TQkCFDoOHiLxTIClNTU7wRsqGosSY2iFbgRLO1tUUN0Ys9f/48OjpaQkICrkgGRJQ4k9amMYYlW7Zswf+2srIKCgpSUlKqqqr68uWLiKcYtj+ix2xMWmllZWVwUcHF7vww39hBz+GGMmPmVbiek4g6U1s7JBAIfx7i7CMQmh745Pvt2zcsCQgIgH/Q782AYQf/oG3z5s1iYmLV1dWDBg3CH4o5HM7p06cfPnw4YcIEkMDIiWf8hOOnd89YSA/cqlUrvCEXP9KrVy84NH3fvn3nz5/HA6OsrKxJkyb169cPNglu3boVIfTr16/BgwfDEX4IoaKiovnz53ft2hVPjCEwPX5EO7wJ5wlC6MaNGzAcLCkp4ckf/ixiqCSHw/H29sZPycrKwpKoGg+TBg8mvx8TCS+yAwcOtGzZsqqq6vr163R5QkLChw8fxo4dO378eCwUuNMHC3nGVSCvcYQqMKspigJVBRoJfhHD/BQGzg2edMFbeF7NryeLxVq5ciVCyM/Pb/HixXDgDkKosrJy27ZtwcHBw4YNA4mwzIfqFhkZiYssOTk5JSUF/W91E5hF6B+DpPunsrKyXr9+jf7XGkUoAB6unJwcS0tL+gKHu3fvOjg44AP4CwsLnZycNm/eXIfLc9q1azdq1CiE0NChQ/Flu/gnuHlwxIgRrVq1ov9U55JlXih37tyhP7ho0SJEq2ICGxn4h0DzELYJDuSiqwZe8kxveKFY6Rlen3ZPIPgnnjQybJ8ZGo9ABE748SvojQZzI2+M1gkhdO7cOSUlJbgeFDFOtbAaJxAxMbFTp04hhIKDg3luHrh9+3Z1dfWOHTvwVcVIZDsvEPBN8CQW5wxdzrA7qI/Fwj8EViIRVsGj/NatWwMDA8eNG1dDykXCs8zq5s2bxcXF27Ztg0uou3fvDmV98+ZNnqy+fv06i8WCa1t49KxbioTVRH5qqxV/Mjkcjru7u5qampOTE6pNgym6F2Oz2XjtWEVFxdevX5GgXkxgGvmzjvmwRFgV48/kkJAQcG8BnTt3ho5J9LCKYfsjeszGsJWGkg0JCaH7+yiKgjYBOxOFITCH62AwAqPlH6HhhPOEZziJEDi2FxghEtIICHxvjXZIIBCaEopAIDQ1cEwvi8VasWKFu7v79OnTlyxZAh8G582bd+zYsbi4OIqiYOBiYGDAH8O6detwpVZRURk0aJCqqqqmpiacaw7Y29sjhBQUFCoqKrAQDw7OnTuHhS9evADhvXv36G8xMDBACJmamn769Ono0aNnz56lKOrTp0/4kI5u3bpNnjx52LBhsrKy8+bNww9WV1fjO8gQQlpaWv3791dQULCysuJwODgY3CbWqlWr58+f37t3b+nSpfAsbARu167dsWPHTp48OWzYsD179sCpKL169cKaw1gQIXTnzh2e/GGipJOTU4cOHbKysrBk1apVysrKv3//Fl18oLaYmFhmZiZdXlJSApdpQkJ48PLykpWVlZOTCwwMBElZWdno0aMNDQ2TkpLoIWHcP3DgQLrw1q1bkJx3795hIYfDgRNh6OkSoTNPVoPDCyG0b98+emBY/6WmpsblckHCJD+F4eLiAg8mJiZiYVFREdzUuXz58hr1LCgowOfdtGnTZty4cZaWlsrKysbGxgUFBfhZYfXlwIED8KyFhYWHh4eTk5OFhQXsuzE3N/fw8Hj69ClFUXj79smTJ/38/KZPn15SUkJRFJ4IWVtbe3h4rF271sTEBCqgmJjYpk2bvn//TtVU+lZWVhCJlJTUsGHDrKystLS0WrVqFRoayp9RXl5eNeYqP7Cm4PLly/w/PXz4ECF06dIl/p/qXLJMCuXBgwdiYmLv37+nayImJvb69WssEdjIUBQFpxAaGhrSX7p7926EkLi4eGVlJRbiY5hg1QwGriLp0qUL/FleXg6+TgUFhSNHjpw6dWr48OHz5s2DZ52dnXfu3ClCpTpn1O/fv3HbHhgY6ODgEBISQtWmfWZiPAI5c+YMPBgZGYmFz58/B+GTJ0+wkKGRU/VunWDhT4cOHdLS0rDw1KlTysrKPj4+9DiZpBq+KrFYLJ6mWARr1qxBCGlpaeXk5IAkISGhc+fOU6ZMqaqqoocEMxDY//LD4XDgfs8xY8bQ5Tdv3oRUfPv2jS5n0h3U02IpIZUI9vK3b9+e3hdv2LABcrKoqAgL4bNB69ate/bsOXTo0PHjx8+cOXPZsmXu7u5lZWWiMyQ0NBT0HDNmTF5eHghDQkJat249depUelaXlJQMHjwYITRlyhTc4zx//lxOTu7IkSP0OAX2GhwOB3YSTJ06lR64T58+CKFevXphibCaKBDmWsHyNxkZmfv374OEy+Xa29srKSm9ffsWB6tnLwYXniKEtLW1L1y4sHfv3gEDBoAzTkVF5eLFi1DowtIorMNlOCyJjIyEt585cwYLCwsLYc342rVrsdDY2NjS0rK6uhqXjqmp6dChQ+nGxg/D9qfGMRuTVrqsrAzWAIIH9vXr12/evJk8eTJ4DMXExGbMmAFDAoEIy2HmBiMQgSMfEUNchpMIgWN75lWGH4Z2SCAQmhDi7CMQmp5fv37ho0M6deoEc3u8w2737t2PHz8eM2YM+oeRI0fu27cPDyCACxcu4KN/xcTEFixYkJubCz+lpKQsXrwYxnYIoQEDBty6dYuiqMOHD8OxJjCC37FjR3x8/KZNm/BSIDU1tU2bNmVkZEA8Dx8+hEgkJSW3bNmCX/3jxw8TExOsnoqKipubGxzgjamsrFy2bBkeeMF0BQ8Bgbi4OLySYsKECYWFhSAPDg7G34Hbtm178+ZNiqLatm1rZGS0d+/eX79+FRcXr1q1Cp/p3qZNGycnp4CAAHrkNSq5atUqIyOj7t27b9u27eLFi4sXLzY1NRUx+qco6u7du/Pnz8cZq6ent3Xr1qCgIIqijh07hs9kkZSUnDt37vXr13kej4yMHDRokIKCwrhx4xwcHAwMDJYuXVpeXo4D+Pn52dvbwxpGFou1aNEiX1/f0tLSbdu24TLq2rXr7t27y8vL7927hy93k5OTW7FiRXh4uDDN+bP68uXL+FBneXl5JyenzMzM0NDQ2bNn4/srRo8e/fnzZ+aFzsO3b9+WLVuGt30ZGRnt3r27oKBgz549ZmZmIJSRkXFwcIA8FGESeXl5kydPxifuSUtLr127Fs+RRNeX8vJy2K8EKYXd5cePHwfJ+PHjS0tLKYqqqqqCiRZCSFdXl+4fcXJywtusJk+e/Pv379OnTysoKMyYMcPb27u8vLzG0udwONu2baNfYWllZRUdHU0P4+Pjw2KxJCUlsWOlVpSWliorK9Pn6pjq6mo1NTXs3eChDiULiC4UiqIePHigpaXVqVOnpUuXuru779ixQ1tbG9oiDH8j4+HhYW1tTc8oLy+v+Pj4DRs24EtUR48eDW7Ns2fPwhQFIaSgoLB27dqYmJiQkJD58+djxWxsbJ4/f05R1P3797E19u3bNzg4uKSkBOxNXl7+2bNnwlSqZ0aBJwUhpKSk5O3tzbx9hseZGA8/27Ztwx2Eqampm5sbRVEnTpzAu8k6d+58/PhxHL5GI2+Q1qmkpGTz5s1qamoSEhKmpqZTp07V09MbP3483ffHJNURERFz584F9zpU2G3btiUnJ9dYFhRFPXr0SFtbu23btra2trNnz9bW1obMwRw5cgQ2mWJj27NnT3p6urAInz9/PmXKFAjMYrFsbGxev35dWVm5efNmZWVlkBsbG586dYr+VI3dAVUPixVYiVJSUhwdHfFpBlZWVj4+PsXFxatXr8a3Hg8cOPDx48cQ7c2bN4Udy9W1a1f6l0V+sLNv79696urqEyZMGDp0aJs2bXg+LAEcDmffvn3t27fX1NRcunTpuHHjevToQf9IUFVVJbDXCAgIwFsZpKSkli1bFhUVFRkZSb+wePbs2bhn5KmJIvRnohUAzr7Ro0ePGzfOyMho6tSpnTp1MjMzi4mJ4QlZn16MoqgtW7ZA7RMXF585c+bv378/f/4MsXXv3j02NlZgGoVlHY62Rju8du0aHiqoqqpu3bq1tLT06dOnffv2BWGLFi3WrFkDXc+AAQMMDQ379Olz+PDh8+fPjx492tramu6eE0aN7Q/FbMzGpJUuKiqaPXs2boHbt2/v6elpZ2enoqKyYsUK7PcUhjArYmgwAuEZ+aSnp4se4tY4iRA2tq9tleGHoR0SCISmgkU19EWTBAKhDlRVVcGuARMTE7hdF4410dTUxB08E+Li4nJycgwMDFq0aNEYemZmZkZHR2tpafFrlZ+fHxkZqaqq2rlzZ2HXm3I4nB8/fkhISOjo6Ai8G6GkpOT/sXfXYVFs/+PAD0tJiChK2SggSIrd3d2KrVcFu/WqV722XhW7EcXAxi4UUOygu5Tu7mV3fn+8v/f85rO7LEMj9/16fHyWs2dmzsycqfeeOcfb21tTU1NfX5+dzufzAwMDhUKhiYkJTBgVFQVNJ8pESiFzc3NVVFSEQmFYWFhcXFyLFi1at25datc5FVdYWOjj48MwjLGxMftRtqqVtKnLhMtOryAp5SwoKPD19a1Xr56BgQF9zucoMDAwNTXVwsKCbvMfP36oqKgYGhrSPMXFxdDRkqWlpcjaJSUlhYaGGhgYQAAlKSlJTU0N+hfjTigUBgcHZ2VlGRkZwVtsIsLDw1VUVOBX93IICAhg96TD8StQ7j0rZacUFBQoKCjweLy4uLiwsDB1dXUjIyN2z4lAykmm0mVmZvr6+mpqahoYGEBKWlpaYGCghYUF+xRawfOeuJCQkOTkZJGllEmplafiKqWScxQREREbGysnJ2dgYCClo/eqW+uMjAw/P7/69eubmJjAJbhGlHo5qHiNLbfXr187OTlt2rQpKioqJSUlJycnNzc3JycnJibm/v37M2bM2LdvX0nTent7Q4QoLCxMR0fH398frubS639MTEx4eHjTpk1p1LvSleNIlF6qcePGOTs7z5gxw9HR8devXz9//mzevLmUt1YrchWLjY0NDw83MDCg14jIyMi0tDRzc3P2/VU51rGybkvgtqqwsDAoKCgjI8PIyIjGu0tV6vmH+z0bl7N0YWGhr69vw4YNIWoGO47jqUD6Fi5fNS7rHVplPUSUA8d6iBCqERjsQwghhBBCCEn24MGDBQsWhIaGinT0CYKDg7dv337jxo2SJmcH+2gTpDqJHeyr6bIghBD6r8MBOhBCCCGEEEISZGVlzZkzp02bNhIjfYSQkJCQHj16VG+hEEIIIVQKDPYhhBBCCCGEJEhMTMzIyPj27dutW7dEviouLr57966DgwOMrF2SkgZornskDgWOEEII1Qh8lx4hhBBCCCEkgb6+/pQpU27evDllypQ///yzW7duTZo0yc3NTUhI8PLymj59uoODAx3fQKJfv37Bh4iICHa/qHVPVFQUISQ8PLymC4IQQghhn30IIYQQQgihEjAMc/369UuXLvn6+iYlJcFYRoMGDVqxYoWOjo6UCbOzs7ds2XLjxo3k5GRCiJaW1sSJE4cPHz58+PDqKns1cXZ2PnPmzIsXL+DPoUOHWlpabt26lQ5tjBBCCFUzDPYhhBBCCCGESicUCqto7HWEEEIIVSIM9iGEEEIIIYQQQgghVEfgT3MIIYQQQgghhBBCCNURGOxDCCGEEEIIIYQQQqiOwGAfQgghhBBCCCGEEEJ1hFxNFwAhVPOSkpKeP39+9+7d1atX9+nTh/uE6enpbm5ujx8/bteu3bp166TkLCgocHV1vXv3rrm5+bJlyypc5FqB++qDp0+f/vXXXwsWLFi8eHE1LE5ceHj43LlzjY2Nz5w5U47Jq0JxcbGHh8e9e/fU1dX//vvvmi5Ome3bt+/+/fuHDh3q2bNnOSavhXuEEFJcXGxtbZ2Zmeng4KCtrV3TxSmb2NjYR48e3bp16/r1679d4cvtdzzBxsXF7d+//9u3b/Hx8UZGRhs2bOjdu3epU+Xk5Li4uHz48CEhISE/P19TU7Ndu3b9+vVr3749wzDLly8/dOiQoqIiIcTd3T0jI0Nkch6Pp66urqOj07ZtW3Z6TEzM9+/fxRfXrl07Q0ND+qeXl9evX7/YGerXr9+/f3/OK13rCASCr1+/PnjwIDY29sqVKzVdHAlqfwkrUQVvEn535TsngN/9XqJy1VRF+k8drQj9HhiE0H/bunXrmjZtCieE58+fc59ww4YNbdq0gQnXrl0rJef169ctLS0h5/bt2ytc5FqB++pTgwYNIoRoaWlVz+LE7d69G+YQGBhYvjlULg8Pj169ekGRFixYUNPFKbPi4mIlJSVCyJQpU8o3h9q2R8D79++hVKdPn67pspTNypUrGzZsCIWPiYmp6eJUk9/xBBseHm5oaOjn58fn86dMmUIIUVRUzM7OljJJenr6xo0blZWVCSHdunXbsmXL8ePH165dO3DgQAjhQWw3NTUV8n/+/Hn37t0qKiqwZRo1atS/f38TExN1dXVCiIaGxuzZs728vCBzUlLSvXv3xo0bR2+Pe/bsee3atV+/frHLEBgYePz4cU1NTSjwpk2b3r17V0WbqBq8f/9+2LBhMjIyhJCuXbvWdHEkqP0lrFwVuUn43ZXjnED97vcSla5GKtJ/7WhF6LeAwT6EEBMUFFSOYB/DMEKhsEWLFlzCTwKBQEtL6zd6FuWC++oDZ2dnMzOz48ePV8/ixAUHB3fp0mXu3Lnlm7yKQJziN71B3759u6Wlpaura/kmr517pKioaOzYsf369YuLi6vpspTZgwcP/mvBPuY3PMGamZn169cPPufl5Q0aNGjUqFFCobCk/P7+/np6eoQQTU3NJ0+eiHz7+fNnc3Nz2O/R0dHsrxYtWgTpp06dghShUPj06dNOnToRQmRlZY8dO0YzFxQUQDCRECKl8t+/f58Q8tdff5V1rWunqVOn1vKH89pfwspSwZuE31pZzwnifut7icpVgxXpv3O0IvRbwD77EEIEnhLLQUZGplmzZlxy8ni8xo0bl28ptZaU1Y+NjT127JhI4pgxY7y9vZcuXVrpi+PIwMDg06dP9vb2FZlJRQgEgj///FMkEZrJ1MiiK27btm0/fvzo27dv+Sav8T1CCNmyZQufz2enyMvL379//82bNzo6OjVVKo6ePn3q7u7OTqme6lTb/F4n2A8fPvj4+DRp0gT+VFJSevny5cOHD6FJiLhfv3716dMnIiLCwsLCx8dn+PDhIhk6d+7s4eFhYmJCCMnPz2d/Rc+Zampq8EFGRmbYsGEvXrxo3bq1QCBYtWqVp6cnfKWoqAilkpOTk1L54Z39zp07l3G9a6ly3wBUm9pfwnKoipuECqqiqyQXZT0n1OC9RHUSv8BxVIMVqU4erQj9vjDYhxAiJd1OccHjcT2NVGQptZbE1S8qKpo0aVJ8fHz1LO43sn79eldXV5HE6qkYEheNHjx4sGfPHqFQWNMFKY+fP3/OmjWroKCAnVgnzzNc/EYr/vr1a8L5bMYwzMSJE1NSUuTl5a9du1bSk6SqqqqTk5OsrGxeXh47XV5eXmL+hg0b2tjYEEIEAgG7x0zILycnrUtreBFYVVWVS/lrv9pfc2p/Ccuq6m4SKqIGr5JlOieQGr2XqDYSL3C1Xx3bCwj97n7v50aEEKqFli5d+vHjx5ouRa1z7dq1w4cP/9cWXZsFBgbOmjWLYZiaLkh55Obmjhs3LjU1taYLgsqsTDGOW7duffv2jRAyduxYY2NjKTnbt28/ceJEkWCfFNASkBASEBDAvTzk31Cg9IAgQlLUwpuEmr1Klumc8F+4oOMFDiFUKfBOBaFaITU19d69ewzDLFy4MD8//8KFC25ubsrKyjNmzBgyZAghRCgUPnjw4OnTp5mZmZ06dVqxYoWCgoLITLKzs58+ferp6Qn9HFtaWo4aNUo8GyEkIyPj8uXLnz9/lpWV7dGjx6RJk0oq2I8fP548eeLn56ejo9OlS5cpU6ZUSuOyV69e3bt3Lz09vUuXLuPGjWvVqhWkBwUFPXnyhJ1z5MiRMBhiREQE9JRECOnXr1+HDh1Kmnm5N2ZcXNyNGzdgJk2aNJk1axYhJCwsjPYCRgixtbWFMRkkKi4uXrp06fnz5wkhnz9/PnToECFk1KhRBgYGhBCGYTw8PO7cuXP06FH2VIGBgVeuXLGxsdHV1b1+/fqrV69kZGR69Ogxbdo0+uqZROXYXMHBwU5OTpMnTzYyMqJTeXp63rx5c/bs2UZGRm5ubo6Ojnl5eT169LCxsZGVlRVfrrOzs7u7e2xsbPv27a2srPr37087uiqJvb09jAoXFxcHm8XCwmLAgAEi2YKDgy9evBgWFtaiRQtra2voV0tEWeuklEUXFxc/ffo0IiJi5cqVXl5ednZ2GhoamzdvbtSoESFEKBQ+fvzYzc0tLi5OU1PTwsJi5syZIg2FCgoKnjx5EhkZuXbtWppYXFz85s2be/funTlzRigUOjo6Pnv2TEVFZezYsaNGjRJfZfE9Eh0dffv27RYtWkycODE0NPT06dO/fv1q3779ihUrNDQ0xNfx27dvDx8+DAwMbNWqlYWFxaBBg0p9oenz58/jxo3LysoihBw5ckReXl5DQ2POnDnwbXJy8p07d7S1tdlDFhQWFj5//vzjx4/79u0jhNy7d+/+/fsFBQX9+/dfuHAhVJUvX744OTn9+vXL0NBwyZIldPAf9ha7fv36jx8/UlNTLS0tx4wZwx7tlKOEhIQJEyZ4eXkRQu7cuePn50ckHZvFxcXXrl1zcXEpLi7u3LnzkiVLxM+H5SsPlx1kZ2cnEAjg89KlSxUVFQsLC0+cOEFnQs8MIDQ09NatWz179uzTp09sbOzp06f9/PyaNm26bNmydu3aEUKys7OvXbvm4eHB4/GGDx8OXSNJVNIJlq3U4wh25eHDhxMTEw8cOJCWlrZ69WpTU1Mpm4VhmBcvXnz79s3X11dHR8fS0nLYsGHsqnj69Om8vDwI3gUFBcHxSAixsbEp6RxCt5i1tbWURYNz585JvORJlJubCx/ooC4V9O3bt5s3b2ZkZGhqapqbmyspKYkf79Tdu3d//vwJn8eMGQOjA1+5ciU5ORkSxc+Q9+7dc3V1LSoqatGihZWVlaKiYr9+/dgZOFZmDw+PmzdvxsbG6unpzZ07t2IrTQgh379/v3fvXlRUlLy8vLm5+dSpU0UaYGZlZT18+DA6OnrTpk2ZmZknT578/v17ixYt5s6da2ZmVrklrPTCSPT48WMPD4/w8PB27doNHDiwT58+IhlKqgzluElgGObjx483b97866+/NDQ0vn37Zm9vHx8fb2pqumrVKqi9v379unLlio+Pj5aW1owZM7p27SpSntzc3GvXrnl6eqalpenp6fXq1Yv9OnypF+hy3wqWel9a1nNCDd5LgPj4+PPnz0dFRampqRkbG2tra/fs2VNdXf3Lly/v3r2DPB06dIBj09XV9cePH5DIvsKCkipJqRe4Uit5SXebZbrZq6nzCUKoMtVUZ4EIIfD27dvhw4fDVXbBggWurq5t2rQxNTWFa6qMjMzDhw9//frVrVu3Fi1adOvWDS72I0aMEJmPm5tbq1atbG1tvby8/Pz8tm3bxuPxTE1Nv3//LpLTx8endevW8+bN+/r1a3h4+LZt22jX5uwBOvLz821sbCZOnOjk5HT9+vX+/fsTQvr27ZuQkMCeG/RexGXICGhG8ddff82cOZN9FmrQoAHtcF0oFN67dw9aTDRt2jQpKYk9B7jjWbJkSVFRURVtzPj4+Pbt2xNCLCwsIEUgEERFRU2fPh1Km5KSImX1w8PD7ezsIMAxYMAAOzs7Ozu70NDQyMjIdevWNW/enBAiJydHJ9+9e7eFhQXM+c2bN507d5aTk6O3m8bGxuHh4VIWx31z5eTkHDx4kIb8PDw8IM/x48eh23tCiJub29KlS9XV1fX19eFFjAkTJohs4ezs7CFDhvTo0cPf39/Ly2vEiBGEEB6Pp6+vb2FhUdKgtAUFBXZ2dvA41LJlS9gsdFCLoUOHwv5ycHBQUlIyMTGBcImCgsKrV6/Y8+FYJ7ks2svLa9GiRdBD0MiRI11dXelttJ2dHdQEMzOzXr16+fj4CAQCFxeXevXq9enTJzMzE+bs4eExa9as+vXrE0IGDhwIiUVFRba2tvCWn5ycXFJSUq9evZo1a6arqwszp6MBlLRHnj9/TkcV3LVrl729fYMGDYyMjODpqFWrVllZWSLruHHjxqZNm7q4uERERGzatAmmbdasmYWFhYWFRUmjGV67dm3+/PmQ+eDBg3Z2do6OjgzDXL16dejQoVCpdu3aBZljYmLmzJkDA5u2bdv258+fffr0ad26dadOnaC6rlmzpqCgYN68eQ0bNuzRowf0H9eiRQs6NCpwdXXt2rXrP//88+DBg927d6uqqiorK1+6dKmk3VcSd3f3vXv3QuEXLVoEuzU/P59hmE+fPkG6t7d3t27dNDQ0TExMoJC09/eKlIf7DsrLy5sxYwbkzMjIgMSUlBTaJuXOnTuQaG9vT4fTdXR0PHHihKamZpcuXeD5rVGjRr9+/Xrz5k2LFi1MTU1pzoMHD7ILxuUEC6QfR4mJiVu2bIFzpqqqalxcHB0HfOzYsVK2zM+fP/v27du3b193d/fIyMjz5883adJEQ0PDycmJ5jl37pydnR30dmdqamr3r9zcXInzzMjIoA3okpOTpSy9JBCYJoRcvXpV5Kt169bBVydOnKCJEHSrV6+e9Nmyj1lw/Pjxtm3bwrDakZGRXbt2bdKkiZQ5CASCkydPQgGcnZ0hMTs728XFBc5FS5cuZeefPHnyyJEjU1NThUKhu7u7lpbWpEmT2Bm4VGaBQLBhwwZtbW0nJ6fo6GgXF5dOnTrBKpevQ30+nz99+nQtLS0XF5eioqLQ0FBDQ8PmzZv7+vpChpCQkEmTJsHRMXDgwC9fvrRo0cLAwAB+x1JSUvr8+XNllbDSCyPRz58/hwwZsmbNmvv37587dw4uoMuWLePz+TSPlMpQ1puEDRs20O4jg4ODFy9erKmp2b17dzgbd+7cWSAQHD16VF1dvUuXLlAYRUVFkUGi371716hRoyVLlkRHRxcVFcFBsXLlSvhW+gW6HJddist9aZnOCTV4LwE+ffqkoaHx+PFjhmGysrIWLFhACPH394dvv379qqioyN62hYWFvr6+8BuJiYkJe1ZSKomUC1yplbykilTWm70aOZ8ghCodBvsQqnkFBQVwo6Orq7tgwQJ4pBEKhRMmTCCEGBoadujQgd6D/vjxA67Nnp6edA7v3r3j8Xg2Njbs2V64cAFu+4KCgmiin5+fqqqqyOif27ZtgzsAdrBv4sSJEydOpH8WFhZC8xB2IlP2YF/Lli0XLVoUFBQUHx9/6dIl6PZIXl4+NDSU5oQmdWpqaiJBvc+fP6uqqpYUvAAV35irVq0irGAfeP78OWwi6cE+APG7jRs30pSUlJSwsDBoJMW+/fr169dff/0FczY1Nb1x40ZhYWFqauq+ffugYJ07dy51cVw2V15eXnh4+J49e2BZ7MfUt2/fQqKZmdnFixeLi4sZhnFwcIDEr1+/sucJ4SF6W5mfn9+yZUt4aHFxcfn06ZOkffJ/Vq5cKfEWEG7Q9fX1p02bBvHKnJwc2Im9evVi5+RYJ7ksOj4+/sePHzC5lZXVnDlzIiIi1q9fr6Oj4+bmxjDMmDFjCCFnz56lk0DA9+jRo/BnVFSUj4+Pvr4+YQX7wN9//00I4fF4AwYMgIcuoVA4b948QoiGhkZBQQEjdY+kpaVBsExfX3/58uU5OTkMwwQGBkIIQCTE4+joSAhhD7o3efJkQkjjxo1fvXrl4uLCfgQV4ezsDEuHIoHQ0FAXFxeImNNgH1ixYgUhRFVVdfjw4T4+PpAIfcwrKir27dv34sWLMHhieno6VAyInIIvX77Ur1/fy8uLppw9exY2lLe3d0mFLElGRob4WYthBfu6d+/u7OwsEAgYhrl16xYkvn79uuLl4b6DaKNgGuxjGIb2wUSDfQzDxMXF1atXD+a5f/9+OJYzMzNbt24NB0KvXr0SExMh85EjRwghTZo0YR/y3E+w0o+jzMzM4OBgaPWjrKw8b948Ly8vBwcHXV3df/75R8o20dTUNDMzYxfpx48fEEZxcHBgZ4bmLdJDh+D9+/ewrRo3blxqZolKCvZ5enrCY3m7du1gD4LyBfsSExMVFBQOHTpEUyIiIqQH+xiGyczMhLLRYB+AZlnsYB8002aHSK5fvz569Gj6J8fKPGfOHEVFxeDgYJqSlJQElbl8D+dQFadNm0ZTzp07RwgZN24cTSkqKoI29U2bNh05cuSvX79g3eFXRpFfLitSwkovjLj09PS2bduyo8MBAQFwtqSJXCoD95sEhmHu3bsH9aRbt260qnh5ecFvGAMHDly4cCGcwwUCwfjx40UOLj6fDzEv9laF35/YdaOkC3S5L7vc70uZspwTpBS1Gu4lzM3NR40aRf/k8/nGxsY/fvygKfBjDA32gY0bN5L/DfaVWklKusCVWsmlVCTuN3s1dT5BCFU6DPYhVCvAjY7I1fHx48dwDaa/WwJ4qKN3CUVFRdAYLTo6WmS28OZCr1694AmcYZhhw4bxeLyIiAh2trCwMJG7ihcvXhBC3r9/z85G20GwnxvLGuwbOXIkO9HNzQ3mOXnyZJro5+cHoa4rV66wMy9fvnzevHmlLqgiG5P597ZMJNhHy1m+YB+4dOmS+O0XvKlBCIHfiqnNmzdD+sOHD6UvjvvmioyMhHmyH1NzcnIg8eLFizRRKBTC48GpU6doInQfw+PxIIACtm7dCg9OTGmk36APGjSI1lLm3xtQRUVFmsi9TnJf9JQpUwghjRs3Fm8uBy2b2MG+DRs2EEKgkztq9uzZRCzYRysbu2Gmj48PJAYEBNBEiXuEYZiOHTsSQmbOnMlOhKc49pHCMAy8dwYBSgA9nZe6TZgSgn0AQnUiwT7YKTwej93mgtafbdu2sTPDOIDs56Ju3boNGjSInScrKwueV2fMmCG9qOJKDfaJtG2BqNmePXsqpTwcdxD9kYAd7GP+7SSRHexjGAaeNkXOG/B6uKqqKruKpqSkwBzY0R+OJ1iOx9Hp06ch5cOHD9I3BVi4cCEpufVco0aNaKSSKcuDPe2LwNDQsKQ8t2/ftra27sgyZswYd3d3+JYG+w4dOhQXFwex7BUrVkBo1cLCQqT1dPmCfU+fPiWELF68mJ2npJbOFA37igT7YHRvdrBv/fr1hBB2G0k+n29tbU3/5FKZP3/+TAgR+amPYRhof1q+h/NFixaR/w09PHv2jBDSokULdjaoxnp6euxYMPxOoKmpSVMqWMLKLYxEmzZtUlZWFvldDW60mjVrBn9yqQxlukmgr3uLnOvgLNS3b1924p07dwgh9evXpymxsbFwh8COr3Xp0oUQYm9vT1MkXiXLfdkt030pU6nBvqq7l8jNzZWRkTE2NmbP387Ojn2ShEi9SLBv+/bt5H+DfaVWkpIucBwrucSKxP1mr6bOJwihSod99iFUK8CbSiKj+9HuruBlQwq6QEpISIA/X7x44e/vr6mp2axZM5HZLlq06OvXr+/evfP29rawsPjw4cOzZ89MTU3huZeCH+LY7O3tCSHXrl27ffs2TfT394cPHz58gCeicrCysmL/2adPn969e799+/bhw4dCoRBuJtq3bz9y5MhHjx79888/9JU0gUBw8+ZNuJGVriIbs0pJHBeSvqQm0q/W8uXLDx48WFRU9Pr1aykdP5GybC7pBWB3BicjI6Ojo5OamhoTE0MTPT09CSFCoTA3NxdeXyWE9OnTZ+fOnfHx8QzDVGQUtpYtW7InhypaWFiYkpICu6wq6iRskE6dOtHVoa5du/bx40f2G5EMwxBC0tLSxOcgQuImpW/yxsTE0O75ShoqFOYg0j0fzIG9RwoLC2ELQNd7oGfPnjweTygUxsbGlvs4lbJePB6P3ZWSiopKgwYNMjMzpR9ZQUFBHz9+7NChA7ScZU+enZ1NG3BVIpGzXKtWrSIjI+nWq2B5OO6gMpFy4qpXrx67ijZq1EhWVlYgEIifuEo9wXI8jqACKCkpdevWrdSSZ2VlQR9k4l2pLly48ODBg2lpadevX4dH9DKhnR5KAY10Nm/eDO1k+/Tpc+fOHfHRMzZu3Ag/k6irqzdp0mTMmDFTp04dMWJEScdgmUDlP3funKGh4YoVK+BUtmvXrorPmT1/W1tbTU1N6AtMTk4OIgiEc2XesmULIWTkyJEiMxe/AeDur7/+MjAwgNbEQOJ5EnaHmpoae2vD8ZKUlMTn8yG9giWs3MJIBK1lIfZKQQwlJiYmKiqqRYsWFakM0i8o4ieHb9++iWwcOPFmZ2fn5eXBiVpXV/f27dsCgYDd4ZrELSOu3Jdd7vel0gtQDlV3L6GkpKSiohIQEDBr1qwzZ87Am9SzZ8/mcpoSUe5KwrGSV+RmrwbPJwihSofBPoRqr5LuOKHHmfT0dPjT19eX/HuHJ6J79+7wgQb7CCEtWrQoddFv377l8Xi9evVi3zN17doV3uLk3om1OPF40MSJE9++fVtQUBAZGUk7h9q4ceOjR498fHxevnw5ePBgQsjr16/r168PTdvKgePGrD00NTU7dOjw6dOnkJCQUjNX+uYi/94XCoVCmkK3YWhoKH2qh+rUqlWrikT6xMEbdoSQoqIi+FB1dVLiICRWVlYQN2EY5tWrV1evXv369SshpLi4uHxLoffZ7E1ajjmI7BFoaBkaGkoTFRQUtLW14+LiaAc9VU3iwSVyZMELRG3bthXpOR7+rJRhf6SDGsWuTpVeHvEdVHESt62MjIy8vLxAIBA/cZV6gi3TcSTx0BAHXVYRSZehtm3bampqJiUleXt7c5mVCPpQWmpUYtCgQRDs6969u8Rxci9dujR9+nShUMhxpcqkY8eO/fr1c3V1XbVqlZOT0/Hjx2nfVZVi7ty5+/fvT0lJ6d+/v7W19YEDB3R1den8OVZmGP6Vyw0Ad7q6uqtXr4bPfn5+58+fh9+EuJwnxU+JFSxh5RZGXEhISEJCgomJicTtTAiB6E9VVwZKyomXEJKenk5/lYFuTAgh+fn5N2/ehGGpCIctU+7LLvf7UukFqLhKvJeQkZFZu3bt9u3br1696urq+s8//0ydOhW66C2rcleSilTykohcuWrwfIIQqnQY7EPotxcYGEhKaJsGffQSQiAWADnhflQKoVCYmJgoFAqHDh1avvuYMoEXBgkhCQkJNNjXvXv3Xr16vXv37uDBgxC9unbt2n9tnK8WLVp8+vQpLy+v1JzVs7k6d+6sra2dkJBw+fJlGuyLj48nhAwbNqxylyWimuskEAgE9vb2ly5dmjp16smTJ/fs2UNf2B5p1AABAABJREFUCawleDzeiBEjnJ2dL1++TB8AioqKUlNTTUxM6OFfG8TFxRFCdHR04NXpGlfbylN12CfY1q1bV8VxBFcWWIT4aNHNmzdPSkpix6O5MzExkZGRYRgmOTk5ISFBW1u7pJx0deizvTgZGZlSI30wufSILXwrEmq5f//+uHHjXF1dP3/+3LVr1wULFvzzzz/iTYbLR0NDw9XVdcSIEVFRUdeuXXv48OHOnTuXL18O0QoulTk6OhoaoJV6A1AOLi4u//zzj7Gx8dq1awMCAuBtyrKqrBJWSmEkgu0sKytb6kmjSitD+aSnpx8+fNjd3X316tU3btzo2bMn7Q2gJBW57HK/L61mFbyX+Ouvv7Kysg4fPhwbGztt2rQzZ85cuHChfJHcilSSqqvkpBacTxBClajKf0tHCFU1aDOfkpKSn58v8pWqqir0TAQvMsCNV3R0dKnzhCci2pdflYIRJ9kfAPSd5+Li4unpmZ+f//DhQxiJ4r8D3tmhr3xKVw2bq169erdv31ZXVz99+jS8/5Kfn79r165OnTodOHCgcpclrjrrJCEkIiKiY8eOp06dcnZ2Xr58ec0+p0lx+vRpExMTHx+fdevW8fl8QsjOnTvr169Ph6SoJap595WqtpWn6oicYKtixel7WxIvLvDCmshb1dznTAeLh562SlJZLYuhnEVFReLXUyo3N5cQInJOaNCgwevXr0+ePKmuri4UCs+dO9exY0f4LaRSmJiY+Pn5LVu2TFZWNjs7e+XKlRMmTIDmPFz2KQ27cLkB4C4vL2/atGkTJkzYtWvX4cOHK/IDQ8VLWImFkQi288+fP0ttRVXVlaGsXrx4YWBgEBMT4+LiMnbsWO6NW8t9uuB+X1r9KnIOlJGROXTokIuLi4GBASHE3d3dwsKCDnxRJuWrJFVdyUmNnk8QQpUOg30I/fZMTU3hAx0BgMrJyYEuwOHFBGg3FxoaKr2HER6PBzdhdAw4EdIfusoqKSmJEKKlpUWb9YHhw4dDB8///PPPo0ePunTpIt75S90GXahwDPZVz+bq2bPn9+/f1dTUDh061KdPn1mzZo0dO/bjx4/sTtyqQjXXyfz8/BEjRnh5eV26dEnii0i1h7a29rdv3ywtLd+8edOjRw94/g8JCeFYbaoNtH1wcXFhdy9IpaWlwVvS/9nyVB32CbaKjiN6DZL4rm5ycjKpwIv2tKe/HTt20Bfxqg59+V1KR66RkZFycnK0ySQhJCMjo6CgQEZGxtbWNiwsDN4HDAkJgbEgKg4KU79+/WPHjnl7e/fo0YMQcv/+feiGn0tlppfXoKCgSikSsLGxcXJy2rFjBwwWUREVL2ElFkYi2M6ZmZlv3ryRmOHly5ek6itDWQUGBo4dO1ZGRubs2bP0JV8uKnK64H5fWs0qslICgQDOZgMGDPDz8zt8+LCCgkJubu7MmTPL2m1fuStJVVdyUqPnE4RQpcNgH0K/vT59+sD7RNeuXRP5Kjw8nBDSunVraBxhbGxMCElJSaGjcIpg/h0mEiIFZ8+eFX/g+fz5Mwz+VVmgTzr6RhIb3PfcunXr0KFD1fYOLxQDGklRXN6lFVGRLlQIIQKBwM/PT1lZmfa2U6pq2FzZ2dmbNm1yd3f/9OmTu7v77du3bW1ty9QHVrk3S8XrJPdFP3/+PCgoiMfj0SeWMk1ezf7888/Nmzd///79y5cvd+/e3bt3b8OGDcs0h2pYNdh9hYWF+/fvF/mKYRhbW1spb2hKV77CV1152Og5jX0+kdJkrCqInGCr4tzeqlUreD68evWqyFdCofDnz59ycnIjRowox5wJIdbW1r169SKEREZGHj9+vKyT04saR/QRWsqmePTo0bBhw9i/cLi7u9ORlzU0NC5cuAC910sf6UVi9SCSLjcHDhygvRa2b9/+zZs3AwYMoPPnUpkbNWoETTthKBVxZd1QhJC0tLQrV64QQmjrS1KBk0kFS1i5hZFIR0cHXvzcuXOneGEOHz6cmZlJylIZqueacubMmYKCAiMjI3akr6RFi6SX+3TB/b603Kr/XiInJ2f37t3wWV5eftWqVZA5KiqKNnDjeANZvkpSDZWc1Nz5BCFUFTDYh1CtILGHIJooct8A6fRSqqenB1GeGzduiNxSODo6EkKOHz8ON3nW1tbQTGnr1q3w+yTw8vKCD3CrSgiB/r/S0tKgkyCa89mzZ1OnTmUPRQe/Z5a7T3qhUHj+/HljY+N169aJfztz5kxDQ8Pi4uLQ0NCxY8dyn6eUROkbk/zbxVVYWBi9EYSRbeEzdFNCSVx96LUK+j0hrFsxaJYiEAgk3gbBq2HUw4cP4+LiVq9eTccRLmlxVKmbi7aLYW+Ekn6RhswiRR09erSOjg40ISyrkjYLFEZkv9Ci0mzc6yT3RcPaiW8BuHEXCoW0pRKfz//06RO7YOxyihRe4ialE7I3qcQ9In0OIntk+/btbm5u3I8ONtq9Wkl1VeLBIg7SpR9ZFhYWMIrovn37jh8/TmeVnp4+bdq0li1b0teRTpw40adPn3379kl/WqCDaYoUnhZDYo2iK8i9PBJx3EG08ZeHhwdNvHr1KjwQipxMpJy4RNaFSDpxSSR+guV4HMGcOT5GysjInDx5khDi5+f35csX9ldPnz5NTU1duXIl/NQECgsL6f+lkpOTu3XrFkz+119/lfQoDm2FxNHzKscfbKytrWHQgAMHDvz8+VM8Q1hY2JEjR2xsbETS2SN7EkIWLVpEWO0EJYKxdMj/Vg8fHx8/Pz/yv9WDYZi7d++yJ5wzZw6dP8fKDLvez88PdhYFNwD06k8IKSwsXL58eb9+/R48eCCl/HT4ThgiAMArjXw+n105OZ4SuZewGgojEYxP6uHhMWfOHFq1+Hz+gQMH7ty5M3HiREgptTKU6SahHCde9orA5SwoKIj+zJCQkAD95bEvZxKLVO7LLvf7UlCmc0IN3ks4Ozuzz4r9+/dv06aNoqIijOZM/j3nv3//nm7/jIwMOGuJnPClVxKJFzjulVxiReJ4s1fp5xOEUE1iEEK1wKBBgwghnTp1YifS3/3u3LnDTod3H/r27UtT8vLy+vfvTwgZNWpUcXExJL59+7Z+/fqbNm1iT+vo6AhjaRkYGJw9e/bLly8XLlygY7a2bt16yZIlaWlpDMNMnToVEuXl5fv27Tt+/HhjY2NlZWUPDw86t+LiYghFTZ06tdR17NOnDyGkX79+eXl5kMLn821tbfX19X18fEqaysHBgRBia2tb6vypCm7MyMhI6FDGxMTk3Llz+/bts7CwOHbsGEw+YsSI27dvS199uAeqX7/+o0ePHjx4sGDBAkiHX24JIVFRUTQzPNcRQvr375+cnAyJQUFBOjo6Q4YMyc7Opjm5bG3pm8vFxQWWdeXKFZoYHBwMiYcOHaKJBQUFampqhJA5c+bQRPro27Rp006dOg0YMGDs2LEzZsxYs2bN7du34bZSiocPH8Lk//zzj7u7+7Rp09LT0xmGMTQ0JIT06tWLnRleTyOEsOsblzpZpkX37duXENK2bVuRwkNcjxDSpk2bc+fO7dmzp0ePHvAgp6amdu7cucOHD0NOOHb09PTYk589exYm//79O02EsbAJIQ4ODjRR4h4RCoWwo0eNGsWeLQy90qpVK3Ziq1atCCENGza0tLTs27fvqFGjpk2bZmtre/r06czMTOmbJTk5mf4M8OnTpxUrVsCWzM7OhkNg3rx57Py0UUNOTg5NzM3NhaadS5cuZWdevnw5IURRUbGgoABSvn37Bt1QwlqMGzdu0KBBqqqq48ePp2etxMREOtifo6Oj9PLDKDGmpqYfPnw4fvz4kSNHGIa5ceMGTP769Wv2JoUNNXDgQJrIpTwScd9BAoEAIuMNGzY8cODAiRMnhg0btnbtWngHytjY+NixY/CQVlhYCIVZt24de57QF6eMjAw9OTAMQxt5bd++nSZyP8FyOY62b98ufrKSbsOGDYSQ1q1bJyQkQEpcXJyRkdHgwYPZ5zGGYWCca319faFQyHHmmZmZ48ePJ4TIysouXLgwMDCQ/W1QUBCsvpqa2qNHj9hf0bGD5s6dy3FZ79+/h7Nf06ZNHz9+nJKSAunJyclHjx5VV1ffvHmzyCTOzs48Hs/NzY2mvH79mhBy//596ctavHgxrNTKlSsvXLiwYMGCgQMHwg5q0KDBnj17YPuvXLlSR0eHbliGYbZu3aqurh4ZGQl/cqnMBQUFtKmyra3t8+fP3dzcpk+fTocXmDBhwo0bNxiGoWEIeXn5iIiIkgqfn58PjYhVVFT2799/4sSJwYMHL1iwAKbdtm3b1q1bi4qKGIaZNm0aIURDQwP+BDB6MiHk58+fZS1hNRRGouzsbH19fcjZsGHD4cOHjxw5UldXV19fPzExEfJwqQxlukmgrb2ePHnCLgyMjmpiYsJOpG+n0gLQcaUGDRp06dKltWvXDhw4sEuXLnDydHBwcHJyYkq+Spb7ssv9vpQp4zmhpu4lMjIyCCF///03TUlPT1dTU1u5ciVNcXJygjmPHj360qVLmzZtMjMz27p1KySuXLny48ePDLdKIn6B417JJVYk7jd7lXs+QQjVIAz2IVTD3N3dFyxYQF/nmT9//osXL4qKirZv305/4tPR0fnrr79SU1NdXFwmTZoEibKysnPmzKG3JgKB4MCBAzo6Oq1bt7axsRk7dmz79u2fP38uvkQXFxfaL7KsrOzgwYMDAgIIId27dz9x4kRSUhKdIfT0T/41bNgwX19fOp8jR4707t0bvpKTk5s/f/7jx4+lrOmvX79mz56tpqamrKzcu3fvcePGtW3bdtGiRezAgTj4Ufrr16/VuTFv3LgBdz+EEENDQw8Pj5iYGB6P17t372PHjsXGxkpf/Z8/f9JmQUOGDElLS3Nzc7O1taWvfZmbm+/YsQPumWiw7++//27RosXIkSMHDhzYsGHDjRs3siMOHLd2SZsrIiJi06ZNtFSamprr16//+fOno6OjpaUlTdy0aVNGRsarV68gCkYIUVRUXLp0aXx8PMxn48aNJXX607Nnz8LCQil7p7i4GH4uJoS0bdvWy8vr3bt3Y8aMoXOYNGmSh4dHdnb2+vXrGzVqBIl6enpnzpyBOZRaJ7kv+vHjx8OHD6fz6du3765du9jl37p1K0SdeDzexIkTY2JiAgMDYaTOZs2aQXd+tP4QQkaOHHn58mWYkHbzZ25ubmdnxzDMmTNnaB83Ojo6O3bsKGmPeHh40G0iJye3ePFiX1/fsLCw2bNn04o9bty4T58+QTlv3rxJh0cQoaWlJeVBna4mZG7QoMH169eLiop27twJz5CEEAUFhT/++MPd3T0iImLjxo107MIBAwacO3eOYRh7e3taLRUVFZctW+bp6RkWFrZkyRJa2/v373/9+nVYXGBgYKdOneiKaGhoHDlyhP3ILRAI6AtK9AG4JE+fPoU9Iicnt2bNGoFA8Oeff9LXb5s3b75161aGYZ48eTJkyBBIlJGRmT59Og2RlFoecWXdQV5eXjQ6oK6ufuHCBYZh2rZta2RktHXrVm9vb4ZhLl++DD9RwI5Yv359YGBgRETE6tWraW3v2LEjRDPPnTsHD+qEkIYNG65fv/7Xr19MWU6w0o8jf3//OXPmQLSXEKKvr7958+awsDDp+4LuESMjIw0NDWtr69mzZxsaGh48eJD99H7o0CH2+7wDBgzYuXOn9PAK2/v378eNGwdPoc2aNevevXvv3r319PQUFRV79ux59OhRdkh07969nTt3Zh8RvXv3Zj+WSxEXF2dtbU13aPPmzXV0dHg8nqWl5YMHD8TzOzs76+vrt2zZ0sbG5vz583///behoeGlS5dKXVBaWhqtnLKysqtXr87Ly1uwYIGWltbixYtdXFzgKrBq1Spzc/O2bdtu2bLl4sWLS5cuNTc3h6gBxaUy5+TkzJs3jw5Y3Lhx4/Pnz69cubJRo0aLFi1yd3eHnRUWFkYP9qtXr0op/4MHD+i10srK6uPHj0VFRfC0r6ioeO3atejo6CVLltCRi0eOHPno0aPCwsL169fTaIKVlRU9RXAsYfUURqLMzMypU6fSmcjLyy9dupRd8bhUBo43CTAEFsSwCCEGBgY7d+7MyclxdXWdOXMmrdgTJ0588OBBYWHh5s2b6WwNDAz27NlTXFycn59PR2tVVlbetGlTfn4+DYH17t07NTWVkXSVhKKW+7LLcLsvLcc5oabuJTIyMrS1tfX19UePHn3ixImjR49aWlquWbNG5Chbu3Yt/clq2LBhUVFRV69ehS5Zbty4Ab98cKkk4hc4hkMlL+lus6w3e5V4PkEI1SAZBl+qR6huiY+PDwkJ0dbWpjeIJWX7+fOnqampqqqqQCCIjo6Gli8iGIYJCQlJT09v164dvfuvCHjJNDExUVVV1cjISEVFRXr+u3fv7tixQ7yP56qWm5vr5+enoaHRpk0bGRmZoqKitLQ07t145ebment7a2hoSN8LhBB/f38TExNCiJ+fn56eXkBAAJ/Pb9++ffmGf63qzXX06NGCgoLRo0fHxMSkp6fn5OTk5OTk5uaGhYXduXPn6NGj8HJZSQQCgbe3t1AotLS0LFNPf2zlq5PlWHRcXFxYWFjbtm3pGzrR0dHx8fEWFhZl6ua8Srm6ujo4OGzfvj0qKiolJYXukbi4uIcPH/bu3Rsae0oBb6xbWFjQx92qlpmZ6efnp6mpqaenJ3Ff/Pr1y87OTl1dfdu2bdJnlZSUFBQUZGBgUJEu9kotTwUVFxcHBgYWFxebmJhAjCAqKqpFixaVviBSlhNspZ/bqaysLF9fXxUVlfbt29OYSCXi8/leXl4JCQnp6enq6upNmzY1NTWtikOyoKAgIiIiLCwsOztbU1OzXbt2Jb3cXVBQoKCgwOPx4uPjw8LC6tevb2xszL1IkZGRCQkJcDkmhMTGxkJskWbIzc1VUVFhGCY8PDwmJqZZs2ZwYRKfFZfKXFRU5Ovrq6amBmHoqKgoHR0dkT3F5/NjY2OnTJly4MABaDVZEtjdjRo1ooMCQRlMTU1pVKKsuJSw2gojUWFhoZ+fn5ycnKGhIY2MA46VgftNQqUICgpKTk62sLCgtxa+vr6ysrJGRka0Ikm5SlbwdMHxvpS7GrmXYBimoKBASUkpKysrKCgITukS61VycnJISEjbtm2hY7vU1FQlJSV2L58cK4nEC1y1VXJSeecThFBNwWAfQqhWGzBgwLRp0+h7CnUPO9hXvr7w2Kp0c23atMnV1fXjx48SHzLv3r3r5+dXanQGVaJHjx7NmjUrNDRUYuO+lJSUyZMnlzRwZG0mFAoHDBhw6dIlib9AIISqQWJi4rhx4969e1cVEXCEEEIIVTUcoAMhVHu9e/fO29vb2tq6pgvye6jSzfX+/ft9+/Z17dpVYqSPEBISEtKjR4+qWDSSKDs7e/bs2S1btizpNd7Q0NDu3btXc6kqrri4eNGiRTNmzMBIH0I1JSUlZfr06SdOnMBIH0IIIfSbkqvpAiCE0P+4devW5cuXzczM9PT0tm/fvm3bNiUlpZouVBWiI51xHPhSRLVtrpCQEELIzZs3Z86cCX1pU3l5eefPn4+Jidm0aVNVLBpJlJKSkp6enpGR4ejoyO6/iRAiFAqfP39+8uRJOor0b+TChQujR48eNWpUTRcEof+u/fv3Hz9+nD2GMkIIIYR+L/gaL0KodjExMfH394fPvXr1cnNzY3ddVPc8fvwY4hp3796F4SbLpNo2V0ZGRu/evX19fQkhVlZWZmZmDRs2zMjIiI2NDQ4OXrly5ZIlS+Tk8AekajVz5syrV68SQlq1atWjR48mTZrk5+cnJCR4enqOHTv2r7/+0tDQqOkyov+wEloBc4J3pwghhBBCFYDBPoRQ7XLz5s2dO3fGxMSMHTv25MmTpY7g8fvKzs7esmWLk5NTUlISIaRx48aTJk0aPHjw2LFjuc+kOjdXTk7O2bNnnZycQkNDMzMz1dXVzczMRo4caWNjU21jOyARTk5O9vb23t7eycnJSkpK+vr6/fv3X7lyZRWN/4BQGWCwDyGEEEKohmCwDyGEUJkJhcK63eLyt4N7BNU6NNhX8q1mYmIiDFjJfRKEEEIIIVQqfDBACCFUZhhXqm1wj6Dfwr59+0z/ZWxsfPfu3ZouEUIIIYRQHYQt+xBCCCGEUGUTa6aXk5PTuXPnNm3awJ/KysoXLlyoX7++lEkQQgghhFA5YLAPIYQQQghVNrHI3ZEjR7S0tKZPn859EoQQQgghVA741g9CCCGEEKpafD7fzs7Oy8vr8ePH+fn5NV0chBBCCKG6DFv2IfR72L1796NHj44cOdKtW7eqWwrDMD4+Ps+fP3/48KGrq6uCgkLVLatmlW97hoaGzps3z9zc/MSJE1VXtlIlJiYePHjQz88vKyurS5cuPXr0yM3NvXHjxtmzZ1u2bFnNhamemln9Hj16tGPHDhsbm/nz59d0WWq1WnJQlOTDhw9nz579/v27QCDo1q3brl27dHV1a7ZIxcXF7969u3fvXuPGjbdt21bxuXl4eLx58yY2NjYjI0NDQ0NPT69v375WVlby8vIHDhwYNWqUkZERIcTd3T0jI0Nkch6P17BhQ11dXT09PXZ6TEzM9+/fxRdnbGysr69P//z27VtsbCw7Q+PGjXv06PF/f/xvM72bN29OnToVEho2bHjw4MF58+bJiIzYiy37EEIIIYQqBYMQqvWKiooUFRUJIbNnz666pbx7927cuHFycnJwcigoKKi6ZdWscm/PHTt2wMaJjIyskpJxEBQU1KpVqytXrjAMc+zYMfb5PCwsrJoLUz01s0b069ePENKyZcuaLkhtVxsOipI8ePCgY8eO2dnZCQkJEMwaPHhwzRbJw8OjV69esMUWL15ckVnx+fyTJ09qa2sTQgwNDVevXn306NGtW7eOGTNGRUVFSUkJQv+vX7+G/J8+fdq5c6eysjIsXUNDo3///u3bt2/QoAEhpEmTJgsWLPD394fMSUlJ9+7dGzNmDD299OnT58aNG9HR0ewy+Pr6Hj58uGHDhoQQFRWVHTt2fPny5f9/Tcj//WMYhmEEAkFQUNCtW7cmTpwoKytLCNm2bZvoWv3vJAghhBBCqHzwdgqh38PWrVs7dOjw7t27ql7Q2bNn63ywjynv9gwICOjcufMff/xRRaXion///pqamvTPf/75p0OHDjUV7GOqsWZWs7t375qZmZ0+fbqmC1Lb1YaDQqLY2Fh5efndu3fDn76+voaGhkeOHKnRQv0fMzOzCgb70tLSBgwYQAhRUFA4fPiwUChkf/vz58/hw4fDaeHRo0fsr2hL1XPnzkGKQCB48OCBubk5zO3ChQs0c15eXr169SB/UlJSSYW5dOkSIeTo0aOiX5Qcufv27Zuurq6cnFxQUBDHSRBCCCGEEHd4O4UQ+h9Pnz79LwT7flOenp6EkCFDhrATvby8qifY9+DBAw8PjypdRF3C5/P//PPPmi5FTdq4caNAIKippe/cuZMd0qpVIE5X7mBfQUFBp06dCCENGzb09PSUmEcoFI4dO5YQcuvWLXY6fXHYycmJnZ6YmAgvOCsoKAQHB9P0pk2bEkLq1asnpTyhoaGEkE+fPol+ITVy9+zZM0LIpUuXuE+CEEIIIYQ4wgE6EEL/g8fD00Lt5erqSghRV1ev/kVHRETMmTOnoKCg+hf9m1qzZs3bt29ruhQ15t69e/v372dqruc1FxcXUltPaKId1ZXR2rVrv379Sgg5evSohYVFSYu4fPly48aN8/Ly2Ony8vIS82tqai5YsIAQUlRUdPHiRZH8tHsHieCMpKqqWpaVIEOHDlVVVU1ISCjTVAghhBBCiIvaeBOMEEJIol+/fhFC6It11SYnJ2fcuHHp6enVvNzfl6Ojo0iPiv8pAQEBc+bMqcFIHyEkPj6+BpdedSIjI6GzhRYtWlhbW0vJqaamtnLlSpFgnxQmJibwISAgoExF4hIQlEhNTY093AdCCCGEEKosZb4zQwhVkbt377q6uvL5/JYtW1pZWSkpKfXu3Zt+m5+f//jx49jY2JUrV9LErKysBw8exMXFbdiwIT09/dSpU9+/f2/VqtUff/wBwy+KiI6Ovn79upeXV6NGjczNzfv379+2bVvuJczPz7927Zqnp2d6enqHDh3GjRvXpk0b7tM6OzunpqYuXbo0MDDQ3t4+IiLC2Nh41KhRnTt3ZufMzMy8ceNG8+bNR4wYcfPmzXv37nXv3n3FihU0w7dv354+ferv79+0adOuXbtOmjRJvJlMYWHhrVu3YKTI1q1bT5gwoWvXriLlEd+ehJC4uLjz589HR0c3aNDA2NhYU1Ozb9++9evXpxmCgoJu3Lgxa9Ys8XX/+vWrh4eHp6dnvXr1LC0tBw4cKP4c++PHj5s3b86bN8/Q0PD169dXr14tLCzs06fPH3/8Ib0J0vPnz/39/T98+EAI8ff3P3ToEP1KZDTMcpSKEPLt27d79+5FRUUpKiqam5tPmzatSZMm8FV8fPyECRN8fHwIIbdu3YK3hpcuXQpDc0jcksXFxa9fv37w4MGpU6cEAsHly5efP3/eoEGDCRMmDB06VHzpFa+ZHGsX+Pnz56tXrzw9PTMyMiwtLbt37/7/xw/9F8Mw7969c3Z2Pnz4MDudyx68cOGCjY0NISQmJgb2VIcOHWDEDykSEhLu3r3748ePgoKCdu3azZo1S2RsZaFQ+PDhQ3d39/j4eC0trQ4dOlhbW4tEWAICAi5fvrx8+XJNTc2rV6++fv1aVla2Z8+e06dPV1FRKevcgI+Pz9OnT728vFRUVDp06DBr1iz2EUHEDopPnz6NGzcuOzubEHLo0CFZWdkmTZpMmDDhzJkz7KmsrKz69u1LCMnPzz916hQkGhsbDxs2TPqGkr770tLSoAu5pKQkQsiLFy9gCNomTZrMmjVL+pxLPbdw32gvXrx49+5dUFCQtrZ23759x40bB0NSiAsICIAa27Jly9mzZ5fUUo86d+4cn88nhEydOrXUdotr1qwpLCyUnofKzc2FDzDgRlVLSkrKy8vr2bNnNSwLIYQQQug/p4ZfI0YIMQzDMBMnThw1alRaWppQKHzz5k3jxo1nzZoFX3l4eNCn65EjR0JiSEjIpEmTFBQUCCFDhw79+PFjs2bNDA0NIZuqqqqPj4/IIm7evNmoUaNLly5FR0fb29vDtBoaGubm5hYWFt++fYNsz58/h5ODSJ99r1+/7tKly6FDhx48ePD3338rKyurqqreuHGj1FX7/v27tbU1FGzq1KmnTp2Sl5eHOBEhRFZWlnaZ/+DBg3HjxkGzNTs7u927d9MzFfQhlZeXt2jRosmTJzs5OV2/fh2CoYMHD05JSWEv8c2bN4aGhuvXr//8+bOnpyeE+ebOnQvdh0ncnuDjx48aGhpPnjxhGCYzM3P27NmEkIiICIZhcnJyDh48aGlpCeWhmwtkZ2cvWrSoZcuWzs7O0dHRt2/fNjMzU1BQ2L17d3FxMeQ5fvx469atYfJ3794tWrSoYcOGNKRV6mi2L1++tLOzMzY2JoR07tzZjmXdunUwE5E++7iUimEYPp8/bdo0HR2dN2/e8Pn8oKCgtm3btmrVinac7+7uvnfvXliEjY0NLLSwsFDiliwqKrK1tYXX+urVq5eQkNC9e/cWLVrAmKEyMjL29vYiq8axZkrEvXYBoVB4/PjxRo0anThxIiIi4s2bN+PHjyeETJ8+PTU1FfJERESsXbu2WbNmcCjRaTnuwYKCAjs7uz59+hBCWrduDZvL3d1d+v49ceJEixYtTp48GRAQ8ObNmwYNGsjJybH7m4uPjzc1Ne3bt6+vr69AIHj+/LmCgsLAgQNzcnIgw+7du2mcyM3NzcrKSk5OjgaDLCwsoqKiuM8N5OTkLF261NTU9Pbt26GhoRCt09DQ+Pr1K1PyQXHt2jU6CsShQ4fs7OyuXbvGMIy/vz9sVUIIzIFKTU1t2rRpv379MjIypGwlLrsvLS0NtjkMMjthwgT4E8pQEi7nFo4bLSIiYuDAgWPHjn358mVwcPDChQsJIR06dGDPauDAgYSQxYsXnz9/vl69eqamphBfq1ev3vv376WUk2EYupcfPHggPadEu3btgslF+uxjGAYi1IQQR0dHmtiqVSuRA0EchFNFh9pg/qcDvuLi4lWrVtnb28N5WCgUzp8/X7TDPgb77EMIIYQQqhx4O4VQzbt37x4h5MePHzTl0qVLU6dOhc/R0dE+Pj7QZIYdnCoqKho0aBAhpFmzZqNHj46OjmYYJi0trV27doSQKVOmsBcRFBSkpKRE58kwDG2vdOvWLRcXl/T0dEiXGOz7/PmzSADx6NGjhBB5efmQkBDpa5eZmRkcHAxPjNra2jNmzIiIiBAIBF++fIGiEkLevn3LMExkZOSjR48gZejQoTt37gwKChoxYoSRkVFWVhbDMOPGjZs8eTKdc15eHnQez46UBQQEKCkpsSNKL168gHneuXNHyvZkGMbMzGz06NH0z8LCQn19fXiCzcvLi4iIgC7/xYN9/fr1q1evHjuYkpSUZGhoSAhZtmwZTXRzc4PJzc3NL1++DA+9p0+fhihYQECA9C3JMMzIkSMJIfCCJFXSAB0cS3XkyBFCyMyZM2nK8ePHCSHW1tY0BR7mCSEuLi40UcqW3L59OyFETk5uwIABHz58YBimuLh4+vTphBBdXV32oA3ca6ZE3GsXu2C3b9+mKQKBYOLEiYQQS0tLPp/PMExKSkpYWNiYMWPEYxzc9yC0c+zZs6eUwlMODg716tXz9/enKZs2bSKE8Hg8OgQqlOfixYs0z4QJEwgh58+fhz+jo6NpTNbMzOzmzZuFhYUpKSm00vbv359OW+rcwJw5c0xMTDIzM2lKt27dCCEmJiaM1IPC2dkZEtlhZYZhaGdwIsNKZGdnKysri0QAxXHZfRQEFk+cOCF9noDLuYXLRsvLyzMyMpo9ezYdHjczMxN+wFi6dCnNBsE+AwODGTNmQBAwMzMTxsMdNmyYlHIKBALaQpA9jAZ3JQX7Pn78CE0UO3ToUFhYSNMrK9iXn58P5worK6tt27YtW7ZM8mg/GOxDCCGEEKoMeDuFUM1bv349+d8xEwsLC+fOncvOA30ziYRUIKCgr6/Pfsrdv38/IURPT4+dc/ny5YSQXbt20ZTU1FRoQnX16lV2TonBvi5duoiMAJuSkgLZFi1axGUdIUrVtWtXdmJcXBy8WtivXz+aqKGhQQgZPny4yBxgmOCPHz+yE5ctWwYxkZiYGIZhioqKLC0tzc3N6ZM2wzDFxcXQF9Xdu3dpovj2zM3NlZGRad++PXv++/fv9/b2pn/CoJMicY0rV64QQv744w+RAt+6dQvKRgeppCGzy5cv02wCgQA6tr9y5YqkLfc/uAf7uJcKWh6xg333798nhBgbG9MUicE+ILFm0ljPr1+/aOLnz58hkR1/5F4zpeBYu0JCQhQVFQ0MDNjVA3JCmOPAgQM08fz58+IxDu57kHuwLyIion79+itWrGAn/vz5s169eoqKirGxsZBiYGAgEmmCd9ttbW1pyrdv36B4z58/Z89t9erVkE4bGHKZ2507dwgh9+/fZ8/KycmJEGJqakpTJB4UJQX7CgsLYchX9oIYhnF0dGTPU6Iy7T6mLME+LucWhttGW7p0ab169eLi4tizWrx4MfnfCDsE+4YOHcpeF2g42ahRIylFTU5OJv9KTk6WmOfjx49//PFHp06dOv5r8ODBtBU2DfbZ2dnFxcWFhIS8evVqyZIlcNB16dKFri+orGAfwzBCoTA8PPzz58/w441kGOxDCCGEEKoM2GcfQjUPOkezsbHR1NSEt/8UFBQ2b97MziNxCEV4xIU3/mgiNEiJiYlh5/zx4wchJCsri6Y0atTIxMTkx48fIjnF+fv7f/782crKatWqVex0ZWXlvLw8Dw8PLusIJYTnRkpHR8fa2vrcuXNv377l8/mwjvD/gAEDROZgb29PCLl69erNmzfZZSOECIXCjx8/Tpw48caNG56enps3b2b3tCUrK/vhw4fo6Gh4BxaIb08lJSUVFRV/f/85c+acOnVKWVmZEDJ//nz2tpW4F2inbCLpY8eO1dTUTEpKOnXqVJcuXQirA3sIaAIej6etrR0WFlbqjigT7qXatm2boaHhlClTaB6GYQghaWlpXBYkpWaS/11TqJmEkJiYmObNm8PnitRMkcWVWrtOnjxZWFhoaWkp0hGbjo7OyJEjnZ2dT5w4Qd+J5r5eFdyDO3fuzM7Ohngl1bJly5CQEDk5OR0dHUi5du3a58+foXUkEN9NtHgim2LVqlVHjx4VCASvX7+GF1S5zG39+vU8Hk+kB70pU6a0b99eT0+PppQ0uqtECgoKK1euXL9+/aVLl3bs2NG4cWNIv379+ty5c6VPW6bdVyZczi2Ew0aLioo6ceJEjx496F6jJV+4cCE03GNr1aoVe13gJfG0tLT8/HwlJSWJRRUIBPRzSSNvdO3atWvXrrdv3548eTIhRFdX982bNyLdLBJC1q1bB6d0dXX1xo0bT5gwYdq0acOGDSvHOBscycjI6OnpsSsPQgghhBCqIhjsQ6jmzZkzZ9++fampqf369Zs1a9b+/fu1tLS4j30hAh7VhEIhOxEeyGkbHNCyZcsfP36UuqC3b98SQgwMDETGuIA/6aN+dHS0yEJlZWVpF10lGTJkyLlz5wQCQWRkJDScodOKF0NeXr5Xr14iZYCGaWZmZoSQly9fEkLE16h+/frsSJ9EMjIya9as2bFjx+XLl9+8eXP48OGJEyeyYzoSFRcXBwYGkn8jtmzy8vIdO3Z8+vQpDG0hhcRdVhFlKpWuri5t+eXt7X3+/HlPT0+YSWWVB9AgAntNOdbM5ORk8dBG06ZNpQcmRGqXr68vkbRNCCHdu3d3dnaOiorKyMiADgfLpCJ7sKRKS+OhAJpoEUIYhnnx4sW1a9e+fPlCuO2mZs2aGRsb+/r60u1c6tzCwsIiIiJatGhBO0Ck6Jit5bNo0aLdu3dnZmaeOnXqr7/+IoQkJye7ubk5ODhIn7Dqdh+XcwvhsNFK2pU8Ho/2bCgF3dRFRUUlBfsaNWpEP4eGhrZo0aKkuUEnD4QQc3Nz8UgfIcTR0XHy5MlCobCkkUMQQgghhNDvC4N9CNW8xo0bv3nzZvjw4bGxsZcvX75///7evXttbW0rcRGjRo1ydXV9+vRpcnIyfVpOTExUU1MTH4dURFxcHCFEV1eX3fhLHPRAz05p1qxZdHS09JnTiEZJrVQAn8+Hd9ZGjBgBr0xKFBUVRQgpKCiQvtCSbNu2LT09/dixY9HR0ZMmTRowYMCFCxdEGkmJiIiIKCoqIoQkJiaKfwtrJxLJqgblKNXLly//+ecfMzOzjRs3fv/+fezYsdVSUq4109bWFt4qZQsLC5MeqhapXRAAlbJNYJ4Q0KkexcXFcHxxqbTFxcX29vYODg7W1tZnzpzZsmVLSEgIxwU1b97c19eXfZRJn1sFDyUp1NTUFi9evH//fmiIp6SkdPv27SFDhmhqakqfsIp2H8dzC6ipjUbJy8sbGRnBpggNDRVvAU3RoKd4uJaSkZEpNdIHk0sPZEN7wzK18UQIIYQQQlUKg30I1QpmZmb+/v4bN248d+5cVlbWkiVLPDw8rl69SgfTrKBly5Z9/Pjx9u3bU6dOvXfvXoMGDZ48efLjx4+rV6+KvHEmDsoQFhYmPdvUqVNzcnLYKTC+pHTwdC0jI8Nu1idORkaGx+MJBIKwsDA6GGVJRS13cE1GRubo0aMjRoywtbUNDw9//fq1ubn5y5cv4XVXieh7iBJf4YToFR3CtdqUqVS5ubnz5s17+fLlmzdvoP3R9+/fq6ukXGtm3759xQMxEtsrsYnUrsaNG8fHx0vZJqQmdpasrGxxcXFoaKiRkZGUbGFhYRMmTJCXl3/+/Dndv9zBpqCDlpQ6NziUkpKSMjMzYVjbSrRy5Uo7O7vk5OTLly8vXrz42rVr0G+pdFW0+zieWwjnjVbVwf1BgwZBsO/r16/QG2CVat26dXBwcF5eXlFREfTrJy43N5dwOB4RQgghhFC1qZw4AkKoIhISEgghDRo0OH36tKenJ4SWbty4cePGjcpahJyc3K1btxYuXBgTE9OvX7/x48c/fPjQ19cXRpOUrm3btoSQly9fwhOdiMTERG9vb0LI8ePHL/0vOqyqFPDo3rJlS+gjT0r5W7ZsSQiBkYvFwZC7EMt48uQJu2crSnozQ4FAAJ3fDx48OCAgYP/+/fLy8llZWTByRUlTNWrUCAYcgI0gAmZIXwOsNmUq1aJFi27durVz504ubxpWOo41c8mSJZfElNoWTKR2mZqaEkICAwOh2SMbbBNdXd1SX9yuXHJyctA48eHDhxIzQKXNz88fMWKEj4+Pg4NDOSJ95N9NAfFELnOjYUE6QDZbfn5+ampqOYoBYNxkQsihQ4ciIiLCw8OHDx9e6lRVtPs4nlu4bzQ/P7/IyEjxb+Pj4yWel8pq2bJl0BzP0dGxGloN0y722GODiPj586eamprEN6wRQgghhFCNwGAfQjXv4MGD6enp8NnMzMzNzQ060ec49gVHTk5OTZo0CQgI+PHjx717986ePQtRvFLRAMGBAwdEvhIIBDY2NqW2DZQColGzZs3iWIzTp08nJSWJfOXm5ubm5kYIgXBVSEgI9LjPFhAQcOzYMSnzz8nJ2bNnD3xWUFBYv349jCgaGhoqvkQ26Bvr5cuX4g/DERERhJBqeyW2HKVKS0u7du0aIYQ9eoCUbuAqvSM/UoGaWSqR2gXbJCMjA0ZfZauiPcVlc0GlvXz5MjTXYrt8+fLHjx8JIc+ePYPxOtq3b1+mmYOioqKgoCB1dfVRo0ZxnJu2tra2tjYhZPv27fn5+SIzXLVqlcTQv7iSCrlu3TpZWdmwsLCZM2daW1tzeQO06nYfl3MLl40GDQP5fP6WLVtE5pObm7tq1SqRoUXKp23btjAOCZ/P37hxY1knl/LThUT0tWg6ULu4J0+ejBs3rqwlQQghhBBCVQeDfQjVPKFQeP/+ffpnvXr1Zs6cSf73lTRozMLn89kTSmwnAjlFnuhevHgxd+7cNWvWlNpDE50n7aSpY8eO0HX97t27T58+TeeckpIyfvx4CwuLUhtYUSIBAoZhzp07p6mpuXbtWnaixFWDQSRSUlJGjBjBfpXv/v378+bNgznMnj0beu+ytbU9ceIEncnbt28nT57MflVQ4vZ88OAB++l9+PDhTZs2VVVVpQ15aJMi9oR79+5VU1MrLi52dHRkzy02NtbV1XXAgAGTJk2ClJLa9UjcZRJBn2siOemeYqdzLBXdkjAoB4AoM7v9lJqaGoxBAR3MEVaYo6w1U6Sc3GtmqUqtXbNmzYJms+Kx4KtXrzZu3HjXrl0ipeWyXkTSHoSBFMQ3l7g///yTx+MVFxf36dPH1dUVEoVC4ZkzZxwdHdm7qbi4GAapgCXC6BDizdzEN8Xt27fT0tI2bdoEpeI4t61btxJCwsPDe/fu/fPnT0jMzs5etGiRpqYmHRpC4kFBx5EoaQsYGhrCsLYfPnyYM2dOSRuHrUy7jxBSWFhI/5eOy7mFy0Zr164d7C8YXJh2a/Dr16/Bgwf/8ccftFsG2FYitYvOp9Qw7q5du2CA4Hv37u3bt09inpJWnNYN6d2kUrNmzYIhWfbt2yexcbSfn9/Zs2dhMBOEEEIIIVRbMAihmrZy5UpdXd2EhASasm7dOg0NjejoaJoCQ98aGRmxJ5w8eTIhRFNTs7i4mCbCAJeEkJSUFJoIL83Vq1fPxMSkV69ew4cPnzx58oIFCw4dOvTr1y/2PI8fPw6Th4WF0cTPnz+rqKhAup6e3rhx4wYMGKCsrDx9+nShUMhlHaHRjaKi4u3bt2mira2tmpraixcvaEpubi70B79o0SLxmcDzLSFEQUEBXvk0NDSsX7/+169faR5nZ2fasZSKikqPHj2MjIwUFBTc3d3ZsxLfnhkZGYSQnTt30pTk5GQlJaWNGzfSFNq25ebNm+y53bx5U1lZWUlJ6cOHD5BSUFAwevRoIyOjiIgImi0gIAAmP3bsGE3MycmBkTeXLl0qfRsKBAIYLWTgwIHsdPr64cePH8taqvz8fOjIX1VV9cCBA8ePHx80aND8+fNhhnv37t28eTPkhFZLlpaWHz58sLOzO3HiRElbkmGYU6dOwRx8fHxo4ps3byDxzp07NJF7zZSCY+1iGMbPzw+iVEeOHKGJe/bsUVdXf/bsGTsnbTOVmJhIE7nvQWgWSgixs7Nzc3ObOnVqVlZWSeXfsGEDvShramr27t1bV1e3ZcuW8fHxkAHa9xFC9PX1z507t3v37p49e0JbKg0NjQsXLkB5vLy8INugQYNSU1NhWl9f3yZNmowdOzY/P79Mc+Pz+ewxUtq2bdujRw81NbURI0awTzgSD4qkpCRorDdr1qxPnz7Z2tp++fJFZK2DgoIIIVZWViVtFnHcd19ycjKEjyWeScSVem7huNEiIiJoS2c5OTlLS8uOHTvKycnt27ePvTh9fX1CSL9+/diJp0+fhgm9vLxKLbBQKPzrr79gIw8ePNjDw0MgENBvExISIIQqLy9/6NAh9oSDBw+GpSxcuJDLlmEY5u3bt9DnY4sWLZ4+fUqrVkJCwoEDB+rXr3/gwAHJUxLyf/+4+3eSqKioMkyFEEIIIYT+Fwb7EKp5q1atMjc3b9u27datWy9evGhjY2NpaUkfMi9dusTuv2zMmDHXrl2Ljo62tbWl776NGTPm2bNnubm5a9eupZ3fdevW7e7duzATT09PY2NjIomSktKrV68YhvH29l66dKmamhqkm5qa7tixg0Yo/P39rays6GtoTZo0OX78OJ/P57iOEI4ZOHDgmDFjTE1NJ0+e3KpVqw4dOgQGBtI8hw8fpi+TysnJzZw509HRkT0TgUCwfft29kANo0aNYs8BfP78mf2qXffu3b29vem3ErcnwzAZGRk6Ojpt27YdO3bsyZMn7ezszM3NN2zYAOsYERGxadOmZs2awVTa2tobN26MiYmhsw0MDOzTp0/9+vWHDx9ua2trZma2ePHi3NxcmsHR0ZF2/6+trb158+bs7Oxnz5717NmT7oiVK1eyQ7Rsz549oy0ECSETJ058/vx5cXHxli1baHDBxMRE5MG+1FIxDOPs7Ex71u/cufPXr1/z8/OhFzMlJSVahR4/fgxxWDk5uY0bNwqFwpK25NatW2ljT0tLy5MnTzIMc+LECTqucfPmzWnsg0vNLBWX2kVlZGTMmjVLTU2tc+fOy5cv79Wr14ABA37+/EkzuLm52draQvyOENKhQwcIAZdpD0JLPUg3NDT08/OTvgr29vZ0o/F4vNmzZyclJbEzbNmyBY4+Ho83ZcqUuLg4b29viGu3atUqICCAYQX7du7c2bx581GjRg0YMKBRo0bbtm0TCcpzmRvDMEVFRStWrKCBfhUVlT179hQVFcG30g+KzZs3Q7q6ujo7CMvWtGlTGjXmqNTdl5ycvG3bNujgjxAiLy8/a9as/fv3S58tl3MLx40WHx8/dOhQaAlLCGnRogU9iBiGeffu3ejRo+lSpk6d+unTp4yMjLVr19Lxcw0MDBwcHLhsjYiIiEWLFsEL1xoaGp06derXr5+xsbGcnJy5ufmOHTvYPzbs3bu3U6dO7EOsb9++69at47Kg2NjYKVOm0PN/ixYttLW1eTxe586dpR2kFQj20dg0QgghhBAqBxmmjL23IIQqXW5uroqKilAoDA8Pj42Nbd68uZ6eXqX07kT9/Plz1apVe/fuTUtLS0pKyvlXamqqi4sLn8+nYQLpMjIy/P39tbS09PT0yjRS8Lhx45ydnadOnXrjxo2oqKjIyMimTZuWr2s2oVAYEhKSmZnZrl07KeOEpqSkBAcHGxgYcOw2nmGYgoICJSWlzMzMoKAgoVBoYmJS1vElCwsLfX19BQKBsbFx7RmbstRSZWZm+vn5NW7c2NDQEFJgR5uZmbHzJyYmBgcHGxoaamlpVVbZKqVmlqN2QS1KTk7W19eHWEmlKy4uhoaNlpaWHA+WyMjIpKQkExMTGl9ji4uLCwsL09fXp+HdX79+JSUlWVhYQNzf29sbwpFBQUHNmzcPCAgQCATt27cXH8iYy9wogUDg5+fH4/GMjIxoDIsL6O/SwsJC4urExMQYGhpGR0fTd365q6LdV+q5hftGy8vL8/Hx0dLSqobxnRmGCQgIiI6OTk5OVlZW1tHRMTU1rYrzT35+fkRERFhYWG5urqamprGxMQwEVCJ6FeN+q1mOSRBCCCGEkBgM9iFU94WFhfXu3fvy5cvQw70IoVDYuXPnb9++VWkZ2OGYKl0Q+o1UVs3E2gXYwT4at621tm7dGh8ff+HChZouCKoyGOxDCCGEEKohZfiJHiH0m1q4cGF8fDz0rSYuJyeHvl+JUHXCmvmflZKScvr0adqNI0IIIYQQQqgS4Wi8CNV9wcHBhJAtW7aIDP5ICPHy8po4cWJJ4zlWIhgxttRRJtF/SmXVTKxdgI7LXDs3hZeX1+jRo5cuXWpvbz9y5MjBgwebmZnVdKEQQgghhBCqg7BlH0J135YtW5YsWXLs2DFHR8f+/fvr6uoyDJOYmOjj42NsbHz8+PHy9Z1XJtHR0YSQiIiIql4Q+o1UVs3E2gWioqLgQ0REBHuMmlrizJkzjx49gs9aWlr0M0IIIYQQQqhyYZ99CP0nvH///tSpU+/evYuPj+fxeM2bN+/Vq5etra3I4IxVwdnZ+cyZMy9evIA/Bw8ebGVltXnzZol99qP/mgrWTKxdIDs7e8uWLU5OTklJSYSQJk2aTJo0adiwYSNHjqzpov1/Xl5eK1as+PbtW9euXS9evIjvaNd92GcfQgghhFANwWAfQv8tQqGwTKPoIlQ9sGYiVNdgsA8hhBBCqIZgsA8hhBBCCFU2DPYhhBBCCNUQ7LMPIYQQQghVGRrCQwghhBBC1QLfmUIIIYQQQgghhBBCqI7AYB9CCCGEEEIIIYQQQnUEvsaLEEIIIYQqG/a7hxBCCCFUQ7BlH0IIIYQQQgghhBBCdQQG+xBCCCGEEEIIIYQQqiMw2IcQQgghhBBCCCGEUB2BwT6EEEIIIYQQQgghhOoIDPYhhBBCCCGEEEIIIVRHYLAPIYQQQgghhBBCCKE6AoN9CCGEEEIIIYQQQgjVERjsQwghhBBCCCGEEEKojsBgH0IIIYQQQgghhBBCdQQG+xBCCCGEEEIIIYQQqiMw2IcQQgghhBBCCCGEUB2BwT6EEEIIIYQQQgghhOoIDPYhhBBCCCGEEEIIIVRHYLAPIYQQQgghhBBCCKE6AoN9CCGEEEIIIYQQQgjVERjsQwghhBBCCCGEEEKojsBgH0IIIYQQQgghhBBCdQQG+xBCCCGEEEIIIYQQqiMw2IcQQgghhBBCCCGEUB2BwT6EEEIIIYQQQgghhOoIDPYhhBBCCCGEEEIIIVRHYLAPIYQQQgghhBBCCKE6AoN9CCGEEEIIIYQQQgjVERjsQwghhBBCCCGEEEKojsBgH0IIIYQQQgghhBBCdQQG+xBCCCGEEEIIIYQQqiMw2IcQQgghhBBCCCGEUB2BwT6EEEIIIYQQQgghhOoIDPYhhBBCCCGEEEIIIVRHYLAPIYQQQgghhBBCCKE6AoN9CCGEEEIIIYQQQgjVERjsQwghhBBCCCGEEEKojsBgH0IIIYQQQgghhBBCdQQG+xBCCCGEEEIIIYQQqiMw2IcQQgghhBBCCCGEUB2BwT6EEEIIIYQQQgghhOoIDPYhhBBCCCGEEEIIIVRHYLAPIYQQQgghhBBCCKE6AoN9CCGEEEIIIYQQQgjVERjsQwghhBBCCCGEEEKojsBgH0IIIYQQQgghhBBCdQQG+xBCCCGEEEIIIYQQqiMw2IfQb4BhGO6ZhUJh1ZUE1SpZWVnFxcVVNPO0tLQqmjNCCCGEEEIIoaojV9MFQAhJlpWV9fr161evXnl6ehYVFX3//l16fj6ff+DAgfv37/v4+DRp0qRDhw7bt2+3srKquhJ+/fr177//Fkls3rz5qVOnpEz16tWrY8eOiSRaWFjs3LmzIoVZvXp1aGgoO+XcuXM6OjocJ1+1alVYWBj9c968eePGjZOYs6CgYNKkSSXNZ+/evSYmJvB51KhR7K80NDQcHBw4loeLxYsXnzt3rkGDBjdv3hw8eHAlzjkzM3Po0KGfPn1q167dixcvWrRoUYkzRwghhBBCCCFUtRiEUC0THR1tY2OjoqJSr169TZs2eXt7lzpJQkKCpaWlyNEtKyt75syZKi1qfHz8/v37RZb7/ft3KZP07duXndnY2PjLly8FBQUVLAmfz/fy8tLU1KRzDgsL4z55dHR0165d6bT//POPlMxxcXHTp09nr4WcnNzTp09zcnLY2fLy8n78+GFgYEAI2bp1a2pqajnXTZKPHz/SpRsZGVVkVu/fvw8ODman7N27l87cxsamYiVFCCGEEEIIIVSt8DVehGqXEydOGBkZnT592sDA4Pv373v27DEzM5M+SXFx8ZQpUzw9PUXSBQLBqlWrRNq7VS5tbe3169fr6uoqKyvTxBMnTpSU38/PLzo6WlZWlqYsW7asU6dOioqKFSyJnJycubl5hw4dyjd5s2bNpkyZwjGzjo7O+fPntbS0aIqKisqQIUNUVFTY2ZSUlCwtLS0sLPr16/f33383atSofGWTSCAQ0M98Pr/c8/n169fIkSNFKk9lzRwhhBBCCCGEUPXDYB9CtYVAILC1tV22bFlOTo6pqemHDx+MjY25THjkyJHg4OALFy4EBgY+fvx47Nix9Kv8/PxLly5VVYn/paamZm1tTf+8ceNGamqqxJzHjx9fvHixjIwMTWG3xas4Obnyd01Qv3597pmVlZWXLl1K/8zMzHz16pV4tuLiYnd39xUrVpS7VCXp3r07vE2srKx84MCB8s0EXklOT08XSf/jjz+g7rVs2XL9+vUVLCpCCCGEEEIIoeqEwT6EaosZM2acPn2aEKKionLr1q169epxmaqoqOjMmTNubm7z589v167diBEj7t69O3HiRJrBy8urigrMNmzYsLZt28LngoKCCxcuiOfJyMi4e/fu3Llz2YnsVn6/l4ULF7IbJErsqfDp06cyMjIjRoyo9KXLyMjcunUrISEhJSWlpO4FpRMIBLNmzfr69av4V5qamn5+fjExMeHh4fr6+hUuLEIIIYQQQgih6oPBPoRqBXt7eycnJ/i8cuXKdu3acZwwLCxs165dhoaGNIXH4+3YsYP+mZubW4nlLAmPx1uyZAn989SpU+xXQYG9vf3IkSM1NDQ4zrOoqCgrK6vUbAUFBWUakbaoqKhSRrDV1NRkv/n75MmTqKgokTwXL16cM2eOeHvD4uLi7Ozssi5RfBItLS0lJaWS8ktZCp/Pnzp16u3bt0uaVkZGpmnTplJCsUKhMCMjgynLONGFhYWFhYVSMmRnZ9NqU3WjDCOEEEIIIYRQ3YbBPoRqXnJyMvtNz9mzZ3Of1tjYeNq0aSKJRkZG8vLy8LnaWmbNnTuXdloXFRX16NEj9rdCofDUqVPsgKAU58+fNzMza9KkiaWlpYaGxoIFC5KTk8Wzubu7d+zYUUlJqUGDBkOGDPH395cyT4FAsHfv3q5du7Zv375ly5adO3e+efMm55WTbNmyZez5nzt3jv1tYmLis2fP5s2bR1OCgoJsbGxatWplZmbWvHnzhg0bduvW7fLly0VFRTTPvXv3+rHAFrtx40bz5s3V1NSMjY1DQ0MvXLjAzrN8+XL2crksZcqUKXfu3KF//v333/369Rs+fDghZOnSpeyZ29vbi6z127dvBwwYoKqq2q1bN1VV1eHDh7Nbj8bGxvbv3589h8TExA8fPvTo0UNJSUlFRcXKyur9+/fsGWZlZa1YsaJp06YtW7bU0tIaMGDAunXr6KDGCCGEEEIIIYTKpqZHCEEIMUeOHKGHZJs2bQ4dOrRw4cJ169ZdvXq1fEO4xsbG0hm+evWq0gssol27ds7OzgzDLF68mC63f//+7DyPHj2ysrKCz+yWbjAhlZqaOmrUKEKIsbFxXl4ewzDQSlFbW9vFxYWd8+rVqwoKCoQQGRmZyZMnL1iwoGXLlvRVYvK/o/GmpKTAKMCDBg0SCoVFRUXm5uaEkI0bN9I87FePpY/Gy9atWzc6lZaWVlFREf1q//79ffr0oX++fftWWVlZQ0MjISGBYZiCgoLJkyfDhCNHjuTz+TQnOwzXtWvXM2fOsE/a1tbWQqHw7NmzNKVnz55lXcqRI0f69etH5zBt2rR9+/YdOnQIJmGHm7ds2UJnLhAINm/ezOPxlJSUgoKCGIZxc3OTkZFRUFA4fPgwzRYbG6urq0vncOHCBVVVVXNzc9hfhBBFRcWQkBDIXFBQAPvi0KFD0Ojy0KFDPB5PW1ub4y5ACCGEEEIIIcSGwT6Eap6FhQWNjDRv3nzy5MnNmzen8aNHjx6VdYb379+HyXv37i0UCquizGw02Ofn58cOS/n7+9M8gwcPtre3h89Sgn20t0FHR0dIyc3NhZdJ69evHxsbC4k/f/6kTRfXrFkDiV+/fqWJIsE+2rzu5s2bkGJnZ0cIkZGR+f79O6SUL9h3/fp19io7OTnRrwwNDa9cuUL/7NixIyzRx8cHUr58+UIndHBwoDn5fD6NizVp0sTU1PTSpUs6OjqQsnDhQoZhEhISJAb7uC9ly5YtEovNMAx9o5z8b7CPjrM8f/58mti1a1dIZFfUoUOH0jl069bt58+fDMM4OjrSRFtbW8h5+PBhSGHHtXfu3InBPoQQQgghhBAqH3yNF6EaxufzfXx86J8XLly4efOmr6+vnp4eISQxMXHChAkhISFlmic0FWzYsOHFixfZQ99Wtfbt2/fv35/+SWNDwcHB3759mzp1qvTJnzx5Qhu1WVpawgdlZWUDAwNCSHZ29po1ayBx165dfD4fPi9atAg+dOzYceDAgeKz9fPzc3BwgM+mpqbwoVmzZoQQhmF27dpVllUUNXHiRBqGI6xhOjw8PBISEtgjpcBOZBjmypUrkMKe8N27d/SznJwcDfYlJyfb29vPmTPn5cuXHTt2tLS0XLlyJSl53GHuS5FC4szj4uL+/PNP+Ez3Dvvz8uXL8/Pzxedw/Pjxli1bEkKGDBlCE2lcmL7SS6N+hJAVK1aoq6tzKSpCCCGEEEIIIREY7EOohsXGxgqFQvpn06ZNCSENGjRYsGABpBQVFW3YsIH7DN+8efP27VsVFZUnT56wX2utHuxu7BwdHTMzMwkhJ0+enDNnjpShJMDFixfpZ3aICkJFhJCbN2/m5uYyrPZ0Ghoa7E4JJQ4o4ezsTLdwo0aN4EODBg3gg7u7O6cVK4G8vDz75eW3b99C14EXL16cPn06e5XnzZsnIyOjpqbWp08fSGHHYWFDiatfvz401jMxMfn69euPHz+MjIyklKd8S+Hizp07dLwUiXsnMjLSzc1NfEJVVVW6LjSRzooWb/fu3YMHD/706RPkDAwMLHdREUIIIYQQQui/DIN9CNWw1NRU9p90jAt2Z3AeHh4c55aXl7d48WJVVdUnT56w51BtRo0aRaM/OTk5Dg4O2dnZV65cYUfESuLt7U0/0wgRYQWJGIbx9/ePiYnJy8uDFC5j+7LbRQ4bNqxTp06dOnVaunQppKSlpVUkBEYIWbRoEW2IRwg5ffp0dnb27du3abgWHDlyJDo6OikpaeTIkQUFBQ8fPly3bl2pM6dBSY7KtxQuSt07hBBfX18pc5DYyHTs2LH086tXr7p16zZs2DD2iB8IIYQQQgghhMoEg30I1TDaPR+gYaMWLVrQxJSUlJSUFC5zW758eUpKiouLC23YVc1kZWVtbGzonydPnrx06VKXLl1KHRSYz+dHRkbSP9m977E/BwYGhoWF0T/ZkaaShIeH08/Lly8/ceLEiRMnLl++/Olf9erVK3UmUmhpadFBMAghV65cuXjxor6+focOHURyNm3a1N/ff/78+VpaWocOHYKBKSpdFS0lODiYfpayd8o6W2tr64ULF7JTnj9/3rVr14qPlYwQQgghhBBC/00Y7EOohmlqarIjVkVFRfCBncjj8WiLPynOnTv38OHDN2/edOnShSYyDDNjxozKK2/pFixYQMNnoaGhmzdvXrJkSalTycvLa2pq0j8FAgH9TLcJIURXV5cdmyssLCx1zo0bN6aftbS0uohRVFQsdSbSLV++nH7Ozs7euHGjSLM+Qkhubu7EiROtrKzs7e23bdvm7u7O7tGvslTdUqCXQyBl75RjzmfPnr18+TI7ul1YWDh//nz2oNIIIYQQQgghhDjCYB9CNa9du3b0M+3LjD3EQZs2bWjvb0FBQa6uruJBrlevXu3Zs8fd3Z09ti8h5NixY9nZ2VVR7JJoaGhMnz6d/tmwYcMRI0ZwmdDExIR+zs3NlfjZ1NSU3REhlzdwYXwP4OnpyaUkZdWpUyc6KC0hREZGhr0FCCEFBQUjRoy4e/cuIWTgwIGrV6+uimJU6VI47p2yzhYaA86aNSs0NPTUqVPQZyXM1sXFpfzFRQghhBBCCKH/Kgz2IVTz2INaxMfHw4eCggKa2KNHD/iwd+9eIyOj/v37d+jQgR3n8vLymjBhgqam5l9//TVp0qRJkyZNmDBh5MiR7dq1W7lyZefOnat6FdhNvQghtEc8QsjixYsljpsBGIahnwcNGkQ/s9cuIyMDPpiYmGhrazdp0oSGhOLi4tg52XOjOnXqRD9fuXJFJM/Hjx9pD4AVwd6J48ePb9iwIfvbly9f0pFA2OWpXOVeCh3AROIGBOyRjiXunXr16vXs2ZP7QsH06dO/fPlCCFFQULCxsQkICBgwYAB8lZOTU9a5IYQQQgghhBDCYB9CNc/a2trY2Bg+w1iuhNXET0VF5e+//yaE5Ofnb9u2DRIDAgLs7e3hc1hY2NChQ7Ozs79+/XrnX/fu3Xvy5An0s8Z+q7cq5OXlJScns1MsLS0hQKmgoDB//nz2V8XFxezIILtd2MqVK2mrPTochFAo9PPzg88nT56ED3R4YoFA8OLFC/gcHx//4cMHOrf8/Hz4MHHiRDMzM/gcHBy8du1amsfJyWnt2rUQi+Tz+exCcl77/zNp0iQ6Rq34O7zfv3+nnz9+/Agf7ty5QxNFomw0+lbSe8rsEZzp5zIthf3yMnSDeObMmcOHD5c0865du86cORM+swfroJ///PNP+qqvxDmwE9klOX78OP2spqa2fft2+Ez3GkIIIYQQQggh7jDYh1DN4/F4V69ehZE6njx5Aok/fvwghMjJydnZ2cFXQqGQHSaDMFBsbOygQYMSExNLmrmMjEzVNSUjhHz69CkmJubixYtpaWnsdGjpNmHCBC0tLXb648eP2YGeu3fv0k7fFBUV7e3toU0cHZ/h+fPn8Brypk2bevfuDYkLFy6kb5WuWrXq0aNHDx8+7NWrFzuAtXPnTnhpl8fjHTt2jHZ6ePjwYX19/YkTJ7Zu3XrDhg03btyAqd6+fUun/fz5c1m3g7y8/KJFiwghenp6ffv2FfmWPUqGm5vbqFGjevXq9fDhQzpA7ZcvXzZu3Jienk4IiY6Opo0NU1NTIyIixBfHHi4jICAAIpVlWoqlpSXNvGfPnv79+x8+fBgGTWbPnB3XO3jwYPv27QkhDx8+hFhqfHw8DBXdr1+/9evXQzahUPjz5086FdRk8r+xyISEBFqZr169yo73wcbv0aMHbdCKEEIIIYQQQqgMGIRQ7ZCUlDR8+HAZGZmZM2cePHiwefPmhoaGHz58YOdZs2YNHLmtWrVKTExkGGbIkCHSj3EjI6MqKvCbN29MTU1pIElFRaVbt24pKSnwbVFRka6u7rt372j+O3fuQKhIhIaGxurVq2m26OhoeJFz/PjxK1asaNy4sba2NoQI2TIyMoYOHUpnoqam5ujoOGfOHEJIy5YtBw4c+Mcff9jb29P8/v7+Ii3FBg8eHB8fD99OmjRJpFQTJ04s6waJj49XUFDYvXu3+FdCoZA9TIqqqurff//N5/PHjx8PKXJycidOnGAY5u7du40aNWKXpEGDBpMnT2bP7cyZM8rKyuw8zZs3z83N5b4UAJuL1pPQ0FCGYebOnUv3KRg/fjydJD8/f+nSpbKysl26dFm/fr2RkVG9evX27NkjEAggQ0xMjMjIy3JycufOnXv79q3IIDOmpqZRUVEWFhZDhgxp3769mZnZ3LlzraysCCEDBgyIjo4u6/ZHCCGEEEIIIcQwjAxTcg9NCKHqFx0d7efnJxQKraystLW1xTN8+fIlOTm5b9++XMbn/X2Fh4d7e3vn5+e3atWqS5cu7OFK2OLi4j59+qSjo2NpaVmvXj1fX18tLS32qL4iYmNjv337JhAI2rdvb2hoWOnFvnz58pAhQyTuOEKIp6dncHBwu3btTExMYI34fP7z58+FQmH37t2bNGlSKWUo01KCgoJCQkKaNGnSpUsXHo9rW++UlJTPnz+npaVpamr26NFDVVW13KV9/vz54MGDeTzer1+/fH198/LyTExM6FvtCCGEEEIIIYTKCoN9CCGEEEIIIYQQQgjVEdhnH0IIIYQQQgghhBBCdQQG+xBCCCGEEEIIIYQQqiMw2IcQQgghhBBCCCGEUB2BwT6EEEIIIYQQQgghhOoIDPYhhBBCCCGEEEIIIVRHYLAPIYQQQgghhBBCCKE6AoN9CCGEEEIIIYQQQgjVERjsQwghhBBCCCGEEEKojsBgH0IIIYQQQgghhBBCdQQG+xBCCCGEEEIIIYQQqiMw2IcQQgghhBBCCCGEUB2BwT6EEEIIIYQQQgghhOoIDPYhhBBCCCGEEEIIIVRHYLAPIYQQQgghhBBCCKE6AoN9CCGEEEIIIYQQQgjVERjsQwghhBBCCCGEEEKojsBgH0IIIYQQQgghhBBCdQQG+xBCCCGEEEIIIYQQqiMw2IcQQgghhBBCCCGEUB2BwT6EEEIIIYQQQgghhOoIDPYhhBBCCCGEEEIIIVRHYLAPIYQQQgghhBBCCKE6AoN9CCGEEEIIIYQQQgjVERjsQwghhBBCCCGEEEKojsBgH0IIIYQQQgghhBBCdQQG+xBCCCGEEEIIIYQQqiMw2IcQQgghhBBCCCGEUB2BwT6EEEIIIYQQQgghhOoIDPYhhBBCCCGEEEIIIVRHYLAPIYQQQgghhBBCCKE6AoN9CCGEEEIIIYQQQgjVERjsQwghhBBCCCGEEEKojsBgH0IIIYQQQgghhBBCdQQG+xBCCCGEEEIIIYQQqiMw2IcQQgghhBBCCCGEUB2BwT6EEEIIIYQQQgghhOoIDPYhhBBCCCGEEEIIIVRHyNV0ARD6T4uOjv7x4wf3/G3atDExMam68lS6oqKiL1++PH361NfX99GjRzVdnErg7+//8OFDNze3Fy9eVN1SXFxcNm3aNHXq1DVr1lTdUlDVKSwsPHny5Js3b/z8/Bo3bjxhwoT169fLyspynDw6OvrZs2f37t07ePCgqakpJEZFRc2ZM6dp06YODg4SZ3X37t07d+78+PFDTk6ud+/eO3fubNy4ccULg1A1q2CNzcnJefXq1b1794YMGTJjxoyqKOGrV6/y8vK45x8xYoScXG285caTw3+BUCicM2dOTEyMvb19q1atKj5DPp//4cMHZ2dnPp9/4sSJis+wShUUFLi5uTk7O7ds2XLTpk01XZzaLjMz8/Xr1/fv3+/bt+/8+fO5T8gwjL+//8uXLx89euTo6NisWbOqKyRCiCsGIVRzLly4AEcij8fT0NBo27Ztu3btFBQUINHAwMDAwEBLS4umLF26tKaLXAb29va9evWCkrdq1aqmi1NRsbGx06ZNk5eXJ4Q0bty4Spc1atQoQoiKikpxcXGVLghVhdzc3I4dO+7atYthmPv37/N4PELI8ePHuUybk5OzaNEiDQ0NOHC+fftGvzp06BAkfvz4UXxCGxubYcOG8fn80NBQTU1NQsiECRMqWBiEql8Fa+zFixcNDAzgSDlx4kQVFZI+xyorKzdr1szQ0FBXVxdSGjVq1K5du1atWjVo0IDebKekpFRRSSoCTw7/Ed+/f4d6uHfv3orP7cGDB/TWbuTIkRWfYZW6fv16hw4doLQrV66s6eLUdvv379fX14fN9c8//3Cf8N27dxMnToQ7ZELIz58/q66QCCHuMNiHUE06duwYIWT58uVJSUk0sV27dnCxzMnJgZT8/Pzz58/LysrOmzevhkpafn/88UfdCPYB+Fm4qoN9T58+NTMzq5T7clT9tmzZwn68P3r0qIGBwefPn7nPwcXFRTzYFxER0aNHj8mTJ/P5fIn579y5A3++e/eubdu2Z8+erZTCIFSdKl5jc3NzoRld1QX7GjVq1Lx5cxcXF6FQCClOTk5wzK5du5Zm8/f379u3LyEkKiqqikpSEXhy+I8oLi6ePHlyz549w8PDK2uePXv2/C2CfQzDCIXC5s2bY7CPo7y8PIjZlSnYB65du4bBPoRqFeyzD6GalJ+f3717dzs7uyZNmkjJVq9evQULFixfvrxM7w3VEnCPVQ1SU1MPHDhQiTNkGGbjxo0iidBmqqoNGzbM29tbfOm/o0rfL7Ucn88/f/48tNWFlOXLlwcHB3fu3Jn7TLS0tMQTW7du7eHhcfPmTfH3AU+ePElYlbNnz56hoaELFy6slMJUp3379qWnp9d0KRAhhHh4eFR/3wvlqLHi5VRWVlZRUanScubn51+4cGHAgAEyMjJSshkbG9+4cUNJSakWXrt/u5MDKjdZWdmbN2++e/dOT0+vsuYp8SJVO8nIyFTPnVvdoKSkpKamVr5pq+2GHyHEEQb7EKpJeXl5M2bMkP60QE2fPj03N7eqi1Tp4M2gqiYQCKZMmRIZGVmJ89yxY8eDBw9EEjnuLASqYr/Ucj4+PomJiRWs9mWtZq9evSKSjrVKKUy1cXFx2bJlS1FRUU0XBJG4uLjJkydXf4iqrDW2pHJW6YmaYZgGDRoMGjSIS2Ztbe3+/fvXwmv373VyQLXN73Uv9HuVtsaVe3Ph+QSh2gaPSYRq0ujRoydNmsQxs5WV1fLly6u0PL+vVatWvX79uhJnePfu3b///rsSZ/jfVOn7pfaLi4ur5iVmZ2fn5OTUksKUW1hY2JQpUwQCQU0XBJH8/Pzx48fHx8dX/6LLVGNrqpwMwzg6OnJ/Ht61a1ctbPDyG50cEEIIIVQOtXFoMIT+O2i3wVzIyMgMHDgQPvv6+rq7u3t5eRUVFVlaWvbq1atjx47cZ/Xq1St3d/fg4GA9Pb0ePXqMHj0a0r99++bu7s7OuWzZMhge5OvXr2/fvoXEvn37WllZEUIKCgqcnJy+ffuWnJzcvHnzTp06TZ48udRHIDs7O/pI/8cff8D7AseOHePz+ZDYvXv3bt26sSfx9fW9fft2VFSUQCAwMzMbP358mzZt4Kvi4uIVK1acOnWKEOLl5QWDGPTr14+9bUta35I4ODgsWrSIYZjU1FSYYdu2bceMGSOSLTs728HB4ePHj7Kysj179vzjjz/Ef9VMSUm5fv26r69vYWGhlZXVhAkTuIxQxjDM58+fnZyc9u/fr6ioSNMjIiJu375tamo6fPjw1NTUM2fOfP/+vWHDhvPnz+/evTshRCAQ3L9///nz55mZmR07dlyxYkW9evXYcw4JCbl69eq0adPatWt39+7dJ0+eFBQUdO3addKkSbSDebbXr19//frV29u7UaNGlpaWAwcOFB/LLzs7+/bt2yoqKlOmTHn16pW9vb2+vv6GDRvWr18vZb9wqTw+Pj63b98eM2ZMx44dv379am9vn5KSYmlpuWLFComv6bm7u7u6uvr7+zdq1KhTp07W1tZKSkoiefz8/B49euTr66uurt6xY8dp06aJ5ymJ9K3x+PHj4OBgb29vQohQKKTjafTq1avUN+Oio6MdHBx8fHwaNGjQv39/MzMzidnCw8Nv3rw5ZMgQOAC9vb1dXFxopO/GjRufPn0ihLRq1UpRUZFLYUrdGtHR0Y6OjiNGjDA1NT179qyrq+uwYcPmzp1LM0g/uAQCgZub2+3bt+3s7OrVq3f37t379+8TQgYOHDhnzhya7cuXLxMmTEhLSyOEnDp1Sk1NTVFRcenSpdI3WnFx8b179758+RIZGdm8efPBgwcPGzZM/PzD5WyZlpbm7OyclZW1cuXK4uLiK1euvHz5UkZGZvjw4TNnzoQ8Hz58uHXrVlRUlKGhoY2NTYsWLdhzSElJuXnzpra29oQJEz5//nzt2rXY2Fhzc/PRo0dbWFiIFz4uLu7ly5deXl7x8fEmJiZWVlZDhw4VOYHk5OQ8fvw4MDBwx44d+fn5Z8+e9fDwaNKkyZQpU6AnOBGl7k0uB1RCQsKUKVM+f/5MCHn06FFMTAwhZNasWdDXRFFR0aVLlzw9PXk8np6enoWFhaqqateuXaXuqP9TuYeP9HKyff78+erVq7GxsW3atJk3b56RkZH43Mp0jeDxePRazAWtANKPpso9JUrZUxw3NZcDRyAQvHjxwsfHZ+PGjaGhoYcOHZKRkdmwYQP00vvly5dbt26tWLGiRYsWgYGB58+fj4iI0NPTW7lyJRw+GRkZ165d+/jxo4yMzNChQ62trcW3XqnXUIkXoM2bN7MvnSVJT0+3t7enfyorK9vY2AQFBT158oSdbcSIEdCZ8pMnT4KCggghjRs3nj17Ns3A5/OdnJy+f/8eFxdnZmY2bNgwOEWXJDEx8erVq+yUsWPHwl1NQkIC7fisTZs2Y8eOJYQwDPPy5ctXr17Fxsaqq6ubmprOnDmzfv363LdDWlravXv3lJWVp0+fLlIYLtdEhmEePHjw5MmT9PR0AwMDGxsbKWsnkfTD/+7duz9//oTPQ4cObd++PSHk/v37ERERkNi8efPJkydzXFnpPnz44OTkFB8f36FDh5EjR9Lx7tkrW+rWJoQ8fvz41atX+fn5zZo1s7CwUFRUHDJkiEiest58wtIr5cDhcokBOTk5jo6OHz58KC4u7tixo62tbUllK98NLUKoJtVUZ4EIoZKID9DBlp+fv3btWm1t7evXr0dFRT19+hReJpo/f35GRkapM4+Ojh45cuSSJUvu3r3r4OBgbm5OCBk3blxmZibDMEKh0NHREZbetGnTxMREOmFxcfHp06cJIX/++WdhYSHDMF+/ftXU1Jw9e3ZERAQ8HhNCJkyYAN9Su3btIv87QEd+fj59vKGd+Kanp9+6dQtuRLZv304zCwSCBQsWNGzY8PHjx/n5+bGxsd27d1dXV3/9+jVkCA8Pt7Oza9myJSEEOkC0s7Pz9PTksr4SFRQU2NnZDRgwgBCiqakJM3z06BF8a2dnRwhp3LhxYGBgmzZtmjZtamhoCOsydepUkVmdP3++W7duZ86ccXZ2XrdunZycXMOGDR88eCBlB0VGRq5fvx5Wh10H7O3t6YPW8ePHHR0ddXR0rKysIKe8vLyHh0dwcHCHDh1atWrVrVs3uFkfMGAAnfOePXvo48eTJ08GDBjA4/Fo729aWlrv379nlyQmJmbo0KEdO3Z0cXGJiopydHRs06aNkpLSoUOHBAIB5Hn48OHEiROVlZUJIVu2bLl48SJ9ND116pSU/VJq5Tlx4kTbtm1hVg8fPtyyZUv9+vXbtWsnKytLCDE3NxcZpCIqKmrMmDH9+/d//PhxWFjYzp07CSG6urp+fn40T2Fh4cqVK0eNGnX16tVbt25NmDCBEGJiYhIYGChlj3DfGk+fPrWzs5s1axYU+9ChQ7DWX79+lT7zhw8fqqurb9++PSAgICAgYMmSJSYmJjATGKAjJyfnn3/+oRXg8ePHMKGPj4+dnR1tgmpjYwNLvH//fqmFkb41iouLjx8/3qdPHzgkX716RZ9sZWVlYUAh6QdXUVHR0qVLGzVqBFPFxsaOGDFCU1OTPuDZ2trSLXDt2rVFixZB+t9//21nZ3f69GnpG83Dw8PU1NTGxubt27cBAQHjx48nhPTq1Yt9aHM5W757927EiBFQryZOnPj169f27du3a9eOBmj279+fnZ09depUDQ2Nnj17Nm7cGKpWQkICzOHOnTtDhw6FHs137ty5bt06GRkZ+swpLy9/5swZkcJfunRJQ0Nj7969ISEhnz9/hrBmjx49goODIUNISMjkyZNhJh07dgwICDA0NGzTpg10r8bj8ZycnNgzLLVucz+g3N3dDx48CDlnzZoF1SY5OZlhmKysLFNT0zVr1hQUFBQXF9++fVtJSWndunXS9xRTNYePlHIyDKOurg6nyj///FNBQcHCwgKe1VVUVL5//86eTzmuERJJHKADcDmaKveUKH1PlbqpuRw4Xl5eNjY20HFbz549f/z4AducELJs2bINGzY0bdoU/vz+/fv69esbN27crVu3hg0bEkKaNWuWmpr6+PFjXV1dMzMzeqzt3LlTZNNJv4ZKuQDdvXuX446jQTdLS0vYFwKBwNnZGX4n09LSokclwzA5OTmzZs2Sl5dnD2Pi4eHRtWvX3bt3P3jw4PDhw40bN5aVlWXfxkjk7e1Nt5jIxff79+8KCgpDhw6F+pySktK5c+cOHTp8+/ZNIBB8/fq1SZMm7du3p6O+SN8O165dGz58OJygRConx2tiVlbWmDFjzM3Nnz59Ghsbe//+fVNTU4g6cRmgg8vhLxAIzp8/D2V2cHCAxLy8vC9fvkBF6tu3L5eVLakMcPVcsWKFyPsx8vLyp06dYucsdWuD2bNn9+/fH26Sf/z40bZt206dOrEzlO/EUlkHTqmXGCooKKhdu3ZTpkz58OHDr1+/7OzszMzMoPKLDNDB5YbWw8MDioQDdCBUS2CwD6FaR3qwb9SoUbKysv7+/jSloKAAbsS7dOlC75wkysrKMjIyYo/xmpSUBI9AS5YsYS+CENKgQYPi4mL25M+ePWvVqhU8UQiFQmgIxr7lheZ4t2/fZk8lHuxjGOb58+cSbwjgR0L2XfKZM2cIIcOGDaMpL168IIS0b9+ePSE0WFi8eHE51lciGByjXbt2IukQ7FNRUenfv//Hjx8hkT5zent705znz5/X1tZOT08XmWe9evWkjMyYkpISHh4+ZcoU8TqQlJSkqqpKCNHT09uyZUtubi7DMEVFRb169SKEWFhYWFhY0AJ4enrCHfCHDx8gJTo6GgpPCDE0NLxw4UJeXl52dvalS5eg8WaTJk2ys7Mhc2ZmZtOmTVu3bp2Xl0cL8OvXL3gZjT40RkZGvnz5Ep42Bw0atGrVqvDw8Dlz5rRo0QKqqMT9wrHyfPnyBUrbvn37w4cPFxUVMQzj5uYG63X58mWaMyEhoXXr1mPGjKGPuwKBAFpJdO/enWaztrYeOnQouyRQPDMzM+kHDsetAegzv8jhU5IbN27IyMgcO3aMnThv3jyYCQT78vLywsPDaQMcGuyjqw/pLi4uIjOXUhjpW0MgEISGhsLBSwgZM2bMxYsXPT09e/bs2bFjRz6fz/HgomOz9OrV69mzZ5C4Y8cOQoisrCx7aEh6TqBBNCnevn2rqKjIfhShrUL+/PNPmsjxbFlYWAgHkba29rRp06KjoyF9w4YNhBB1dfUePXrcuHEDxl1NT09v3bo1IWTPnj2QLTk5+f3793AU6Orqrl+/PiEhgc/nu7m50Zc3P336RMvwzz//EEJEIoB//fUXnHVjY2Mhhc/nQzMQTU3NwYMHw+N3YWHh0KFDCSGtW7dmT86lbnM/oAoKCiCnSEhxw4YNCgoK7KDSrl27li9fXuJ+YhimKg+fksrJ/BvsMzIyWr9+PZzWEhMTYceNGzeOZqvINUKElGBfqUdTpZ8SuewpKZuay4ETHx/v5eWlr68P5bG2tg4LC9u3b5+WltbVq1cZhnn69CnM38rKytHREQ6fuLg4CFv0799/wIABqampMP+jR49C/YfrGij1GlrqBYij3r17wzmKnQjtm0xNTUUyb9y40dramv7p6elZv359drSOnspE7oXE0VO6SEQ7OztbXl6exmWmTp1KCNm/fz/NsGnTJnYVlb4dQkJC3Nzc4CovUjm5nDeKiorMzc319PTYOyIkJATiQaUG+7gf/vRwpsE+MGPGDMIK9pVvp0Owr1WrVtOmTfP19U1OTr5z546Ojo74JbXUrc0wzLNnzwgh7u7uNMXDw8PMzIz+WZETS8UPHI6XGIZhQkJC1NTUJkyYQEcVZxgGfmkg/xvs43hDi8E+hGobDPYhVOtICfbdunWLEDJx4kSRdHivhBBy5MgRKXP+66+/5OXl2fcEDMNAp4FycnJpaWmQAm8CEkLu37/Pzjlr1ix67U9JSYGbLfbDCYSo1q9fz55KYrDPzc1N4g0BtFxgB/tWrFhB/jfYFxgYCNOy7x0lBpU4rq9E0oN9ysrK8fHxNLGwsBB+Zz537hykpKWlNWjQYPPmzexp/f39oeRr1qyRsmiGYW7cuCGxDkDdgFeMqQsXLkBm2m4OwC/Jf//9N02h9QQexqjLly9D+q5duyAFnnNOnDghUrCzZ88SQmRlZdkPJ9B8r3Pnzuz7RSBxv3CsPMXFxeJ3nMy/d+3sdmEzZsyQk5MLDQ1lZ4Ob4G7dusGfb968IYS8evWKnQcGsSWE0JabEpVpa5QpWpGTk6Orq6utrS3SHpYeIBDsA7RvsooH+zhuDTqyinj8guPB5eLiIr4i9L3jW7du0UTuwb7CwkI9Pb02bdqItO4cN24cIWTTpk3wZ5nOlosXLxY/3sPCwiSeV+Gk1L9/f3aitrY2IWTBggXsxNDQUGhQM3DgQEiJjIxUVlbW1taGSA1VXFwMT79jxoyhiVu2bCGEaGpqss91jx8/hlJBKySG897kfkCVFESDmAj7QEtOTi712bXqDp9Sg32rV69mJ8LFSFdXl6ZU5BohQkqwD0g5mir9lMhlT5W0qct04EATRRUVFfZ7AICel27cuMFOh9iNtrY2u1bTMbjfvn0LKdyvoVIuQBzdvHmTEMLj8Wign2GYHz9+wLJEQkjGxsbs0F7v3r179OghMkN4l9zKykr6cnNzc6Gt7ooVK9jp9vb27CAaXMrZ4Sd4zaJz587sqaRvB3gFgV33OJ43jhw5Qgg5fvy4yAyhJ4FSg31lOvxh0SLBvgULFhBWsI/LyoqDY6RHjx7s/CEhIXDn1r59exrc5LK1IXB2/vx59iImT57MzlDuE0sFD5wyXWKgdxofHx92zsLCQmhUTs8z3A9GDPYhVNtgn30I/U6gBZl4T3+Ghob9+vVzdXU9evToypUrS5r84sWL9evX37x5MzsxKiqKEFJcXPz582doM9KlS5devXq9e/fuzJkz0F8MISQrK+vRo0eHDx+GPzU0NO7du5eVlcXu4odhGEIIdLxVWTZt2tSiRQtaDLoUWBB930EijutbDnAvRf9UUFBo2rRpaGgo9BtFCLlz505mZqanp+eqVatoNqFQKCMjw7Duh0oCAQJx8NatSN8xdCOIdFYFL1jRG0c6OSFEpN+96dOnb968OSYmxsXFZfPmzTk5OdCgUrymWVtbr127Njs7++TJk5cuXWKXdsCAARx7rOdYeeDplxACL05SzZo1+/btG93U8fHx165dMzU1pe+4geXLl/fp04e+ZA0h0du3b7M7Y4qOjoYPHh4eI0eOlFjasm6NMjlx4kRcXNycOXOg2QUlssqgpFpRDhy3Bl2i+MCjHA8uWuXYa6SioqKurp6RkUF3YplAR07Lly+nMwc3b9708/OjbzaV6WwJs4KWsxT3I4vOQeTIatu27YQJE5ycnNzc3IqLi+Xk5E6dOpWXl9ezZ0+RvSkrK7tgwYJt27Y9ePDg169f8CgL81RWVmZ3oUX7SIqJiYGCcdybHA8oKWBxo0aNcnZ2hiOrcePGUq44pIoPn1Lp6emx/4SWfQkJCQKBALZG1V0jxEk5mir3lEjKtaeoMh04sFJmZmaampoi+enhKfGCpaqqyq7V6urqCgoKRUVF9LDifg0t6wVI3Lhx4zQ1NZOSkhwcHCDCTggxMTFRVVXNycm5dOkSbbzv4eHB4/Ggh1xCSGho6Nu3b83MzNiFJITA+fzHjx95eXkQTpJIWVnZ1tZ2586dly9f3rt3L90gFy9ehNZk4PLly+7u7uyeUiXea0nfDuKXDy7njYKCAugmAl74YJN4kRJRdYd/+XZ6x44d2fn19fWnT59+4cIFf3//4OBg6M2Ty9aG42v16tXa2tr0zmH37t00Q0VOLBU8cLhfYj5//vzgwYNWrVqJ9FqooKBQv379wsJCmlLBG1qEUA3CYB9Cvw2hUAi/pInfVRNCevTo4erq+vPnz8zMzAYNGohnCA0NjY2NNTQ0FOlMnf5pYGBAE5ctW/bu3buXL1+Gh4fD65DXr18fMWIE/AoNaE/DRUVF9+/fd3Z2hiaBtPVBpdDS0lq9ejV8DgsLO3/+PHQrXuqCyrS+FQc/hBYVFcGf0DLL0tJS5C4KHhK4jwjBRUkxIHje4BJ7lZOTGzhwoIODQ3BwMCHEz89PKBQSSTVNRUXF3Nzcw8OD7gWKPohyUZHKA7fCUEJCyMuXL5l/24SKgJ/oAeyRPn36sMsJI5MQQmgnieLKtzU4ev/+vfSlV5Gybg2RnVvxg0tkJ5YJtAEU3+Py8vKWlpbwuYJnSzpDiencjyxCyNChQ52cnIqLiyMiIgwMDHx8fKSUCj54e3tLqRL0UZBuvXLXbfYMueyL1atXOzs7BwUFmZqarlixYuvWrWpqahIPPapKD5+ygrO0UCgsLi6WlZWt5msEJfFUWYmnRFKuPQXKd+CU6eQv5bAqKiqih1VZr6FlKoN4kebOnbt//357e/vNmzdDPAjaVRFCrly5snfvXtjIJ0+ehFbA7EK2adOmpCpU6mG1ePHiffv2ZWRkODk5QYApKCgoOjp62LBhNI+5uTm9lnl4eDg4OHh5eZES6gb37cDlvBEQEJCeni4rKyv9h9WSVPXhX9adLh4ZnDx5MgQ9AwICINjHZWvPnDlzz5498fHxo0aNGjNmzOHDh/X09OjxVUUnFo4HDvdLDPfbj+q8oUUIVS4M9iH02/j582deXh4Ra1EC6NCQYWFhEoeBi4uLgw+0Pzgpxo0b16xZs5iYmHPnzu3fv58QcvHiRXibgy07O/vo0aPPnj2ztbW9fPny7Nmz4afLSvfu3buDBw/q6OjA+wL0TWcpyrS+lQ6WbmJiUiNLLweoP1DBAgICIDEhIYGOeiySMzQ0tIJLrKzK8+vXL0JIfn6+lDxCoRCOmv79+7ObZHJRpVsDZi5xZOGqU5GtAWr24OKyxyt4tqxEdFnsg0t6qcpUnSq+N7nr3r37nTt3Zs6cCSPGXL169ciRI9DFVUmq52RSPjVbjcVV4vW0HHsK1J4Dp5qvoQsXLjxw4EBkZKSbm1u/fv0Yhjl48OCpU6fWr1+fmJj49OnT0aNHJyYmPn/+HJqqsQupqalZ7kLq6uqOHz/+5s2bZ86cgWDfxYsX//jjD5FRU4VC4bVr186ePTtixIj9+/ffuXPn+/fvFVhdrucNOH4VFRVFmlFzVJsPf0BDXewKX+rWbtCggaur64gRI8LDwx88ePD8+fP169dv3boV4nE1e2Lhfonhfvvx293QIoQoCSNwI4RqJ/rSBH3Pgo3+jifyHhkFP4FGRUXRfo6kkJOTs7GxIYTY29sXFhb6+Pjk5+f37NmTncfV1dXAwMDT0/PVq1fW1tYi7yFWloKCgjlz5gwZMmT16tVnz57l/otomda30sHSQ0JCqn/R5QM3fMbGxoT10qKUmgYvxJVbJVYe2NSlPjDAs0o59kiVbg24I5c45ypV7q0BasPBJX2PV/BsWYngyOLxePA2JVQn6aUqa3Wq4N4sk7FjxwYEBEC/CgkJCdOmTYMeDEtSDSeTcqvZaiyi0q+nZd1ToPYcONV8DdXT0xs4cCAh5OLFi4SQJ0+e5OfnW1tbz5kzhxBib29PCDl//vyECRPEmzRWsJAwPuyXL19+/PjB5/OvX78+f/58doaoqKiuXbvu3Lnzxo0bmzZtYr9gURFczhtwhcrLyytf9yy1+fAH0CcD+wPHrW1oaOjj47N+/XoFBYXCwsKdO3f27dsXuqOt2RML90sM99uP3+6GFiFEYbAPod+Gmpoa3GFLfOshKSmJENK0adOSbk3gFYP8/Hx2/yxstIN88McffygqKqakpNy9e/fChQuLFi1ifxsaGjpq1KisrKwrV65I6ZKm4pYvX3758uX169dDb9DclXV9Kxcs/fbt2xK/jYmJoX0b1xLQ3xO8xmJmZgaJUmoazVMOlVt5oJlneHi4r6+v+LfZ2dkZGRk8Hg/ubkvaI+/fv6ejRoio0q0BjR1oz/fVoyJbA9TswQV7/Pnz5+xOhajY2FihUFjBs2UlgiOrVatW8K4TVJXIyMisrCyJpSJlrE4V35vcwcNh8+bN79+/7+rqCuHLY8eOwQjpElX1yaQiarYas1X69bQcewrUngOn+q+h8H7u3bt3MzIy9u/fv2HDBllZWYi7PXnyJDY29uzZs+x3eGkh3759m5iYKD7D4uLi169fl7rc7t27Q5d2Z86cefToUdeuXekosYSQwsLCkSNHfv369dy5c3R074rjeN6gzfHKd5GqzYc/oDsOXnHluLXh+FJWVt6/f39gYCC8c/3hwwfotq9mTyzcLzGwcyMjIyVeSdl+uxtahBCFwT6Eah06AIW4wYMHE0I+ffpEh4mkwsPDCSHjx48vaVptbe1GjRoRQnbu3Mnn80W+ffbs2efPn9kpTZo0mTZtGiHk6NGj9+7dmzlzJvvbs2fP5ubmtm3blv0KAPfe+ugrKiIlgReIqKysLOhOhd35mpSlsL8q6/qWOsMygSZyfn5+MLIhG5/Pt7W11dXVLd+cqwj0SgNDK7Zs2RIeDq9fvy5eG0utaeJENmMFK48ICwsL6CUahmFlf8UwzJIlS+B9T9gjly5domNiUjExMUeOHBEZnIGq9K3B1r59e0LI69evxY9ougrlm7N05d4aoFIOrpKUWhPgwTgmJgYGXGZLT09ftWoVdMxUkbNlJYIjC9oH0VIVFRXduXNHYqnMzMzE33eTroJ7UwqRfQFjlIO+fft++vQJFi2lg/YqPXxKKidHlVuNK3KoVu4pkZRrT1G15MCp/mvo6NGjdXR0CgoKli1bFhkZCVdDfX39vn37FhcXT58+XVNTEwZ1FSmkQCDYsWOH+Ay3bNki8jZuSaBx3/Xr1+3s7ETiia9evYLfsTjeBXHH5bwBVyhCCAyeK056tS/r4Q+bS/ptYeWC1mpTp06FACvHrX306NH4+Hj4rKen9/jxY2tra/Lv8VWl18dScb/EwM7Nycm5fv26xFnRXfbb3dAihCgM9iFU69BWGOK3OLt374Zf1OGlEkogEFy/fl1bW3vnzp1S5rxmzRpCiLe39+TJkzMyMiCRYZirV68uX76cjoNBLVu2jBDy5cuXwYMHq6urs7+CvoQiIiIyMzMhJSsrCx5r6SAVtGxErJtq2k/K27dvaeKrV6/g91K6BWJiYuBuw9PTk2ajk7AXBJslNjYW/oSbs7KuLxvMMDExkZYfVgFu3cRv4KAw9KZwzpw5MIcFCxY8evSIZouLixs+fHjfvn0bNmwoZel01UQWJLG3b5ooMbPEe/Hc3Fz2n35+fu/evRs7dmzv3r0h5fTp04SQqKioV69esXMGBQV9+fJl6NCh7Bt0WARsKBES9wvHylNS1+aQh65XmzZt4D77yZMn48ePp28bpaWlTZ8+3cTEBG7ioTLk5uYOGzYMxiEBX79+7dev37p16yQuqxxbg/5IXuqv5YSQ1atXy8jICIXClStXstedVnhab0nJtULKEkv6iuPWoBtZfOdyPLgk1goithPJv1WFiNUWcdbW1vCssnHjxp07d9LNEhwcPGDAABrsK9PZsoqOLD6ff+HCBV1dXdhchJBp06b16dOH/Pu2INvly5dlZGTOnDlDe5GXuPXo+tICcNybHA8oQoiioiJEnUT2RXBwMLv9rLq6OnQDJ/1FvKo7fEoqJynhRE03Hc1WkWuECCkXbjpb+CC+Wyv3lEi47amSNnWZDhwpJ/+SSsu+koqn07Xgfg0tqQxnz57t06fPtm3bOIbG5OTk5s2bRwi5evXq2rVr6ZvUCxYsIIS8fftWJAxHCDEzM4OxlU+fPr179266oLy8vHXr1gUEBPTr14/LoqdOndqkSZPc3NzY2Fh4m5ii/TbSiwLDMO/evSNi91pS9gXNzN7sXM4bBgYGMA7vjRs3XF1daR6BQAAVjH2FkqhMhz/cGbJvC2NiYmB0CJHmydJXlrszZ86oq6vb2dnBn9y3NjuUxuPx4N0XenxV5MRSwQOH+yVm+vTpcHe0c+dO9ljekZGRUGZacu4HI90j5Rt9CyFU+RiEUG0SERFBxxd78OCBeIZHjx6pqanJyso+fPiQJq5Zs6Zx48avX7+WPvPc3Fx4T5MQoqqqOmTIkDFjxrRu3bpx48b+/v4SJ4HBtj59+iSSfujQIZhPz549YQC7Xr16DRkyhBCio6Njb29//PhxyAk/j6uoqBQUFNDJhUIhdO+tqqq6c+fOM2fOTJ48ecqUKQMGDCCEaGpqHjhwIC4urrCwEO4wFBUVd+zYcebMmdGjRy9ZsgQWvWDBgiNHjgQGBjIM8+eff0K2W7duvXjxYurUqXw+vxzrS718+RIm3Lp16/v372fNmhUeHs4wDHR7JCMjk5ycTDPn5+dD25kpU6bQxAsXLtCH9vbt20+YMKFv374KCgo2NjbSF80wzK5du2BCb29vmsjn8+Hn4j/++IOd2cHBATK7uLiw06EBVMeOHWkKbanRuXNnCKQyDJOQkGBiYmJubh4dHc2efPv27TweT1tbOyIiAlKys7P79Oljbm7+69cvmq2wsFBNTY0QMm7cOPEVkbhfOFYeGIcBdgF7nvAjc4cOHWhKdHQ0feOGx+OZmJh069ZNWVl52rRpQqGQZps1axbN07NnzwkTJnTo0IHH48Goi9Jx3Bp0lQkhP378KHW2DMMsXboU8vfq1evq1atfvnw5fPgw7fze0NBw2bJlOTk5DMPQVjlHjx5lz4HW1cOHD0vc/hILw2VrwHig4ktkOJ9MLl++DHnYZ6fExEQ4NFavXk0TMzIy4EXXUaNGff78eevWrbdu3Sppoz1//rxevXowZ0VFxS5dulhYWPB4vDNnzrCzcT9bwgOtvr4+O/Hnz5+wiC1btrDT4VGNx+NlZmbSxGbNmhFCmjRp8v79e0jh8/nTpk1r1KiRq6sre/KwsDDoe3TDhg008d69e0pKSocOHWLnhA77lZSUsrOzaSJtW8FeKS57k/sBxfx75m/Tps3bt28dHBw2btzIMEzXrl379OmTn59Ps02fPr1ly5ZpaWmMVFV3+EgsZ2pqKlSwZcuWsTPTKBVcNZhyXRNLAjuLEGJlZcU+7VBSjqZKPyVy2VNSNjX3A2fo0KGEEG1t7cLCQpGZ0DENrl27xk6HVq716tVj12oa5Vy7di1N5HINLekClJ6eTkcvtbOzY7j5+fMnj8fT0NCAUy7Iz89v2LBh/fr12QWmvLy8oACEkKZNm44ZM+b/tXfXcVEt///AZ2mUEhUTFFBBBQTERCyuXdgBdjd67b52x/Ua10AsRLGTq9iIoqAgKg1KIyHd7Pn9Mb873/PZhWUJRfe+nn/4kNkTc2Jm57x3zky/fv20tbXbt29f4vKloZdj+/btIuk+Pj50440bNz5y5Mju3bu7dOnCyvvevXtXr14t4TywQ1BXVxf/VJp6Izg4mDY8lJWV169f//TpU3d39/79+9PXveXk5MaMGXP16lUJhyZ98adVq0AgmDFjhpOT04IFC9q1a0cbXXJycitWrPDx8SnzYEtEb1QrKyt+pb1jx4569erdv3+/vGd7xYoVderUiYqKYiseO3ZMUVGRNZUrU7FUvuBI/xXj6upKnzj09PT++usvb29vV1fXbt260cEEtLS0pk+fHhAQwEndoD116hRdxtPTU/JhAsCPgWAfwM/i/v379vb2tFFFKSsr29nZ/fnnnyJLRkZG9u3bV01NrUePHgsWLGjfvv2gQYPi4uKk2UtmZubEiROVlZXpLgQCwbhx4yIjI0tb/vz58x06dBBPz8/PHzx4MMvn3Llz09LSPDw86FsYzZs3DwkJuXHjhr29PYtddu3adevWrWwLgYGB7A0RVVXVHTt2FBUV9e3bt3HjxgsWLHj69GlxcTHHcXfv3mW/GbZq1eqff/7hOI7+bikQCNavX0+3Fhsby35T7dixI4tblfd4GaFQSMdhoS34J0+eREdHz549m4UYzM3N//rrL47jzp0716lTJ5ooLy8/efLk2NhYupF//vmHP5C5rq7uqVOnSnwOZJ48eTJv3jz22l2LFi3WrVuXkZFx+vRplh9VVdVFixb5+PgkJyevWrWKTaXXpEmTDRs2pKWl3b9/f+TIkawRb29v/+DBA44X7Fu7dm3Tpk379OkzYMCAWrVqTZo0KTs7Wzwzjx8/btOmjaam5ogRI6ZPn96qVavFixfzg7Z79+61sLBg53bkyJEit2uJ10Wam+fs2bPshSktLa0lS5bExsb6+PgMHTqUnc+xY8eyx9Tk5ORhw4axvhja2tqHDh2itxD/mu7atYs/vHrHjh2fPXtW5s0g5dm4evXq9OnT2R2ip6e3fPny27dvl7nlI0eOsIKvpKQ0ZswYLy8vOTm5nj17Hjt2LCUlJSIiYtWqVaw/rJaW1qJFiz5+/Ojr67t06VI2wJOamtqsWbMOHTokTWYkn43MzMzFixezd3PU1NRmz55969YtfrbLLFzr1q1jeTMyMtq0aRPHcZcuXWKhzBo1aixatCg5OZkuv3XrVpYuHhAR4e/vb2lpyR4/jI2N6U0uosza8unTpzNmzGDV1Lhx4+iD6549e2i4nBBSs2bNRYsWBQQEhISEzJ07l42q1qVLlxMnTtDt0GDfxIkTO3bs2KFDhxEjRjRs2NDa2po93/JlZ2fPmTOnVq1apqamCxYs6NOnj4WFBf83lejo6Hnz5rH7+bfffqOP31u2bOGX9x07dkhzNTmOK2+B8vDwoIcpEAgmT55M7/MePXqYmZlZWFhs2bLlxIkT9vb21tbWLHAm2XcqPuL5PH36NDtSJSWlmTNnBgUFxcbGLliwgF04Y2NjZ2dnuoUKf0cw8+bNE5m9qmXLltOmTaO/D3FSlKYqrxIlXylpTnWZBef27dsDBw7k329//PEHDSYWFBRs2bKFhiDp9tevX5+YmPju3bvZs2ezu9ra2pp+hx49erRDhw40UUNDY/HixUFBQXQvkr9DJXwBCYXC9u3b04/s7Oykv5qDBw/mN1SoBQsWzJs3r7RVwsLCrK2t2Ru7NWvWXLt2bbkifRzHffnyRUNDg1WGfJs3b2YVVN++fYODg5OTk+lXhpqa2q1btySch4KCgk2bNtGYOCFEXl5+woQJd+7coZ9K+Z0YFRVla2vLDlBPT+/mzZsjRoyoX7/+ggULvLy8yjy6Mos/lZmZSaeUoQcyadKk1NTU7du3a2hojB079vLly9nZ2WW2OkoUGxs7bdq0WrVqqaiodOrUacSIEUZGRsOHD09ISCjv2eY4bsWKFW3atNHX11+xYoWTk9PSpUuNjY1FSlAFKpYqLDhlfsUwz549YxPfCQSCDh06BAUF1alTx8TEZPPmzWFhYWxJyYXR399/wYIFrLluaGi4Zs0a9qMyAFQXAfd9xgMCgO+tqKjo48eP2dnZLVq0YDPoSamwsPDTp09FRUXGxsb8QYJKXPLLly90dF5xoaGhtF8Y+4IPCwvLyMho06YNay1JUFxcHBISkpWVZWZmRptEUVFRurq67AGeyszMDAgIUFdXNzU1pSl5eXk+Pj4tWrRgk4sRQnJzc/38/NTV1U1MTCp8vHxCoTAgICAvL8/S0pL1EaiAr1+/BgcH6+np6enpiRzaDxYeHk4vpaenZ7t27QIDA7OysoyNjSWPtp6dnf3+/XslJaVWrVrRvlflUtp1qeTNUyI6c7SGhkbz5s1LGyyJ47iIiIjExMQKFBxS6bMhQURERHJycps2bZSVlbOzszMyMvjDtH8nlTwbpKKFqzSRkZExMTGmpqYi4waUJi0t7ePHj02aNKGxttJUpraUkq6ubkxMzObNm1etWhUeHh4bG9u0aVMWny1NRERETEyMnp5elUxvWvmryZecnPzp0yd9fX3WbTY7O7tmzZqFhYX0AdjQ0LC8MwZ8j+Ijns8KqNrbuGKqsEqs/JWifkDBkUaFv0OjoqKcnJxSU1P//PNPKVeJjo6uW7cui4SyDMjLy0v+oszKygoICNDS0mrWrFnFGgyBgYGsO5iIxMTE4ODgpk2b6unp0ZSkpKTQ0NA2bdpU/naVst7Izs4OCAho1KgRvZciIyObNGki5aCEbAvSFP+oqKjo6OjWrVvTb4GEhIRatWqxqFllFBcXh4WFJSQkqKioGBkZlfYtU+bZpuWL47jPnz9/+fKlXr16LVq0KLGQVnvFIuVXDD3Ali1b0sonLCystGb/z9OgBQBpINgHAPBfwQ/20bnnAKBKsGDf6tWrqzsvAPD/DRgwYNu2bdU75SsAAEC1wAQdAAAAAAAgO4RC4cKFC21tbRHpAwCA/yaF6s4AAAD8IGx+NClnJwQAKdHChZIF8JNwdna2sbEZMWJEdWcEAACgeiDYBwDwX8EmcwwPD6eTnABA5WVlZaWkpBBCwsPDqzsvAEAIIVOmTKnuLAAAAFQnvMYLACD7MjMzHR0dx48fT/9cvHjxjBkzzp49W725ApABu3btsrGxyc/PJ4S4uLiMHDlyw4YN1Z0pAAAAAPhPwwQdAAAAAAAAAAAAMgI9+wAAAAAAAAAAAGQEgn0AAAAAAAAAAAAyAsE+AAAAAAAAAAAAGYFgHwAAAAAAAAAAgIxAsA8AAAAAAAAAAEBGKFR3BgCAEEI8PDxWrFgxevTopUuXfu99ZWRkPHz48Nq1a9bW1jNnzqSJQqFw0qRJMTExJ06cMDAwEF8rPj5+3759r1+//vLlS9OmTWfPnj1q1Cj6kY+Pz9GjR/39/b99+2ZmZrZ69eq2bdt+76MAkN6+fftcXFw2bdrUt2/f6s7L/ykuLvby8rp27ZpAINizZw8y871FRUVNmjSpYcOGzs7OCgq/fPvnypUrbm5u7969U1BQsLGx2bhxo46OTnVnCiTx8fGJjY2VcuF27do1bNjw+2UmPT396dOnd+/e1dLS2r59O0uvWDH5FSsQjuN8fHzu378fFRWVnJysqanZsGFDa2tra2trDQ0NLy+vgIAA1kaCn01pN3Al/ZythR/pVyzLAFAqDgB+AoMGDSKEqKqqFhQUfNcd7d69u0WLFrT4b9++naX7+vrSxM2bN4uv9enTpzp16jx9+rS4uHjJkiV0ybdv33Icd/78eT09vYSEhOzsbBsbG0JIrVq1iouLv+tRAEhPKBRqamoSQn777bfqzsv/8fT07N69Oy1KI0aMQGZ+APbc4unpWd15qazZs2f36dOnsLAwLCyMxviGDBlS3ZmCMnz8+PHIkSO6urr0PlRQUOjYseNv/+rRo4eVlVX9+vXpp66urt8vJ8uXL2/evDndkYODA/+jChSTX64CEQqF58+fb9asGSGkYcOGM2fO3LNnz7Zt28aNG9eoUSM5OTlDQ0N5efnp06dXd06hZBJu4Mr4OVsLP9IvV5YBQDIE+wB+Cnfv3jUzM9uyZcsP2Fdubq6KiopIsK+oqGjUqFHW1tahoaHiq9jY2LRs2ZL+v7i4eMKECVZWVmlpaenp6WpqarNnz6YfJSUltWvXbtKkST/gKACkt337djMzs1u3blV3RkR16tTp52lS/1SZ+R4iIiKsra1HjhxZWFhY3XmplIcPHxJCLl68SP988eJFs2bNjhw5Ur25AimdOXOGPkv37t1b/FOhUHjt2jUFBYWzZ89+12wIhUL6y59IrKTCxeRXqUCysrKGDx9OCBEIBIsXL87Ly+N/mp2dvWLFCtql0d7evroyCWUq7QaupJ+2tfAj/SplGQDK9Mu/xgIgG/r169evX78fsy8VFRVNTc28vDx+ory8/MWLF0tc/sOHD8+fP+/atSv9U05O7vTp0/T/hw4dysrKqlu3Lv2zTp06r1+//m4ZryxPT89v377RTpTwn7J8+fLly5dXbx44jlu5cqXI20Y/1XuXP1Vmvgd9fX1PT8/qzkUVOHLkCCGEVbydO3cODQ2t1hxBOejr69P/aGhoiH8qEAjs7OzGjx9fWFj4XbMhEAgaN24cEhIinr2KFZMqrEC2b98+c+bMWrVqVdUGmeLi4iFDhjx8+FBJSen27du9evUSWaBGjRrbtm1r3rz51KlTc3NzqzwDv5DvdxWqRGk3sPRKbBP+DK2FHyklJeXkyZPLli3jJ8p8YwDgvwMTdAD8FwkEAukX9vDwIITIyZVQXTx48KC0j342cXFxo0aNysnJqe6MwH/UH3/8cePGDZHEcpXE7+2nygxI8AtVvCBOUVGxzGUmT57ctGnT752Tqr2FqqoC8fDwWLNmTUFBQZVsTcS6detox9j169eLR/qYKVOmjBkz5r/cYPiuV6GqVOYGRpuQEFJcXDx69OjIyEiRdDQGAGQGWooAUIb4+PgKfPRTyc3NHTZs2K+SW5A9V65c2bhxY3XnAmRBbm5uenp6decCvi8bG5sePXpUdy6qQVhY2OjRo4uLi7/HxuPi4vbu3UsIUVdXX7BggeSFN2zYIPICxH/Hd70KPwO0CalFixbR2DcAyCq8xgvws3j16tXFixe3bNlSo0YNlvj+/Xs3N7chQ4ZYWVn5+PicPHkyOTnZ3Nx84cKFampqUm45Kyvr7NmzXl5eRUVFbdu2nTNnTomLpaamXrt2TUlJafz48TTlyJEjOTk5Xl5ehJDo6Gg2bnf//v3v3r1LEwkhXl5e9CN5eXlHR0e2weTk5AsXLrx//z4/P9/S0nLEiBGNGzfm7zEzM9PNza1mzZqjR49+8OCBk5NTs2bNVq1apaqqKuUW4uLiLl++rKWlNWHChNjY2MOHDwcGBjZr1mzmzJmGhoZ0mYSEhNGjR3t7exNCbt26FRMTQwhxcHCoV69eiefhzp07QUFB7M/ffvutTZs2p06dSk1NZYmamprTpk0jhKSnp584cYImDhw40MjIiP4/ICDAzc0tKiqquLjYzMxs6NChdCxwQkhQUNCdO3f4e+zZs6eFhQUhJC0t7eTJkzTR2Nh4wIAB7DwcO3YsIiKiZs2aRkZGhoaGLVq0YO+ClUtBQcH9+/c9PDz2799PCPHw8Lhw4cK3b9+srKwWLlxYs2ZNQkhkZOTZs2cDAgLq1q07duxYOu+KiI8fP968eTMgIEBLS6tt27bjxo1jV42RcBL4y7i4uCQlJWlra5uamtasWXPw4MEKCgqvX79+/vw5XaZ169Z0Xry3b98+fvyYrbt48WL+789v3751dnbev39/Zmbmrl27IiMjp0+fToeazs/P/+eff/z8/NatW8ffe3nLl7u7+8OHDz9//mxkZNSmTZvffvtNmlecnJ2dZ86cyXFcSkoKLSkGBgZDhw4VWSwqKurEiRMfP37U0dEZPHhwie/1l1kiJHv27Nnjx48/fPigra1tZWVlb2/Pr23KlZm8vDxXV1cfH5+kpCRdXV0rK6vRo0eLdAco7+kNCAi4du3ahw8fGjZsaGpqamtrW1rnpgcPHjx9+jQ4ONjAwMDa2nrw4MFSnoHw8PBLly7Z2tq2b9+eJUpTjUiWnJx88eLF+vXrDx8+/PXr1+fOnYuNjTUzMxs8eDAt2uXNf4k3c61atTw8PFhXlIsXL/r4+BBCmjZtSscgo+Lj4+/fv+/n5xcXF2diYmJpadmvXz/xLjASyktERISbm5upqWn//v1TU1OPHj3q4+OjpaU1depUa2trQkhxcfGNGzfu3r2bnp7etm3bhQsXVqD4Z2Vl3b59OzAw8I8//sjNzT127Njz58/r1q07atSoEuNcxcXFV69eff36dUREhK6u7m+//TZgwADxHigVvjekPHXlzXZ5cRw3b968Q4cOVWanhYWF586de/36dXp6uqmpabt27WxsbJSVlaXMQ4nFhJReXYtvQZraTNzr16+HDx9Ov2oPHz6soaGhqKjIj8rl5ubeuXPH398/KCjIwMDA3Ny8f//+dEYFaRw/fpzG74YOHVpmC8rIyOjChQvi6ZVp2HAc9+bNm4sXLy5cuFBPTy8oKOj48ePh4eH6+vqOjo5NmjQhhKSnp7u4uLx48UIgEPTq1WvChAkiGZCm7q1MnSb5KkioNwghiYmJ586dCwkJSU9PNzIysrW1ZSPAUMXFxU+fPnVzc9u3b5+KisrVq1evXr1KCLG1tZ00aZLIUVS45cNx3P379x88eBAbG6ulpWVqaurg4MDempfcJiyttUCkvv2qpN0uzb7KdTJFFBUVLVy48PDhw4QQPz8/2jLp1q2blZWVyJLSlGVpGoQAUD2qdcRAAOAiIyOXLVtGG3mEkLS0NJr+119/sUejmzdvrlmzRl1d3djYWF5enhBiamoq5by9QUFBxsbGo0aN8vLy+vLly4EDB0xNTekTPpug4/z58wMGDKDvFjk6OrJ1jx07tn///i5duhBCmjRpsv9fQUFB9D903kBbW1v658GDB9m6x48f79ix49GjR69fv75s2TJFRcVatWrduHGDfnrz5s0RI0bQbKxZs+bkyZOsXcJGnZe8BXd3927dutG1lixZcuXKldq1a7ds2ZJuU1NTMygoiC759OnTXbt20Y1PmDCBZjUxMbG0M5acnNyhQwe6vJOTE51ZODY2lj1OL1u2LDs7my3/9u1bZWVlOzu7/Px8juOKi4unTZtWq1at27dv5+bmxsbGWltba2pq3r9/ny4vFApv3bqlpKRECKlVq1Z0dDR/74mJiTo6Ora2tt++faMpfn5+tWvXvnDhglAozM3NXbNmDSGEnQfpxcTETJ48mbY169Spk5CQMGjQoEaNGnXq1Ik+BNLR4nfs2KGpqdmhQwcDAwNCiKKi4j///MPfTn5+vqOj48CBA8+dO3fp0qURI0YQQlq3bv3p0ye2TJkngTp58mTjxo3fvXtHD5wGN9PT09mJpVeTTflC5x7t06cPvRBFRUV0xXXr1rVs2ZImJiUltWnThv6/ZcuWnp6ekyZNok1kc3Nztuvylq/c3Fw7Oztzc/O3b98GBgZOnjyZrtukSRNzc3Nzc3OaGXF5eXn79++3tbUlhOjo6NDbj10+Ozs7QsiIESOuXbtGM9CwYUO65UOHDolsSnKJkCw6Onro0KE9e/a8fft2WFjY5s2bCSH169f39/dny0ifmTdv3ujo6EycODEiIqKoqOjs2bOEEDs7OzbUfQWqr02bNuno6Ny6devLly979+6lEZY6deq0adPG3Nz81atX7EAGDhw4Z86cK1euODs7m5ubE0KGDh3KbpsSZWVl7d69u127djRL169fp+nSVyOluXz5ct++fWnluWnTpmXLlgkEAhZVUVBQEDl1kvMv+WZ+//79/v376bUjhMycOZPeTteuXWPbd3Z2rl279rZt20JCQry9vefPn08I6dSpEzsQybtwcnJiD3sHDx48e/ZsgwYN2rZtS6OuCgoKT58+DQ0NtbKyatKkSadOnegTXbdu3fjHWGbxDwkJGT16ND1LVlZWnz59MjIyMjAwqFOnDiFETk7u/PnzIufZy8vLzMxs9uzZz549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'}]" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "results = RAG.search(query, k=1)\n", - "results" - ] - }, - { - "cell_type": "markdown", - "id": "cfc2db00", - "metadata": {}, - "source": [ - "Here, you can also see that we have a very long `base64` attribute to our results! This is because, earlier, we requested that our index also stores the image representation of the pages with the `store_collection_with_index` flag, to make it even easier to pass to Claude. If you're indexing a lot of documents, or have limited disk space or RAM, you might want to disable this flag and instead just use the page_number to retrieve the relevant page yourself!" - ] - }, - { - "cell_type": "markdown", - "id": "c955b0a9-139b-4fcb-b4cd-262048ff23e2", - "metadata": {}, - "source": [ - "#### Getting Claude to use this context\n", - "\n", - "This is great! Page 8 is indeed where the relevant table is. But ColPali is just a retrieval model, so all Byaldi can do for you is get the relevant page. It's time for another hero to step in: Claude!\n", - "\n", - "As mentioned earlier, using images as inputs to VLMs such as Sonnet 3.5 is very simple, and we mean it! If everything works as expected Claude should tell us that the BLEU score is 27.3 for EN-DE and 38.1 for EN-FR.\n", - "\n", - "*The image passed to Claude is large so depending on your account settings you might hit a token limit. This is because we currently keep the images at a large resolution, so users are free to resize them to their liking (there is accuracy trade-offs with lower resolutions, and they're domain-dependent!). If you'd like to, you are free to do it! If you'd like an example with an already smaller image for testing, you can also skip to the next section about the financial reports.*\n", - "\n", - "Not all VLMs use the same input format for images. Claude expects bytes, so let's decode our base64 image:" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "67b865be-e12f-4833-b451-9d6867cdf0e9", - "metadata": {}, - "outputs": [], - "source": [ - "image_bytes = base64.b64decode(results[0].base64)" - ] - }, - { - "cell_type": "markdown", - "id": "c717985b", - "metadata": {}, - "source": [ - "Next, we create a `claudette` `Chat` object, which will handle all interaction with Claude in a very simple way:" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "7f5eadf3", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "('claude-3-opus-20240229',\n", - " 'claude-3-5-sonnet-20240620',\n", - " 'claude-3-haiku-20240307')" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "image_bytes = base64.b64decode(results[0].base64)chat = Chat(models[1])\n", - "# models is a claudette helper that contains the list of models available on your account, as of 2024-09-06, [1] is Claude Sonnet 3.5:\n", - "models" - ] - }, - { - "cell_type": "markdown", - "id": "3751c4f8", - "metadata": {}, - "source": [ - "And we're now all set! Moment of truth: will Claude tell us that the BLEU score is 27.3 for EN-DE and 38.1 for EN-FR?" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "b6d53f8a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "According to the table in the image, the BLEU score for the Transformer (base model) is:\n", - "\n", - "- 27.3 for EN-DE (English to German)\n", - "- 38.1 for EN-FR (English to French)\n", - "\n", - "
\n", - "\n", - "- id: `msg_01R7hnmFd4EotEbVLuE9BDYy`\n", - "- content: `[{'text': 'According to the table in the image, the BLEU score for the Transformer (base model) is:\\n\\n- 27.3 for EN-DE (English to German)\\n- 38.1 for EN-FR (English to French)', 'type': 'text'}]`\n", - "- model: `claude-3-5-sonnet-20240620`\n", - "- role: `assistant`\n", - "- stop_reason: `end_turn`\n", - "- stop_sequence: `None`\n", - "- type: `message`\n", - "- usage: `{'input_tokens': 1520, 'output_tokens': 58, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0}`\n", - "\n", - "
" - ], - "text/plain": [ - "Message(id='msg_01R7hnmFd4EotEbVLuE9BDYy', content=[TextBlock(text='According to the table in the image, the BLEU score for the Transformer (base model) is:\\n\\n- 27.3 for EN-DE (English to German)\\n- 38.1 for EN-FR (English to French)', type='text')], model='claude-3-5-sonnet-20240620', role='assistant', stop_reason='end_turn', stop_sequence=None, type='message', usage=In: 1520; Out: 58; Total: 1578)" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "chat([image_bytes, query])" - ] - }, - { - "cell_type": "markdown", - "id": "8d532214", - "metadata": {}, - "source": [ - "And here you are! A basic, functional chat-with-your-pdf app in 7 lines of code." - ] - }, - { - "cell_type": "markdown", - "id": "3f771e13-55d5-463d-aba1-f20b073978a6", - "metadata": {}, - "source": [ - "### Financial Report\n", - "\n", - "Let's move to another situation: I am an executive at a ficticious company called ACME, and I've just been given a report containing the monthly revenue for our 529 products, with just one per page. Thankfully, my assistant has forwarded me the mini-version of the report, which has the data for just our creatively named top 5 products (A, B, C, D, E).\n", - "\n", - "I personally proposed Product C, so I'm very interested in how it's doing. I'm going to ask \"In which month did Product C generate the most revenue?\". As you can see, the expected answer is **June**:\n", - "\n", - "\"A" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "e8a931d6-f28c-496c-a0c4-31bd4eb0ffd5", - "metadata": {}, - "outputs": [], - "source": [ - "query = \"In which month did Product C generate the most revenue?\"" - ] - }, - { - "cell_type": "markdown", - "id": "7fd8752a-951a-4763-baff-9a09bfc1b928", - "metadata": {}, - "source": [ - "We're going to go through the same process as for the Attention is All You Need paper, and we'll index our financial report:" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "65c2a13c-ee4e-4b45-8eb9-f466b5b2b93d", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Added page 1 of document 1 to index.\n", - "Added page 2 of document 1 to index.\n", - "Added page 3 of document 1 to index.\n", - "Added page 4 of document 1 to index.\n", - "Added page 5 of document 1 to index.\n", - "Added page 6 of document 1 to index.\n", - "Index exported to .byaldi/financial_report\n", - "Index exported to .byaldi/financial_report\n" - ] - }, - { - "data": { - "text/plain": [ - "{0: 'docs/attention.pdf', 1: 'docs/financial_report.pdf'}" - ] - }, - "execution_count": 15, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "RAG.index(\n", - " input_path=\"./docs/financial_report.pdf\",\n", - " index_name=\"financial_report\",\n", - " store_collection_with_index=True,\n", - " overwrite=True\n", - ")" - ] - }, - { - "cell_type": "markdown", - "id": "55cc7aa6-baf3-4297-ad47-3e404702009f", - "metadata": {}, - "source": [ - "Now, let's search the index for our query. We expect the top result to be page 4." - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "a3d9fec7-b262-41a4-a3e4-82b7c7356983", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "4" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "results = RAG.search(query, k=1)\n", - "results[0].page_num" - ] - }, - { - "cell_type": "markdown", - "id": "2218061e-57fd-477a-8812-d206db602e6c", - "metadata": {}, - "source": [ - "Finally, we again repeat the process we went through earlier: first, convert the top search result to bytes, then pass it to Claude with our query. If everything works as expected Claude should tell us Product C generated the most revenue in **June**." - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "8598f8af-a701-45c5-9c48-f214ef5bd4ff", - "metadata": {}, - "outputs": [ - { - "data": { - "text/markdown": [ - "According to the bar graph showing monthly revenue for Product C, the month with the highest revenue was June. The bar for June is visibly the tallest, reaching above 2500 on the revenue scale, indicating it generated the most revenue compared to all other months shown.\n", - "\n", - "
\n", - "\n", - "- id: `msg_01Q963zBYFRuKvJPFbTDGYCN`\n", - "- content: `[{'text': 'According to the bar graph showing monthly revenue for Product C, the month with the highest revenue was June. The bar for June is visibly the tallest, reaching above 2500 on the revenue scale, indicating it generated the most revenue compared to all other months shown.', 'type': 'text'}]`\n", - "- model: `claude-3-5-sonnet-20240620`\n", - "- role: `assistant`\n", - "- stop_reason: `end_turn`\n", - "- stop_sequence: `None`\n", - "- type: `message`\n", - "- usage: `{'input_tokens': 1573, 'output_tokens': 59, 'cache_creation_input_tokens': 0, 'cache_read_input_tokens': 0}`\n", - "\n", - "
" - ], - "text/plain": [ - "Message(id='msg_01Q963zBYFRuKvJPFbTDGYCN', content=[TextBlock(text='According to the bar graph showing monthly revenue for Product C, the month with the highest revenue was June. The bar for June is visibly the tallest, reaching above 2500 on the revenue scale, indicating it generated the most revenue compared to all other months shown.', type='text')], model='claude-3-5-sonnet-20240620', role='assistant', stop_reason='end_turn', stop_sequence=None, type='message', usage=In: 1573; Out: 59; Total: 1632)" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "chat = Chat(models[1])\n", - "image_bytes = base64.b64decode(results[0].base64)\n", - "chat([image_bytes, query])" - ] - }, - { - "cell_type": "markdown", - "id": "19df90e9", - "metadata": {}, - "source": [ - "Hooray! Claude gets this one right too. It seems to be pretty decent at reading documents, so why not try it with your own :)?" - ] - }, - { - "cell_type": "markdown", - "id": "d66a12c3", - "metadata": {}, - "source": [ - "### Full Attention is All You Need Example Code\n", - "\n", - "Below is the full Python code for the notebook for easier copy-pasting:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9c56b198", - "metadata": {}, - "outputs": [], - "source": [ - "import base64\n", - "import os\n", - "os.environ[\"HF_TOKEN\"] = \"YOUR_HF_TOKEN\" # to download the ColPali model\n", - "os.environ[\"ANTHROPIC_API_KEY\"] = \"YOUR_ANTHROPIC_API_KEY\"\n", - "from byaldi import RAGMultiModalModel\n", - "from claudette import *\n", - "\n", - "# Load model\n", - "RAG = RAGMultiModalModel.from_pretrained(\"vidore/colpali-v1.2\", verbose=1)\n", - "\n", - "# Index document\n", - "RAG.index(\n", - " input_path=\"./docs/attention.pdf\",\n", - " index_name=\"attention\",\n", - " store_collection_with_index=True,\n", - " overwrite=True\n", - ")\n", - "\n", - "# Define query\n", - "query = \"What's the BLEU score for the transformer base model?\"\n", - "\n", - "# Query model\n", - "results = RAG.search(query, k=1)\n", - "\n", - "# Pass top result to Claude\n", - "image_bytes = base64.b64decode(results[0].base64)\n", - "chat = Chat(models[1])\n", - "\n", - "# This will print claude's answer\n", - "print(chat([image_bytes, query]))" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "base", - "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.10.14" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/docs/attention.pdf b/examples/docs/attention.pdf deleted file mode 100644 index 97d7c51..0000000 Binary files a/examples/docs/attention.pdf and /dev/null differ diff --git a/examples/docs/attention_table.png b/examples/docs/attention_table.png deleted file mode 100644 index ea37459..0000000 Binary files a/examples/docs/attention_table.png and /dev/null differ diff --git a/examples/docs/attention_with_a_mustache.pdf b/examples/docs/attention_with_a_mustache.pdf deleted file mode 100644 index 97d7c51..0000000 Binary files a/examples/docs/attention_with_a_mustache.pdf and /dev/null differ diff --git a/examples/docs/financial_report.pdf b/examples/docs/financial_report.pdf deleted file mode 100644 index e6bbb6d..0000000 Binary files a/examples/docs/financial_report.pdf and /dev/null differ diff --git a/examples/docs/product_c.png b/examples/docs/product_c.png deleted file mode 100644 index de6200c..0000000 Binary files a/examples/docs/product_c.png and /dev/null differ diff --git a/examples/quick_overview.ipynb b/examples/quick_overview.ipynb deleted file mode 100644 index eeeca7c..0000000 --- a/examples/quick_overview.ipynb +++ /dev/null @@ -1,494 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Verbosity is set to 1 (active). Pass verbose=0 to make quieter.\n" - ] - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "`Qwen2VLRotaryEmbedding` can now be fully parameterized by passing the model config through the `config` argument. All other arguments will be removed in v4.46\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "5514749b070c4a679a7c4b40fc0396fe", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Loading checkpoint shards: 0%| | 0/2 [00:00] 2.11M --.-KB/s in 0.06s \n", - "\n", - "2024-11-13 07:58:42 (33.3 MB/s) - ‘1706.03762’ saved [2215244/2215244]\n", - "\n", - "mkdir: cannot create directory ‘docs’: File exists\n" - ] - } - ], - "source": [ - "# Let's get everyone's favourite paper in here\n", - "!wget https://arxiv.org/pdf/1706.03762\n", - "!mkdir docs\n", - "!mv 1706.03762 docs/attention.pdf\n", - "!cp -r docs/attention.pdf docs/attention_with_a_mustache.pdf" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "overwrite is on. Deleting existing index attention_index to build a new one.\n", - "Indexing file: docs/financial_report.pdf\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Added page 1 of document 0 to index.\n", - "Added page 2 of document 0 to index.\n", - "Added page 3 of document 0 to index.\n", - "Added page 4 of document 0 to index.\n", - "Added page 5 of document 0 to index.\n", - "Added page 6 of document 0 to index.\n", - "Index exported to .byaldi/attention_index\n", - "Indexing file: docs/product_c.png\n", - "Added page 1 of document 1 to index.\n", - "Index exported to .byaldi/attention_index\n", - "Indexing file: docs/attention.pdf\n", - "Added page 1 of document 2 to index.\n", - "Added page 2 of document 2 to index.\n", - "Added page 3 of document 2 to index.\n", - "Added page 4 of document 2 to index.\n", - "Added page 5 of document 2 to index.\n", - "Added page 6 of document 2 to index.\n", - "Added page 7 of document 2 to index.\n", - "Added page 8 of document 2 to index.\n", - "Added page 9 of document 2 to index.\n", - "Added page 10 of document 2 to index.\n", - "Added page 11 of document 2 to index.\n", - "Added page 12 of document 2 to index.\n", - "Added page 13 of document 2 to index.\n", - "Added page 14 of document 2 to index.\n", - "Added page 15 of document 2 to index.\n", - "Index exported to .byaldi/attention_index\n", - "Indexing file: docs/attention_with_a_mustache.pdf\n", - "Added page 1 of document 3 to index.\n", - "Added page 2 of document 3 to index.\n", - "Added page 3 of document 3 to index.\n", - "Added page 4 of document 3 to index.\n", - "Added page 5 of document 3 to index.\n", - "Added page 6 of document 3 to index.\n", - "Added page 7 of document 3 to index.\n", - "Added page 8 of document 3 to index.\n", - "Added page 9 of document 3 to index.\n", - "Added page 10 of document 3 to index.\n", - "Added page 11 of document 3 to index.\n", - "Added page 12 of document 3 to index.\n", - "Added page 13 of document 3 to index.\n", - "Added page 14 of document 3 to index.\n", - "Added page 15 of document 3 to index.\n", - "Index exported to .byaldi/attention_index\n", - "Indexing file: docs/attention_table.png\n", - "Added page 1 of document 4 to index.\n", - "Index exported to .byaldi/attention_index\n", - "Index exported to .byaldi/attention_index\n", - "Search results for 'what's the BLEU score of this new strange method?':\n", - "Doc ID: 2, Page: 1, Score: 14.9375\n", - "Doc ID: 3, Page: 1, Score: 14.9375\n", - "Doc ID: 3, Page: 8, Score: 14.6875\n", - "Doc ID: 2, Page: 8, Score: 14.6875\n", - "Doc ID: 4, Page: 1, Score: 14.5625\n", - "Test completed successfully!\n" - ] - } - ], - "source": [ - "# Test indexing\n", - "metadata = [{\"filename\":file_name} for file_name in os.listdir(\"docs\")]\n", - "\n", - "index_name = \"attention_index\"\n", - "model.index(\n", - " input_path=Path(\"docs/\"),\n", - " index_name=index_name,\n", - " store_collection_with_index=False,\n", - " metadata=metadata,\n", - " overwrite=True\n", - ")\n", - "\n", - "# BLEU tables are on page 8 and 9. We've indexed the pdf and its evil mustached twin, so we should see similar scores occur twice for every relevant page.\n", - "query = \"what's the BLEU score of this new strange method?\"\n", - "results = model.search(query, k=5)\n", - "\n", - "print(f\"Search results for '{query}':\")\n", - "for result in results:\n", - " print(f\"Doc ID: {result.doc_id}, Page: {result.page_num}, Score: {result.score}\")\n", - "\n", - "print(\"Test completed successfully!\")" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "62.5 ms ± 1.28 ms per loop (mean ± std. dev. of 7 runs, 10 loops each)\n" - ] - } - ], - "source": [ - "%%timeit\n", - "model.search(query, k=3)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Verbosity is set to 1 (active). Pass verbose=0 to make quieter.\n" - ] - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "e827b1252bb843bbad57550986c88e4f", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "Loading checkpoint shards: 0%| | 0/2 [00:00 bool: + """Deprecated compat property — use self.storage_backend instead.""" + return self._storage_qdrant_compat + + @storage_qdrant.setter + def storage_qdrant(self, value: bool) -> None: + self._storage_qdrant_compat = value + + def _resolve_model_and_processor_classes(self): + """Return (model_cls, processor_cls) for the configured model name.""" + name = self.pretrained_model_name_or_path.lower() + if "colpali" in name: + return ColPali, ColPaliProcessor + elif "colqwen3.5" in name or "colqwen3_5" in name: + if not _COLQWEN3_5_AVAILABLE: + raise ImportError( + "ColQwen3_5 requires colpali-engine>=0.3.15. " + "Upgrade with: pip install 'colpali-engine>=0.3.15'" + ) + return ColQwen3_5, ColQwen3_5Processor + elif "colqwen2.5" in name: + return ColQwen2_5, ColQwen2_5_Processor + else: + # ColQwen2 and ColQwen3 (non-3.5) both use the ColQwen2 interface + return ColQwen2, ColQwen2Processor + + def _load_model_and_processor(self): + token = self.kwargs.get("hf_token", None) or os.environ.get("HF_TOKEN") + is_cuda = "cuda" in str(self.device) # ok for [Union[str, torch.device]] + device_map = str(self.device) if is_cuda else None + + model_cls, processor_cls = self._resolve_model_and_processor_classes() + + quantization_config = None + if self.load_in_4bit or self.load_in_8bit: + try: + from transformers import BitsAndBytesConfig + except ImportError as exc: + raise ImportError( + "4-bit/8-bit quantization requires the bitsandbytes package.\n" + "Install it with: pip install \"foretrieval[quantization]\"\n" + "or: uv add foretrieval --extra quantization" + ) from exc + if not is_cuda: + raise ValueError( + "4-bit/8-bit quantization requires a CUDA device. " + f"Current device: {self.device}" + ) + _dtype_map = { + "float16": torch.float16, + "bfloat16": torch.bfloat16, + "float32": torch.float32, + } + compute_dtype = _dtype_map.get(self.bnb_4bit_compute_dtype, torch.float16) + quantization_config = BitsAndBytesConfig( + load_in_4bit=self.load_in_4bit, + load_in_8bit=self.load_in_8bit, + bnb_4bit_quant_type=self.bnb_4bit_quant_type, + bnb_4bit_compute_dtype=compute_dtype, + ) + + load_kwargs: Dict[str, Any] = dict( + torch_dtype=torch.bfloat16, + device_map=device_map, + token=token, + ) + if quantization_config is not None: + load_kwargs["quantization_config"] = quantization_config + + self.model = model_cls.from_pretrained( + self.pretrained_model_name_or_path, + **load_kwargs, + ) + self.processor = processor_cls.from_pretrained( + self.pretrained_model_name_or_path, + token=token, + ) + + self.model = self.model.eval() + if device_map is None: + self.model = self.model.to(self.device) + + def _load_processor_only(self): + """Load only the processor (no model weights) for remote embedding mode.""" + token = self.kwargs.get("hf_token", None) or os.environ.get("HF_TOKEN") + _, processor_cls = self._resolve_model_and_processor_classes() + self.processor = processor_cls.from_pretrained( + self.pretrained_model_name_or_path, + token=token, + ) + self.model = None + + # ------------------------------------------------------------------ + # Bookkeeping persistence + # + # The index-level bookkeeping (model name, doc metadata, file-name map, + # per-embedding extras and a few scalar flags) is normally written to + # local sidecar files under ``index_root/index_name``. When the active + # vector store stores this on the server (``supports_remote_bookkeeping``), + # we route the blob through the store instead so the client needs no local + # index directory at all — this is the remote ``vector_db_server`` mode. + # ------------------------------------------------------------------ + + def _uses_remote_bookkeeping(self) -> bool: + if self.storage_backend != "remote": + return False + supports = getattr(self.vector_store, "supports_remote_bookkeeping", None) + try: + return bool(supports()) if callable(supports) else False + except Exception: + return False + + def _build_bookkeeping_blob(self, description: str = "") -> Dict[str, Any]: + """Assemble the in-memory bookkeeping into a single serialisable blob.""" + index_config = { + "model_name": self.model_name, + "full_document_collection": self.full_document_collection, + "highest_doc_id": self.highest_doc_id, + "resize_stored_images": ( + True if self.max_image_width and self.max_image_height else False + ), + "max_image_width": self.max_image_width, + "max_image_height": self.max_image_height, + "library_version": VERSION, + "storage_backend": self.storage_backend, + # storage_config is intentionally omitted: in remote mode the + # connection config comes from the caller (vector_db_server config), + # never from persisted bookkeeping. + "storage_config": None, + "description": description, + } + return { + "index_config": index_config, + "embed_id_to_extra": self.embed_id_to_extra, + "doc_ids_to_file_names": self.doc_ids_to_file_names, + "doc_id_to_metadata": self.doc_id_to_metadata, + } + + def _apply_bookkeeping_blob(self, blob: Dict[str, Any]) -> None: + """Populate in-memory bookkeeping from a server-loaded blob.""" + index_config = blob.get("index_config", {}) or {} + self.full_document_collection = index_config.get( + "full_document_collection", False + ) + self.resize_stored_images = index_config.get("resize_stored_images", False) + self.max_image_width = index_config.get("max_image_width", None) + self.max_image_height = index_config.get("max_image_height", None) + self.index_description = index_config.get("description", "") + + self.embed_id_to_extra = { + int(k): v for k, v in (blob.get("embed_id_to_extra") or {}).items() + } + self.doc_ids_to_file_names = { + int(k): v for k, v in (blob.get("doc_ids_to_file_names") or {}).items() + } + self.doc_id_to_metadata = { + int(k): v for k, v in (blob.get("doc_id_to_metadata") or {}).items() + } + # Derive doc_ids / highest_doc_id from the file-names map (always + # present in bookkeeping, even when add_metadata=False). + id_source = self.doc_ids_to_file_names or self.doc_id_to_metadata + self.highest_doc_id = max(id_source.keys(), default=-1) + self.doc_ids = set(id_source.keys()) + + def _load_index_state(self): + + # Remote bookkeeping mode: pull everything from the server, no local + # index directory is read. + if self.storage_backend == "remote" and self._uses_remote_bookkeeping(): + blob = self.vector_store.load_bookkeeping() + if blob is None: + raise FileNotFoundError( + f"No bookkeeping found on the server for collection " + f"'{self.index_name}'. The collection may not have been " + "indexed yet." + ) + self._apply_bookkeeping_blob(blob) + return + + index_path = Path(self.index_root) / self.index_name + index_config_path = index_path / "index_config.json.gz" + index_config: dict = srsly.read_gzip_json(index_config_path) + self.full_document_collection = index_config.get("full_document_collection", False) + self.resize_stored_images = index_config.get("resize_stored_images", False) + self.max_image_width = index_config.get("max_image_width", None) + self.max_image_height = index_config.get("max_image_height", None) + self.index_description = index_config.get("description", "") + + if self.full_document_collection: + collection_path = index_path / "collection" + json_files = sorted( + collection_path.glob("*.json.gz"), + key=lambda x: int(x.stem.split(".")[0]), + ) + for json_file in json_files: + loaded_data = srsly.read_gzip_json(json_file) + self.collection.update({int(k): v for k, v in loaded_data.items()}) + + # Load sidecar files (metadata, filenames, extras) + self._load_local_sidecars(index_path) + + # Load vector store sidecar (embeddings for local; nothing for qdrant/milvus) + self.vector_store.load_sidecar(index_path) + + # Inject metadata map and processor into local store + if isinstance(self.vector_store, LocalVectorStore): + self.vector_store.set_processor(self.processor) + self.vector_store.set_doc_id_to_metadata(self.doc_id_to_metadata) + + # Derive doc_ids / highest_doc_id from the file-names map (always + # written, even when add_metadata=False) so that a no-metadata index + # loads correctly. Fall back to the metadata map only when the + # file-names map is empty (e.g. legacy in-memory-only indexes). + id_source = self.doc_ids_to_file_names or self.doc_id_to_metadata + self.highest_doc_id = max(id_source.keys(), default=-1) + self.doc_ids = set(id_source.keys()) + + def _load_local_sidecars(self, index_path: Path): + extra_path = index_path / "embed_id_to_extra.pt" + if extra_path.exists(): + self.embed_id_to_extra = torch.load(extra_path, map_location="cpu") + self.embed_id_to_extra = {int(k): v for k, v in self.embed_id_to_extra.items()} + else: + self.embed_id_to_extra = {} + + doc_names_path = index_path / "doc_ids_to_file_names.json.gz" + if doc_names_path.exists(): + self.doc_ids_to_file_names = srsly.read_gzip_json(doc_names_path) + self.doc_ids_to_file_names = {int(k): v for k, v in self.doc_ids_to_file_names.items()} + else: + self.doc_ids_to_file_names = {} + + metadata_path = index_path / "metadata.json.gz" + if metadata_path.exists(): + self.doc_id_to_metadata = srsly.read_gzip_json(metadata_path) + self.doc_id_to_metadata = {int(k): v for k, v in self.doc_id_to_metadata.items()} + else: + self.doc_id_to_metadata = {} + + def set_enable_heatmaps_and_circle(self, enable_heatmaps: bool, enable_circle: bool): + self.enable_heatmaps = enable_heatmaps + self.enable_circle = enable_circle + + # ============================================================ + # Persistence and index export + # ============================================================ + + @classmethod + def from_pretrained( + cls, + pretrained_model_name_or_path: Union[str, Path], + ingestion: Dict[str, Any] = {"backend": "default"}, + n_gpu: int = -1, + verbose: int = 1, + device: Optional[Union[str, torch.device]] = None, + index_root: str = ".foretrieval", + embedding_server: Optional[EmbeddingServerConfig] = None, + storage_backend: str = "local", + storage_config: Optional[Dict[str, Any]] = None, + # Deprecated + storage_qdrant: Optional[bool] = None, + **kwargs, + ): + return cls( + pretrained_model_name_or_path=pretrained_model_name_or_path, + ingestion=ingestion, + n_gpu=n_gpu, + verbose=verbose, + load_from_index=False, + index_root=index_root, + device=device, + embedding_server=embedding_server, + storage_backend=storage_backend, + storage_config=storage_config, + storage_qdrant=storage_qdrant, + **kwargs, + ) + + @staticmethod + def _fetch_remote_model_name( + index_name: str, storage_config: Dict[str, Any] + ) -> str: + """Fetch the indexed model name from the remote server bookkeeping. + + Used by from_index() in remote mode to bootstrap the instance before + any local state exists. Raises if the server has no bookkeeping for + the collection. + """ + from .vector_store.factory import make_vector_store + + vs = make_vector_store("remote", storage_config) + vs.open(index_name, Path("."), create=False) + try: + blob = vs.load_bookkeeping() + finally: + try: + vs.close() + except Exception: + pass + if not blob: + raise FileNotFoundError( + f"No bookkeeping found on the server for collection " + f"'{index_name}'. Has it been indexed?" + ) + model_name = (blob.get("index_config", {}) or {}).get("model_name") + if not model_name: + raise ValueError( + f"Server bookkeeping for '{index_name}' has no model_name." + ) + return model_name + + @classmethod + def from_index( + cls, + index_path: Union[str, Path], + n_gpu: int = -1, + verbose: int = 1, + device: Optional[Union[str, torch.device]] = None, + index_root: str = ".foretrieval", + embedding_server: Optional[EmbeddingServerConfig] = None, + storage_backend: Optional[str] = None, + storage_config: Optional[Dict[str, Any]] = None, + **kwargs, + ): + # Remote bookkeeping mode: the connection config is supplied by the + # caller (from the vector_db_server config), and the model name plus + # all index state live on the server — no local index directory is + # read. Triggered by storage_backend="remote". + if (storage_backend or "").strip().lower() == "remote": + index_name = Path(index_path).name + model_name = cls._fetch_remote_model_name(index_name, storage_config or {}) + instance = cls( + pretrained_model_name_or_path=model_name, + n_gpu=n_gpu, + index_name=index_name, + verbose=verbose, + load_from_index=True, + index_root=index_root, + device=device, + storage_backend="remote", + storage_config=storage_config, + embedding_server=embedding_server, + **kwargs, + ) + return instance + + index_path = Path(os.path.join(Path(index_root), Path(index_path))) + index_config: dict = srsly.read_gzip_json(index_path / "index_config.json.gz") + disk_backend = index_config.get("storage_backend", "local") + # Caller-supplied backend wins if given, else use on-disk value. + storage_backend = (storage_backend or disk_backend) + + # For the remote backend, merge on-disk storage_config (URL, backend, …) + # with caller-supplied storage_config (api_key, etc.). Caller wins on + # overlap so that credentials can be injected at load time without + # modifying the on-disk index. + disk_storage_config = index_config.get("storage_config") or {} + if disk_storage_config and storage_backend == "remote": + merged_storage_config = {**disk_storage_config, **(storage_config or {})} + else: + merged_storage_config = storage_config + + instance = cls( + pretrained_model_name_or_path=index_config["model_name"], + n_gpu=n_gpu, + index_name=index_path.name, + verbose=verbose, + load_from_index=True, + index_root=str(index_path.parent), + device=device, + storage_backend=storage_backend, + storage_config=merged_storage_config, + embedding_server=embedding_server, + **kwargs, + ) + instance.index_description = index_config.get("description", "") + return instance + + def _export_index(self, description: str = ""): + if self.index_name is None: + raise ValueError("No index name specified. Cannot export.") + + # Remote bookkeeping mode: push everything to the server, no local + # index directory is written. + if self.storage_backend == "remote" and self._uses_remote_bookkeeping(): + if not description: + existing = self.vector_store.load_bookkeeping() + if existing: + description = (existing.get("index_config", {}) or {}).get( + "description", "" + ) + blob = self._build_bookkeeping_blob(description=description) + self.vector_store.export_bookkeeping(blob) + if self.verbose > 0: + print( + f"Index bookkeeping stored on server for " + f"collection '{self.index_name}'" + ) + return + + index_path = Path(self.index_root) / self.index_name + index_path.mkdir(parents=True, exist_ok=True) + + # Preserve existing description on incremental updates when none is supplied + if not description: + cfg_path = index_path / "index_config.json.gz" + if cfg_path.exists(): + try: + description = srsly.read_gzip_json(cfg_path).get("description", "") + except Exception: + pass + + # For the remote backend, persist the storage_config so from_index() + # can reconstruct the client. Sensitive fields (api_key) are stripped + # and must be re-supplied by the caller at load time. + persisted_storage_config: Optional[Dict[str, Any]] = None + if self.storage_backend == "remote" and self.storage_config: + persisted_storage_config = { + k: v + for k, v in self.storage_config.items() + if k != "api_key" + } + + index_config = { + "model_name": self.model_name, + "full_document_collection": self.full_document_collection, + "highest_doc_id": self.highest_doc_id, + "resize_stored_images": ( + True if self.max_image_width and self.max_image_height else False + ), + "max_image_width": self.max_image_width, + "max_image_height": self.max_image_height, + "library_version": VERSION, + "storage_backend": self.storage_backend, + "storage_config": persisted_storage_config, + "description": description, + } + srsly.write_gzip_json(index_path / "index_config.json.gz", index_config) + + # Shared sidecar files + torch.save(self.embed_id_to_extra, index_path / "embed_id_to_extra.pt") + srsly.write_gzip_json(index_path / "doc_ids_to_file_names.json.gz", self.doc_ids_to_file_names) + srsly.write_gzip_json(index_path / "metadata.json.gz", self.doc_id_to_metadata) + + if self.full_document_collection: + collection_path = index_path / "collection" + collection_path.mkdir(exist_ok=True) + for i in range(0, len(self.collection), 500): + chunk = dict(list(self.collection.items())[i : i + 500]) + srsly.write_gzip_json(collection_path / f"{i}.json.gz", chunk) + + # Delegate vector persistence to the backend + self.vector_store.export_sidecar(index_path) + + if self.verbose > 0: + print(f"Index exported to {index_path}") + + # ============================================================ + # Index building and ingestion + # ============================================================ + + def _cleanup_failed_index(self, index_name: str) -> None: + """Remove artefacts created by a failed ``index()`` call. + + Called when indexing raises an exception after the vector store and/or + local index directory were already created, so the caller can retry + with the same index name without hitting "index already exists". + + For the local backend the index directory is removed with + ``shutil.rmtree``. For the remote (server-side bookkeeping) backend + the remote collection is deleted via the server client. Errors during + cleanup are logged as warnings and swallowed so that the original + exception propagates cleanly. + """ + # Reset in-memory state so the object is reusable after cleanup. + self.index_name = None + self.highest_doc_id = -1 + self.doc_ids = set() + self.doc_ids_to_file_names = {} + self.doc_id_to_metadata = {} + + # Local backend: remove the partial index directory. + index_path = Path(self.index_root) / index_name + if index_path.exists(): + try: + shutil.rmtree(index_path) + logger.info( + "Cleaned up partial local index directory: %s", index_path + ) + except Exception as exc: # noqa: BLE001 + logger.warning( + "Could not remove partial index directory %s: %s", + index_path, + exc, + ) + + # Remote backend: delete the collection on the server. + if self.storage_backend == "remote" and self._uses_remote_bookkeeping(): + from .vector_store.remote import RemoteVectorStore + if isinstance(self.vector_store, RemoteVectorStore): + try: + self.vector_store._client.delete_collection(index_name) + logger.info( + "Cleaned up partial remote collection: %s", index_name + ) + except Exception as exc: # noqa: BLE001 + logger.warning( + "Could not delete remote collection %s: %s", + index_name, + exc, + ) + + def index( + self, + input_path: Union[str, Path], + index_name: Optional[str] = None, + doc_ids: Optional[List[int]] = None, + store_collection_with_index: bool = False, + overwrite: bool = False, + metadata: Optional[Union[List[DocMetadata], Dict[int, DocMetadata]]] = None, + max_image_width: Optional[int] = None, + max_image_height: Optional[int] = None, + batch_size: int = 1, + description: str = "", + ai_cfg: Optional[Dict[str, Any]] = None, + on_progress: Optional[Callable[[Dict[str, Any]], None]] = None, + ) -> Union[Dict[int, str], None]: + if ( + self.index_name is not None + and (index_name is None or self.index_name == index_name) + and not overwrite + ): + raise ValueError( + f"An index named {self.index_name} is already loaded.", + "Use add_to_index() to add to it or search() to query it.", + "Pass a new index_name to create a new index.", + "Exiting indexing without doing anything...", + ) + if index_name is None: + raise ValueError("index_name must be specified to create a new index.") + + index_path = Path(os.path.join(Path(self.index_root), Path(index_name))) + # In remote bookkeeping mode the local index directory is irrelevant + # (vectors and bookkeeping live on the server), so skip the local-dir + # existence guard entirely. + if ( + not (self.storage_backend == "remote" and self._uses_remote_bookkeeping()) + and index_path.exists() + ): + if not overwrite and ( + (index_path.is_dir() and len(list(index_path.iterdir())) > 0) + or index_path.is_file() + ): + logger.warning( + f"An index named {index_name} already exists.", + "Use overwrite=True to delete the existing index and build a new one.", + "Exiting indexing without doing anything...", + ) + return None + else: + logger.info( + f"overwrite is on. Deleting existing index {index_name} to build a new one." + ) + shutil.rmtree(index_path) + + if store_collection_with_index: + self.full_document_collection = True + self.index_name = index_name + + # Open / create vector store for the new index + self.vector_store.open( + index_name, + Path(self.index_root), + create=True, + ) + if isinstance(self.vector_store, LocalVectorStore): + self.vector_store.set_processor(self.processor) + self.vector_store.set_doc_id_to_metadata(self.doc_id_to_metadata) + + self.max_image_width = max_image_width + self.max_image_height = max_image_height + + input_path = Path(input_path) + if not hasattr(self, "highest_doc_id") or overwrite is True: + self.highest_doc_id = -1 + + # Validate before entering the try block so that validation errors also + # trigger cleanup of the just-opened vector store / local directory. + if input_path.is_dir(): + items = sorted( + (p for p in input_path.rglob("*") if p.is_file()), + key=lambda p: p.relative_to(input_path), + ) + if doc_ids is not None and len(doc_ids) != len(items): + self._cleanup_failed_index(index_name) + raise ValueError( + f"Number of doc_ids ({len(doc_ids)}) does not match number of documents ({len(items)})" + ) + if metadata is not None and len(metadata) != len(items): + self._cleanup_failed_index(index_name) + raise ValueError( + f"Number of metadata entries ({len(metadata)}) does not match number of documents ({len(items)})" + ) + else: + items = None # single-file path handled below + + try: + if items is not None: + # Directory indexing path + n_files = len(items) + if on_progress is not None: + try: + on_progress({"stage": "start", "n_files": n_files}) + except Exception: # noqa: BLE001 + pass + for i, item in tqdm( + enumerate(items), total=n_files, desc="Indexing files" + ): + doc_id = doc_ids[i] if doc_ids else self.highest_doc_id + 1 + if metadata is None: + doc_md = None + elif isinstance(metadata, list): + doc_md = metadata[i] + elif isinstance(metadata, dict): + doc_md = metadata.get(doc_id) + else: + doc_md = metadata[doc_id] if metadata else None + + if on_progress is not None: + try: + on_progress({ + "stage": "file_start", + "file": item.name, + "file_idx": i, + "n_files": n_files, + }) + except Exception: # noqa: BLE001 + pass + + try: + self.add_to_index( + item, + store_collection_with_index, + doc_id=doc_id, + metadata=doc_md, + batch_size=batch_size, + on_progress=on_progress, + _file_idx=i, + _n_files=n_files, + ) + except Exception as e: + logger.warning(f"Skipping faulty PDF {item}:\n{str(e)}") + continue + + if on_progress is not None: + try: + on_progress({ + "stage": "file_done", + "file": item.name, + "file_idx": i, + "n_files": n_files, + }) + except Exception: # noqa: BLE001 + pass + + else: + # Single-file indexing path + if metadata is not None and len(metadata) != 1: + raise ValueError( + "For a single document, metadata should be a list with one dictionary" + ) + doc_id = doc_ids[0] if doc_ids else self.highest_doc_id + 1 + doc_metadata = metadata[0] if metadata else None + if on_progress is not None: + try: + on_progress({"stage": "start", "n_files": 1}) + on_progress({ + "stage": "file_start", + "file": input_path.name, + "file_idx": 0, + "n_files": 1, + }) + except Exception: # noqa: BLE001 + pass + self.add_to_index( + input_path, + store_collection_with_index, + doc_id=doc_id, + metadata=doc_metadata, + on_progress=on_progress, + _file_idx=0, + _n_files=1, + ) + if on_progress is not None: + try: + on_progress({ + "stage": "file_done", + "file": input_path.name, + "file_idx": 0, + "n_files": 1, + }) + except Exception: # noqa: BLE001 + pass + + if on_progress is not None: + try: + on_progress({"stage": "all_done"}) + except Exception: # noqa: BLE001 + pass + + # Auto-generate index description from per-doc AI metadata when available + if not description and ai_cfg and self.doc_id_to_metadata: + from .metadata import generate_index_description + description = generate_index_description(self.doc_id_to_metadata, ai_cfg) + + self._export_index(description=description) + if self.highest_doc_id == -1: + logger.warning("No documents were indexed.") + + except Exception: + # Indexing failed after the vector store / local directory were + # already created. Remove the partial artefacts so the caller can + # retry cleanly. + self._cleanup_failed_index(index_name) + raise + + return self.doc_ids_to_file_names + + def add_to_index( + self, + input_item: Union[str, Path, Image.Image, List[Union[str, Path, Image.Image]]], + store_collection_with_index: bool, + doc_id: Optional[Union[int, List[int]]] = None, + metadata: Optional[Union[List[DocMetadata], DocMetadata]] = None, + batch_size: int = 1, + on_progress: Optional[Callable[[Dict[str, Any]], None]] = None, + _file_idx: int = 0, + _n_files: int = 1, + ) -> Dict[int, str]: + if self.index_name is None: + raise ValueError( + "No index loaded. Use index() to create or load an index first." + ) + if not hasattr(self, "highest_doc_id"): + self.highest_doc_id = -1 + + # Ensure vector store is open for this index. + # We check if the client is initialised (not whether the collection exists), + # to avoid re-opening when the collection was just opened but not yet populated. + if not self._vector_store_is_open(): + self.vector_store.open( + self.index_name, + Path(self.index_root), + create=True, + ) + if isinstance(self.vector_store, LocalVectorStore): + self.vector_store.set_processor(self.processor) + self.vector_store.set_doc_id_to_metadata(self.doc_id_to_metadata) + + # Convert single inputs to lists for uniform processing + if isinstance(input_item, (str, Path)) and Path(input_item).is_dir(): + input_items = list(Path(input_item).iterdir()) + else: + input_items = ( + [input_item] if not isinstance(input_item, list) else input_item + ) + + doc_ids = ( + [doc_id] + if isinstance(doc_id, int) + else (doc_id if doc_id is not None else None) + ) + + if doc_ids and len(doc_ids) != len(input_items): + raise ValueError( + f"Number of doc_ids ({len(doc_ids)}) does not match number of input items ({len(input_items)})" + ) + + for i, item in enumerate(input_items): + current_doc_id = doc_ids[i] if doc_ids else self.highest_doc_id + 1 + i + current_metadata = metadata if metadata else None + + if current_doc_id in self.doc_ids: + raise ValueError( + f"Document ID {current_doc_id} already exists in the index" + ) + + self.highest_doc_id = max(self.highest_doc_id, current_doc_id) + + if isinstance(item, (str, Path)): + item_path = Path(item) + if item_path.is_dir(): + self._process_directory( + item_path, + store_collection_with_index, + current_doc_id, + current_metadata, + batch_size, + on_progress=on_progress, + _file_idx=_file_idx, + _n_files=_n_files, + ) + else: + stored_path = self._process_and_add_to_index( + item_path, + store_collection_with_index, + current_doc_id, + current_metadata, + batch_size, + on_progress=on_progress, + _file_idx=_file_idx, + _n_files=_n_files, + ) + if stored_path is None: + self.doc_ids_to_file_names[current_doc_id] = "In-memory Image" + else: + self.doc_ids_to_file_names[current_doc_id] = str(stored_path) + + elif isinstance(item, Image.Image): + self._process_and_add_to_index( + item, store_collection_with_index, current_doc_id, current_metadata, + on_progress=on_progress, + _file_idx=_file_idx, + _n_files=_n_files, + ) + self.doc_ids_to_file_names[current_doc_id] = "In-memory Image" + else: + raise ValueError(f"Unsupported input type: {type(item)}") + + self._export_index() + return self.doc_ids_to_file_names + + def _process_directory( + self, + directory: Path, + store_collection_with_index: bool, + base_doc_id: int, + metadata: Optional[Dict[str, Union[str, int]]], + batch_size: int, + on_progress: Optional[Callable[[Dict[str, Any]], None]] = None, + _file_idx: int = 0, + _n_files: int = 1, + ): + files = sorted( + (p for p in directory.rglob("*") if p.is_file()), + key=lambda p: p.relative_to(directory), + ) + for i, item in tqdm(enumerate(files), total=len(files), desc=f"Indexing {directory.name}"): + logger.debug(f"Indexing file: {item}") + current_doc_id = base_doc_id + i + stored_path = self._process_and_add_to_index( + item, store_collection_with_index, current_doc_id, metadata, batch_size, + on_progress=on_progress, + _file_idx=_file_idx, + _n_files=_n_files, + ) + if stored_path is None: + self.doc_ids_to_file_names[current_doc_id] = "In-memory Image" + else: + self.doc_ids_to_file_names[current_doc_id] = str(stored_path) + + def _process_and_add_to_index( + self, + item: Union[Path, Image.Image], + store_collection_with_index: bool, + doc_id: Union[str, int], + metadata: Optional[Dict[str, Union[str, int]]] = None, + batch_size: int = 1, + on_progress: Optional[Callable[[Dict[str, Any]], None]] = None, + _file_idx: int = 0, + _n_files: int = 1, + ) -> Optional[Path]: + """ + Process and index an image or any file (converted to PDF if needed). + Returns the 'canonical' path (PDF or image) used, or None for in-memory images. + """ + def _emit_page(file_name: str, page_idx: int, n_pages: int) -> None: + if on_progress is None: + return + try: + on_progress({ + "stage": "page", + "file": file_name, + "file_idx": _file_idx, + "n_files": _n_files, + "page_idx": page_idx, + "n_pages": n_pages, + }) + except Exception: # noqa: BLE001 + pass + + if isinstance(item, Path): + ext = item.suffix.lower() + + # 0) docling chunking (if enabled) + if self.ingestion_backend == "docling": + + if ext == ".pdf": + pdf_file = item.resolve() + else: + existing_pdf = self._find_existing_pdf(item) + if existing_pdf is not None: + pdf_file = existing_pdf + else: + pdf_file = _convert_to_pdf(item) + if pdf_file is None: + logger.warning(f"Docling ingestion: failed to convert {item} to PDF. Skipping.") + return None + + if self.docling_dir is None: + assert self.index_name is not None, "index_name must be set to use docling ingestion" + self.docling_dir = Path(self.index_root) / self.index_name / "docling_chunks" + self.docling_dir.mkdir(parents=True, exist_ok=True) + chunks = chunk_pdf_to_images(pdf_file, output_dir=self.docling_dir) + + n_chunks = len(chunks) + for i in range(0, n_chunks, batch_size): + batch_chunks, batch_page_ids, batch_chunk_ids = [], [], [] + for j in range(i, min(i + batch_size, n_chunks)): + ch = chunks[j] + image = Image.open(ch.path) + batch_chunks.append(image) + batch_page_ids.append(ch.page_id) + batch_chunk_ids.append(ch.elem_id) + _emit_page(item.name, j, n_chunks) + self._add_to_index( + batch_chunks, + store_collection_with_index, + doc_id, + page_ids=batch_page_ids, + chunk_ids=batch_chunk_ids, + metadata=metadata, + ) + + return Path(pdf_file).resolve() + + elif ext in [".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".gif"]: + image = Image.open(item) + _emit_page(item.name, 0, 1) + self._add_to_index(image, store_collection_with_index, doc_id, metadata=metadata) + return item.resolve() + + elif ext == ".pdf": + pdf_file = item.resolve() + + with tempfile.TemporaryDirectory() as path: + images = convert_from_path( + pdf_file, + thread_count=os.cpu_count() - 1, + output_folder=path, + paths_only=True, + ) + n_pages = len(images) + for i in range(0, n_pages, batch_size): + batch_images, batch_page_ids = [], [] + for j in range(i, min(i + batch_size, n_pages)): + image_path = images[j] + image = Image.open(image_path) + batch_images.append(image) + batch_page_ids.append(j + 1) + _emit_page(item.name, j, n_pages) + self._add_to_index( + batch_images, + store_collection_with_index, + doc_id, + page_ids=batch_page_ids, + metadata=metadata, + ) + return pdf_file + else: + existing_pdf = self._find_existing_pdf(item) + if existing_pdf is not None: + pdf_file = existing_pdf + else: + pdf_file = _convert_to_pdf(item) + if pdf_file is None: + return None + + with tempfile.TemporaryDirectory() as path: + images = convert_from_path( + pdf_file, + thread_count=os.cpu_count() - 1, + output_folder=path, + paths_only=True, + ) + n_pages = len(images) + for i in range(0, n_pages, batch_size): + batch_images, batch_page_ids = [], [] + for j in range(i, min(i + batch_size, n_pages)): + image_path = images[j] + image = Image.open(image_path) + batch_images.append(image) + batch_page_ids.append(j + 1) + _emit_page(item.name, j, n_pages) + self._add_to_index( + batch_images, + store_collection_with_index, + doc_id, + page_ids=batch_page_ids, + metadata=metadata, + ) + return Path(pdf_file).resolve() + + elif isinstance(item, Image.Image): + _emit_page("", 0, 1) + self._add_to_index(item, store_collection_with_index, doc_id, metadata=metadata) + return None + else: + raise ValueError(f"Unsupported input type: {type(item)}") + + def _add_to_index( + self, + images: Union[Image.Image, List[Image.Image]], + store_collection_with_index: bool, + doc_id: Union[str, int], + page_ids: Union[int, List[int]] = 1, + chunk_ids: Optional[Union[int, List[int]]] = None, + metadata: Optional[Dict[str, Union[str, int]]] = None, + ): + # Convert single image to list for uniform processing + if isinstance(images, Image.Image): + images = [images] + + if isinstance(page_ids, int): + page_ids = [page_ids] + + if chunk_ids is None: + chunk_ids = [None] * len(images) + elif isinstance(chunk_ids, int): + chunk_ids = [chunk_ids] + + if len(images) != len(page_ids): + raise ValueError(f"Number of images ({len(images)}) does not match number of page_ids ({len(page_ids)})") + if len(images) != len(chunk_ids): + raise ValueError(f"Number of images ({len(images)}) does not match number of chunk_ids ({len(chunk_ids)})") + + # Check for existing entries + for page_id, chunk_id in zip(page_ids, chunk_ids): + pid = make_point_id(int(doc_id), int(page_id), int(chunk_id) if chunk_id is not None else None) + if self.vector_store.point_exists(pid): + if chunk_id is not None: + raise ValueError(f"Document ID {doc_id} with chunk ID {chunk_id} already exists in the index") + raise ValueError(f"Document ID {doc_id} with page ID {page_id} already exists in the index") + + # Process images locally (CPU) for heatmap sidecars + processed_images = self.processor.process_images(images) + input_ids_cpu = processed_images["input_ids"].detach().cpu() + grid_cpu = processed_images.get("image_grid_thw") + if grid_cpu is not None: + grid_cpu = grid_cpu.detach().cpu() + orig_sizes = [img.size for img in images] + + # Generate embeddings — remote or local path + if self._remote_client is not None: + embeddings_list = self._remote_client.embed_images(images) + else: + with torch.inference_mode(): + processed_images_gpu = { + k: v.to(self.device).to( + self.model.dtype + if v.dtype in [torch.float16, torch.bfloat16, torch.float32] + else v.dtype + ) + for k, v in processed_images.items() + } + embeddings = self.model(**processed_images_gpu) + embeddings_list = list(torch.unbind(embeddings.to("cpu"))) + + # Determine dim for lazy collection creation + dim = int(embeddings_list[0].shape[-1]) + if not self.vector_store.collection_exists(): + self.vector_store.create_collection(dim) + + # Store metadata + if metadata is not None: + md_jsonable = ( + metadata.as_jsonable() if isinstance(metadata, DocMetadata) + else (DocMetadata(**metadata).as_jsonable() if isinstance(metadata, dict) else metadata) + ) + self.doc_id_to_metadata[int(doc_id)] = md_jsonable + if isinstance(self.vector_store, LocalVectorStore): + self.vector_store.set_doc_id_to_metadata(self.doc_id_to_metadata) + + # Build StoredPoints and upsert + points_to_upsert = [] + for i, (embedding, page_id, chunk_id) in enumerate(zip(embeddings_list, page_ids, chunk_ids)): + pid = make_point_id(int(doc_id), int(page_id), int(chunk_id) if chunk_id is not None else None) + + payload = { + "doc_id": int(doc_id), + "page_id": int(page_id), + "chunk_id": int(chunk_id) if chunk_id is not None else None, + "metadata": ( + metadata.as_jsonable() if isinstance(metadata, DocMetadata) + else (DocMetadata(**metadata).as_jsonable() if isinstance(metadata, dict) else {}) + ) if metadata is not None else {}, + } + + points_to_upsert.append(StoredPoint( + point_id=pid, + vector=embedding.cpu(), + payload=payload, + )) + + # Heatmap sidecar + self.embed_id_to_extra[pid] = { + "input_ids": input_ids_cpu[i], + "image_grid_thw": grid_cpu[i] if grid_cpu is not None else None, + "orig_size": orig_sizes[i], + } + + if store_collection_with_index: + img_str = self._post_process_image(images[i]) + self.collection[int(pid)] = img_str + + self.vector_store.upsert(points_to_upsert) + self.doc_ids.add(int(doc_id)) + + # ============================================================ + # Index maintenance + # ============================================================ + + def update_index_from_folder( + self, + folder: Union[str, Path], + store_collection_with_index: bool = False, + metadata_provider: Optional[Callable] = None, + batch_size: int = 1, + reindex_modified: bool = False, + ) -> Dict[int, str]: + """ + Adds only NEW files from a folder to the current index. + """ + folder = Path(folder) + assert folder.is_dir(), f"{folder} n'est pas un dossier existant." + + known = self._already_indexed_paths() + + inverse_map: Dict[str, int] = {} + for did, p in self.doc_ids_to_file_names.items(): + if p and p != "In-memory Image": + try: + inverse_map[str(Path(p).resolve())] = int(did) + except Exception: + inverse_map[p] = int(did) + + added = 0 + updated = 0 + + for item in sorted( + (p for p in folder.rglob("*") if p.is_file()), + key=lambda p: p.relative_to(folder), + ): + + ext = item.suffix.lower() + + if ext == ".pdf" and self._is_mirror_pdf(item): + if self.verbose > 1: + print(f"[skip] Mirror PDF ignored: {item}") + continue + + cand_keys = self._candidate_keys(item) + if any(k in known for k in cand_keys) and not reindex_modified: + if self.verbose > 1: + print(f"[skip] Already indexed: {item}") + continue + + if reindex_modified: + target_key = None + for k in cand_keys: + if k in known: + target_key = k + break + + if target_key is not None: + try: + src_stat = item.stat().st_mtime + tgt_stat = Path(target_key).stat().st_mtime + except Exception: + src_stat, tgt_stat = None, None + + if ( + src_stat is not None + and tgt_stat is not None + and src_stat <= tgt_stat + ): + if self.verbose > 1: + print(f"[skip] Unchanged (mtime): {item}") + continue + + old_doc_id = inverse_map.get(target_key) + if old_doc_id is not None: + if self.verbose > 0: + print( + f"[update] Reindexing (modified): {item} (doc_id {old_doc_id})" + ) + updated += 1 + + doc_id = self.highest_doc_id + 1 + md = metadata_provider(item) if metadata_provider else None + stored_path = self._process_and_add_to_index( + item, + store_collection_with_index=store_collection_with_index, + doc_id=doc_id, + metadata=md, + batch_size=batch_size, + ) + if stored_path is None: + self.doc_ids_to_file_names[doc_id] = "In-memory Image" + else: + self.doc_ids_to_file_names[doc_id] = str(Path(stored_path).resolve()) + + self.doc_ids.add(doc_id) + self.highest_doc_id = max(self.highest_doc_id, doc_id) + added += 1 + + self._export_index() + + if self.verbose > 0: + print(f"[incr] added: {added} | reindexed: {updated}") + + return self.doc_ids_to_file_names + + def remove_from_index(self): + raise NotImplementedError("This method is not implemented yet.") + + # ============================================================ + # Search + # ============================================================ + + def _encode_search_query(self, query: str): + if self._remote_client is not None: + return self._remote_client.embed_query(query) + + with torch.inference_mode(): + batch_query = self.processor.process_queries([query]) + batch_query = { + kk: vv.to(self.device).to( + self.model.dtype + if vv.dtype in [torch.float16, torch.bfloat16, torch.float32] + else vv.dtype + ) + for kk, vv in batch_query.items() + } + embeddings_query = self.model(**batch_query) + qs = list(torch.unbind(embeddings_query.to("cpu"))) + + input_ids = batch_query["input_ids"][0].detach().cpu().tolist() + tokens = self.processor.tokenizer.convert_ids_to_tokens(input_ids) + valid_idxs = [i for i, tok in enumerate(tokens) if tok not in {"<|endoftext|>", "Query", ":"}] + return [qs[0][valid_idxs]] + + def search( + self, + query: str, + k: int = 10, + filter_metadata: Optional[Dict[str, str]] = None, + return_base64_results: Optional[bool] = None + ) -> List[Result]: + + if return_base64_results is None: + return_base64_results = bool(self.collection) + + if k < 1: + return [] + + qs = self._encode_search_query(query) + + mvq = MultiVectorQuery( + vectors=qs[0], + filter_metadata=filter_metadata, + ) + hits = self.vector_store.search(mvq, k) + + results = self._hits_to_results(hits, qs[0], k, return_base64_results) + return self._finalize_results(results, return_base64_results) + + def _hits_to_results( + self, + hits: List[SearchHit], + q_emb: torch.Tensor, + k: int, + return_base64_results: bool, + ) -> List[Result]: + results: List[Result] = [] + for hit in hits: + payload = hit.payload + doc_id = int(payload.get("doc_id", 0)) + page_id = int(payload.get("page_id", 1)) + chunk_id = payload.get("chunk_id") + + result = Result( + doc_id=doc_id, + page_num=page_id, + chunk_num=int(chunk_id) if chunk_id is not None else None, + score=float(hit.score), #force float conversion to avoid pyTorch Tensors + metadata=payload.get("metadata", self.doc_id_to_metadata.get(doc_id, {})), + base64=self.collection.get(hit.point_id) if return_base64_results else None, + ) + + extra = self.embed_id_to_extra.get(hit.point_id) + if (self.enable_heatmaps or self.enable_circle) and extra is not None: + p_emb = self.vector_store.fetch_vector(hit.point_id) + if p_emb is not None: + result = self._attach_heatmaps_local( + result=result, + q_emb=q_emb, + p_emb=p_emb, + extra=extra, + k=k, + ) + + results.append(result) + + return results + + def filter_embeddings(self, filter_metadata: Union[Dict[str, Any], MetadataFilter]): + """Legacy method kept for backward compat. Use search(filter_metadata=...) instead.""" + if not isinstance(self.vector_store, LocalVectorStore): + raise NotImplementedError( + "filter_embeddings() is only supported for the local backend. " + "Use search(filter_metadata=...) for other backends." + ) + f = ( + filter_metadata + if isinstance(filter_metadata, MetadataFilter) + else MetadataFilter(**filter_metadata) + ) + return self.vector_store._filter_by_metadata(f) + + # ============================================================ + # Result enrichment and visualization + # ============================================================ + + def _get_image_token_id_from_extra(self, extra: dict) -> int: + if hasattr(self.processor, "image_token_id"): + return int(self.processor.image_token_id) + return majority_token_id(extra["input_ids"]) + + def _attach_heatmaps_local(self, result: Result, q_emb, p_emb, extra: dict, k: int) -> Result: + img_tok = self._get_image_token_id_from_extra(extra) + + result.metadata = dict(result.metadata or {}) + heat_soft, heat_global = None, None + + if self.enable_circle or self.enable_heatmaps: + heat_soft, _ = compute_patch_heatmap( + q_emb=q_emb, + p_emb=p_emb, + input_ids=extra["input_ids"], + image_grid_thw=extra["image_grid_thw"], + image_token_id=img_tok, + mode="soft_topk", + topk=min(k, 8), + temperature=0.2, + normalize=False, + ) + + if self.enable_heatmaps: + heat_global, _ = compute_patch_heatmap( + q_emb=q_emb, + p_emb=p_emb, + input_ids=extra["input_ids"], + image_grid_thw=extra["image_grid_thw"], + image_token_id=img_tok, + mode="global_sum", + topk=k, + temperature=0.2, + normalize=False, + ) + + hm = {"soft_topk": {"heat_2d": heat_soft}} + if self.enable_heatmaps: + hm["global_sum"] = {"heat_2d": heat_global} + result.metadata["heatmaps"] = hm + + return result + + def _finalize_results(self, results: List[Result], return_base64_results: bool) -> List[Result]: + if not return_base64_results: + return results + + for r in results: + self.fetch_result_img(r) + + for r in results: + if not r.base64: + continue + + meta = r.metadata or {} + need_overlay, need_circle = bool(self.enable_heatmaps), bool(self.enable_circle) + + if not (need_overlay or need_circle): + continue + + hm = meta.get("heatmaps") or {} + img = None + if (need_overlay and hm) or need_circle: + img = pil_from_base64(r.base64) + + if need_overlay and hm: + meta["heatmap_overlays_base64"] = build_heatmap_overlays_base64( + img=img, + heatmaps=hm, + interps=("nearest", "bilinear"), + alpha=0.45, + cmap="jet", + shift_x=0.0, + shift_y=0.0, + patch_grow_pct=300.0, + grow_mode="mean", + ) + + if need_circle: + soft = (hm.get("soft_topk") or {}).get("heat_2d") + if soft is not None: + img_marked = draw_circle_on_max_patch( + img=img, + heat_2d=soft, + patch_grow_pct=300.0, + grow_mode="mean", + ) + meta["soft_topk_max_patch_circle_base64"] = pil_to_base64_png(img_marked) + + r.metadata = meta + + return results + + def fetch_result_img(self, result: Result) -> Result: + if result.base64: + return result + + doc_id = result.doc_id + file_name = self.doc_ids_to_file_names.get(doc_id) + if not file_name or file_name == "In-memory Image": + return result + + path = Path(file_name) + + if self.ingestion_backend == "docling": + try: + if self.docling_dir is None: + assert self.index_name is not None, "index_name must be set to use docling ingestion" + self.docling_dir = Path(self.index_root) / self.index_name / "docling_chunks" + self.docling_dir.mkdir(parents=True, exist_ok=True) + assert result.chunk_num is not None, "Result.chunk_num must be defined" + path_chunk = Path(self.docling_dir) / f"{path.stem}_p{result.page_num}_{result.chunk_num}.png" + assert path_chunk.exists(), f"Path {path_chunk} for chunk {result.chunk_num} does not exists" + image = Image.open(path_chunk) + result.base64 = self._post_process_image(image) + return result + except Exception as e: + if self.verbose > 0: + logger.warning(f"[fetch_result_img] Docling chunk fetch error: {e}") + + ext = path.suffix.lower() + + try: + if ext in [".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".gif"]: + image = Image.open(path) + result.base64 = self._post_process_image(image) + return result + + if ext != ".pdf": + sibling_pdf = self._find_existing_pdf(path) + if sibling_pdf is not None: + self.doc_ids_to_file_names[doc_id] = str(sibling_pdf) + path = sibling_pdf + ext = ".pdf" + else: + pdf_path = _convert_to_pdf(path) + if pdf_path and pdf_path.exists(): + self.doc_ids_to_file_names[doc_id] = str(pdf_path) + path = pdf_path + ext = ".pdf" + else: + if self.verbose > 0: + print( + f"[fetch_result_img] Impossible de convertir {path} en PDF." + ) + return result + + with tempfile.TemporaryDirectory() as tmpdir: + images = convert_from_path( + str(path), + thread_count=os.cpu_count() - 1, + first_page=result.page_num, + last_page=result.page_num, + paths_only=True, + output_folder=tmpdir, + ) + image = Image.open(images[0]) + result.base64 = self._post_process_image(image) + return result + + except Exception as e: + if self.verbose > 0: + print(f"[fetch_result_img] Erreur: {e}") + return result + + def _post_process_image(self, image: Image.Image) -> str: + if self.max_image_width and self.max_image_height: + img_width, img_height = image.size + aspect_ratio = img_width / img_height + if img_width > self.max_image_width: + new_width = self.max_image_width + new_height = int(new_width / aspect_ratio) + else: + new_width = img_width + new_height = img_height + if new_height > self.max_image_height: + new_height = self.max_image_height + new_width = int(new_height * aspect_ratio) + if self.verbose > 2: + print( + f"Resizing image to {new_width}x{new_height}", + f"(aspect ratio {aspect_ratio:.2f}, original size {img_width}x{img_height}," + f"compression {new_width / img_width * new_height / img_height:.2f})", + ) + image = image.resize((new_width, new_height), Image.LANCZOS) + + buffered = io.BytesIO() + image.save(buffered, format="PNG") + img_str = base64.b64encode(buffered.getvalue()).decode() + return img_str + + # ============================================================ + # File helpers + # ============================================================ + + def _looks_like_pdf(self, path: Path) -> bool: + try: + if not path.exists() or path.stat().st_size < 5: + return False + with open(path, "rb") as f: + return f.read(5) == b"%PDF-" + except Exception: + return False + + def _find_existing_pdf(self, src: Path) -> Optional[Path]: + cand = src.with_suffix(".pdf") + if cand.exists() and self._looks_like_pdf(cand): + return cand.resolve() + return None + + def _already_indexed_paths(self) -> set: + vals = set() + for p in self.doc_ids_to_file_names.values(): + if not p or p == "In-memory Image": + continue + try: + vals.add(str(Path(p).resolve())) + except Exception: + vals.add(p) + return vals + + def _candidate_keys(self, path: Path) -> List[str]: + keys = [] + try: + keys.append(str(path.resolve())) + except Exception: + keys.append(str(path)) + + sibling_pdf = path.with_suffix(".pdf") + if sibling_pdf.exists() and self._looks_like_pdf(sibling_pdf): + try: + keys.append(str(sibling_pdf.resolve())) + except Exception: + keys.append(str(sibling_pdf)) + return keys + + def _is_mirror_pdf(self, path: Path) -> bool: + if path.suffix.lower() != ".pdf": + return False + stem = path.with_suffix("") + parent = path.parent + for ext in self.SOURCE_EXTS: + if Path(os.path.join(parent, f"{stem.name}{ext}")).exists(): + return True + return False + + def _vector_store_is_open(self) -> bool: + """Return True if the vector store has an active client connection.""" + from .vector_store.qdrant import QdrantVectorStore + from .vector_store.milvus import MilvusVectorStore + from .vector_store.remote import RemoteVectorStore + if isinstance(self.vector_store, LocalVectorStore): + return self.vector_store._index_name is not None + if isinstance(self.vector_store, QdrantVectorStore): + return self.vector_store._client is not None + if isinstance(self.vector_store, MilvusVectorStore): + return self.vector_store._client is not None + if isinstance(self.vector_store, RemoteVectorStore): + return self.vector_store.is_opened + return False + + # ============================================================ + # Accessors for backward compat + # ============================================================ + + def get_doc_ids_to_file_names(self): + return self.doc_ids_to_file_names + + @property + def indexed_embeddings(self): + """Backward-compat: return embedding list for local backend only.""" + if isinstance(self.vector_store, LocalVectorStore): + return self.vector_store.indexed_embeddings + return [] + + @property + def embed_id_to_doc_id(self): + """Backward-compat: return embed_id mapping for local backend only.""" + if isinstance(self.vector_store, LocalVectorStore): + return self.vector_store.embed_id_to_doc_id + return {} + + @property + def qdrant_client(self): + """Backward-compat: expose qdrant client for tests that inspect it.""" + from .vector_store.qdrant import QdrantVectorStore + if isinstance(self.vector_store, QdrantVectorStore): + return self.vector_store.client + return None + + @property + def qdrant_collection(self): + """Backward-compat: expose qdrant collection name.""" + from .vector_store.qdrant import QdrantVectorStore + if isinstance(self.vector_store, QdrantVectorStore): + return self.vector_store.collection_name + return None diff --git a/foretrieval/docling_ingest.py b/foretrieval/docling_ingest.py new file mode 100644 index 0000000..18bd203 --- /dev/null +++ b/foretrieval/docling_ingest.py @@ -0,0 +1,397 @@ +from pathlib import Path +import hashlib +from typing import Dict, Iterable, List, Optional, Tuple, NamedTuple +from PIL import Image + +class PendingImg(NamedTuple): + page_no: int + y_top: float # pour trier dans la page + img: Image.Image + +class ExportedImg(NamedTuple): + path: Path + page_id: int + elem_id: int + +# ----------------------------- +# Geometry helpers +# ----------------------------- +def bbox_to_norm_ltrb(bbox, page) -> Tuple[float, float, float, float]: + bb = bbox.to_top_left_origin(page.size.height).normalized(page.size) + return (bb.l, bb.t, bb.r, bb.b) + +def norm_ltrb_to_px(ltrb: Tuple[float, float, float, float], page_img: Image.Image) -> Tuple[int, int, int, int]: + l, t, r, b = ltrb + return (int(l * page_img.width), int(t * page_img.height), + int(r * page_img.width), int(b * page_img.height)) + +def area_inclusion(a: Tuple[float, float, float, float], b: Tuple[float, float, float, float]) -> float: + l = max(a[0], b[0]) + t = max(a[1], b[1]) + r = min(a[2], b[2]) + bb = min(a[3], b[3]) + + inter_w = max(0.0, r - l) + inter_h = max(0.0, bb - t) + inter = inter_w * inter_h + + a_w = max(0.0, a[2] - a[0]) + a_h = max(0.0, a[3] - a[1]) + area_a = a_w * a_h + + return 0.0 if area_a <= 0 else inter / area_a + + +# ----------------------------- +# Lightweight indexes +# ----------------------------- +def build_picture_index(doc) -> List[dict]: + pics = [] + for it, _lvl in doc.iterate_items(): + if getattr(it, "label", None) != "picture": + continue + for prov in getattr(it, "prov", []): + if not getattr(prov, "bbox", None): + continue + page_no = prov.page_no + page = doc.pages[page_no] + pics.append({ + "self_ref": getattr(it, "self_ref", None), + "page_no": page_no, + "bbox_norm": bbox_to_norm_ltrb(prov.bbox, page), + }) + return pics + +def build_heading_text_index(doc) -> List[dict]: + HEADING_LABELS = {"title", "heading", "section_header", "header", "subtitle"} + headings = [] + for it, _lvl in doc.iterate_items(): + if getattr(it, "label", None) not in HEADING_LABELS: + continue + text = getattr(it, "text", None) or getattr(it, "value", None) or getattr(it, "content", None) + for prov in getattr(it, "prov", []): + if not getattr(prov, "bbox", None): + continue + page_no = prov.page_no + page = doc.pages[page_no] + headings.append({ + "text": text, + "page_no": page_no, + "bbox_norm": bbox_to_norm_ltrb(prov.bbox, page), + }) + return headings + +def build_caption_index(doc) -> List[dict]: + CAPTION_LABELS = {"caption", "figure_caption", "table_caption"} + out = [] + for it, _lvl in doc.iterate_items(): + if getattr(it, "label", None) not in CAPTION_LABELS: + continue + txt = getattr(it, "text", None) or getattr(it, "value", None) or getattr(it, "content", None) + for prov in getattr(it, "prov", []): + if not getattr(prov, "bbox", None): + continue + page_no = prov.page_no + page = doc.pages[page_no] + out.append({ + "page_no": page_no, + "bbox_norm": bbox_to_norm_ltrb(prov.bbox, page), + "text": txt, + }) + return out + +def image_fingerprint(img: Image.Image, thumb: int = 512) -> str: + # Fingerprint stable: convert, resize to limit cost, then hash bytes + x = img.convert("RGB") + x.thumbnail((thumb, thumb)) + h = hashlib.blake2b(x.tobytes(), digest_size=16) + return h.hexdigest() + +# ----------------------------- +# Heading matching (ton code) +# ----------------------------- +def normalize_text(s: Optional[str]) -> str: + if not s: + return "" + return " ".join(str(s).strip().lower().split()) + + +def find_heading_item_for_chunk(chunk, headings_index: List[dict], lookback_pages: int = 3) -> Optional[dict]: + if not getattr(chunk.meta, "headings", None): + return None + target = normalize_text(chunk.meta.headings[-1]) + if not target: + return None + + chunk_pages = set() + for it in chunk.meta.doc_items: + for prov in getattr(it, "prov", []): + if getattr(prov, "bbox", None): + chunk_pages.add(prov.page_no) + + def text_match(h_text: str) -> bool: + ht = normalize_text(h_text) + return bool(ht) and (ht == target or target in ht or ht in target) + + if not chunk_pages: + candidates = [h for h in headings_index if text_match(h.get("text"))] + if not candidates: + return None + candidates.sort(key=lambda x: (x["page_no"], x["bbox_norm"][3]), reverse=True) + return candidates[0] + + min_page = min(chunk_pages) + allowed_pages = set(chunk_pages) + for k in range(1, lookback_pages + 1): + p = min_page - k + if p >= 0: + allowed_pages.add(p) + + candidates = [] + for h in headings_index: + if h["page_no"] not in allowed_pages: + continue + if not text_match(h.get("text")): + continue + dist = 0 if h["page_no"] in chunk_pages else (min_page - h["page_no"]) + candidates.append((dist, h)) + + if not candidates: + return None + + candidates.sort(key=lambda x: (x[0], -x[1]["page_no"], -x[1]["bbox_norm"][3])) + return candidates[0][1] + + +# ----------------------------- +# Minimal rendering helpers +# ----------------------------- +def enclosing_bbox_norm(boxes: Iterable[Tuple[float, float, float, float]]) -> Tuple[float, float, float, float]: + boxes = list(boxes) + return (min(b[0] for b in boxes), min(b[1] for b in boxes), max(b[2] for b in boxes), max(b[3] for b in boxes)) + +def stack_vertical(top: Image.Image, bottom: Image.Image, gap: int = 6) -> Image.Image: + w = max(top.width, bottom.width) + h = top.height + gap + bottom.height + out = Image.new("RGB", (w, h), "white") + out.paste(top, (0, 0)) + out.paste(bottom, (0, top.height + gap)) + return out + +def best_caption_for_picture(pic: dict, captions: List[dict], max_gap: float = 0.06, min_h_overlap: float = 0.5) -> Optional[dict]: + pno = pic["page_no"] + p_l, p_t, p_r, p_b = pic["bbox_norm"] + best, best_score = None, -1.0 + + for cap in captions: + if cap["page_no"] != pno: + continue + c_l, c_t, c_r, c_b = cap["bbox_norm"] + if c_t < p_b: + continue + gap = c_t - p_b + if gap > max_gap: + continue + + inter_w = max(0.0, min(p_r, c_r) - max(p_l, c_l)) + p_w = max(1e-6, (p_r - p_l)) + overlap = inter_w / p_w + if overlap < min_h_overlap: + continue + + score = (1.0 - (gap / max_gap)) * overlap + if score > best_score: + best_score, best = score, cap + + return best + + +# ----------------------------- +# Export orphans (seul export "secondaire") +# ----------------------------- +def export_orphan_pictures_pending(doc, + pictures: List[dict], + captions: List[dict], + included: set, + gap: int = 6) -> List[Tuple[int, float, Image.Image]]: + pending = [] + for pic in pictures: + key = (pic["page_no"], tuple(round(x, 4) for x in pic["bbox_norm"])) + if key in included: + continue + + page_no = pic["page_no"] + page_img = doc.pages[page_no].image.pil_image + + pic_crop = page_img.crop(norm_ltrb_to_px(pic["bbox_norm"], page_img)) + + cap = best_caption_for_picture(pic, captions) + if cap is not None: + cap_crop = page_img.crop(norm_ltrb_to_px(cap["bbox_norm"], page_img)) + out_img = stack_vertical(pic_crop, cap_crop, gap=gap) + else: + out_img = pic_crop + + # y_top = top de la picture => ordre naturel + y_top = pic["bbox_norm"][1] + pending.append((page_no, y_top, out_img)) + + return pending + + +# ----------------------------- +# MAIN: chunk_pdf_to_images (structuré) +# ----------------------------- +def chunk_pdf_to_images(pdf_path: str, output_dir: str, scale: float = 2.0, max_tokens: int = 512) -> List[ExportedImg]: + """ + Convertit un PDF en images "chunks" (texte+images) + images orphelines (pictures non capturées). + Paramètres: + - pdf_path: chemin vers le PDF d'entrée + - output_dir: dossier de sortie où seront créés des sous-dossiers par document + - scale: facteur de mise à l'échelle pour les images extraites (par exemple, 2.0 pour doubler la résolution) + - max_tokens: nombre maximum de tokens par chunk (utilisé pour le chunking du texte) + + Sortie: + output_dir// + """ + from docling.document_converter import DocumentConverter, PdfFormatOption + from docling.datamodel.base_models import InputFormat + from docling.datamodel.pipeline_options import PdfPipelineOptions + from docling_core.transforms.chunker import HybridChunker + + AREA_THRESH = 0.10 # seuil d'inclusion d'une picture dans un chunk (en fonction de la proportion de sa surface incluse dans le chunk) + GAP = 15 # gap en pixels entre picture et caption lors du stacking vertical + + pdf_path = str(pdf_path) + doc_stem = Path(pdf_path).stem + + # (A) Préparer dossier de sortie + out_images_dir = Path(output_dir) + out_images_dir.mkdir(parents=True, exist_ok=True) + + # (B) Convertir PDF -> docling document (avec images pages) + pipeline_options = PdfPipelineOptions( + generate_page_images=True, + generate_picture_images=True, + images_scale=scale, + ) + converter = DocumentConverter( + format_options={InputFormat.PDF: PdfFormatOption(pipeline_options=pipeline_options)} + ) + chunker = HybridChunker(max_tokens=max_tokens, merge_peers=True) + + doc = converter.convert(pdf_path).document + + # (C) Indexes (une fois) + pictures = build_picture_index(doc) + headings = build_heading_text_index(doc) + captions = build_caption_index(doc) + + # index pictures par page pour éviter de filtrer à chaque fois + pics_by_page: Dict[int, List[dict]] = {} + for p in pictures: + pics_by_page.setdefault(p["page_no"], []).append(p) + + chunks = list(chunker.chunk(doc)) + + # (D) Export chunks + # - included = set des pictures déjà incluses dans un chunk + included_pic_keys = set() + out_items: List[ExportedImg] = [] + pending: List[PendingImg] = [] + + for chunk_id, chunk in enumerate(chunks): + header_item = find_heading_item_for_chunk(chunk, headings) + + # bboxes du chunk par page (norm) + boxes_by_page: Dict[int, List[Tuple[float, float, float, float]]] = {} + for item in chunk.meta.doc_items: + for prov in getattr(item, "prov", []): + if not getattr(prov, "bbox", None): + continue + pno = prov.page_no + page = doc.pages[pno] + boxes_by_page.setdefault(pno, []).append(bbox_to_norm_ltrb(prov.bbox, page)) + + if not boxes_by_page: + continue + + for page_no, item_boxes_norm in boxes_by_page.items(): + page_img = doc.pages[page_no].image.pil_image + + # enclosing bbox = crop final + chunk_bb_norm = enclosing_bbox_norm(item_boxes_norm) + enc_px = norm_ltrb_to_px(chunk_bb_norm, page_img) + + # construire la liste des "regions" à garder (items + pictures incluses) + regions_px: List[Tuple[int, int, int, int]] = [ + norm_ltrb_to_px(b, page_img) for b in item_boxes_norm + ] + + for pic in pics_by_page.get(page_no, []): + if area_inclusion(pic["bbox_norm"], chunk_bb_norm) >= AREA_THRESH: + regions_px.append(norm_ltrb_to_px(pic["bbox_norm"], page_img)) + included_pic_keys.add((pic["page_no"], tuple(round(x, 4) for x in pic["bbox_norm"]))) + + # rendre le chunk masqué puis crop + mask = Image.new("RGB", page_img.size, "white") + for box in regions_px: + mask.paste(page_img.crop(box), box) + chunk_crop = mask.crop(enc_px) + + # préfixer header si trouvé + if header_item is not None: + header_img = doc.pages[header_item["page_no"]].image.pil_image + header_crop = header_img.crop(norm_ltrb_to_px(header_item["bbox_norm"], header_img)) + out_img = stack_vertical(header_crop, chunk_crop, gap=GAP) + else: + out_img = chunk_crop + + y_top = chunk_bb_norm[1] # top normalisé du chunk sur la page + pending.append(PendingImg(page_no=page_no, y_top=y_top, img=out_img)) + + # (E) Sauver les pictures orphelines (non incluses dans les chunks) + pending_orphans = export_orphan_pictures_pending( + doc=doc, + pictures=pictures, + captions=captions, + included=included_pic_keys, + gap=GAP, + ) + for page_no, y_top, img in pending_orphans: + pending.append(PendingImg(page_no=page_no, y_top=y_top, img=img)) + + # (F) Tri + retirer les doublons + écriture finale (ordre pages) + pending.sort(key=lambda x: (x.page_no, x.y_top)) + + seen_fps = set() + elem_id = 0 + for it in pending: + fp = image_fingerprint(it.img) + if fp in seen_fps: + continue + seen_fps.add(fp) + + elem_id += 1 + out_path = out_images_dir / f"{doc_stem}_p{it.page_no}_{elem_id}.png" + it.img.save(out_path) + + out_items.append(ExportedImg( + path=out_path, + page_id=it.page_no, + elem_id=elem_id, + )) + + return out_items + + +# --------------------------------------------------- +# MAIN TEST +# --------------------------------------------------- +if __name__ == "__main__": + pdfs = ["data/doc_arduino.pdf", "data/doc_cnes.pdf", "data/doc_cvpr.pdf"] + out_root = "chunks_output" + + for pdf_path in pdfs: + paths = chunk_pdf_to_images(pdf_path, out_root, scale=5.0) + print(f"✅ {len(paths)} images générées pour {pdf_path}") \ No newline at end of file diff --git a/foretrieval/embedding_server/__init__.py b/foretrieval/embedding_server/__init__.py new file mode 100644 index 0000000..9c92b5f --- /dev/null +++ b/foretrieval/embedding_server/__init__.py @@ -0,0 +1,17 @@ +"""Remote embedding server package for FORetrieval. + +Provides: +- EmbeddingServerConfig — Pydantic config model +- EmbeddingServerClient — HTTP client for the vLLM /pooling endpoint +- EmbeddingServerManager — SSH-based Docker deployment manager +""" + +from .client import EmbeddingServerClient +from .config import EmbeddingServerConfig +from .manager import EmbeddingServerManager + +__all__ = [ + "EmbeddingServerConfig", + "EmbeddingServerClient", + "EmbeddingServerManager", +] diff --git a/foretrieval/embedding_server/client.py b/foretrieval/embedding_server/client.py new file mode 100644 index 0000000..ff69a72 --- /dev/null +++ b/foretrieval/embedding_server/client.py @@ -0,0 +1,233 @@ +"""HTTP client for the remote vLLM embedding server. + +Communicates with a vLLM instance serving a ColQwen3/ColQwen3.5 model via the +/pooling endpoint. Returns multi-vector embeddings as CPU tensors, matching +the shape produced by the local colpali-engine pipeline. + +OOM handling: the server may return HTTP 500 with an OOM message when a batch +is too large. The client detects this and retries with progressively halved +batch sizes down to 1. + +Auth: when EmbeddingServerConfig.api_key is set, every request includes an +"Authorization: Bearer " header. Deploy vLLM with --api-key to match. + +SSL: verify_ssl=False disables certificate verification (self-signed certs). +""" + +from __future__ import annotations + +import base64 +import io +import logging +from typing import List + +import httpx +import torch +from PIL import Image + +from .config import EmbeddingServerConfig + +logger = logging.getLogger(__name__) + +# Substrings in vLLM error responses that indicate GPU OOM. +_OOM_MARKERS = ( + "CUDA out of memory", + "out of memory", + "OOM", + "RESOURCE_EXHAUSTED", +) + +_POOLING_ENDPOINT = "/pooling" +_HEALTH_ENDPOINT = "/health" + + +class ServerOOMError(RuntimeError): + """Raised when the server reports a GPU out-of-memory condition.""" + + +class EmbeddingServerClient: + """HTTP client that talks to a vLLM /pooling endpoint. + + Parameters + ---------- + config: + EmbeddingServerConfig with url, model_name, batch_size, etc. + """ + + def __init__(self, config: EmbeddingServerConfig) -> None: + self.config = config + self._client = httpx.Client( + verify=config.verify_ssl, + headers=self._build_headers(), + timeout=config.request_timeout, + ) + + def _build_headers(self) -> dict: + headers = {} + if self.config.api_key: + headers["Authorization"] = f"Bearer {self.config.api_key}" + return headers + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + def health_check(self) -> bool: + """Return True if the server is reachable and healthy.""" + try: + resp = self._client.get(self.config.url + _HEALTH_ENDPOINT, timeout=10) + return resp.status_code == 200 + except httpx.HTTPError: + return False + + def embed_images(self, images: List[Image.Image]) -> List[torch.Tensor]: + """Embed a list of PIL images via the remote server. + + Sends images in batches to /pooling. Automatically reduces batch size + on OOM until batch_size reaches 1. + + Parameters + ---------- + images: + List of PIL Images (document pages). + + Returns + ------- + List of CPU tensors, one per image, each of shape [n_tokens, embed_dim]. + """ + if not images: + return [] + return self._embed_images_with_oom_retry(images) + + def embed_query(self, query: str) -> List[torch.Tensor]: + """Embed a text query string via the remote server. + + Parameters + ---------- + query: + Plain text query. + + Returns + ------- + List containing a single CPU tensor of shape [n_tokens, embed_dim]. + """ + payload = { + "model": self.config.model_name, + "input": query, + } + resp = self._post_pooling(payload) + data = resp["data"] + if not data: + raise ValueError("Server returned empty data for query embedding") + return [torch.tensor(data[0]["data"], dtype=torch.float32)] + + def close(self) -> None: + """Close the underlying HTTP client.""" + self._client.close() + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _embed_images_with_oom_retry( + self, images: List[Image.Image] + ) -> List[torch.Tensor]: + """Embed images, halving batch size on OOM until batch_size=1.""" + batch_size = self.config.batch_size + while batch_size >= 1: + try: + return self._embed_images_batched(images, batch_size) + except ServerOOMError: + new_size = batch_size // 2 + if new_size < 1: + logger.error( + "Server OOM even at batch_size=1. " + "The model may be too large for the available GPU memory." + ) + raise + logger.warning( + "Server OOM at batch_size=%d — retrying with batch_size=%d", + batch_size, + new_size, + ) + batch_size = new_size + # Unreachable, but satisfies type checkers. + raise ServerOOMError("OOM at all batch sizes") + + def _embed_images_batched( + self, images: List[Image.Image], batch_size: int + ) -> List[torch.Tensor]: + """Split images into batches, call server, collect tensors.""" + results: List[torch.Tensor] = [] + for start in range(0, len(images), batch_size): + batch = images[start : start + batch_size] + tensors = self._embed_image_batch(batch) + results.extend(tensors) + return results + + def _embed_image_batch( + self, images: List[Image.Image] + ) -> List[torch.Tensor]: + """Embed a single batch of images (no retry logic here). + + vLLM >=0.19.0 requires images via PoolingChatRequest (messages array). + The flat 'input' field only accepts token id lists, not image content. + One request per image — server returns one embedding per call. + """ + tensors = [] + for img in images: + b64 = _pil_to_base64(img) + payload = { + "model": self.config.model_name, + "messages": [ + { + "role": "user", + "content": [ + { + "type": "image_url", + "image_url": {"url": f"data:image/png;base64,{b64}"}, + } + ], + } + ], + } + resp = self._post_pooling(payload) + tensors.append(torch.tensor(resp["data"][0]["data"], dtype=torch.float32)) + return tensors + + def _post_pooling(self, payload: dict) -> dict: + """POST to /pooling and return parsed JSON. Raises ServerOOMError on OOM.""" + try: + resp = self._client.post( + self.config.url + _POOLING_ENDPOINT, + json=payload, + ) + except httpx.TimeoutException as exc: + raise TimeoutError( + f"Embedding server timed out after {self.config.request_timeout}s" + ) from exc + except httpx.HTTPError as exc: + raise ConnectionError( + f"Cannot reach embedding server at {self.config.url}" + ) from exc + + if resp.status_code != 200: + body = resp.text + if any(marker in body for marker in _OOM_MARKERS): + raise ServerOOMError(f"Server OOM: {body[:300]}") + raise RuntimeError( + f"Embedding server returned HTTP {resp.status_code}: {body[:500]}" + ) + + return resp.json() + + +# ------------------------------------------------------------------ +# Utility +# ------------------------------------------------------------------ + +def _pil_to_base64(img: Image.Image) -> str: + """Encode a PIL image as a base64 PNG string.""" + buf = io.BytesIO() + img.save(buf, format="PNG") + return base64.b64encode(buf.getvalue()).decode("utf-8") diff --git a/foretrieval/embedding_server/config.py b/foretrieval/embedding_server/config.py new file mode 100644 index 0000000..7bdfee0 --- /dev/null +++ b/foretrieval/embedding_server/config.py @@ -0,0 +1,99 @@ +"""Configuration model for the remote embedding server.""" + +from typing import Optional + +from pydantic import BaseModel, field_validator, model_validator + +# Model name substrings that vLLM supports for the /pooling endpoint. +# Only ColQwen3 / ColQwen3.5 architecture is supported in vLLM >=0.19.0. +_VLLM_COMPATIBLE_PATTERNS = ("colqwen3",) + + +class EmbeddingServerConfig(BaseModel): + """Configuration for a remote vLLM embedding server. + + When set on ColPaliModel, the model weights are NOT loaded locally. + Only the processor (tokenizer + image preprocessor) is loaded for + heatmap sidecar computation and query tokenisation. + + Attributes: + url: Full base URL of the vLLM server, e.g. "http://gpu-server:8000". + model_name: HuggingFace model ID served by the vLLM instance, + e.g. "athrael-soju/colqwen3.5-4.5B-v3". + Must contain "colqwen3" — vLLM >=0.19.0 only supports the + ColQwen3/ColQwen3.5 architecture for the /pooling endpoint. + ColPali, ColQwen2, and ColQwen2.5 are not supported by vLLM. + auto_deploy: When True, FORetrieval will SSH to ssh_host and + deploy a Docker container if the server is not already running. + Requires ssh_host to be set. + ssh_host: Hostname or IP of the GPU server (SSH target). + Required when auto_deploy=True. + ssh_user: SSH username. Defaults to current OS user. + ssh_key_path: Path to SSH private key file. If None, uses the + SSH agent or default keys (~/.ssh/id_rsa etc.). + n_gpus: Number of GPUs to use for tensor parallelism on the server. + -1 (default) means all available GPUs detected via nvidia-smi. + port: Port to expose the vLLM server on. Default 8000. + hf_token: HuggingFace token passed as HF_TOKEN env var to the + Docker container for downloading gated models. + api_key: Optional bearer token for server authentication. + When set, requests include "Authorization: Bearer ". + Deploy the vLLM server with --api-key to enable. + verify_ssl: Whether to verify SSL certificates. Default True. + Set to False for servers with self-signed certificates. + batch_size: Initial number of images per /pooling request. + Automatically halved on CUDA OOM, down to a minimum of 1. + request_timeout: HTTP request timeout in seconds. Default 120. + """ + + url: str + model_name: str + auto_deploy: bool = False + ssh_host: Optional[str] = None + ssh_user: Optional[str] = None # None → resolved to $USER at deploy time + ssh_key_path: Optional[str] = None + n_gpus: int = -1 + port: int = 8000 + hf_token: Optional[str] = None + api_key: Optional[str] = None + verify_ssl: bool = True + batch_size: int = 4 + request_timeout: int = 120 + + @field_validator("url") + @classmethod + def strip_trailing_slash(cls, v: str) -> str: + return v.rstrip("/") + + @field_validator("model_name") + @classmethod + def validate_vllm_compatible_model(cls, v: str) -> str: + lower = v.lower() + if not any(pat in lower for pat in _VLLM_COMPATIBLE_PATTERNS): + raise ValueError( + f"model_name '{v}' does not appear to be compatible with the vLLM " + f"/pooling endpoint. vLLM >=0.19.0 supports only ColQwen3/ColQwen3.5 " + f"models (model name must contain 'colqwen3'). " + f"ColPali, ColQwen2, and ColQwen2.5 are not supported by vLLM. " + f"Use a ColQwen3.5 model such as 'athrael-soju/colqwen3.5-4.5B-v3', " + f"or run the model locally without an embedding server." + ) + return v + + @field_validator("n_gpus") + @classmethod + def validate_n_gpus(cls, v: int) -> int: + if v < -1 or v == 0: + raise ValueError("n_gpus must be -1 (all GPUs) or a positive integer") + return v + + @model_validator(mode="after") + def auto_deploy_requires_ssh_host(self) -> "EmbeddingServerConfig": + if self.auto_deploy and not self.ssh_host: + raise ValueError("ssh_host is required when auto_deploy=True") + return self + + @classmethod + def from_dict(cls, d: dict) -> "EmbeddingServerConfig": + """Convenience constructor from a plain config dict (e.g. from JSON).""" + return cls(**d) diff --git a/foretrieval/embedding_server/manager.py b/foretrieval/embedding_server/manager.py new file mode 100644 index 0000000..dad7be0 --- /dev/null +++ b/foretrieval/embedding_server/manager.py @@ -0,0 +1,395 @@ +"""Remote embedding server deployment manager. + +Handles Docker-based deployment of a vLLM embedding server on a remote GPU +machine via SSH. + +Deployment metadata is stored at ~/.foretrieval/deployment.json on the *remote* +server. This file acts as the authoritative record of what is running: +- If the file is absent → deploy from scratch. +- If the file is present → health-check the running container; redeploy if down. + +The manager always requires auto_deploy=True to trigger any SSH activity; +callers that only want to USE an existing server need not instantiate this. +""" + +from __future__ import annotations + +import json +import logging +from datetime import datetime, timezone +from typing import Callable, Optional + +from .config import EmbeddingServerConfig + +logger = logging.getLogger(__name__) + +# Remote path where deployment metadata is stored. +# Remote path for deployment metadata. +# +# Relative to the SSH user's home directory. Resolved to an absolute path +# at runtime via ``sftp.normalize('.')`` so that any future SFTP usage +# (which does NOT expand ``~``) sees a usable path. The remote shell +# would expand ``~`` for plain commands; the absolute form keeps both +# paths consistent and avoids the foot-gun. +_REMOTE_METADATA_SUBPATH = ".foretrieval/deployment.json" +_CONTAINER_NAME = "foretrieval_embedding_server" + +# vLLM Docker image. +_VLLM_IMAGE = "vllm/vllm-openai:latest" + +# Port vLLM listens on *inside* the container. +# vLLM always binds to 8000 internally; this is not configurable without +# rebuilding the image. EmbeddingServerConfig.port controls only the +# *host-side* binding (left-hand side of Docker's -p HOST:CONTAINER mapping), +# allowing callers to expose the service on any host port. +_CONTAINER_INTERNAL_PORT = 8000 + + +class EmbeddingServerManager: + """Manages deployment of the vLLM embedding server via SSH + Docker. + + Parameters + ---------- + config: + EmbeddingServerConfig with ssh_host, ssh_user, n_gpus, port, etc. + """ + + def __init__(self, config: EmbeddingServerConfig) -> None: + if not config.ssh_host: + raise ValueError("EmbeddingServerManager requires ssh_host in config") + self.config = config + self._ssh: Optional[object] = None # paramiko.SSHClient, lazy + self._cached_home: Optional[str] = None # remote $HOME, lazy + + # ------------------------------------------------------------------ + # Remote path helpers + # ------------------------------------------------------------------ + + def _remote_home(self) -> str: + """Return the SSH user's absolute home directory on the remote host. + + Resolved once per manager instance via ``sftp.normalize('.')``. + """ + if self._cached_home is not None: + return self._cached_home + ssh = self._get_ssh() + sftp = ssh.open_sftp() + try: + self._cached_home = sftp.normalize(".") + finally: + sftp.close() + return self._cached_home + + def _metadata_path(self) -> str: + """Return the absolute path to the remote deployment-metadata file.""" + return f"{self._remote_home()}/{_REMOTE_METADATA_SUBPATH}" + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + def ensure_deployed(self) -> None: + """Ensure the embedding server is running on the remote host. + + Flow: + 1. Check remote metadata file. + 2. If absent → deploy from scratch. + 3. If present → health-check; redeploy if unhealthy. + """ + try: + import paramiko # noqa: F401 + except ImportError as exc: + raise ImportError( + "paramiko is required for auto_deploy. " + "Install it with: pip install 'foretrieval[embedding_server]'" + ) from exc + + logger.info("Ensuring embedding server is deployed on %s", self.config.ssh_host) + metadata = self._read_remote_metadata() + + if metadata is None: + logger.info("No deployment metadata found — deploying from scratch") + self._deploy() + else: + logger.info( + "Found existing deployment (model=%s, deployed_at=%s)", + metadata.get("model_name"), + metadata.get("deployed_at"), + ) + if self._is_container_running(): + logger.info("Container is running and healthy — nothing to do") + else: + logger.warning("Container not running — redeploying") + self._deploy() + + def stop(self) -> None: + """Stop and remove the Docker container, delete metadata file.""" + logger.info("Stopping embedding server on %s", self.config.ssh_host) + self._run_remote(f"docker stop {_CONTAINER_NAME} 2>/dev/null || true") + self._run_remote(f"docker rm {_CONTAINER_NAME} 2>/dev/null || true") + self._run_remote(f"rm -f {self._metadata_path()}") + logger.info("Embedding server stopped") + + def redeploy(self, on_line: Optional[Callable[[str], None]] = None) -> None: + """Force a fresh container pull + restart regardless of current state. + + Args: + on_line: Optional callback invoked with every line of remote + stdout (Docker pull / run output). Best-effort: callback + exceptions are swallowed. + """ + try: + import paramiko # noqa: F401 + except ImportError as exc: + raise ImportError( + "paramiko is required for auto_deploy. " + "Install it with: pip install 'foretrieval[embedding_server]'" + ) from exc + + logger.info("Force redeploying embedding server on %s", self.config.ssh_host) + self._deploy(on_line=on_line) + + def is_running(self) -> bool: + """Return True iff the container is currently up.""" + try: + return self._is_container_running() + except Exception: # noqa: BLE001 + return False + + def get_remote_metadata(self) -> Optional[dict]: + """Return the remote deployment metadata, or None if not deployed.""" + try: + return self._read_remote_metadata() + except Exception: # noqa: BLE001 + return None + + # ------------------------------------------------------------------ + # Deploy + # ------------------------------------------------------------------ + + def _deploy(self, on_line: Optional[Callable[[str], None]] = None) -> None: + """Pull image, resolve GPU count, run container, write metadata. + + Args: + on_line: Optional callback for streaming remote stdout + (Docker pull / run output) into an interactive UI. + """ + # Resolve GPU count. + n_gpus = self._resolve_n_gpus() + logger.info("Using %d GPU(s) for tensor parallelism", n_gpus) + if on_line is not None: + try: + on_line(f"Using {n_gpus} GPU(s) for tensor parallelism") + except Exception: # noqa: BLE001 + pass + + # Stop any stale container first. + self._run_remote(f"docker stop {_CONTAINER_NAME} 2>/dev/null || true", on_line=on_line) + self._run_remote(f"docker rm {_CONTAINER_NAME} 2>/dev/null || true", on_line=on_line) + + # Pull image (no-op if already present). + logger.info("Pulling %s", _VLLM_IMAGE) + if on_line is not None: + try: + on_line(f"Pulling {_VLLM_IMAGE} …") + except Exception: # noqa: BLE001 + pass + self._run_remote(f"docker pull {_VLLM_IMAGE}", on_line=on_line) + + # Build docker run command. + cmd = self._build_docker_run_cmd(n_gpus) + logger.info("Starting container: %s", cmd) + if on_line is not None: + try: + on_line("Starting container …") + except Exception: # noqa: BLE001 + pass + self._run_remote(cmd, on_line=on_line) + + # Write metadata. + metadata = { + "model_name": self.config.model_name, + "container_name": _CONTAINER_NAME, + "port": self.config.port, + "n_gpus": n_gpus, + "image": _VLLM_IMAGE, + "deployed_at": datetime.now(timezone.utc).isoformat(), + } + self._write_remote_metadata(metadata) + logger.info("Deployment complete — server starting up on port %d", self.config.port) + if on_line is not None: + try: + on_line("Deployment complete.") + except Exception: # noqa: BLE001 + pass + + def _build_docker_run_cmd(self, n_gpus: int) -> str: + cfg = self.config + gpu_flag = "--gpus all" if cfg.n_gpus == -1 else f"--gpus {cfg.n_gpus}" + + hf_home = "/opt/huggingface" + env_parts = [f"-e HF_HOME={hf_home}"] + if cfg.hf_token: + env_parts.append(f"-e HF_TOKEN={cfg.hf_token}") + + vol_parts = [f"-v {hf_home}:{hf_home}"] + + # vLLM >=0.19.0: image entrypoint is already "vllm serve". + # Pass model + flags directly. --task removed; use --runner pooling + --convert embed. + # --max-model-len 8192: caps encoder cache budget to avoid OOM on 24GB GPUs. + # --gpu-memory-utilization 0.7: leaves headroom for KV cache allocation. + model_args = ( + f"{cfg.model_name} " + f"--runner pooling " + f"--convert embed " + f"--tensor-parallel-size {n_gpus} " + f"--gpu-memory-utilization 0.7 " + f"--max-model-len 8192 " + f"--trust-remote-code" + ) + + return ( + f"docker run -d " + f"--name {_CONTAINER_NAME} " + f"{gpu_flag} " + # cfg.port → host port (configurable, chosen by the caller) + # _CONTAINER_INTERNAL_PORT → container port (fixed by vLLM image) + f"-p {cfg.port}:{_CONTAINER_INTERNAL_PORT} " + f"{' '.join(env_parts)} " + f"{' '.join(vol_parts)} " + f"--restart unless-stopped " + f"--ipc=host " + f"{_VLLM_IMAGE} " + f"{model_args}" + ) + + # ------------------------------------------------------------------ + # GPU detection + # ------------------------------------------------------------------ + + def _resolve_n_gpus(self) -> int: + """Return actual GPU count: query remote if n_gpus=-1, else use config value.""" + if self.config.n_gpus != -1: + return self.config.n_gpus + stdout, _ = self._run_remote( + "nvidia-smi --query-gpu=name --format=csv,noheader 2>/dev/null | wc -l" + ) + try: + count = int(stdout.strip()) + except ValueError: + count = 1 + if count < 1: + count = 1 + logger.info("Detected %d GPU(s) on remote host", count) + return count + + # ------------------------------------------------------------------ + # Health / container status + # ------------------------------------------------------------------ + + def _is_container_running(self) -> bool: + """Return True if the Docker container exists and is running.""" + stdout, _ = self._run_remote( + f"docker inspect --format='{{{{.State.Running}}}}' " + f"{_CONTAINER_NAME} 2>/dev/null || echo false" + ) + return stdout.strip().lower() == "true" + + # ------------------------------------------------------------------ + # Remote metadata + # ------------------------------------------------------------------ + + def _read_remote_metadata(self) -> Optional[dict]: + path = self._metadata_path() + stdout, stderr = self._run_remote( + f"cat {path} 2>/dev/null || echo '__MISSING__'" + ) + text = stdout.strip() + if text == "__MISSING__" or not text: + return None + try: + return json.loads(text) + except json.JSONDecodeError: + logger.warning("Could not parse remote metadata: %s", text[:200]) + return None + + def _write_remote_metadata(self, metadata: dict) -> None: + path = self._metadata_path() + json_str = json.dumps(metadata).replace("'", "'\\''") + self._run_remote( + f"mkdir -p $(dirname {path}) && " + f"echo '{json_str}' > {path}" + ) + + # ------------------------------------------------------------------ + # SSH helpers + # ------------------------------------------------------------------ + + def _get_ssh(self): + """Return a connected paramiko SSHClient (lazy init). + + Honours ``~/.ssh/config`` (Host aliases, User, Port, IdentityFile, + ProxyCommand, ProxyJump) via :py:func:`foretrieval.ssh_utils.open_ssh_client`. + """ + if self._ssh is not None: + return self._ssh + + from ..ssh_utils import open_ssh_client + self._ssh = open_ssh_client( + ssh_host=self.config.ssh_host, + ssh_user=self.config.ssh_user, + ssh_key_path=self.config.ssh_key_path, + ) + return self._ssh + + def _run_remote( + self, + cmd: str, + on_line: Optional[Callable[[str], None]] = None, + ) -> tuple[str, str]: + """Run a shell command on the remote host and return (stdout, stderr). + + Raises RuntimeError if the exit code is non-zero (for commands that + don't have their own || true fallback). + + When ``on_line`` is provided, stdout is streamed line by line and the + callback is invoked for each. Useful for surfacing Docker output in + interactive UIs. Callback exceptions are swallowed. + """ + ssh = self._get_ssh() + logger.debug("Remote: %s", cmd) + _, stdout_f, stderr_f = ssh.exec_command(cmd) + + if on_line is None: + exit_code = stdout_f.channel.recv_exit_status() + stdout = stdout_f.read().decode("utf-8", errors="replace") + stderr = stderr_f.read().decode("utf-8", errors="replace") + else: + collected: list[str] = [] + for raw in iter(stdout_f.readline, ""): + if not raw: + break + collected.append(raw) + try: + on_line(raw.rstrip("\n")) + except Exception: # noqa: BLE001 + pass + exit_code = stdout_f.channel.recv_exit_status() + stdout = "".join(collected) + stderr = stderr_f.read().decode("utf-8", errors="replace") + + if stderr: + logger.debug("Remote stderr: %s", stderr[:300]) + if exit_code != 0 and "|| true" not in cmd and "2>/dev/null" not in cmd: + raise RuntimeError( + f"Remote command failed (exit {exit_code}): {cmd}\n" + f"stderr: {stderr[:500]}" + ) + return stdout, stderr + + def __del__(self) -> None: + if self._ssh is not None: + try: + self._ssh.close() + except Exception: + pass diff --git a/foretrieval/file_to_pdf.py b/foretrieval/file_to_pdf.py new file mode 100644 index 0000000..141d857 --- /dev/null +++ b/foretrieval/file_to_pdf.py @@ -0,0 +1,179 @@ +import os +import subprocess +import shutil +from pathlib import Path +from typing import Optional + + +def epub_to_pdf(epub_path: Path, pdf_path: Path) -> bool: + """Converts EPUB into PDF via Calibre (ebook-convert).""" + calibre = shutil.which("ebook-convert") + if not calibre: + print("❌ Calibre (ebook-convert) not found in PATH or not installed.") + return False + + try: + cmd = [calibre, str(epub_path), str(pdf_path)] + subprocess.run(cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + if pdf_path.exists(): + print(f"✅ EPUB converted via Calibre : {pdf_path}") + return True + except Exception as e: + print(f"⚠️ Error converting EPUB with Calibre : {e}") + + return False + + +def _find_libreoffice() -> Optional[Path]: + """Finds the path to LibreOffice/soffice if present, otherwise returns None.""" + candidates = [ + os.environ.get("LIBREOFFICE_PATH"), + shutil.which("soffice"), + shutil.which("libreoffice"), + r"C:\Program Files\LibreOffice\program\soffice.exe", + r"C:\Program Files (x86)\LibreOffice\program\soffice.exe", + "/usr/bin/libreoffice", + "/usr/bin/soffice", + "/snap/bin/libreoffice", + "/Applications/LibreOffice.app/Contents/MacOS/soffice", + ] + for c in candidates: + if c and Path(c).exists(): + return Path(c) + return None + + +def _convert_to_pdf(input_file: Path) -> Optional[Path]: + """Converts a file into a persistant PDF in the same folder as the input file.""" + forbidden_ext = {".exe", ".zip", ".tar", ".gz", ".7z", ".bat", ".sh"} + ext = input_file.suffix.lower() + + if ext in forbidden_ext: + print(f"⚠️ File ignored for PDF conversion: {input_file}") + return None + + output_pdf = input_file.with_suffix(".pdf") + + if ext == ".epub": + try: + epub_to_pdf(input_file, output_pdf) + if output_pdf.exists(): + return output_pdf + except Exception as e: + print(f"⚠️ Conversion EPUB→PDF failed: {input_file} ({e})") + return None + + # LibreOffice + lo = _find_libreoffice() + if lo: + try: + cmd = [ + str(lo), + "--headless", + "--convert-to", + "pdf", + "--outdir", + str(input_file.parent), + str(input_file), + ] + subprocess.run( + cmd, check=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + if output_pdf.exists(): + print(f"✅ Converted via LibreOffice : {input_file}") + return output_pdf + except Exception as e: + print(f"⚠️ LibreOffice conversion failed : {input_file} ({e})") + + # MS Office (Windows) + if os.name == "nt": + try: + import win32com.client + + if ext in {".doc", ".docx", ".rtf"}: + try: + word = win32com.client.DispatchEx("Word.Application") + word.Visible = False + doc = word.Documents.Open(str(input_file)) + doc.SaveAs(str(output_pdf), FileFormat=17) # wdFormatPDF + doc.Close(False) + word.Quit() + if output_pdf.exists(): + print(f"✅ Converted via MS Word : {input_file}") + return output_pdf + except Exception as e: + print(f"⚠️ MS Word conversion failed : {input_file} ({e})") + # Fallback docx2pdf + if ext == ".docx": + try: + from docx2pdf import convert + + convert(str(input_file), str(output_pdf)) + if output_pdf.exists(): + print(f"✅ Converted via docx2pdf : {input_file}") + return output_pdf + except Exception as e: + print(f"⚠️ docx2pdf conversion failed: {input_file} ({e})") + elif ext in {".xls", ".xlsx"}: + excel = win32com.client.DispatchEx("Excel.Application") + excel.Visible = False + wb = excel.Workbooks.Open(str(input_file)) + wb.ExportAsFixedFormat(0, str(output_pdf)) # xlTypePDF + wb.Close(False) + excel.Quit() + if output_pdf.exists(): + print(f"✅ Converted via MS Excel : {input_file}") + return output_pdf + elif ext in {".ppt", ".pptx"}: + ppt = win32com.client.DispatchEx("PowerPoint.Application") + pres = ppt.Presentations.Open(str(input_file), WithWindow=False) + pres.SaveAs(str(output_pdf), 32) # ppSaveAsPDF + pres.Close() + ppt.Quit() + if output_pdf.exists(): + print(f"✅ Converted via MS PowerPoint : {input_file}") + return output_pdf + except Exception as e: + print(f"⚠️ MS Office conversion failed : {input_file} ({e})") + + # Fallback text → PDF + if ext in {".txt", ".md", ".json", ".csv", ".yaml", ".yml", ".log"}: + try: + from reportlab.lib.pagesizes import A4 + from reportlab.lib.units import inch + from reportlab.pdfgen import canvas + + text = input_file.read_text(encoding="utf-8", errors="ignore") + c = canvas.Canvas(str(output_pdf), pagesize=A4) + width, height = A4 + margin = inch # Using inches for better consistency + y = height - margin + + # Using a more sophisticated text rendering approach + text_object = c.beginText(margin, y) + text_object.setFont("Courier", 10) # Monospace font for code/text files + for line in text.splitlines(): + if y < margin: + c.drawText(text_object) + c.showPage() + text_object = c.beginText(margin, height - margin) + y = height - margin + + # Handle long lines by splitting them + if len(line) > 120: + for i in range(0, len(line), 120): + text_object.textLine(line[i : i + 120]) + else: + text_object.textLine(line) + + c.drawText(text_object) + c.save() + + if output_pdf.exists(): + print(f"✅ Converted via reportlab : {input_file}") + return output_pdf + except Exception as e: + print(f"⚠️ Text→PDF conversion failed : {input_file} ({e})") + + print(f"❌ Conversion impossible : {input_file}") + return None diff --git a/foretrieval/integrations/__init__.py b/foretrieval/integrations/__init__.py new file mode 100644 index 0000000..4d8ed97 --- /dev/null +++ b/foretrieval/integrations/__init__.py @@ -0,0 +1,8 @@ +_all__ = [] + +try: + from foretrieval.integrations._langchain import FORetrievalLangChain # noqa: F401 + + _all__.append("FORetrievalLangChainRetriever") +except ImportError: + pass diff --git a/byaldi/integrations/_langchain.py b/foretrieval/integrations/_langchain.py similarity index 82% rename from byaldi/integrations/_langchain.py rename to foretrieval/integrations/_langchain.py index f07b0be..1f686c5 100644 --- a/byaldi/integrations/_langchain.py +++ b/foretrieval/integrations/_langchain.py @@ -3,10 +3,10 @@ from langchain_core.callbacks.manager import CallbackManagerForRetrieverRun from langchain_core.retrievers import BaseRetriever -from byaldi.objects import Result +from foretrieval.objects import Result -class ByaldiLangChainRetriever(BaseRetriever): +class FORetrievalLangChain(BaseRetriever): model: Any kwargs: dict = {} @@ -18,4 +18,4 @@ def _get_relevant_documents( ) -> List[Result]: """Get documents relevant to a query.""" docs = self.model.search(query, **self.kwargs) - return docs \ No newline at end of file + return docs diff --git a/foretrieval/metadata.py b/foretrieval/metadata.py new file mode 100644 index 0000000..f7279a3 --- /dev/null +++ b/foretrieval/metadata.py @@ -0,0 +1,458 @@ +from __future__ import annotations + +import os +import json +import mimetypes +from datetime import datetime, timezone + +from pathlib import Path +from typing import Any, Callable, Dict, Optional, List, Union + +from pydantic import BaseModel, Field +import asyncio + +# PydanticAI – core +from pydantic_ai import Agent + +# OpenAI / OpenRouter / Ollama (OpenAI-compatible API) +from pydantic_ai.models.openai import OpenAIModel +from pydantic_ai.providers.openai import OpenAIProvider + +# Mistral (native PydanticAI client) +try: + from pydantic_ai.models.mistral import MistralModel + from pydantic_ai.providers.mistral import MistralProvider + + _MISTRAL_AVAILABLE = True +except Exception: + _MISTRAL_AVAILABLE = False + +# Language & file parsers (optional) +try: + from langdetect import detect as lang_detect, DetectorFactory + + DetectorFactory.seed = 0 +except Exception: + lang_detect = None + +try: + from pypdf import PdfReader +except Exception: + PdfReader = None + +try: + import docx +except Exception: + docx = None + +try: + from pptx import Presentation +except Exception: + Presentation = None + +try: + from PIL import Image +except Exception: + Image = None + + +class AIMetadata(BaseModel): + author: Optional[str] = Field(None) + title: Optional[str] = Field(None) + language: Optional[str] = Field(None) + tags: List[str] = Field(default_factory=list) + document_type: str + short_description: str + + +def _iso_utc(ts: float) -> str: + return ( + datetime.fromtimestamp(ts, tz=timezone.utc).replace(microsecond=0).isoformat() + ) + + +def _guess_mime(path: Path) -> str: + mime, _ = mimetypes.guess_type(str(path)) + return mime or "application/octet-stream" + + +def _pdf_info(path: Path) -> Dict[str, Union[int, str, None]]: + out: Dict[str, Union[int, str, None]] = { + "page_count": None, + "author": None, + "title": None, + "text_preview": "", + } + if not PdfReader: + return out + try: + r = PdfReader(str(path)) + out["page_count"] = len(r.pages) + meta = r.metadata or {} + out["author"] = getattr(meta, "author", None) or meta.get("/Author") + out["title"] = getattr(meta, "title", None) or meta.get("/Title") + chunks = [] + for i in range(min(3, len(r.pages))): + try: + chunks.append(r.pages[i].extract_text() or "") + except Exception: + pass + out["text_preview"] = "\n".join(chunks).strip() + except Exception: + pass + return out + + +def _docx_text(path: Path) -> str: + if not docx: + return "" + try: + d = docx.Document(str(path)) + return "\n".join(p.text for p in d.paragraphs if p.text).strip() + except Exception: + return "" + + +def _pptx_text(path: Path) -> str: + if not Presentation: + return "" + try: + prs = Presentation(str(path)) + t = [] + for s in prs.slides: + for shp in s.shapes: + if hasattr(shp, "text") and shp.text: + t.append(shp.text) + return "\n".join(t).strip() + except Exception: + return "" + + +def _image_dims(path: Path): + if not Image: + return (None, None) + try: + with Image.open(path) as im: + return im.size + except Exception: + return (None, None) + + +def _text_preview(path: Path, ext: str) -> str: + ext = ext.lower() + if ext == ".pdf": + return _pdf_info(path)["text_preview"] or "" + if ext in {".docx"}: + return _docx_text(path) + if ext in {".pptx", ".ppt"}: + return _pptx_text(path) + if ext in {".txt", ".md", ".csv", ".json", ".yaml", ".yml"}: + try: + return Path(path).read_text(encoding="utf-8", errors="ignore")[:10000] + except Exception: + return "" + return "" + + +def _fallback_lang(text: str) -> Optional[str]: + if not text or not text.strip() or not lang_detect: + return None + try: + return lang_detect(text[:4000]) + except Exception: + return None + + +AI_SYSTEM_PROMPT = ( + "You are a meticulous document analyst. " + "Given raw text preview and basic file hints, you MUST output a compact, factual summary; " + "infer document_type from common categories; infer language; " + "return short, meaningful tags (3–8 max). " + "Keep author/title null if unknown; avoid hallucinating." +) + +AI_INSTRUCTIONS = ( + "Analyse the document preview and hints to produce strictly the fields of AIMetadata. " + "Language should be a short code like 'fr' or 'en'. " + "Document type: choose a concise label (e.g., 'technical note', 'scientific paper', 'accounting', 'contract', " + "'manual', 'report', 'invoice', 'specification', 'slide deck'). " + "Short description: 1–3 sentences. Keep it helpful and neutral. " + "Tags: 3–8 short topic tags." +) + + +def _build_model_from_cfg(ai_cfg: Dict[str, Any]): + """ + Dynamically builds a PydanticAI model according to the chosen provider. + + ai_cfg expects for example: + { + "provider": "openai" | "openrouter" | "mistral" | "mistralai" | "ollama", + "api_key": "...", + "base_url": "...", # optional + "name": "model-id" # optional + } + """ + provider_raw = (ai_cfg.get("provider") or "openai").lower() + name = ai_cfg.get("name") + base_url = ai_cfg.get("base_url") + api_key = ai_cfg.get("api_key") + + if provider_raw in ("mistral", "mistralai"): + if not _MISTRAL_AVAILABLE: + raise RuntimeError( + "The 'mistral' provider is requested but the Mistral dependencies for pydantic-ai are not installed.\n" + 'Install: pip install "pydantic-ai-slim[mistral]"\n' + 'or: pip install "pydantic-ai[mistral]"' + ) + api_key = api_key or os.getenv("MISTRAL_API_KEY") + if not name: + name = "mistral-small-latest" + + # Do not pass base_url when it is not explicitly overridden — the + # MistralProvider SDK derives the correct endpoint internally. + # Passing "https://api.mistral.ai/v1" causes the SDK to produce + # double-path URLs (e.g. /v1/v1/...) resulting in HTTP 404. + provider_kwargs: dict = {"api_key": api_key} + if base_url: + provider_kwargs["base_url"] = base_url + + provider = MistralProvider(**provider_kwargs) + return MistralModel(name, provider=provider) + + elif provider_raw == "openrouter": + base_url = base_url or "https://openrouter.ai/api/v1" + api_key = api_key or os.getenv("OPENROUTER_API_KEY") + if not name: + # Important: namespace required by OpenRouter + name = "mistralai/mistral-small-latest" + provider = OpenAIProvider(api_key=api_key, base_url=base_url) + return OpenAIModel(model_name=name, provider=provider) + + elif provider_raw == "ollama": + # Ollama exposes an OpenAI-compatible API locally + base_url = base_url or "http://localhost:11434/v1" + api_key = api_key or os.getenv("OLLAMA_API_KEY") or "ollama" + if not name: + name = "llama3.1" # adjust according to your local tags + provider = OpenAIProvider(api_key=api_key, base_url=base_url) + return OpenAIModel(model_name=name, provider=provider) + + elif provider_raw in ("openai", "openaiapi", "openai-api"): + base_url = base_url or os.getenv( + "OPENAI_BASE_URL" + ) # generally None => api.openai.com + api_key = api_key or os.getenv("OPENAI_API_KEY") + if not name: + name = "gpt-4o-mini" + provider = OpenAIProvider(api_key=api_key, base_url=base_url) + return OpenAIModel(model_name=name, provider=provider) + + else: + raise ValueError(f"Unknown provider: {provider_raw}") + + +def generate_index_description( + doc_metadata: Dict[int, Dict[str, Any]], + ai_cfg: Optional[Dict[str, Any]], +) -> str: + """Generate a global description of an index from per-document AI metadata. + + Collects ``short_description`` fields from ``doc_metadata``, deduplicates + them, then asks the LLM to write a concise corpus-level summary. Returns + an empty string when ``ai_cfg`` is ``None`` or no descriptions are available + (caller should treat empty string as "no description"). + + Args: + doc_metadata: Mapping of doc_id → metadata dict (as stored in the index). + ai_cfg: Same provider config dict used by ``ai_metadata_provider_factory``. + Pass ``None`` to skip LLM generation. + + Returns: + A 1–3 sentence corpus description, or ``""`` on failure / no AI config. + """ + if not ai_cfg: + return "" + + descriptions = [ + md["short_description"] + for md in doc_metadata.values() + if md.get("short_description") + ] + if not descriptions: + return "" + + try: + model = _build_model_from_cfg(ai_cfg) + + class IndexSummary(BaseModel): + description: str = Field( + description="1–3 sentence summary of the document corpus" + ) + + agent = Agent( + model=model, + instructions=( + "You are a librarian. Given short descriptions of individual documents " + "in a collection, write a concise 1–3 sentence summary that describes " + "the overall corpus: its domain, topics, and document types." + ), + output_type=IndexSummary, + model_settings={"temperature": 0, "tools": []}, + ) + + doc_list = "\n".join(f"- {d}" for d in descriptions) + user_prompt = ( + f"Here are the individual document descriptions ({len(descriptions)} documents):\n" + f"{doc_list}\n\n" + "Write a global description for this document collection." + ) + + try: + result = agent.run_sync(user_prompt) + except AttributeError: + result = asyncio.run(agent.run_sync(user_prompt)) + + return result.output.description or "" + except Exception: + return "" + + +def ai_metadata_provider_factory( + ai_cfg: Optional[Dict[str, Any]], +) -> Callable[[Path], Dict[str, Any]]: + """ + Returns a callable(Path) -> Dict complete metadata. + - Without ai_cfg => provider 'no-LLM' (fills only the raw fields). + - With ai_cfg => builds a PydanticAI Agent with the right model according to the provider, + instructions=AI_SYSTEM_PROMPT, output_type=AIMetadata. + """ + + # ----- Without AI: just the "raw" fields ----- + if not ai_cfg: + + def _no_ai_provider(p: Path) -> Dict[str, Any]: + p = p.resolve() + stat = p.stat() + ext = p.suffix.lower() + mime = _guess_mime(p) + md: Dict[str, Any] = { + "source_path": str(p), + "stem": p.stem, + "ext": ext, + "mime": mime, + "mtime": _iso_utc(stat.st_mtime), + "page_count": None, + "image_width": None, + "image_height": None, + "author": None, + "title": None, + "language": None, + "tags": [], + "document_type": "unknown", + "short_description": "", + } + if ext == ".pdf": + pdf_meta = _pdf_info(p) + md["page_count"] = pdf_meta.get("page_count") + if pdf_meta.get("author"): + md["author"] = str(pdf_meta["author"]) + if pdf_meta.get("title"): + md["title"] = str(pdf_meta["title"]) + if ext in {".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".gif"}: + w, h = _image_dims(p) + md["image_width"], md["image_height"] = w, h + return md + + return _no_ai_provider + + # ----- With AI: builds the model & PydanticAI agent ----- + model = _build_model_from_cfg(ai_cfg) + + # NB: PydanticAI canonical structure: instructions = system, output_type = Pydantic schema + # Adding a "tools": [] parameter to avoid any tool-calling attempt if the backend doesn't support it + agent = Agent( + model=model, + instructions=AI_SYSTEM_PROMPT, + output_type=AIMetadata, + model_settings={"temperature": 0, "tools": []}, + ) + + def _provider(p: Path) -> Dict[str, Any]: + p = p.resolve() + stat = p.stat() + ext = p.suffix.lower() + mime = _guess_mime(p) + + md: Dict[str, Any] = { + "source_path": str(p), + "stem": p.stem, + "ext": ext, + "mime": mime, + "mtime": _iso_utc(stat.st_mtime), + "page_count": None, + "image_width": None, + "image_height": None, + "author": None, + "title": None, + "language": None, + "tags": [], + "document_type": "unknown", + "short_description": "", + } + + if ext == ".pdf": + pdf_meta = _pdf_info(p) + md["page_count"] = pdf_meta.get("page_count") + if pdf_meta.get("author"): + md["author"] = str(pdf_meta["author"]) + if pdf_meta.get("title"): + md["title"] = str(pdf_meta["title"]) + + if ext in {".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".gif"}: + w, h = _image_dims(p) + md["image_width"], md["image_height"] = w, h + + preview = _text_preview(p, ext) + lang_hint = _fallback_lang(preview) if preview else None + + # User context (passed in the run prompt) + ctx = { + "file_name": p.name, + "mime": mime, + "ext": ext, + "pdf_author_hint": md.get("author"), + "pdf_title_hint": md.get("title"), + "detected_lang_hint": lang_hint, + "text_preview": (preview[:12000] if preview else ""), + } + user_prompt = ( + AI_INSTRUCTIONS + + "\n\n### CONTEXT (JSON)\n" + + json.dumps(ctx, ensure_ascii=False) + + "\n\n### OUTPUT FORMAT\n" + + "Respond strictly with the required fields; do not invent author/title if unknown." + ) + + # ✅ PydanticAI canonical API: run(prompt) -> result.output (typed output_type) + try: + result = agent.run_sync( + user_prompt + ) # pydantic-ai >= 0.0.15 (depending on versions) + except AttributeError: + result = asyncio.run(agent.run_sync(user_prompt)) # universal fallback + + ai: AIMetadata = result.output + + # Field merging + if ai.author: + md["author"] = ai.author + if ai.title: + md["title"] = ai.title + md["language"] = ai.language or lang_hint + md["tags"] = ai.tags or [] + md["document_type"] = ai.document_type + md["short_description"] = ai.short_description + return md + + return _provider diff --git a/foretrieval/models_metadata.py b/foretrieval/models_metadata.py new file mode 100644 index 0000000..28e7c55 --- /dev/null +++ b/foretrieval/models_metadata.py @@ -0,0 +1,134 @@ +from datetime import datetime +from pathlib import Path +from typing import Any, Dict, List, Optional, Union, Callable +from pydantic import BaseModel, Field, field_validator + + +class DocMetadata(BaseModel): + source_path: Optional[str] = None + stem: Optional[str] = None + ext: Optional[str] = None + mime: Optional[str] = None + mtime: Optional[datetime] = None + page_count: Optional[int] = None + image_width: Optional[int] = None + image_height: Optional[int] = None + author: Optional[str] = None + title: Optional[str] = None + language: Optional[str] = None + tags: List[str] = Field(default_factory=list) + document_type: Optional[str] = None + short_description: Optional[str] = None + + # --- Useful Normalisations --- + @field_validator("ext", mode="before") + def _norm_ext(cls, v): + if v is None: + return v + v = str(v).strip().lower() + return v if v.startswith(".") else f".{v}" + + @field_validator("language", "author", "title", mode="before") + def _norm_str(cls, v): + return v.strip() if isinstance(v, str) else v + + @field_validator("tags", mode="before") + def _norm_tags(cls, v): + if v is None: + return [] + if isinstance(v, str): + v = [v] + return [str(t).strip().lower() for t in v] + + @field_validator("mtime", mode="before") + def _parse_dt(cls, v): + if v is None: + return None + if isinstance(v, datetime): + return v + # support 'Z' + return datetime.fromisoformat(str(v).replace("Z", "+00:00")) + + def as_jsonable(self) -> Dict[str, Any]: + # useful for export (datetime -> isoformat) + d = self.model_dump() + if d.get("mtime"): + d["mtime"] = d["mtime"].isoformat() + return d + + +class MetadataFilter(BaseModel): + # simple values OR lists + language: Optional[Union[str, List[str]]] = None + ext: Optional[Union[str, List[str]]] = None + tags: Optional[Union[str, List[str]]] = None + document_type: Optional[Union[str, List[str]]] = None + # operators on mtime (ISO) + mtime: Optional[Dict[str, str]] = None # ex: {">=":"2025-09-01T00:00:00Z"} + # regex patterns: maps any metadata field name to a Python regex pattern. + # Applied with re.search(..., re.IGNORECASE) — case-insensitive substring + # match by default. Use explicit (?-i) in the pattern to opt out. + # Example: {"stem": "general", "title": "motor|pump"} + regex: Optional[Dict[str, str]] = None + + # global logic + logic: str = "AND" # "AND" or "OR" + + class Config: + extra = "allow" # allow other metadata keys if needed + + @field_validator("ext", mode="before") + def _norm_filter_ext(cls, v): + def nx(s): + s = s.strip().lower() + return s if s.startswith(".") else f".{s}" + + if v is None: + return v + if isinstance(v, list): + return [nx(x) for x in v] + return nx(v) + + @field_validator("tags", mode="before") + def _norm_filter_tags(cls, v): + if v is None: + return v + if isinstance(v, str): + v = [v] + return [str(t).strip().lower() for t in v] + + +def build_metadata_list_for_dir( + input_dir: Path, provider: Callable[[Path], Dict[str, Any]] +) -> List[Optional[DocMetadata]]: + """Build a metadata list aligned with ``ColPaliModel.index()``'s file ordering. + + ``index()`` enumerates all files recursively using:: + + sorted((p for p in input_dir.rglob("*") if p.is_file()), + key=lambda p: p.relative_to(input_dir)) + + This function mirrors that enumeration exactly so that ``metadata[i]`` + corresponds to the ``i``-th file visited by ``index()``. Sub-directories + are **not** represented in the list (``index()`` skips them). + + Args: + input_dir: Root directory whose file tree will be enumerated. + provider: Callable that receives a ``Path`` and returns a metadata + dict compatible with ``DocMetadata``, or ``None``/empty dict to + indicate that no metadata is available for that file. + + Returns: + A list with one entry per file found by recursive ``rglob``, in the + same order used by ``ColPaliModel.index()``. Each entry is either a + ``DocMetadata`` instance or ``None`` when the provider returns nothing. + """ + items = sorted( + (p for p in input_dir.rglob("*") if p.is_file()), + key=lambda p: p.relative_to(input_dir), + ) + md_list: List[Optional[DocMetadata]] = [] + for p in items: + raw = provider(p) + md_list.append(DocMetadata(**raw) if raw else None) + return md_list diff --git a/foretrieval/objects.py b/foretrieval/objects.py new file mode 100644 index 0000000..94f276b --- /dev/null +++ b/foretrieval/objects.py @@ -0,0 +1,37 @@ +from typing import Optional + +from pydantic import BaseModel + + +class Result(BaseModel): + doc_id: int + """The unique identifier for the document.""" + page_num: int + """The page number within the document.""" + chunk_num: Optional[int] = None + """The page number within the document.""" + score: Optional[float] = None + """The relevance score of the document.""" + metadata: Optional[dict] = None + """Additional metadata associated with the document.""" + base64: Optional[str] = None + """Base64 encoded content of the document.""" + + def dict(self): + return { + "doc_id": self.doc_id, + "page_num": self.page_num, + "chunk_num": self.chunk_num, + "score": self.score, + "metadata": self.metadata, + "base64": self.base64, + } + + def __getitem__(self, key): + return getattr(self, key) + + def __str__(self): + return str(self.dict()) + + def __repr__(self): + return self.__str__() diff --git a/foretrieval/plot_utils.py b/foretrieval/plot_utils.py new file mode 100644 index 0000000..501a798 --- /dev/null +++ b/foretrieval/plot_utils.py @@ -0,0 +1,313 @@ +import base64 +import io +import numpy as np +from PIL import Image, ImageDraw +import matplotlib +import torch +import torch.nn.functional as F + +def majority_token_id(input_ids: torch.Tensor) -> int: + """ + input_ids: tensor 1D [L] ou 2D [1,L] + retourne l'ID le plus fréquent. + Utile pour savoir quel token correspond à l'image dans les embeddings ColPali. Ca change régulièrement. TODO: comprendre pourquoi + """ + if input_ids.dim() == 2: + input_ids = input_ids[0] + u, c = torch.unique(input_ids.cpu(), return_counts=True) + return int(u[torch.argmax(c)].item()) + +def pil_to_base64_png(im: Image.Image) -> str: + buf = io.BytesIO() + im.save(buf, format="PNG") + return base64.b64encode(buf.getvalue()).decode("utf-8") + +def pil_from_base64(b64: str) -> Image.Image: + data = base64.b64decode(b64) + return Image.open(io.BytesIO(data)).convert("RGB") + +def grow_heatmap_patches_torch( + heat_2d: torch.Tensor, + patch_grow_pct: float = 100.0, # 100 = no-op + grow_mode: str = "max", # "max" | "mean" +) -> torch.Tensor: + if patch_grow_pct is None or patch_grow_pct <= 100.0: + return heat_2d + + scale = float(patch_grow_pct) / 100.0 + # 200% -> radius=1 (3x3), 300% -> radius=2 (5x5)... + radius = int(round(scale - 1.0)) + radius = max(1, radius) + k = 2 * radius + 1 + + x = heat_2d.float()[None, None, :, :] # [1,1,H,W] + if grow_mode == "max": + y = F.max_pool2d(x, kernel_size=k, stride=1, padding=radius) + elif grow_mode == "mean": + y = F.avg_pool2d(x, kernel_size=k, stride=1, padding=radius) + else: + raise ValueError("grow_mode doit être 'max' ou 'mean'") + return y[0, 0] + +def draw_circle_on_max_patch( + img: Image.Image, + heat_2d, # torch.Tensor [Hp,Wp] ou np array + circle_scale_pct: float = 600.0, # 300% => diamètre 3x patch + outline_width: int = 10, + add_double_stroke: bool = True, + shift_x: float = 0.0, # même sémantique que ton heatmap_overlay_base64 + shift_y: float = 0.0, + color_inner=(255, 255, 255, 255), # blanc + color_outer=(0, 0, 0, 255), # noir + patch_grow_pct: float = 100.0, # apply same grow as heatmap before picking argmax + grow_mode: str = "max", # "max" | "mean" — match heatmap caller +) -> Image.Image: + """ + Dessine un cercle (anneau) centré sur le patch max de heat_2d. + circle_scale_pct est relatif à la taille DU PATCH en pixels : + - 100% => diamètre == patch_w/patch_h (on prend une moyenne) + - 300% => diamètre == 3x patch (donc rayon == 1.5x patch) + + patch_grow_pct / grow_mode: optionally grow the heat grid (same as heatmap_overlay_base64) + before computing argmax, so the circle center aligns with the heatmap highlight. + Pass the same values used in the heatmap call (e.g. patch_grow_pct=300, grow_mode="mean"). + """ + + # --- heat -> torch tensor for optional grow --- + if hasattr(heat_2d, "detach"): + heat_t = heat_2d.detach().float() + else: + heat_t = torch.tensor(np.array(heat_2d), dtype=torch.float32) + + # apply same grow as heatmap so argmax lands on the same visual peak + if patch_grow_pct is not None and patch_grow_pct > 100.0: + heat_t = grow_heatmap_patches_torch(heat_t, patch_grow_pct=patch_grow_pct, grow_mode=grow_mode) + + # --- heat -> numpy + argmax --- + heat_np = heat_t.cpu().numpy() + + Hp, Wp = heat_np.shape + flat_idx = int(np.argmax(heat_np)) + r = flat_idx // Wp + c = flat_idx % Wp + + # --- patch size in pixels --- + W, H = img.size + patch_w = W / float(Wp) + patch_h = H / float(Hp) + + # centre du patch (c,r) en pixels + cx = (c + 0.5) * patch_w + cy = (r + 0.5) * patch_h + + # appliquer le même shift “display-only” que ton overlay (en unités patch) + cx += shift_x * patch_w + cy += shift_y * patch_h + + # on prend un rayon basé sur la moyenne des dimensions patch + scale = float(circle_scale_pct) / 100.0 + radius = 0.5 * max(patch_w, patch_h) * scale + + # --- draw (sur une copie RGBA pour alpha) --- + out = img.convert("RGBA") + draw = ImageDraw.Draw(out) + + bbox = (cx - radius, cy - radius, cx + radius, cy + radius) + + if add_double_stroke: + # stroke noir plus large + stroke blanc par-dessus (super lisible) + draw.ellipse(bbox, outline=color_outer, width=outline_width + 2) + draw.ellipse(bbox, outline=color_inner, width=outline_width) + else: + draw.ellipse(bbox, outline=color_inner, width=outline_width) + + return out.convert("RGB") + +def heatmap_overlay_base64( + img: Image.Image, + heat_2d, + alpha: float = 0.45, + cmap: str = "jet", + resize_interp: str = "bilinear", + shift_x: float = 0.0, + shift_y: float = 0.0, + patch_grow_pct: float = 100.0, # NEW + grow_mode: str = "max", # NEW +) -> str: + # --- optionally grow patches (in patch-grid space) --- + if hasattr(heat_2d, "detach"): + heat_t = heat_2d.detach() + else: + heat_t = torch.tensor(np.array(heat_2d), dtype=torch.float32) + + if patch_grow_pct is not None and patch_grow_pct > 100.0: + heat_t = grow_heatmap_patches_torch(heat_t, patch_grow_pct=patch_grow_pct, grow_mode=grow_mode) + + heat = heat_t.cpu().numpy() + + # normalize 0..1 (viz) + heat = heat - heat.min() + if heat.max() > 1e-9: + heat = heat / heat.max() + + W, H = img.size + hpatch, wpatch = heat.shape + + heat_img = Image.fromarray((heat * 255).astype(np.uint8)) + if resize_interp.lower() == "nearest": + resample = Image.NEAREST + elif resize_interp.lower() == "bilinear": + resample = Image.BILINEAR + else: + raise ValueError("resize_interp doit être 'nearest' ou 'bilinear'") + + heat_img = heat_img.resize((W, H), resample=resample) + heat_img = np.array(heat_img) + + # shift display-only + if abs(shift_x) > 1e-9 or abs(shift_y) > 1e-9: + px_per_patch_x = W / float(wpatch) + px_per_patch_y = H / float(hpatch) + shift_px_x = int(round(shift_x * px_per_patch_x)) + shift_px_y = int(round(shift_y * px_per_patch_y)) + + shifted = np.zeros_like(heat_img) + x_src0 = max(0, -shift_px_x); y_src0 = max(0, -shift_px_y) + x_dst0 = max(0, shift_px_x); y_dst0 = max(0, shift_px_y) + x_w = W - max(0, shift_px_x) - max(0, -shift_px_x) + y_h = H - max(0, shift_px_y) - max(0, -shift_px_y) + if x_w > 0 and y_h > 0: + shifted[y_dst0:y_dst0+y_h, x_dst0:x_dst0+x_w] = heat_img[y_src0:y_src0+y_h, x_src0:x_src0+x_w] + heat_img = shifted + + # colorize + blend (comme ton code) + heat_f = heat_img.astype(np.float32) / 255.0 + cmap_fn = matplotlib.colormaps[cmap] + rgba = cmap_fn(heat_f) + heat_rgb = (rgba[..., :3] * 255).astype(np.uint8) + + base = np.array(img).astype(np.float32) + overlay = heat_rgb.astype(np.float32) + blended = (1 - alpha) * base + alpha * overlay + blended = np.clip(blended, 0, 255).astype(np.uint8) + + out_pil = Image.fromarray(blended, mode="RGB") + buf = io.BytesIO() + out_pil.save(buf, format="PNG") + return base64.b64encode(buf.getvalue()).decode("utf-8") + +def build_heatmap_overlays_base64( + img: Image.Image, + heatmaps: dict, + interps=("nearest", "bilinear"), + alpha=0.45, + cmap="jet", + shift_x=0.0, + shift_y=0.0, + patch_grow_pct=100.0, + grow_mode="max", +): + overlays = {} + for mode, pack in heatmaps.items(): + heat_2d = pack["heat_2d"] + overlays[mode] = {} + for interp in interps: + overlays[mode][interp] = heatmap_overlay_base64( + img=img, + heat_2d=heat_2d, + alpha=alpha, + cmap=cmap, + resize_interp=interp, + shift_x=shift_x, + shift_y=shift_y, + patch_grow_pct=patch_grow_pct, + grow_mode=grow_mode, + ) + return overlays + +@torch.no_grad() +def compute_patch_heatmap( + q_emb: torch.Tensor, + p_emb: torch.Tensor, + input_ids: torch.Tensor, + image_grid_thw: torch.Tensor, + image_token_id: int, + mode: str, # "global_sum" | "soft_topk" + topk: int = 8, + temperature: float = 0.05, + normalize: bool = False, # laisse False pour rester fidèle au score ColPali +): + """ + Faithful uniquement: sim [Q,L] sur TOUS les tokens passage, puis projection sur patches image. + Retourne heat_2d [Hpatch, Wpatch]. + """ + + # shapes + if input_ids.dim() == 2: + input_ids = input_ids[0] + if q_emb.dim() == 3: + q_emb = q_emb[0] + if p_emb.dim() == 3: + p_emb = p_emb[0] + + q = q_emb.float() + p = p_emb.float() + + if normalize: + q = F.normalize(q, dim=-1) + p = F.normalize(p, dim=-1) + + # tokens image + mask_img = (input_ids == image_token_id) # [L] + img_pos = torch.nonzero(mask_img, as_tuple=False).squeeze(1) # [Nimg] + Nimg = int(img_pos.numel()) + if Nimg == 0: + raise ValueError("No image tokens found: check image_token_id") + + # map passage idx -> image idx (or -1) + passage_to_img = torch.full((p.shape[0],), -1, dtype=torch.long) + passage_to_img[img_pos] = torch.arange(Nimg, dtype=torch.long) + + # faithful sim sur tous tokens + sim = q @ p.T # [Q,L] + + heat = torch.zeros(Nimg, dtype=torch.float32) + non_image_mass = 0.0 + + if mode == "global_sum": + sums = sim.sum(dim=0).cpu() # [L] + for pj in range(sums.numel()): + img_j = int(passage_to_img[pj].item()) + if img_j >= 0: + heat[img_j] += float(sums[pj].item()) + else: + non_image_mass += float(sums[pj].item()) + + elif mode == "soft_topk": + vals, idx = sim.topk(k=min(topk, sim.shape[1]), dim=1) # [Q,K] + w = torch.softmax(vals / temperature, dim=1) # [Q,K] + contrib = (w * vals).cpu() # [Q,K] + + for i in range(idx.shape[0]): + for kk in range(idx.shape[1]): + pj = int(idx[i, kk].item()) + img_j = int(passage_to_img[pj].item()) + if img_j >= 0: + heat[img_j] += float(contrib[i, kk].item()) + else: + non_image_mass += float(contrib[i, kk].item()) + else: + raise ValueError("mode doit être 'global_sum' ou 'soft_topk'") + + # reshape Nimg -> grid + _, H, W = [int(x) for x in image_grid_thw.tolist()] + if (H % 2 == 0) and (W % 2 == 0) and ((H // 2) * (W // 2) == Nimg): + H2, W2 = H // 2, W // 2 + elif (H * W) == Nimg: + H2, W2 = H, W + else: + raise ValueError(f"Cannot infer grid: grid={H}x{W}, Nimg={Nimg}") + + heat_2d = heat.reshape(H2, W2) + + return heat_2d, non_image_mass \ No newline at end of file diff --git a/foretrieval/retriever.py b/foretrieval/retriever.py new file mode 100644 index 0000000..4dd076d --- /dev/null +++ b/foretrieval/retriever.py @@ -0,0 +1,276 @@ +from pathlib import Path +from typing import Any, Dict, List, Optional, Union, Callable + +from PIL import Image + +from .colpali import ColPaliModel +from .embedding_server import EmbeddingServerConfig +from .objects import Result +from .models_metadata import MetadataFilter + +# Optional langchain integration +try: + from .integrations import FORetrievalLangChain +except ImportError: + pass + + +class MultiModalRetrieverModel: + """ + Wrapper class for a pretrained multi-modal model, and all the associated utilities. + Allows you to load a pretrained model from disk or from the hub, build or query an index. + + ## Usage + + Load a pre-trained checkpoint: + + ```python + from foretrieval import MultiModalRetriever + + RAG = MultiModalRetriever.from_pretrained("vidore/colpali-v1.2") + ``` + + Both methods will load a fully initialised instance of ColPali, which you can use to build and query indexes. + + ```python + RAG.search("How many people live in France?") + ``` + """ + + model: ColPaliModel + + @classmethod + def from_pretrained( + cls, + pretrained_model_name_or_path: Union[str, Path], + index_root: str = ".rag_index", + ingestion: Dict[str, Any] = {"backend": "default"}, + device: str = "cuda", + verbose: int = 1, + embedding_server: Optional[EmbeddingServerConfig] = None, + # New: generic backend selection + storage_backend: str = "local", + storage_config: Optional[Dict[str, Any]] = None, + # Deprecated: use storage_backend="qdrant" instead + storage_qdrant: Optional[bool] = None, + load_in_4bit: bool = False, + load_in_8bit: bool = False, + bnb_4bit_quant_type: str = "nf4", + bnb_4bit_compute_dtype: str = "float16", + ): + """Load a ColPali model from a pre-trained checkpoint. + + Parameters: + pretrained_model_name_or_path (str): Local path or huggingface model name. + index_root (str): The root directory where indexes will be stored. Default is ".rag_index". + ingestion (Dict[str, Any]): Ingestion configuration for the model. Default is {"backend": "default"}. + device (str): The device to load the model on. Default is "cuda". + verbose (int): Verbosity level. Default is 1. + embedding_server (Optional[EmbeddingServerConfig]): If set, embeddings are computed + on the remote vLLM server instead of locally. Model weights are not loaded locally. + storage_backend (str): Vector storage backend. One of "local", "qdrant", "milvus". + Default is "local". + storage_config (Optional[Dict]): Backend-specific configuration dict. + For Milvus: {"candidate_limit": 64}. + storage_qdrant (bool): Deprecated. Use storage_backend="qdrant" instead. + load_in_4bit (bool): Load model in 4-bit quantization via BitsAndBytes. Requires + foretrieval[quantization] and a CUDA device. Default False. + load_in_8bit (bool): Load model in 8-bit quantization via BitsAndBytes. Requires + foretrieval[quantization] and a CUDA device. Default False. + bnb_4bit_quant_type (str): 4-bit quantization type, "nf4" or "fp4". Default "nf4". + bnb_4bit_compute_dtype (str): Compute dtype for 4-bit quant, e.g. "float16". Default "float16". + + Returns: + cls (MultiModalRetrieverModel): Initialised instance. + """ + instance = cls() + instance.model = ColPaliModel.from_pretrained( + pretrained_model_name_or_path, + index_root=index_root, + ingestion=ingestion, + device=device, + verbose=verbose, + embedding_server=embedding_server, + storage_backend=storage_backend, + storage_config=storage_config, + storage_qdrant=storage_qdrant, + load_in_4bit=load_in_4bit, + load_in_8bit=load_in_8bit, + bnb_4bit_quant_type=bnb_4bit_quant_type, + bnb_4bit_compute_dtype=bnb_4bit_compute_dtype, + ) + return instance + + @classmethod + def from_index( + cls, + index_path: Union[str, Path], + index_root: str = ".rag_index", + device: str = "cuda", + verbose: int = 1, + embedding_server: Optional[EmbeddingServerConfig] = None, + storage_backend: Optional[str] = None, + storage_config: Optional[Dict[str, Any]] = None, + ): + """Load an index and the associated model from disk. + + Parameters: + index_path (Union[str, Path]): Path to the index. + index_root (str): Root directory where the index lives. Default ".rag_index". + device (str): The device to load the model on. Default is "cuda". + embedding_server (Optional[EmbeddingServerConfig]): If set, embeddings are computed + on the remote vLLM server instead of locally. + storage_backend (Optional[str]): Force a storage backend. When set to + "remote", the index state (model name, bookkeeping) is loaded from + the vector_db_server instead of a local index directory. When None + the backend is read from the saved index_config.json.gz. + storage_config (Optional[Dict]): Backend-specific overrides (e.g. Milvus candidate_limit). + The storage_backend is read from the saved index_config.json.gz automatically. + + Returns: + cls (MultiModalRetrieverModel): Initialised instance with index loaded. + """ + instance = cls() + index_path = Path(index_path) + instance.model = ColPaliModel.from_index( + index_path, + index_root=index_root, + device=device, + verbose=verbose, + embedding_server=embedding_server, + storage_backend=storage_backend, + storage_config=storage_config, + ) + return instance + + def index( + self, + input_path: Union[str, Path], + index_name: Optional[str] = None, + doc_ids: Optional[int] = None, + store_collection_with_index: bool = False, + overwrite: bool = False, + metadata: Optional[ + Union[ + Dict[Union[str, int], Dict[str, Union[str, int]]], + List[Dict[str, Union[str, int]]], + ] + ] = None, + max_image_width: Optional[int] = None, + max_image_height: Optional[int] = None, + description: str = "", + ai_cfg: Optional[Dict[str, Any]] = None, + on_progress: Optional[Callable[[Dict[str, Any]], None]] = None, + **kwargs, + ): + """Build an index from input documents. + + Parameters: + input_path (Union[str, Path]): Path to the input documents. + index_name (Optional[str]): The name of the index that will be built. + doc_ids (Optional[List[Union[str, int]]]): List of document IDs. + store_collection_with_index (bool): Whether to store the collection with the index. + overwrite (bool): Whether to overwrite an existing index with the same name. + metadata (Optional[...]): Per-document metadata dicts. + description (str): Optional human-readable description of the corpus. + If empty and ``ai_cfg`` is provided, auto-generated from per-document + AI metadata ``short_description`` fields after indexing. + ai_cfg (Optional[Dict]): Provider config for the summary LLM (same format as + ``ai_metadata_provider_factory``). Only used for auto-generation when + ``description`` is empty. + on_progress (Optional[Callable]): Callback invoked with progress events. + Each event is a dict with at least a ``stage`` key. Possible stages: + ``"start"`` (with ``n_files``), ``"file_start"``, ``"page"``, + ``"file_done"``, ``"all_done"``. The callback is best-effort: + exceptions raised by the callback are swallowed. + + Returns: + None + """ + return self.model.index( + input_path, + index_name, + doc_ids, + store_collection_with_index, + overwrite=overwrite, + metadata=metadata, + max_image_width=max_image_width, + max_image_height=max_image_height, + description=description, + ai_cfg=ai_cfg, + on_progress=on_progress, + **kwargs, + ) + + def add_to_index( + self, + input_item: Union[str, Path, Image.Image], + store_collection_with_index: bool, + doc_id: Optional[int] = None, + metadata: Optional[Dict[str, Union[str, int]]] = None, + ): + """Add an item to an existing index. + + Parameters: + input_item (Union[str, Path, Image.Image]): The item to add to the index. + store_collection_with_index (bool): Whether to store the collection with the index. + doc_id (Union[str, int]): The document ID for the item being added. + metadata (Optional[Dict[str, Union[str, int]]]): Metadata for the document being added. + + Returns: + None + """ + return self.model.add_to_index( + input_item, store_collection_with_index, doc_id, metadata=metadata + ) + + def search( + self, + query: str, + k: int = 10, + filter_metadata: Optional[Union[Dict[str, Any], MetadataFilter]] = None, + return_base64_results: Optional[bool] = None, + ) -> List[Result]: + """Query an index. + + Parameters: + query (Union[str, List[str]]): The query or queries to search for. + k (int): The number of results to return. Default is 10. + filter_metadata (Optional[Union[Dict[str, Any], MetadataFilter]]): Metadata to filter results by. + return_base64_results (Optional[bool]): Whether to return base64-encoded image results. + + Returns: + Union[List[Result], List[List[Result]]]: A list of Result objects or a list of lists of Result objects. + """ + return self.model.search(query, k, filter_metadata, return_base64_results) + + def update_index_from_folder( + self, + folder: Union[str, Path], + store_collection_with_index: bool = False, + metadata_provider: Optional[Callable] = None, + batch_size: int = 1, + reindex_modified: bool = False, + ) -> Dict[int, str]: + return self.model.update_index_from_folder( + folder, + store_collection_with_index, + metadata_provider, + batch_size, + reindex_modified, + ) + + @property + def index_description(self) -> str: + """Human-readable description of the indexed corpus (empty string if not set).""" + return getattr(self.model, "index_description", "") + + def fetch(self, result: Result) -> Result: + """Fetch a result from the index.""" + return self.model.fetch_result_img(result) + + def get_doc_ids_to_file_names(self): + return self.model.get_doc_ids_to_file_names() + + def as_langchain_retriever(self, **kwargs: Any): + return FORetrievalLangChain(model=self, kwargs=kwargs) diff --git a/foretrieval/ssh_utils.py b/foretrieval/ssh_utils.py new file mode 100644 index 0000000..abdbed0 --- /dev/null +++ b/foretrieval/ssh_utils.py @@ -0,0 +1,161 @@ +"""Shared SSH connection helper. + +Opens a paramiko ``SSHClient`` while honouring ``~/.ssh/config`` so that +host aliases, ``HostName``, ``User``, ``Port``, ``IdentityFile``, +``ProxyCommand`` and ``ProxyJump`` all behave the same way as the ``ssh`` +command-line tool. + +This is the single source of truth for SSH connections used by every +deployment manager (currently the embedding server and the vector-DB +server). Keeping it in one place avoids drift between managers. + +The helper is deliberately minimal — it returns a connected +``SSHClient`` and lets callers run commands or open SFTP sessions +themselves. +""" + +from __future__ import annotations + +import logging +import os +from pathlib import Path +from typing import Any, Dict, List, Optional, Union + +logger = logging.getLogger(__name__) + + +def _resolve_identity_files(host_cfg: Dict[str, Any]) -> Optional[Union[str, List[str]]]: + """Extract ``IdentityFile`` entries from a paramiko SSHConfig lookup. + + paramiko returns this as a list when set, even when there is only one + entry. We pass the value through unchanged to paramiko.connect which + accepts either a single path or a list of paths. + """ + identity = host_cfg.get("identityfile") + if not identity: + return None + # paramiko.SSHConfig already expands ~ for IdentityFile values. + return identity + + +def _build_proxy_command(host_cfg: Dict[str, Any]) -> Optional[str]: + """Return a ProxyCommand string from a paramiko SSHConfig lookup. + + Explicit ``ProxyCommand`` wins. If absent, ``ProxyJump`` is + translated to an equivalent ``ssh -W %h:%p `` invocation. + Returns ``None`` when neither is configured. + """ + pc = host_cfg.get("proxycommand") + if pc: + return pc + pj = host_cfg.get("proxyjump") + if pj: + # Translate ProxyJump to a ProxyCommand for paramiko's sock arg. + return f"ssh -W %h:%p {pj}" + return None + + +def open_ssh_client( + ssh_host: str, + ssh_user: Optional[str] = None, + ssh_key_path: Optional[str] = None, + ssh_config_path: Optional[Path] = None, +): + """Open a connected paramiko SSHClient using ``~/.ssh/config`` semantics. + + Args: + ssh_host: The (possibly aliased) host name. This is the same + string you would pass to the ``ssh`` command. + ssh_user: Optional override for the SSH user. When set, it wins + over any ``User`` directive in ``~/.ssh/config``. + ssh_key_path: Optional override for the identity file. When + set, it wins over any ``IdentityFile`` directive. + ssh_config_path: Optional path to an SSH config file. Defaults + to ``~/.ssh/config``. Pass a custom path in tests. + + Returns: + A connected ``paramiko.SSHClient``. Caller is responsible for + closing it. + + Raises: + ImportError: paramiko is not installed. + paramiko.SSHException: connection failed. + socket.gaierror: DNS resolution failed for the resolved host. + OSError: low-level network error (refused, unreachable, …). + """ + import paramiko + + if ssh_config_path is None: + ssh_config_path = Path("~/.ssh/config").expanduser() + + # Load and look up the host alias, if any. + host_cfg: Dict[str, Any] = {} + if ssh_config_path.exists(): + try: + cfg = paramiko.SSHConfig() + with ssh_config_path.open() as f: + cfg.parse(f) + host_cfg = cfg.lookup(ssh_host) + logger.debug( + "SSH config lookup for '%s' -> %s", + ssh_host, + {k: v for k, v in host_cfg.items() if k != "identityfile"}, + ) + except Exception as exc: # noqa: BLE001 + logger.warning( + "Failed to parse %s (%s) — falling back to direct connect", + ssh_config_path, exc, + ) + host_cfg = {} + + # Resolve effective connect args. + resolved_host: str = host_cfg.get("hostname", ssh_host) + resolved_port: int = int(host_cfg.get("port", 22)) + resolved_user: Optional[str] = ( + ssh_user + or host_cfg.get("user") + or os.environ.get("USER") + or "root" + ) + + connect_kwargs: Dict[str, Any] = { + "hostname": resolved_host, + "port": resolved_port, + "username": resolved_user, + "allow_agent": True, + "look_for_keys": True, + } + + # Identity file: explicit override > SSHConfig IdentityFile > paramiko default. + if ssh_key_path: + connect_kwargs["key_filename"] = ssh_key_path + else: + identity = _resolve_identity_files(host_cfg) + if identity is not None: + connect_kwargs["key_filename"] = identity + + # ProxyCommand / ProxyJump + proxy_cmd = _build_proxy_command(host_cfg) + if proxy_cmd: + # Expand the standard tokens that paramiko's SSHConfig.lookup() may + # leave in the string. We follow OpenSSH semantics: %h = remote + # hostname, %p = remote port, %r = remote user. + expanded = ( + proxy_cmd + .replace("%h", resolved_host) + .replace("%p", str(resolved_port)) + .replace("%r", resolved_user or "") + ) + logger.debug("Using ProxyCommand: %s", expanded) + connect_kwargs["sock"] = paramiko.ProxyCommand(expanded) + + client = paramiko.SSHClient() + client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + # Load known_hosts so AutoAddPolicy doesn't blow up the first time. + try: + client.load_system_host_keys() + except Exception: # noqa: BLE001 + pass + + client.connect(**connect_kwargs) + return client diff --git a/foretrieval/utils.py b/foretrieval/utils.py new file mode 100644 index 0000000..004649d --- /dev/null +++ b/foretrieval/utils.py @@ -0,0 +1,101 @@ +import re +from datetime import datetime +from typing import Any, Dict, List, Optional + +from .models_metadata import MetadataFilter + + +def _parse_iso(s: str) -> Optional[datetime]: + """Parse ISO format datetime string.""" + try: + return datetime.fromisoformat(s.replace("Z", "+00:00")) + except Exception: + return None + + +def _any_in(a: List[str], b: List[str]) -> bool: + """Check if any element in a is in b.""" + sa = {x.strip().lower() for x in a} + sb = {x.strip().lower() for x in b} + return len(sa & sb) > 0 + + +def _value_match(meta: Dict[str, Any], f: MetadataFilter) -> bool: + """Check if metadata matches filter criteria.""" + checks = [] + + if f.language is not None: + mv = (meta.get("language") or "").strip().lower() + if isinstance(f.language, list): + checks.append(mv in [x.strip().lower() for x in f.language]) + else: + checks.append(mv == f.language.strip().lower()) + + if f.ext is not None: + mv = (meta.get("ext") or "").strip().lower() + candidates = f.ext if isinstance(f.ext, list) else [f.ext] + checks.append(mv in candidates) + + if f.document_type is not None: + mv = (meta.get("document_type") or "").strip().lower() + cands = ( + f.document_type if isinstance(f.document_type, list) else [f.document_type] + ) + checks.append(mv in [x.strip().lower() for x in cands]) + + if f.tags is not None: + mv = [str(t).strip().lower() for t in (meta.get("tags") or [])] + cands = f.tags if isinstance(f.tags, list) else [f.tags] + checks.append(_any_in(mv, [str(x).strip().lower() for x in cands])) + + if f.mtime is not None: + m = meta.get("mtime") + mdt = _parse_iso(m) if isinstance(m, str) else None + if mdt is None: + checks.append(False) + else: + ok = True + for op, rhs in f.mtime.items(): + rdt = _parse_iso(rhs) + if rdt is None: + ok = False + break + if op == ">=" and not (mdt >= rdt): + ok = False + if op == "<=" and not (mdt <= rdt): + ok = False + if op == ">" and not (mdt > rdt): + ok = False + if op == "<" and not (mdt < rdt): + ok = False + if op == "==" and not (mdt == rdt): + ok = False + if not ok: + break + checks.append(ok) + + if f.regex is not None: + for field, pattern in f.regex.items(): + mv = str(meta.get(field) or "") + try: + checks.append(bool(re.search(pattern, mv, re.IGNORECASE))) + except re.error: + # Malformed pattern — treat as no match rather than crashing + checks.append(False) + + for k, v in f.__dict__.items(): + if k in {"language", "ext", "tags", "document_type", "mtime", "logic", "regex"}: + continue + if v is None: + continue + mv = meta.get(k) + if isinstance(v, list): + checks.append( + str(mv).strip().lower() in [str(x).strip().lower() for x in v] + ) + else: + checks.append(str(mv).strip().lower() == str(v).strip().lower()) + + if not checks: + return True + return all(checks) if f.logic.upper() == "AND" else any(checks) diff --git a/foretrieval/vector_db_server/Dockerfile.vector_db b/foretrieval/vector_db_server/Dockerfile.vector_db new file mode 100644 index 0000000..1ff702f --- /dev/null +++ b/foretrieval/vector_db_server/Dockerfile.vector_db @@ -0,0 +1,45 @@ +# FORetrieval vector-DB server Docker image. +# +# Build context must contain: +# foretrieval/ - the foretrieval Python package +# pyproject.toml - project metadata +# Dockerfile.vector_db - this file +# +# The image installs foretrieval with qdrant, milvus, and vector_db_server +# extras, then starts the FastAPI server on port 18000. +# Data is expected at /data (bind-mount from the host). + +# syntax=docker/dockerfile:1 +FROM python:3.12-slim + +# System dependencies for pdf2image / docling optional extras +RUN apt-get update && apt-get install -y --no-install-recommends \ + poppler-utils \ + libgl1 \ + libglib2.0-0 \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# Copy source into image +COPY foretrieval/ ./foretrieval/ +COPY pyproject.toml ./pyproject.toml + +# Install foretrieval with required extras +# milvus_lite extra needed for file-based Milvus Lite connections on the server +RUN pip install --no-cache-dir \ + ".[qdrant,milvus,vector_db_server]" \ + "pymilvus[milvus_lite]" \ + matplotlib \ + --extra-index-url https://download.pytorch.org/whl/cpu + +# Data volume (collections are persisted here) +VOLUME ["/data"] + +ENV FOR_DB_DATA_DIR=/data +ENV FOR_DB_HOST=0.0.0.0 +ENV FOR_DB_PORT=18000 + +EXPOSE 18000 + +CMD ["python", "-m", "foretrieval.vector_db_server.server_main"] diff --git a/foretrieval/vector_db_server/__init__.py b/foretrieval/vector_db_server/__init__.py new file mode 100644 index 0000000..4536066 --- /dev/null +++ b/foretrieval/vector_db_server/__init__.py @@ -0,0 +1,17 @@ +"""Remote vector-DB server package for FORetrieval. + +Provides: +- VectorDBServerConfig — Pydantic config model +- VectorDBServerClient — HTTP client for the vector-DB API +- VectorDBServerManager — SSH-based Docker deployment manager +""" + +from .client import VectorDBServerClient +from .config import VectorDBServerConfig +from .manager import VectorDBServerManager + +__all__ = [ + "VectorDBServerConfig", + "VectorDBServerClient", + "VectorDBServerManager", +] diff --git a/foretrieval/vector_db_server/client.py b/foretrieval/vector_db_server/client.py new file mode 100644 index 0000000..9f10d8b --- /dev/null +++ b/foretrieval/vector_db_server/client.py @@ -0,0 +1,406 @@ +"""HTTP client for the remote FORetrieval vector-DB server. + +Communicates with a FastAPI server exposing the VectorStore HTTP API. +Multi-vector tensors are transported as raw bytes via torch.save/torch.load +(``Content-Type: application/octet-stream``) for efficiency and dtype +preservation. Control responses (health, exists checks, create) use JSON. + +Auth: when VectorDBServerConfig.api_key is set, every request includes an +``Authorization: Bearer `` header. + +SSL: ``verify_ssl=False`` disables certificate verification (self-signed certs). +""" + +from __future__ import annotations + +import io +import logging +from typing import Any, Dict, List, Optional + +import httpx +import torch + +from ..vector_store.base import MultiVectorQuery, SearchHit, StoredPoint +from .config import VectorDBServerConfig + +logger = logging.getLogger(__name__) + +# Endpoint paths +_HEALTH_ENDPOINT = "/health" +_COLLECTION_OPEN = "/v1/collection/open" +_COLLECTION_EXISTS = "/v1/collection/{name}/exists" +_COLLECTION_CREATE = "/v1/collection" +_COLLECTION_DELETE = "/v1/collection/{name}" +_POINT_EXISTS = "/v1/point/{name}/{point_id}/exists" +_UPSERT = "/v1/upsert/{name}" +_SEARCH = "/v1/search/{name}" +_VECTOR = "/v1/vector/{name}/{point_id}" +_BOOKKEEPING = "/v1/collection/{name}/bookkeeping" +_ADMIN_INDEXES = "/v1/admin/indexes" +_ADMIN_DATA_FOLDERS = "/v1/admin/data_folders" + + +# --------------------------------------------------------------------------- +# Tensor codec (torch.save / torch.load via BytesIO) +# --------------------------------------------------------------------------- + +def _dumps(obj: Any) -> bytes: + """Serialise an arbitrary object (tensors, dicts, lists) via torch.save.""" + buf = io.BytesIO() + torch.save(obj, buf) + return buf.getvalue() + + +def _loads(data: bytes) -> Any: + """Deserialise bytes produced by _dumps.""" + return torch.load(io.BytesIO(data), map_location="cpu", weights_only=False) + + +# --------------------------------------------------------------------------- +# Client +# --------------------------------------------------------------------------- + +class VectorDBServerClient: + """HTTP client that talks to the FORetrieval vector-DB server. + + Parameters + ---------- + config: + VectorDBServerConfig with url, api_key, verify_ssl, request_timeout, etc. + """ + + def __init__(self, config: VectorDBServerConfig) -> None: + self.config = config + self._client = httpx.Client( + verify=config.verify_ssl, + headers=self._build_headers(), + timeout=config.request_timeout, + ) + + def _build_headers(self) -> dict: + headers: Dict[str, str] = {} + if self.config.api_key: + headers["Authorization"] = f"Bearer {self.config.api_key}" + return headers + + # ------------------------------------------------------------------ + # Health + # ------------------------------------------------------------------ + + def health_check(self) -> bool: + """Return True if the server is reachable and healthy.""" + try: + resp = self._client.get(self.config.url + _HEALTH_ENDPOINT, timeout=10) + return resp.status_code == 200 + except httpx.HTTPError: + return False + + # ------------------------------------------------------------------ + # Collection management + # ------------------------------------------------------------------ + + def open_collection( + self, + index_name: str, + backend: str, + *, + create: bool, + dim: Optional[int] = None, + storage_config: Optional[dict] = None, + ) -> dict: + """Ask the server to open (or create) a collection. + + Returns the server's JSON response (``opened``, ``backend``, ``created``). + """ + payload: Dict[str, Any] = { + "index_name": index_name, + "backend": backend, + "create": create, + } + if dim is not None: + payload["dim"] = dim + if storage_config: + payload["storage_config"] = storage_config + resp = self._post_json(_COLLECTION_OPEN, payload) + return resp + + def collection_exists(self, index_name: str) -> bool: + """Return True if the named collection exists on the server.""" + url = self.config.url + _COLLECTION_EXISTS.format(name=index_name) + try: + resp = self._client.get(url) + except httpx.HTTPError as exc: + raise ConnectionError( + f"Cannot reach vector-DB server at {self.config.url}" + ) from exc + _raise_for_status(resp) + return bool(resp.json().get("exists", False)) + + def create_collection( + self, + index_name: str, + backend: str, + dim: int, + storage_config: Optional[dict] = None, + ) -> None: + """Create a new collection on the server (no-op if already exists).""" + payload: Dict[str, Any] = { + "index_name": index_name, + "backend": backend, + "dim": dim, + } + if storage_config: + payload["storage_config"] = storage_config + self._post_json(_COLLECTION_CREATE, payload) + + # ------------------------------------------------------------------ + # Write + # ------------------------------------------------------------------ + + def upsert(self, index_name: str, points: List[StoredPoint]) -> None: + """Upsert a list of StoredPoints to the named collection. + + Tensors are serialised via torch.save for efficient binary transport. + """ + # Convert to a serialisable list of dicts + wire = [ + { + "point_id": sp.point_id, + "vector": sp.vector.cpu(), + "payload": sp.payload, + } + for sp in points + ] + data = _dumps(wire) + url = self.config.url + _UPSERT.format(name=index_name) + try: + resp = self._client.post( + url, + content=data, + headers={"Content-Type": "application/octet-stream"}, + ) + except httpx.TimeoutException as exc: + raise TimeoutError( + f"Vector-DB server timed out after {self.config.request_timeout}s" + ) from exc + except httpx.HTTPError as exc: + raise ConnectionError( + f"Cannot reach vector-DB server at {self.config.url}" + ) from exc + _raise_for_status(resp) + + def point_exists(self, index_name: str, point_id: int) -> bool: + """Return True if the given point exists in the named collection.""" + url = self.config.url + _POINT_EXISTS.format( + name=index_name, point_id=point_id + ) + try: + resp = self._client.get(url) + except httpx.HTTPError as exc: + raise ConnectionError( + f"Cannot reach vector-DB server at {self.config.url}" + ) from exc + _raise_for_status(resp) + return bool(resp.json().get("exists", False)) + + # ------------------------------------------------------------------ + # Read + # ------------------------------------------------------------------ + + def search( + self, + index_name: str, + query: MultiVectorQuery, + k: int, + ) -> List[SearchHit]: + """Execute a nearest-neighbour search and return up to k hits. + + Query tensor and optional filter metadata are serialised via torch.save. + Results are deserialised from the same binary format. + """ + wire = { + "vectors": query.vectors.cpu(), + "filter_metadata": query.filter_metadata, + "k": k, + } + data = _dumps(wire) + url = self.config.url + _SEARCH.format(name=index_name) + try: + resp = self._client.post( + url, + content=data, + headers={"Content-Type": "application/octet-stream"}, + ) + except httpx.TimeoutException as exc: + raise TimeoutError( + f"Vector-DB server timed out after {self.config.request_timeout}s" + ) from exc + except httpx.HTTPError as exc: + raise ConnectionError( + f"Cannot reach vector-DB server at {self.config.url}" + ) from exc + _raise_for_status(resp) + raw_hits: List[dict] = _loads(resp.content) + return [ + SearchHit( + point_id=h["point_id"], + score=float(h["score"]), + payload=h["payload"], + ) + for h in raw_hits + ] + + def fetch_vector( + self, index_name: str, point_id: int + ) -> Optional[torch.Tensor]: + """Retrieve the full multi-vector tensor for a given point. + + Returns None if the point does not exist. + """ + url = self.config.url + _VECTOR.format( + name=index_name, point_id=point_id + ) + try: + resp = self._client.get(url) + except httpx.HTTPError as exc: + raise ConnectionError( + f"Cannot reach vector-DB server at {self.config.url}" + ) from exc + if resp.status_code == 404: + return None + _raise_for_status(resp) + return _loads(resp.content) + + # ------------------------------------------------------------------ + # Bookkeeping (ColPali index-level metadata stored on the server) + # ------------------------------------------------------------------ + + def put_bookkeeping(self, index_name: str, blob: Dict[str, Any]) -> None: + """Store the ColPali bookkeeping ``blob`` for ``index_name``. + + ``blob`` may contain tensors; it is transported via torch.save. + """ + url = self.config.url + _BOOKKEEPING.format(name=index_name) + data = _dumps(blob) + try: + resp = self._client.put( + url, + content=data, + headers={"Content-Type": "application/octet-stream"}, + ) + except httpx.TimeoutException as exc: + raise TimeoutError( + f"Vector-DB server timed out after {self.config.request_timeout}s" + ) from exc + except httpx.HTTPError as exc: + raise ConnectionError( + f"Cannot reach vector-DB server at {self.config.url}" + ) from exc + _raise_for_status(resp) + + def get_bookkeeping(self, index_name: str) -> Optional[Dict[str, Any]]: + """Fetch the ColPali bookkeeping blob for ``index_name``. + + Returns None when the server has no bookkeeping stored (404). + """ + url = self.config.url + _BOOKKEEPING.format(name=index_name) + try: + resp = self._client.get(url) + except httpx.HTTPError as exc: + raise ConnectionError( + f"Cannot reach vector-DB server at {self.config.url}" + ) from exc + if resp.status_code == 404: + return None + _raise_for_status(resp) + return _loads(resp.content) + + # ------------------------------------------------------------------ + # Admin + # ------------------------------------------------------------------ + + def delete_collection(self, index_name: str) -> None: + """Delete a collection from the server.""" + url = self.config.url + _COLLECTION_DELETE.format(name=index_name) + try: + resp = self._client.delete(url) + except httpx.HTTPError as exc: + raise ConnectionError( + f"Cannot reach vector-DB server at {self.config.url}" + ) from exc + _raise_for_status(resp) + + def list_indexes(self) -> dict: + """Return the server's list of index directories under ``data_dir``. + + The returned payload has the shape + ``{"items": [{"name": str, "path": str, "size_bytes": int, + "n_files": int, "modified": float, "has_collection": bool, + "backend": Optional[str]}, ...], "data_dir": str, "count": int}``. + """ + try: + resp = self._client.get(self.config.url + _ADMIN_INDEXES) + except httpx.HTTPError as exc: + raise ConnectionError( + f"Cannot reach vector-DB server at {self.config.url}" + ) from exc + _raise_for_status(resp) + return resp.json() + + def list_data_folders(self) -> dict: + """Return every direct subdirectory under ``data_dir`` on the server. + + Each item carries ``is_index`` so the caller can filter client-side. + Same envelope as :py:meth:`list_indexes`. + """ + try: + resp = self._client.get(self.config.url + _ADMIN_DATA_FOLDERS) + except httpx.HTTPError as exc: + raise ConnectionError( + f"Cannot reach vector-DB server at {self.config.url}" + ) from exc + _raise_for_status(resp) + return resp.json() + + # ------------------------------------------------------------------ + # Lifecycle + # ------------------------------------------------------------------ + + def close(self) -> None: + """Close the underlying HTTP client.""" + self._client.close() + + # ------------------------------------------------------------------ + # Internal helpers + # ------------------------------------------------------------------ + + def _post_json(self, path: str, payload: dict) -> dict: + """POST JSON to the given path and return parsed JSON response.""" + try: + resp = self._client.post(self.config.url + path, json=payload) + except httpx.TimeoutException as exc: + raise TimeoutError( + f"Vector-DB server timed out after {self.config.request_timeout}s" + ) from exc + except httpx.HTTPError as exc: + raise ConnectionError( + f"Cannot reach vector-DB server at {self.config.url}" + ) from exc + _raise_for_status(resp) + return resp.json() + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _raise_for_status(resp: httpx.Response) -> None: + """Raise RuntimeError with message if the response is not 2xx.""" + if resp.status_code >= 400: + try: + detail = resp.json().get("detail", resp.text[:500]) + except Exception: + detail = resp.text[:500] + raise RuntimeError( + f"Vector-DB server returned HTTP {resp.status_code}: {detail}" + ) diff --git a/foretrieval/vector_db_server/config.py b/foretrieval/vector_db_server/config.py new file mode 100644 index 0000000..00b3499 --- /dev/null +++ b/foretrieval/vector_db_server/config.py @@ -0,0 +1,96 @@ +"""Configuration model for the remote vector-DB server.""" + +from __future__ import annotations + +from typing import Optional + +from pydantic import BaseModel, field_validator, model_validator + +# Valid backend names that the server can use on its own store. +_SERVER_BACKENDS = ("local", "qdrant", "milvus") + + +class VectorDBServerConfig(BaseModel): + """Configuration for a remote FORetrieval vector-DB server. + + When ``storage_backend="remote"`` is passed to ``make_vector_store()``, + FORetrieval will talk to this server instead of maintaining a local + vector store. The server itself runs one of ``local``, ``qdrant``, or + ``milvus`` as its underlying backend. + + Attributes: + url: Full base URL of the server, e.g. ``"http://gpu-server:18000"``. + Trailing slashes are stripped automatically. + backend: Backend used by the server for the collection. + One of ``"local"``, ``"qdrant"``, or ``"milvus"``. + Default ``"qdrant"``. Once a collection is created on the server + with a given backend the backend cannot be changed without + recreating the collection. + storage_config: Optional backend-specific kwargs forwarded to + the server (e.g. ``{"candidate_limit": 128}`` for Milvus). + auto_deploy: When True, FORetrieval will SSH to ssh_host and + deploy a Docker container if the server is not already running. + Requires ``ssh_host`` to be set. + ssh_host: Hostname or IP of the server (SSH target). + Required when ``auto_deploy=True``. + ssh_user: SSH username. Defaults to the current OS user at + deploy time. + ssh_key_path: Path to SSH private key. If None the SSH agent or + default keys (``~/.ssh/id_rsa`` etc.) are used. + port: Port the server listens on. Default ``18000``. + api_key: Optional bearer token for server authentication. + When set, requests include ``"Authorization: Bearer "``. + Start the server with ``FOR_DB_API_KEY=`` to match. + verify_ssl: Whether to verify SSL certificates. Default ``True``. + request_timeout: HTTP request timeout in seconds. Default ``120``. + data_dir: Path **on the remote host** where the server persists its + data (bind-mounted into the Docker container as ``/data``). + Default ``/var/lib/foretrieval_db``. + """ + + url: str + backend: str = "qdrant" + storage_config: Optional[dict] = None + auto_deploy: bool = False + ssh_host: Optional[str] = None + ssh_user: Optional[str] = None + ssh_key_path: Optional[str] = None + port: int = 18000 + api_key: Optional[str] = None + verify_ssl: bool = True + request_timeout: int = 120 + data_dir: str = "/var/lib/foretrieval_db" + + @field_validator("url") + @classmethod + def strip_trailing_slash(cls, v: str) -> str: + return v.rstrip("/") + + @field_validator("backend") + @classmethod + def validate_backend(cls, v: str) -> str: + lower = v.lower() + if lower not in _SERVER_BACKENDS: + raise ValueError( + f"backend '{v}' is not a valid server-side backend. " + f"Valid choices: {', '.join(_SERVER_BACKENDS)}." + ) + return lower + + @field_validator("port") + @classmethod + def validate_port(cls, v: int) -> int: + if not (1 <= v <= 65535): + raise ValueError("port must be in range 1–65535") + return v + + @model_validator(mode="after") + def auto_deploy_requires_ssh_host(self) -> "VectorDBServerConfig": + if self.auto_deploy and not self.ssh_host: + raise ValueError("ssh_host is required when auto_deploy=True") + return self + + @classmethod + def from_dict(cls, d: dict) -> "VectorDBServerConfig": + """Convenience constructor from a plain config dict (e.g. from JSON).""" + return cls(**d) diff --git a/foretrieval/vector_db_server/manager.py b/foretrieval/vector_db_server/manager.py new file mode 100644 index 0000000..96a9a4a --- /dev/null +++ b/foretrieval/vector_db_server/manager.py @@ -0,0 +1,462 @@ +"""Remote vector-DB server deployment manager. + +Handles Docker-based deployment of the FORetrieval vector-DB server on a +remote host via SSH. Mirrors the structure of EmbeddingServerManager. + +Deployment flow: +1. Check remote metadata file (``~/.foretrieval/db_deployment.json``). +2. If absent → build image and run container from scratch. +3. If present → health-check the running container; redeploy if down. + +The Docker image is built **on the remote host** from the local +``foretrieval/`` package source, transferred via SFTP. This avoids needing +a public image registry. +""" + +from __future__ import annotations + +import json +import logging +import os +import tarfile +import tempfile +from datetime import datetime, timezone +from pathlib import Path +from typing import Callable, Optional + +from .config import VectorDBServerConfig + +logger = logging.getLogger(__name__) + +# Remote paths and container constants. +# +# Paths are intentionally relative. They are joined to the SSH user's +# absolute home directory (obtained via ``sftp.normalize('.')`` so that +# SFTP put / get commands receive a real absolute path — SFTP does NOT +# expand ``~``, unlike the remote shell. +_REMOTE_METADATA_SUBPATH = ".foretrieval/db_deployment.json" +_REMOTE_BUILD_SUBDIR = "foretrieval_db_build" +_CONTAINER_NAME = "foretrieval_vector_db_server" +_IMAGE_NAME = "foretrieval-vector-db:local" + +# Port the server listens on *inside* the container. +# This is fixed by the Dockerfile CMD / server_main.py and is always 18000. +# VectorDBServerConfig.port controls only the *host-side* binding +# (the left-hand side of Docker's -p HOST:CONTAINER mapping), allowing +# callers to expose the service on any host port without rebuilding the image. +_CONTAINER_INTERNAL_PORT = 18000 + + +class VectorDBServerManager: + """Manages deployment of the FORetrieval vector-DB server via SSH + Docker. + + Parameters + ---------- + config: + VectorDBServerConfig with ssh_host, port, data_dir, api_key, etc. + """ + + def __init__(self, config: VectorDBServerConfig) -> None: + if not config.ssh_host: + raise ValueError("VectorDBServerManager requires ssh_host in config") + self.config = config + self._ssh: Optional[object] = None # paramiko.SSHClient, lazy + self._cached_home: Optional[str] = None # remote $HOME, lazy + # Absolute remote build dir, set by _upload_build_context() once + # _remote_home() has been resolved. Read by _deploy() and tests. + self._remote_build_dir: Optional[str] = None + + # ------------------------------------------------------------------ + # Remote path helpers + # ------------------------------------------------------------------ + + def _remote_home(self) -> str: + """Return the SSH user's absolute home directory on the remote host. + + Resolved once per manager instance via ``sftp.normalize('.')``. + """ + if self._cached_home is not None: + return self._cached_home + ssh = self._get_ssh() + sftp = ssh.open_sftp() + try: + self._cached_home = sftp.normalize(".") + finally: + sftp.close() + return self._cached_home + + def _metadata_path(self) -> str: + """Return the absolute path to the remote deployment-metadata file.""" + return f"{self._remote_home()}/{_REMOTE_METADATA_SUBPATH}" + + # ------------------------------------------------------------------ + # Public API + # ------------------------------------------------------------------ + + def ensure_deployed(self) -> None: + """Ensure the vector-DB server is running on the remote host. + + Flow: + 1. Check remote metadata file. + 2. If absent → build image and deploy from scratch. + 3. If present → health-check container; redeploy if unhealthy. + """ + try: + import paramiko # noqa: F401 + except ImportError as exc: + raise ImportError( + "paramiko is required for auto_deploy. " + "Install it with: pip install 'foretrieval[vector_db_server]'" + ) from exc + + logger.info( + "Ensuring vector-DB server is deployed on %s", self.config.ssh_host + ) + metadata = self._read_remote_metadata() + + if metadata is None: + logger.info("No deployment metadata found — deploying from scratch") + self._deploy() + else: + logger.info( + "Found existing deployment (deployed_at=%s)", + metadata.get("deployed_at"), + ) + if self._is_container_running(): + logger.info("Container is running — nothing to do") + else: + logger.warning("Container not running — redeploying") + self._deploy() + + def redeploy(self, on_line: Optional[Callable[[str], None]] = None) -> None: + """Force a fresh build + container restart regardless of current state. + + Unlike :py:meth:`ensure_deployed`, this always rebuilds the image and + restarts the container. Use it after pulling new FORetrieval source + on the local machine. + + Args: + on_line: Optional callback invoked with every line of remote + stdout (Docker pull / build / run output). Best-effort: + callback exceptions are swallowed. + """ + try: + import paramiko # noqa: F401 + except ImportError as exc: + raise ImportError( + "paramiko is required for auto_deploy. " + "Install it with: pip install 'foretrieval[vector_db_server]'" + ) from exc + + logger.info( + "Force redeploying vector-DB server on %s", self.config.ssh_host + ) + self._deploy(on_line=on_line) + + def is_running(self) -> bool: + """Return True iff the container is currently up.""" + try: + return self._is_container_running() + except Exception: # noqa: BLE001 + return False + + def get_remote_metadata(self) -> Optional[dict]: + """Return the remote deployment metadata, or None if not deployed.""" + try: + return self._read_remote_metadata() + except Exception: # noqa: BLE001 + return None + + def stop(self) -> None: + """Stop and remove the Docker container; delete metadata file.""" + logger.info("Stopping vector-DB server on %s", self.config.ssh_host) + self._run_remote(f"docker stop {_CONTAINER_NAME} 2>/dev/null || true") + self._run_remote(f"docker rm {_CONTAINER_NAME} 2>/dev/null || true") + self._run_remote(f"rm -f {self._metadata_path()}") + logger.info("Vector-DB server stopped") + + # ------------------------------------------------------------------ + # Deploy + # ------------------------------------------------------------------ + + def _deploy(self, on_line: Optional[Callable[[str], None]] = None) -> None: + """Upload source, build image, run container, write metadata. + + Args: + on_line: Optional callback invoked with every line of remote + stdout produced by the long-running build/run commands. + """ + # Stop stale container + self._run_remote(f"docker stop {_CONTAINER_NAME} 2>/dev/null || true", on_line=on_line) + self._run_remote(f"docker rm {_CONTAINER_NAME} 2>/dev/null || true", on_line=on_line) + + # Upload foretrieval source + Dockerfile to remote build dir + if on_line is not None: + try: + on_line("Uploading build context …") + except Exception: # noqa: BLE001 + pass + self._upload_build_context() + + # Build Docker image on the remote host + logger.info("Building Docker image '%s' on %s …", _IMAGE_NAME, self.config.ssh_host) + if on_line is not None: + try: + on_line(f"Building image {_IMAGE_NAME} …") + except Exception: # noqa: BLE001 + pass + self._run_remote( + f"cd {self._remote_build_dir} && " + f"docker build -t {_IMAGE_NAME} -f Dockerfile.vector_db .", + on_line=on_line, + ) + + # Create data directory on remote + self._run_remote(f"mkdir -p {self.config.data_dir}", on_line=on_line) + + # Run container + cmd = self._build_docker_run_cmd() + logger.info("Starting container: %s", cmd) + if on_line is not None: + try: + on_line("Starting container …") + except Exception: # noqa: BLE001 + pass + self._run_remote(cmd, on_line=on_line) + + # Write metadata + metadata = { + "container_name": _CONTAINER_NAME, + "image": _IMAGE_NAME, + "port": self.config.port, + "data_dir": self.config.data_dir, + "deployed_at": datetime.now(timezone.utc).isoformat(), + } + self._write_remote_metadata(metadata) + logger.info( + "Deployment complete — server starting on port %d", self.config.port + ) + if on_line is not None: + try: + on_line("Deployment complete.") + except Exception: # noqa: BLE001 + pass + + def _build_docker_run_cmd(self) -> str: + cfg = self.config + env_parts = [ + "-e FOR_DB_DATA_DIR=/data", + # FOR_DB_PORT sets the port the server listens on inside the container. + # This must always match _CONTAINER_INTERNAL_PORT — not cfg.port, which + # is the host-side binding. cfg.port is used below in -p HOST:CONTAINER. + f"-e FOR_DB_PORT={_CONTAINER_INTERNAL_PORT}", + ] + if cfg.api_key: + env_parts.append(f"-e FOR_DB_API_KEY={cfg.api_key}") + + # Run the container as the SSH user so files written under the + # bind-mounted data_dir have the same ownership as SFTP-uploaded + # files. Falls back to the Docker default (root) if UID resolution + # fails, so existing deployments without SSH are unaffected. + uid, gid = self._resolve_remote_uid_gid() + user_flag = f"--user {uid}:{gid}" if uid is not None else "" + + parts = [ + "docker run -d", + f"--name {_CONTAINER_NAME}", + ] + if user_flag: + parts.append(user_flag) + parts += [ + # cfg.port → host port (configurable, chosen by the caller) + # _CONTAINER_INTERNAL_PORT → container port (fixed by the image) + f"-p {cfg.port}:{_CONTAINER_INTERNAL_PORT}", + f"-v {cfg.data_dir}:/data", + " ".join(env_parts), + "--restart unless-stopped", + _IMAGE_NAME, + ] + return " ".join(parts) + + def _resolve_remote_uid_gid(self) -> tuple: + """Return ``(uid, gid)`` of the SSH user on the remote host. + + Uses a single ``id -u && id -g`` call. Returns ``(None, None)`` + on any failure so callers can omit the ``--user`` flag gracefully. + """ + try: + stdout, _ = self._run_remote("id -u && id -g") + lines = [ln.strip() for ln in stdout.strip().splitlines() if ln.strip()] + if len(lines) >= 2: + return int(lines[0]), int(lines[1]) + except Exception: # noqa: BLE001 + pass + return None, None + + # ------------------------------------------------------------------ + # Build context upload + # ------------------------------------------------------------------ + + def _upload_build_context(self) -> None: + """Create a tar of foretrieval/ + Dockerfile.vector_db and upload to remote.""" + + # Locate the local foretrieval package root (parent of vector_db_server/) + package_root = Path(__file__).parent.parent.parent # …/FORetrieval/ + foretrieval_src = package_root / "foretrieval" + dockerfile_src = Path(__file__).parent / "Dockerfile.vector_db" + + if not foretrieval_src.is_dir(): + raise RuntimeError( + f"Could not locate foretrieval source at {foretrieval_src}" + ) + if not dockerfile_src.exists(): + raise RuntimeError( + f"Dockerfile.vector_db not found at {dockerfile_src}" + ) + + with tempfile.NamedTemporaryFile(suffix=".tar.gz", delete=False) as tmp: + tmp_path = tmp.name + + logger.info("Creating build context archive …") + with tarfile.open(tmp_path, "w:gz") as tar: + tar.add(foretrieval_src, arcname="foretrieval") + tar.add(dockerfile_src, arcname="Dockerfile.vector_db") + # Include pyproject.toml if available (for pip install -e .) + pyproject = package_root / "pyproject.toml" + if pyproject.exists(): + tar.add(pyproject, arcname="pyproject.toml") + + # Resolve the absolute remote build directory. SFTP does NOT expand + # ``~`` (unlike the remote shell), so we must compute the absolute + # path explicitly via the SSH user's home directory. + remote_dir = f"{self._remote_home()}/{_REMOTE_BUILD_SUBDIR}" + self._remote_build_dir = remote_dir + + logger.info("Uploading build context to %s:%s …", self.config.ssh_host, remote_dir) + ssh = self._get_ssh() + # Ensure remote dir exists (shell expansion not needed any more, + # but a missing parent would still ENOENT below). + self._run_remote(f"mkdir -p {remote_dir}") + sftp = ssh.open_sftp() + try: + sftp.put(tmp_path, f"{remote_dir}/build_context.tar.gz") + finally: + sftp.close() + os.unlink(tmp_path) + + # Extract on remote + self._run_remote( + f"cd {remote_dir} && " + f"tar -xzf build_context.tar.gz && " + f"rm build_context.tar.gz" + ) + logger.info("Build context uploaded and extracted.") + + # ------------------------------------------------------------------ + # Health / container status + # ------------------------------------------------------------------ + + def _is_container_running(self) -> bool: + """Return True if the Docker container exists and is running.""" + stdout, _ = self._run_remote( + f"docker inspect --format='{{{{.State.Running}}}}' " + f"{_CONTAINER_NAME} 2>/dev/null || echo false" + ) + return stdout.strip().lower() == "true" + + # ------------------------------------------------------------------ + # Remote metadata + # ------------------------------------------------------------------ + + def _read_remote_metadata(self) -> Optional[dict]: + path = self._metadata_path() + stdout, _ = self._run_remote( + f"cat {path} 2>/dev/null || echo '__MISSING__'" + ) + text = stdout.strip() + if text == "__MISSING__" or not text: + return None + try: + return json.loads(text) + except json.JSONDecodeError: + logger.warning("Could not parse remote metadata: %s", text[:200]) + return None + + def _write_remote_metadata(self, metadata: dict) -> None: + path = self._metadata_path() + json_str = json.dumps(metadata).replace("'", "'\\''") + self._run_remote( + f"mkdir -p $(dirname {path}) && " + f"echo '{json_str}' > {path}" + ) + + # ------------------------------------------------------------------ + # SSH helpers + # ------------------------------------------------------------------ + + def _get_ssh(self): + """Return a connected paramiko SSHClient (lazy init). + + Honours ``~/.ssh/config`` (Host aliases, User, Port, IdentityFile, + ProxyCommand, ProxyJump) via :py:func:`foretrieval.ssh_utils.open_ssh_client`. + """ + if self._ssh is not None: + return self._ssh + + from ..ssh_utils import open_ssh_client + self._ssh = open_ssh_client( + ssh_host=self.config.ssh_host, + ssh_user=self.config.ssh_user, + ssh_key_path=self.config.ssh_key_path, + ) + return self._ssh + + def _run_remote( + self, + cmd: str, + on_line: Optional[Callable[[str], None]] = None, + ) -> tuple[str, str]: + """Run a shell command on the remote host; return (stdout, stderr). + + When ``on_line`` is provided, stdout is streamed line by line and the + callback is invoked for each. Useful for surfacing Docker build + progress in interactive UIs. Callback exceptions are swallowed. + """ + ssh = self._get_ssh() + logger.debug("Remote: %s", cmd) + _, stdout_f, stderr_f = ssh.exec_command(cmd) + + if on_line is None: + exit_code = stdout_f.channel.recv_exit_status() + stdout = stdout_f.read().decode("utf-8", errors="replace") + stderr = stderr_f.read().decode("utf-8", errors="replace") + else: + collected: list[str] = [] + # Stream stdout + for raw in iter(stdout_f.readline, ""): + if not raw: + break + collected.append(raw) + try: + on_line(raw.rstrip("\n")) + except Exception: # noqa: BLE001 + pass + exit_code = stdout_f.channel.recv_exit_status() + stdout = "".join(collected) + stderr = stderr_f.read().decode("utf-8", errors="replace") + + if stderr: + logger.debug("Remote stderr: %s", stderr[:300]) + if exit_code != 0 and "|| true" not in cmd and "2>/dev/null" not in cmd: + raise RuntimeError( + f"Remote command failed (exit {exit_code}): {cmd}\n" + f"stderr: {stderr[:500]}" + ) + return stdout, stderr + + def __del__(self) -> None: + if self._ssh is not None: + try: + self._ssh.close() + except Exception: + pass diff --git a/foretrieval/vector_db_server/server.py b/foretrieval/vector_db_server/server.py new file mode 100644 index 0000000..d2faf07 --- /dev/null +++ b/foretrieval/vector_db_server/server.py @@ -0,0 +1,673 @@ +"""FORetrieval vector-DB server — FastAPI application. + +Entry point: + uvicorn foretrieval.vector_db_server.server:app --host 0.0.0.0 --port 18000 + +Environment variables: + FOR_DB_DATA_DIR Root directory where collections are persisted (default: /data). + FOR_DB_API_KEY Optional bearer token. When set, all requests must include + ``Authorization: Bearer ``. + FOR_DB_HOST Bind host (default: 0.0.0.0) — used by server_main.py only. + FOR_DB_PORT Bind port (default: 18000) — used by server_main.py only. + +Each named collection is backed by one of the three local VectorStore +implementations (local / qdrant / milvus). The backend choice is fixed when +the collection is first created and stored in +``//index.json``. + +Concurrency: + A per-collection asyncio.Lock serialises all operations on the same index. + This avoids issues with Qdrant-embedded's single-client-per-path constraint + and Milvus Lite's internal thread safety. +""" + +from __future__ import annotations + +import asyncio +import io +import json +import logging +import os +import time +from pathlib import Path +from typing import Any, Dict, List, Optional, Tuple + +import torch +from fastapi import FastAPI, HTTPException, Request, Response +from fastapi.responses import JSONResponse + +from ..vector_store.base import MultiVectorQuery, StoredPoint +from ..vector_store.factory import make_vector_store + +logger = logging.getLogger(__name__) + +# --------------------------------------------------------------------------- +# Configuration from environment +# --------------------------------------------------------------------------- + +_DATA_DIR = Path(os.environ.get("FOR_DB_DATA_DIR", "/data")) +_API_KEY: Optional[str] = os.environ.get("FOR_DB_API_KEY") or None + +# --------------------------------------------------------------------------- +# Server-side MAX_SIM scorer (used when backend="local") +# --------------------------------------------------------------------------- + +class _TorchScorer: + """Minimal scorer for LocalVectorStore.set_processor() on the server side. + + LocalVectorStore needs a processor with a .score(queries, docs) method. + On the server there is no ColPali model, so we compute MAX_SIM directly + using pytorch — identical numerics to colpali_engine. + """ + + def score( + self, + queries: list, # list of 1 tensor (n_q_tokens, dim) + docs: list, # list of tensors (n_tokens, dim) + ) -> torch.Tensor: + """Compute MAX_SIM scores: shape (1, n_docs).""" + q = queries[0].float() # (n_q, dim) + results = [] + for d in docs: + d_f = d.float() # (n_d, dim) + # Late-interaction: (n_q, n_d) → max over doc tokens → sum over query tokens + sim = torch.matmul(q, d_f.T) # (n_q, n_d) + score = sim.max(dim=1).values.sum() + results.append(score.item()) + return torch.tensor([results]) # (1, n_docs) + + +_TORCH_SCORER = _TorchScorer() + +# --------------------------------------------------------------------------- +# Tensor codec (mirrors client.py — must stay in sync) +# --------------------------------------------------------------------------- + + +def _dumps(obj: Any) -> bytes: + buf = io.BytesIO() + torch.save(obj, buf) + return buf.getvalue() + + +def _loads(data: bytes) -> Any: + return torch.load(io.BytesIO(data), map_location="cpu", weights_only=False) + + +# --------------------------------------------------------------------------- +# Index registry +# --------------------------------------------------------------------------- + +# Mapping index_name → VectorStore instance (opened, ready to use) +_registry: Dict[str, Any] = {} + +# Per-collection locks: index_name → asyncio.Lock +_locks: Dict[str, asyncio.Lock] = {} + + +def _get_lock(index_name: str) -> asyncio.Lock: + if index_name not in _locks: + _locks[index_name] = asyncio.Lock() + return _locks[index_name] + + +def _meta_path(index_name: str) -> Path: + """Return the path to the collection metadata JSON file.""" + return _DATA_DIR / index_name / "index.json" + + +def _read_meta(index_name: str) -> Optional[Dict[str, Any]]: + p = _meta_path(index_name) + if not p.exists(): + return None + try: + return json.loads(p.read_text()) + except Exception: + return None + + +def _write_meta(index_name: str, meta: Dict[str, Any]) -> None: + p = _meta_path(index_name) + p.parent.mkdir(parents=True, exist_ok=True) + p.write_text(json.dumps(meta)) + + +def _bookkeeping_path(index_name: str) -> Path: + """Return the path to the ColPali bookkeeping blob for a collection. + + Stored as a torch.save'd dict alongside ``index.json`` so the client no + longer needs a local index directory in remote mode. + """ + return _DATA_DIR / index_name / "bookkeeping.pt" + + +def _inject_scorer(vs: Any) -> None: + """Inject the server-side MAX_SIM scorer into a LocalVectorStore.""" + from ..vector_store.local import LocalVectorStore + if isinstance(vs, LocalVectorStore): + vs.set_processor(_TORCH_SCORER) + + +def _open_store(index_name: str, backend: str, storage_config: Optional[dict]) -> Any: + """Instantiate and open a VectorStore for an existing collection.""" + vs = make_vector_store(backend, storage_config) + vs.open(index_name, _DATA_DIR, create=False) + vs.load_sidecar(_DATA_DIR / index_name) + _inject_scorer(vs) + return vs + + +def _get_or_load(index_name: str) -> Any: + """Return the VectorStore for an existing, previously-opened collection. + + Raises HTTPException 404 if the collection doesn't exist. + Raises HTTPException 409 if the collection exists on disk but was never + opened in this process (auto-reloads it from disk). + """ + if index_name in _registry: + return _registry[index_name] + + # Try to reload from disk (e.g. after a server restart) + meta = _read_meta(index_name) + if meta is None: + raise HTTPException( + status_code=404, + detail=f"Collection '{index_name}' does not exist on this server.", + ) + backend = meta["backend"] + storage_config = meta.get("storage_config") + vs = _open_store(index_name, backend, storage_config) + _registry[index_name] = vs + logger.info( + "Auto-reloaded collection '%s' (backend=%s) from disk.", index_name, backend + ) + return vs + + +# --------------------------------------------------------------------------- +# FastAPI app +# --------------------------------------------------------------------------- + +app = FastAPI(title="FORetrieval Vector-DB Server", version="0.1.0") + + +# ------------------------------------------------------------------ +# Auth middleware +# ------------------------------------------------------------------ + +@app.middleware("http") +async def _auth_middleware(request: Request, call_next): + if _API_KEY is not None and request.url.path != "/health": + auth = request.headers.get("authorization", "") + if not auth.lower().startswith("bearer "): + return JSONResponse({"detail": "Missing Authorization header"}, status_code=401) + token = auth.split(" ", 1)[1].strip() + if token != _API_KEY: + return JSONResponse({"detail": "Invalid API key"}, status_code=401) + return await call_next(request) + + +# ------------------------------------------------------------------ +# Health +# ------------------------------------------------------------------ + +@app.get("/health") +async def health(): + """Liveness check — always returns 200 OK.""" + return {"status": "ok"} + + +# ------------------------------------------------------------------ +# Collection management +# ------------------------------------------------------------------ + +@app.post("/v1/collection/open") +async def open_collection(request: Request): + """Open or create a named collection. + + Request JSON: + index_name (str): Collection name. + backend (str): "local" | "qdrant" | "milvus". + create (bool): If True, create if not exists. + dim (int, optional): Embedding dimension (required when create=True + and collection does not yet exist). + storage_config (dict, optional): Backend-specific config (e.g. candidate_limit). + """ + body = await request.json() + index_name: str = body["index_name"] + backend: str = body.get("backend", "qdrant") + create: bool = body.get("create", False) + dim: Optional[int] = body.get("dim") + storage_config: Optional[dict] = body.get("storage_config") + + async with _get_lock(index_name): + meta = _read_meta(index_name) + already_exists = meta is not None + + if already_exists: + # Ensure in-memory registry is populated + _get_or_load(index_name) + return JSONResponse({ + "opened": True, + "backend": meta["backend"], + "created": False, + }) + + if not create: + raise HTTPException( + status_code=404, + detail=( + f"Collection '{index_name}' does not exist. " + "Pass create=true to create it." + ), + ) + + # Create new collection + vs = make_vector_store(backend, storage_config) + vs.open(index_name, _DATA_DIR, create=True, dim=dim) + if dim is not None: + if not vs.collection_exists(): + vs.create_collection(dim) + _inject_scorer(vs) + _registry[index_name] = vs + + meta = {"backend": backend, "storage_config": storage_config} + _write_meta(index_name, meta) + _invalidate_size_cache(_DATA_DIR / index_name) + + logger.info( + "Created collection '%s' (backend=%s, dim=%s).", + index_name, backend, dim, + ) + return JSONResponse({"opened": True, "backend": backend, "created": True}) + + +@app.get("/v1/collection/{name}/exists") +async def collection_exists(name: str): + """Check whether a named collection exists on this server.""" + meta = _read_meta(name) + if meta is None: + return {"exists": False, "backend": None} + return {"exists": True, "backend": meta.get("backend")} + + +@app.post("/v1/collection") +async def create_collection(request: Request): + """Create a new collection (no-op if already exists). + + Request JSON: + index_name (str), backend (str), dim (int), + storage_config (dict, optional). + """ + body = await request.json() + index_name: str = body["index_name"] + backend: str = body.get("backend", "qdrant") + dim: int = body["dim"] + storage_config: Optional[dict] = body.get("storage_config") + + async with _get_lock(index_name): + meta = _read_meta(index_name) + if meta is not None: + return JSONResponse({"created": False, "detail": "already exists"}) + + vs = make_vector_store(backend, storage_config) + vs.open(index_name, _DATA_DIR, create=True, dim=dim) + vs.create_collection(dim) + _inject_scorer(vs) + _registry[index_name] = vs + + _write_meta(index_name, {"backend": backend, "storage_config": storage_config}) + _invalidate_size_cache(_DATA_DIR / index_name) + logger.info("Created collection '%s' backend=%s dim=%d.", index_name, backend, dim) + return JSONResponse({"created": True}) + + +@app.delete("/v1/collection/{name}") +async def delete_collection(name: str): + """Remove a collection from the registry and delete its data directory.""" + import shutil + + async with _get_lock(name): + vs = _registry.pop(name, None) + if vs is not None: + try: + vs.close() + except Exception: + pass + + coll_dir = _DATA_DIR / name + if coll_dir.exists(): + shutil.rmtree(coll_dir) + logger.info("Deleted collection directory '%s'.", coll_dir) + _invalidate_size_cache(coll_dir) + + _locks.pop(name, None) + return {"deleted": True} + + +# ------------------------------------------------------------------ +# Bookkeeping (ColPali index-level metadata, stored server-side) +# ------------------------------------------------------------------ + +@app.put("/v1/collection/{name}/bookkeeping") +async def put_bookkeeping(name: str, request: Request): + """Persist the ColPali bookkeeping blob (torch.save bytes body). + + The blob is a dict of index-level metadata (model name, doc metadata, + file-name map, per-embedding extras, …) that previously lived in local + sidecar files on the client. Stored under ``//``. + """ + data = await request.body() + blob = _loads(data) + + async with _get_lock(name): + path = _bookkeeping_path(name) + path.parent.mkdir(parents=True, exist_ok=True) + torch.save(blob, path) + _invalidate_size_cache(_DATA_DIR / name) + + return {"stored": True} + + +@app.get("/v1/collection/{name}/bookkeeping") +async def get_bookkeeping(name: str): + """Return the ColPali bookkeeping blob for a collection. + + Returns 404 if no bookkeeping has been stored. Response body is the + torch.save'd blob. + """ + async with _get_lock(name): + path = _bookkeeping_path(name) + if not path.exists(): + raise HTTPException( + status_code=404, + detail=f"No bookkeeping stored for collection '{name}'.", + ) + blob = torch.load(path, map_location="cpu", weights_only=False) + + return Response(content=_dumps(blob), media_type="application/octet-stream") + + +# ------------------------------------------------------------------ +# Write +# ------------------------------------------------------------------ + +@app.post("/v1/upsert/{name}") +async def upsert(name: str, request: Request): + """Upsert a list of StoredPoints (torch.save bytes body). + + The request body is a torch.save'd list of dicts with keys: + point_id (int), vector (Tensor), payload (dict). + """ + data = await request.body() + raw_points = _loads(data) + + points = [ + StoredPoint( + point_id=int(p["point_id"]), + vector=p["vector"].cpu(), + payload=p["payload"], + ) + for p in raw_points + ] + + async with _get_lock(name): + vs = _get_or_load(name) + + # Lazy collection creation: create on first upsert if not yet created + dim = points[0].vector.shape[-1] if points else None + if dim is not None and not vs.collection_exists(): + meta = _read_meta(name) + backend = meta["backend"] if meta else "local" + vs.create_collection(dim) + _inject_scorer(vs) + logger.info( + "Lazily created collection '%s' (backend=%s, dim=%d).", + name, backend, dim, + ) + + vs.upsert(points) + vs.export_sidecar(_DATA_DIR / name) + _invalidate_size_cache(_DATA_DIR / name) + + return {"upserted": len(points)} + + +@app.get("/v1/point/{name}/{point_id}/exists") +async def point_exists(name: str, point_id: int): + """Check if a point exists in the named collection.""" + async with _get_lock(name): + vs = _get_or_load(name) + exists = vs.point_exists(point_id) + return {"exists": exists} + + +# ------------------------------------------------------------------ +# Read +# ------------------------------------------------------------------ + +@app.post("/v1/search/{name}") +async def search(name: str, request: Request): + """Execute a nearest-neighbour search (torch.save bytes body). + + The request body is a torch.save'd dict with keys: + vectors (Tensor), filter_metadata (dict | None), k (int). + + Returns a torch.save'd list of dicts: + [{point_id, score, payload}, ...] + """ + data = await request.body() + wire = _loads(data) + + query = MultiVectorQuery( + vectors=wire["vectors"].cpu(), + filter_metadata=wire.get("filter_metadata"), + ) + k: int = int(wire.get("k", 10)) + + async with _get_lock(name): + vs = _get_or_load(name) + hits = vs.search(query, k) + + result = [ + {"point_id": h.point_id, "score": h.score, "payload": h.payload} + for h in hits + ] + return Response(content=_dumps(result), media_type="application/octet-stream") + + +@app.get("/v1/vector/{name}/{point_id}") +async def fetch_vector(name: str, point_id: int): + """Retrieve the full multi-vector tensor for a point. + + Returns 404 if the point does not exist. + Response body is torch.save'd tensor. + """ + async with _get_lock(name): + vs = _get_or_load(name) + tensor = vs.fetch_vector(point_id) + + if tensor is None: + raise HTTPException( + status_code=404, + detail=f"Point {point_id} not found in collection '{name}'.", + ) + return Response(content=_dumps(tensor), media_type="application/octet-stream") + + +# ------------------------------------------------------------------ +# Admin endpoints (read-only) +# +# These walk the on-disk data_dir to surface index and folder information +# (size, file count, mtime). Auth is inherited from the same middleware as +# the data-plane routes. +# ------------------------------------------------------------------ + +_SIZE_CACHE: Dict[str, Tuple[float, int, int, float]] = {} +# Cache key = absolute path (str) +# Cache value = (cached_at_epoch, size_bytes, n_files, mtime) + +_CACHE_TTL = 60.0 # seconds + +_CACHE_LOCK = asyncio.Lock() + + +def _is_index_dir(path: Path) -> bool: + """Return True when ``path`` looks like a FORetrieval index directory. + + Uses the server-side collection metadata file written by ``_write_meta`` + (``/index.json``) as the sentinel. The client-side sidecar files + (``index_config.json.gz``, ``metadata.json.gz``) live on the Streamlit + host, not on the server, so they are never present here. + """ + return (path / "index.json").exists() + + +def _dir_stats_sync(path: Path) -> Tuple[int, int, float]: + """Synchronously walk ``path`` returning (size_bytes, n_files, mtime). + + Symlinks are not followed. Entries that resolve outside ``_DATA_DIR`` + are skipped silently to prevent jail escapes. + """ + root_resolved = _DATA_DIR.resolve() + total_size = 0 + n_files = 0 + max_mtime = 0.0 + try: + max_mtime = path.stat().st_mtime + except OSError: + return (0, 0, 0.0) + + for dirpath, dirnames, filenames in os.walk(path, followlinks=False): + dp = Path(dirpath) + # Containment check + try: + resolved = dp.resolve() + except OSError: + continue + try: + resolved.relative_to(root_resolved) + except ValueError: + # Outside the jail — skip + dirnames[:] = [] + continue + + for fname in filenames: + fp = dp / fname + try: + st = fp.lstat() + except OSError: + continue + total_size += st.st_size + n_files += 1 + if st.st_mtime > max_mtime: + max_mtime = st.st_mtime + + return (total_size, n_files, max_mtime) + + +async def _dir_stats(path: Path) -> Tuple[int, int, float]: + """Cached, async wrapper around ``_dir_stats_sync`` with a 60s TTL.""" + key = str(path) + now = time.time() + async with _CACHE_LOCK: + cached = _SIZE_CACHE.get(key) + if cached is not None: + cached_at, size_b, n_f, mtime = cached + if now - cached_at < _CACHE_TTL: + return (size_b, n_f, mtime) + + # Compute in a worker thread so a slow FS doesn't block the loop + size_b, n_f, mtime = await asyncio.to_thread(_dir_stats_sync, path) + async with _CACHE_LOCK: + _SIZE_CACHE[key] = (time.time(), size_b, n_f, mtime) + return (size_b, n_f, mtime) + + +def _invalidate_size_cache(path: Path) -> None: + """Drop the cached stats for ``path`` so the next call recomputes.""" + _SIZE_CACHE.pop(str(path), None) + + +@app.get("/v1/admin/indexes") +async def admin_list_indexes(): + """List FORetrieval index directories visible under ``data_dir``. + + A subdirectory is considered an index when it contains either + ``index_config.json.gz`` or ``metadata.json.gz``. + + Returns: + ``{"items": [...], "data_dir": str, "count": int}`` + where each item has keys + ``name, path, size_bytes, n_files, modified, has_collection, backend``. + """ + items: List[Dict[str, Any]] = [] + root_resolved = _DATA_DIR.resolve() + + if not _DATA_DIR.exists(): + return {"items": [], "data_dir": str(_DATA_DIR), "count": 0} + + for entry in sorted(_DATA_DIR.iterdir(), key=lambda p: p.name): + if not entry.is_dir(): + continue + try: + resolved = entry.resolve() + resolved.relative_to(root_resolved) + except (OSError, ValueError): + continue + if not _is_index_dir(entry): + continue + + size_b, n_f, mtime = await _dir_stats(entry) + meta = _read_meta(entry.name) + items.append({ + "name": entry.name, + "path": str(entry), + "size_bytes": size_b, + "n_files": n_f, + "modified": mtime, + "has_collection": entry.name in _registry or meta is not None, + "backend": (meta or {}).get("backend"), + }) + + return {"items": items, "data_dir": str(_DATA_DIR), "count": len(items)} + + +@app.get("/v1/admin/data_folders") +async def admin_list_data_folders(): + """List every subdirectory under ``data_dir``. + + Each item is annotated with ``is_index=True`` when the directory looks + like a FORetrieval index (so the UI can filter client-side). + + Returns: + ``{"items": [...], "data_dir": str, "count": int}`` + where each item has keys + ``name, path, size_bytes, n_files, modified, is_index``. + """ + items: List[Dict[str, Any]] = [] + root_resolved = _DATA_DIR.resolve() + + if not _DATA_DIR.exists(): + return {"items": [], "data_dir": str(_DATA_DIR), "count": 0} + + for entry in sorted(_DATA_DIR.iterdir(), key=lambda p: p.name): + if not entry.is_dir(): + continue + try: + resolved = entry.resolve() + resolved.relative_to(root_resolved) + except (OSError, ValueError): + continue + + size_b, n_f, mtime = await _dir_stats(entry) + items.append({ + "name": entry.name, + "path": str(entry), + "size_bytes": size_b, + "n_files": n_f, + "modified": mtime, + "is_index": _is_index_dir(entry), + }) + + return {"items": items, "data_dir": str(_DATA_DIR), "count": len(items)} diff --git a/foretrieval/vector_db_server/server_main.py b/foretrieval/vector_db_server/server_main.py new file mode 100644 index 0000000..de7bdc4 --- /dev/null +++ b/foretrieval/vector_db_server/server_main.py @@ -0,0 +1,36 @@ +"""Entry point for running the vector-DB server via uvicorn. + +Usage: + python -m foretrieval.vector_db_server.server_main + +Or via the installed script (if declared in pyproject.toml): + foretrieval-db-server + +Environment variables: + FOR_DB_HOST Bind address (default: 0.0.0.0) + FOR_DB_PORT Bind port (default: 18000) + FOR_DB_DATA_DIR Data root (default: /data) + FOR_DB_API_KEY Bearer token (optional) +""" + +from __future__ import annotations + +import os +import uvicorn + +from .server import app # noqa: F401 — imported so uvicorn picks it up + + +def main() -> None: + host = os.environ.get("FOR_DB_HOST", "0.0.0.0") + port = int(os.environ.get("FOR_DB_PORT", "18000")) + uvicorn.run( + "foretrieval.vector_db_server.server:app", + host=host, + port=port, + log_level="info", + ) + + +if __name__ == "__main__": + main() diff --git a/foretrieval/vector_store/__init__.py b/foretrieval/vector_store/__init__.py new file mode 100644 index 0000000..18da1ad --- /dev/null +++ b/foretrieval/vector_store/__init__.py @@ -0,0 +1,42 @@ +"""foretrieval.vector_store — generic vector-store interface for FORetrieval. + +Public API: + VectorStore — abstract base class + StoredPoint — dataclass for indexed items + SearchHit — dataclass for search results + MultiVectorQuery — dataclass for search queries + make_point_id — deterministic integer point-ID helper + make_vector_store — factory function + + LocalVectorStore — in-memory + .pt/.json.gz file backend + QdrantVectorStore — embedded Qdrant (local path) backend + MilvusVectorStore — Milvus Lite two-collection backend + RemoteVectorStore — HTTP client to a remote vector-DB server +""" + +from .base import ( + VectorStore, + StoredPoint, + SearchHit, + MultiVectorQuery, + make_point_id, +) +from .local import LocalVectorStore +from .qdrant import QdrantVectorStore +from .milvus import MilvusVectorStore +from .remote import RemoteVectorStore +from .factory import make_vector_store, BACKEND_REGISTRY + +__all__ = [ + "VectorStore", + "StoredPoint", + "SearchHit", + "MultiVectorQuery", + "make_point_id", + "make_vector_store", + "BACKEND_REGISTRY", + "LocalVectorStore", + "QdrantVectorStore", + "MilvusVectorStore", + "RemoteVectorStore", +] diff --git a/foretrieval/vector_store/base.py b/foretrieval/vector_store/base.py new file mode 100644 index 0000000..afa77e0 --- /dev/null +++ b/foretrieval/vector_store/base.py @@ -0,0 +1,273 @@ +"""Generic vector-store interface for FORetrieval. + +This module defines the VectorStore ABC and the shared dataclasses used by all +backend implementations (local, Qdrant, Milvus) and by the future remote-DB-server +client (RemoteVectorStore). + +Design for remote-server forward-compatibility +----------------------------------------------- +Every method on VectorStore takes/returns plain Python dataclasses whose fields +are either primitive types, dicts, or torch.Tensors — all serialisable to JSON or +to torch.save(). A future RemoteVectorStore subclass will simply wrap an httpx +client and forward each call to a corresponding HTTP endpoint: + + open() → POST /v1/collection/open (or GET /health + lazy init) + close() → (disconnect client) + collection_exists() → GET /v1/collection/{name} + create_collection() → POST /v1/collection + upsert() → POST /v1/upsert (body: list[StoredPoint]) + point_exists() → GET /v1/point/{point_id} + search() → POST /v1/search (body: MultiVectorQuery) + fetch_vector() → GET /v1/vector/{point_id} + export_sidecar() → no-op for remote + load_sidecar() → no-op for remote + +The factory function make_vector_store() in factory.py will accept "remote" as a +backend name and return a RemoteVectorStore when that module is implemented. +""" + +from __future__ import annotations + +import abc +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, ClassVar, Dict, List, Optional + +import torch + + +# --------------------------------------------------------------------------- +# Shared point-ID helper (same formula used everywhere, including Qdrant side- +# cars and local embed_id_to_doc_id) +# --------------------------------------------------------------------------- + +def make_point_id(doc_id: int, page_id: int, chunk_id: Optional[int] = None) -> int: + """Compute a deterministic integer point ID from (doc_id, page_id, chunk_id). + + The formula guarantees uniqueness as long as: + - doc_id < 10^7 + - page_id < 10^4 + - chunk_id < 10^4 + which comfortably covers any realistic corpus. + """ + chunk_val = 0 if chunk_id is None else int(chunk_id) + return int(doc_id) * 10_000_000 + int(page_id) * 10_000 + chunk_val + + +# --------------------------------------------------------------------------- +# Dataclasses +# --------------------------------------------------------------------------- + +@dataclass +class StoredPoint: + """One indexed item (a page or chunk) ready to be written to the store. + + Attributes: + point_id: Deterministic integer key (output of make_point_id). + vector: Multi-vector embedding, shape (n_tokens, dim). CPU tensor. + payload: Arbitrary metadata dict stored alongside the vector. + Must be JSON-serialisable. + """ + point_id: int + vector: torch.Tensor # (n_tokens, dim) + payload: Dict[str, Any] # doc_id, page_id, chunk_id, metadata, … + + +@dataclass +class SearchHit: + """One result returned by VectorStore.search(). + + Attributes: + point_id: Integer key that matches StoredPoint.point_id. + score: Relevance score (higher is better for all backends). + payload: Full payload dict as stored at index time. + """ + point_id: int + score: float + payload: Dict[str, Any] + + +@dataclass +class MultiVectorQuery: + """A multi-vector query embedding ready to be sent to VectorStore.search(). + + Attributes: + vectors: Query embeddings, shape (n_query_tokens, dim). CPU tensor. + filter_metadata: Optional key/value dict for payload filtering. + """ + vectors: torch.Tensor # (n_query_tokens, dim) + filter_metadata: Optional[Dict[str, Any]] = field(default=None) + + +# --------------------------------------------------------------------------- +# Abstract base class +# --------------------------------------------------------------------------- + +class VectorStore(abc.ABC): + """Abstract interface for a vector store backend. + + Concrete subclasses: + LocalVectorStore — in-memory + .pt/.json.gz files + QdrantVectorStore — embedded Qdrant (local path) + MilvusVectorStore — Milvus Lite (file-based) + RemoteVectorStore — future HTTP client to a remote DB server + + Lifecycle + --------- + Instances are created via make_vector_store() then initialised with open(). + open() is called once per index name (either creating or loading the store). + close() is called on teardown (context manager not required but recommended). + + Backend implementations must be stateless with respect to the index name + until open() is called — this allows the same VectorStore object to be + reused for multiple index names by calling open() again after close(). + """ + + backend_name: ClassVar[str] + """Identifier string, e.g. "local", "qdrant", "milvus".""" + + supports_multivector_native: ClassVar[bool] + """True if the backend natively scores multi-vector queries (e.g. Qdrant MAX_SIM). + False means the implementation uses an approximation (mean-pool + re-rank).""" + + # ------------------------------------------------------------------ + # Lifecycle + # ------------------------------------------------------------------ + + @abc.abstractmethod + def open( + self, + index_name: str, + index_root: Path, + *, + create: bool, + dim: Optional[int] = None, + ) -> None: + """Initialise or connect to the store for a given index. + + Parameters: + index_name: Logical name for the collection / index directory. + index_root: Root directory where local files live. + create: If True, create the collection if it does not exist. + dim must be provided when create=True and the collection + does not yet exist. + dim: Embedding dimension. Required when create=True and the + collection doesn't exist yet. + """ + + @abc.abstractmethod + def close(self) -> None: + """Release any resources held by the store (clients, file handles).""" + + # ------------------------------------------------------------------ + # Collection management + # ------------------------------------------------------------------ + + @abc.abstractmethod + def collection_exists(self) -> bool: + """Return True if the collection backing this store already exists.""" + + @abc.abstractmethod + def create_collection(self, dim: int) -> None: + """Create the collection with the given embedding dimension. + + No-op if the collection already exists. + """ + + # ------------------------------------------------------------------ + # Write + # ------------------------------------------------------------------ + + @abc.abstractmethod + def upsert(self, points: List[StoredPoint]) -> None: + """Write a list of StoredPoints into the store. + + Implementations must be idempotent: upserting the same point_id twice + must not raise an error (last-write-wins semantics). + """ + + @abc.abstractmethod + def point_exists(self, point_id: int) -> bool: + """Return True if a point with the given ID already exists in the store.""" + + # ------------------------------------------------------------------ + # Read + # ------------------------------------------------------------------ + + @abc.abstractmethod + def search( + self, + query: MultiVectorQuery, + k: int, + ) -> List[SearchHit]: + """Execute a nearest-neighbour search and return up to k hits. + + Parameters: + query: Multi-vector query embedding + optional metadata filter. + k: Maximum number of results to return. + + Returns: + List of SearchHit, sorted by descending score. + """ + + @abc.abstractmethod + def fetch_vector(self, point_id: int) -> Optional[torch.Tensor]: + """Retrieve the stored multi-vector tensor for a given point. + + Returns: + CPU tensor of shape (n_tokens, dim), or None if not found. + + Used by the heatmap / visual-grounding code path that needs the original + per-token embeddings to compute patch attention scores. + """ + + # ------------------------------------------------------------------ + # Persistence helpers (called by ColPaliModel._export_index / from_index) + # ------------------------------------------------------------------ + + def export_sidecar(self, index_path: Path) -> None: + """Persist any backend-specific sidecar data into index_path. + + Default implementation is a no-op (e.g. for remote backends where the + data lives on the server). Local/embedded backends override this to + write .pt / .json.gz files. + """ + + def load_sidecar(self, index_path: Path) -> None: + """Load backend-specific sidecar data from index_path. + + Default implementation is a no-op. Matches export_sidecar(). + """ + + # ------------------------------------------------------------------ + # Index-level bookkeeping (model name, doc metadata, file-name map, + # per-embedding extras, …). For local/embedded backends this lives in + # local sidecar files written by ColPaliModel. For the remote backend it + # is round-tripped to the server so the client needs no local index dir. + # ------------------------------------------------------------------ + + def supports_remote_bookkeeping(self) -> bool: + """Whether this backend persists ColPali bookkeeping on the server. + + Local/embedded backends return False (ColPaliModel keeps writing the + local sidecar files). RemoteVectorStore returns True so ColPaliModel + routes the bookkeeping blob through export/load_bookkeeping instead of + touching the local filesystem. + """ + return False + + def export_bookkeeping(self, blob: Dict[str, Any]) -> None: + """Persist the ColPali bookkeeping ``blob`` on the server. + + ``blob`` is a plain dict whose values are JSON-serialisable or + torch.Tensors (it is transported with torch.save). Default no-op; + only RemoteVectorStore implements this. + """ + + def load_bookkeeping(self) -> Optional[Dict[str, Any]]: + """Load the ColPali bookkeeping blob from the server. + + Returns ``None`` when no bookkeeping is stored (or for backends that + do not support remote bookkeeping). Default no-op. + """ + return None diff --git a/foretrieval/vector_store/factory.py b/foretrieval/vector_store/factory.py new file mode 100644 index 0000000..a7e2821 --- /dev/null +++ b/foretrieval/vector_store/factory.py @@ -0,0 +1,189 @@ +"""Factory for VectorStore instances. + +Usage: + from foretrieval.vector_store import make_vector_store + + # Local backends + vs = make_vector_store("qdrant") + vs.open("my_index", Path(".foretrieval"), create=True, dim=128) + + # Remote server backend + vs = make_vector_store( + "remote", + { + "url": "http://gpu-server:18000", + "backend": "qdrant", # server-side backend + "api_key": "secret", # optional bearer token + } + ) + vs.open("my_index", Path(".foretrieval"), create=True, dim=128) + +Supported backend names: + "local" — LocalVectorStore + "qdrant" — QdrantVectorStore (requires foretrieval[qdrant]) + "milvus" — MilvusVectorStore (requires foretrieval[milvus]) + "remote" — RemoteVectorStore (requires foretrieval[vector_db_server] for + auto_deploy; plain httpx for client-only use) + +Future backends can be registered via BACKEND_REGISTRY without changing callers. +""" + +from __future__ import annotations + +import logging +import time +from typing import Any, Dict, Optional, Type + +from .base import VectorStore +from .local import LocalVectorStore +from .qdrant import QdrantVectorStore +from .milvus import MilvusVectorStore +from .remote import RemoteVectorStore + +logger = logging.getLogger(__name__) + +# Registry maps backend name → class. Add new entries here to register future +# backends without touching make_vector_store(). +BACKEND_REGISTRY: Dict[str, Type[VectorStore]] = { + "local": LocalVectorStore, + "qdrant": QdrantVectorStore, + "milvus": MilvusVectorStore, + "remote": RemoteVectorStore, +} + +# Config keys consumed at the factory level for "remote"; not forwarded as +# server-side storage_config. +_REMOTE_CLIENT_KEYS = { + "url", "api_key", "verify_ssl", "request_timeout", + "auto_deploy", "ssh_host", "ssh_user", "ssh_key_path", + "port", "data_dir", +} +# Health-check wait parameters for auto_deploy +_HEALTH_CHECK_RETRIES = 30 +_HEALTH_CHECK_INTERVAL = 2 # seconds + + +def make_vector_store( + backend: str, + storage_config: Optional[Dict[str, Any]] = None, +) -> VectorStore: + """Instantiate a VectorStore for the given backend name. + + Parameters: + backend: Backend identifier: "local", "qdrant", "milvus", or + "remote". + storage_config: Optional dict of backend-specific keyword arguments. + + For "remote", the following keys are consumed at the + factory level and used to build the client/config: + url (required) — base URL of the server + backend (str, default "qdrant") — server-side + backend to use for the collection + api_key (str, optional) + verify_ssl (bool, default True) + request_timeout (int, default 120) + auto_deploy (bool, default False) + ssh_host (str, optional) — required if auto_deploy + ssh_user (str, optional) + ssh_key_path (str, optional) + port (int, default 18000) + data_dir (str, optional) — remote data directory + + Any remaining keys are forwarded as server-side + storage_config (e.g. candidate_limit for Milvus). + + For "milvus", ``candidate_limit`` is accepted. + + Returns: + An uninitialised VectorStore. Call .open() before using it. + + Raises: + ValueError: Unknown backend name. + RuntimeError: Required optional dependency not installed. + """ + key = (backend or "local").strip().lower() + cls = BACKEND_REGISTRY.get(key) + if cls is None: + supported = ", ".join(sorted(BACKEND_REGISTRY)) + raise ValueError( + f"Unknown storage backend {backend!r}. " + f"Supported backends: {supported}." + ) + + kwargs = dict(storage_config or {}) + + if cls is LocalVectorStore: + return LocalVectorStore() + + if cls is QdrantVectorStore: + return QdrantVectorStore() + + if cls is MilvusVectorStore: + candidate_limit = kwargs.get("candidate_limit", 64) + return MilvusVectorStore(candidate_limit=int(candidate_limit)) + + if cls is RemoteVectorStore: + return _make_remote_vector_store(kwargs) + + # Generic fallback for future registered backends + return cls(**kwargs) # type: ignore[call-arg] + + +# --------------------------------------------------------------------------- +# Remote store construction helper +# --------------------------------------------------------------------------- + +def _make_remote_vector_store(kwargs: Dict[str, Any]) -> RemoteVectorStore: + """Build a RemoteVectorStore from a storage_config dict.""" + from ..vector_db_server.config import VectorDBServerConfig + from ..vector_db_server.client import VectorDBServerClient + + # Split kwargs into client-level config and server-side storage_config + client_kwargs: Dict[str, Any] = {} + server_storage_config: Dict[str, Any] = {} + for k, v in kwargs.items(): + if k in _REMOTE_CLIENT_KEYS or k == "backend": + client_kwargs[k] = v + else: + server_storage_config[k] = v + + if "url" not in client_kwargs: + raise ValueError( + "storage_config must include 'url' when using the 'remote' backend." + ) + + cfg = VectorDBServerConfig.from_dict(client_kwargs) + server_backend = cfg.backend + srv_storage = server_storage_config if server_storage_config else None + + # Auto-deploy: SSH to remote host and ensure server container is running + if cfg.auto_deploy: + from ..vector_db_server.manager import VectorDBServerManager + manager = VectorDBServerManager(cfg) + manager.ensure_deployed() + logger.info("Auto-deploy complete. Waiting for server to be ready …") + _wait_for_health(cfg) + + client = VectorDBServerClient(cfg) + return RemoteVectorStore(client, backend=server_backend, storage_config=srv_storage) + + +def _wait_for_health(cfg: Any, retries: int = _HEALTH_CHECK_RETRIES) -> None: + """Poll /health until the server responds or raise TimeoutError.""" + from ..vector_db_server.client import VectorDBServerClient + client = VectorDBServerClient(cfg) + for attempt in range(retries): + if client.health_check(): + logger.info("Vector-DB server is healthy.") + client.close() + return + logger.debug( + "Health check attempt %d/%d failed — retrying in %ds …", + attempt + 1, retries, _HEALTH_CHECK_INTERVAL, + ) + time.sleep(_HEALTH_CHECK_INTERVAL) + client.close() + raise TimeoutError( + f"Vector-DB server at {cfg.url} did not become healthy " + f"after {retries * _HEALTH_CHECK_INTERVAL}s." + ) diff --git a/foretrieval/vector_store/local.py b/foretrieval/vector_store/local.py new file mode 100644 index 0000000..79aae46 --- /dev/null +++ b/foretrieval/vector_store/local.py @@ -0,0 +1,296 @@ +"""LocalVectorStore — in-memory multi-vector store with .pt/.json.gz persistence. + +This is a direct extraction of the "local" code-path from ColPaliModel into the +VectorStore interface. All logic that previously lived in _search_local, +_load_local_index, and _export_index (local branch) is now here. + +Storage layout on disk (mirrored from the existing format so old indexes remain +compatible): + + // + embeddings/ + embeddings_0.pt — list[Tensor], up to 500 per file + embeddings_500.pt + … + embed_id_to_doc_id.json.gz — {str(embed_id): {"doc_id": int, "page_id": int, …}} + +The embed_id is simply the zero-based position of the point in the flat +indexed_embeddings list. This is intentionally different from the integer +point_id produced by make_point_id() — for backward-compatibility the local +backend keeps its own sequential embed_id scheme internally, but exports a +point_id in SearchHit.point_id by constructing it via make_point_id so that +ColPaliModel can look up embed_id_to_extra consistently. + +For that reason LocalVectorStore also maintains an internal embed_id → point_id +and point_id → embed_id mapping so callers that only know the point_id can still +fetch vectors (used by the heatmap path). +""" + +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Any, ClassVar, Dict, List, Optional + +import srsly +import torch + +from .base import ( + MultiVectorQuery, + SearchHit, + StoredPoint, + VectorStore, + make_point_id, +) +from ..utils import _value_match + +logger = logging.getLogger(__name__) + + +class LocalVectorStore(VectorStore): + """In-process multi-vector store backed by .pt / .json.gz files. + + Scoring uses the full late-interaction MAX_SIM formula via + processor.score(), which is injected at open() time so that the store + remains independent of the ColPali processor class hierarchy. + """ + + backend_name: ClassVar[str] = "local" + supports_multivector_native: ClassVar[bool] = True # exact MAX_SIM + + def __init__(self) -> None: + # Set by open() + self._index_name: Optional[str] = None + self._index_root: Optional[Path] = None + self._processor: Any = None # injected via set_processor() + + # In-memory state + self._embeddings: List[torch.Tensor] = [] # shape (n_tokens, dim) each + self._embed_id_to_doc_id: Dict[int, Dict[str, Any]] = {} + + # Reverse mapping: point_id → embed_id (built lazily on first use) + self._point_id_to_embed_id: Dict[int, int] = {} + + # ------------------------------------------------------------------ + # Processor injection + # ------------------------------------------------------------------ + + def set_processor(self, processor: Any) -> None: + """Inject the ColPali processor used for score().""" + self._processor = processor + + # ------------------------------------------------------------------ + # Lifecycle + # ------------------------------------------------------------------ + + def open( + self, + index_name: str, + index_root: Path, + *, + create: bool, + dim: Optional[int] = None, + ) -> None: + self._index_name = index_name + self._index_root = Path(index_root) + + def close(self) -> None: + pass # nothing to release for the in-memory store + + # ------------------------------------------------------------------ + # Collection management + # ------------------------------------------------------------------ + + def collection_exists(self) -> bool: + if self._index_name is None or self._index_root is None: + return False + embeddings_dir = self._index_root / self._index_name / "embeddings" + return embeddings_dir.exists() and any(embeddings_dir.glob("*.pt")) + + def create_collection(self, dim: int) -> None: + # No setup required — directories are created at export_sidecar() time. + pass + + # ------------------------------------------------------------------ + # Write + # ------------------------------------------------------------------ + + def upsert(self, points: List[StoredPoint]) -> None: + for sp in points: + if not self.point_exists(sp.point_id): + embed_id = len(self._embeddings) + self._embeddings.append(sp.vector.cpu()) + + entry: Dict[str, Any] = { + "doc_id": int(sp.payload["doc_id"]), + "page_id": int(sp.payload["page_id"]), + } + chunk_id = sp.payload.get("chunk_id") + if chunk_id is not None: + entry["chunk_id"] = int(chunk_id) + + self._embed_id_to_doc_id[embed_id] = entry + self._point_id_to_embed_id[sp.point_id] = embed_id + else: + # Last-write-wins: update in place + embed_id = self._point_id_to_embed_id[sp.point_id] + self._embeddings[embed_id] = sp.vector.cpu() + + def point_exists(self, point_id: int) -> bool: + return point_id in self._point_id_to_embed_id + + # ------------------------------------------------------------------ + # Read + # ------------------------------------------------------------------ + + def search( + self, + query: MultiVectorQuery, + k: int, + ) -> List[SearchHit]: + if not self._embeddings: + return [] + + if self._processor is None: + raise RuntimeError( + "LocalVectorStore.set_processor() must be called before search()." + ) + + filter_md = query.filter_metadata + + if filter_md: + req_embeddings, req_embed_ids = self._filter_by_metadata(filter_md) + if not req_embeddings: + logger.warning( + "Metadata filter matched no documents — returning empty results." + ) + return [] + else: + req_embeddings = self._embeddings + req_embed_ids = list(range(len(self._embeddings))) + + k = min(k, len(req_embeddings)) + qs = [query.vectors] + scores = self._processor.score(qs, req_embeddings).cpu().numpy() + top_local = scores.argsort(axis=1)[0][-k:][::-1].tolist() + + results = [] + for local_idx in top_local: + embed_id = req_embed_ids[local_idx] + doc_info = self._embed_id_to_doc_id[embed_id] + pid = make_point_id( + doc_info["doc_id"], + doc_info["page_id"], + doc_info.get("chunk_id"), + ) + results.append( + SearchHit( + point_id=pid, + score=float(scores[0][local_idx]), + payload=dict(doc_info), + ) + ) + + return results + + def fetch_vector(self, point_id: int) -> Optional[torch.Tensor]: + embed_id = self._point_id_to_embed_id.get(point_id) + if embed_id is None: + return None + return self._embeddings[embed_id].cpu() + + # ------------------------------------------------------------------ + # Metadata filtering helpers + # ------------------------------------------------------------------ + + def _filter_by_metadata( + self, + filter_md: Dict[str, Any], + ) -> tuple[List[torch.Tensor], List[int]]: + """Return (embeddings, embed_ids) matching the metadata filter.""" + # We need the full doc_id_to_metadata which is owned by ColPaliModel. + # Inject it via set_doc_id_to_metadata(). + metadata_map = getattr(self, "_doc_id_to_metadata", {}) + + from ..models_metadata import MetadataFilter + f = ( + filter_md + if isinstance(filter_md, MetadataFilter) + else MetadataFilter(**filter_md) + ) + + matching_doc_ids = { + int(did) + for did, md in metadata_map.items() + if _value_match(md, f) + } + + req_embed_ids = [ + eid + for eid, info in self._embed_id_to_doc_id.items() + if int(info["doc_id"]) in matching_doc_ids + ] + req_embeddings = [self._embeddings[eid] for eid in req_embed_ids] + return req_embeddings, req_embed_ids + + def set_doc_id_to_metadata(self, mapping: Dict[int, Any]) -> None: + """Inject the doc_id→metadata mapping needed for filter_by_metadata.""" + self._doc_id_to_metadata = mapping + + # ------------------------------------------------------------------ + # Persistence + # ------------------------------------------------------------------ + + def export_sidecar(self, index_path: Path) -> None: + embeddings_dir = index_path / "embeddings" + embeddings_dir.mkdir(exist_ok=True) + + chunk_size = 500 + for i in range(0, len(self._embeddings), chunk_size): + chunk = self._embeddings[i : i + chunk_size] + torch.save(chunk, embeddings_dir / f"embeddings_{i}.pt") + + srsly.write_gzip_json( + index_path / "embed_id_to_doc_id.json.gz", + self._embed_id_to_doc_id, + ) + + def load_sidecar(self, index_path: Path) -> None: + embeddings_path = index_path / "embeddings" + if not embeddings_path.exists(): + return + + embedding_files = sorted( + embeddings_path.glob("embeddings_*.pt"), + key=lambda x: int(x.stem.split("_")[1]), + ) + self._embeddings = [] + for f in embedding_files: + self._embeddings.extend(torch.load(f, map_location="cpu")) + + id_path = index_path / "embed_id_to_doc_id.json.gz" + if id_path.exists(): + raw = srsly.read_gzip_json(id_path) + self._embed_id_to_doc_id = {int(k): v for k, v in raw.items()} + + # Rebuild point_id → embed_id reverse map + self._point_id_to_embed_id = {} + for embed_id, info in self._embed_id_to_doc_id.items(): + pid = make_point_id( + int(info["doc_id"]), + int(info["page_id"]), + info.get("chunk_id"), + ) + self._point_id_to_embed_id[pid] = embed_id + + # ------------------------------------------------------------------ + # Accessors used by ColPaliModel (legacy compatibility) + # ------------------------------------------------------------------ + + @property + def indexed_embeddings(self) -> List[torch.Tensor]: + return self._embeddings + + @property + def embed_id_to_doc_id(self) -> Dict[int, Dict[str, Any]]: + return self._embed_id_to_doc_id diff --git a/foretrieval/vector_store/milvus.py b/foretrieval/vector_store/milvus.py new file mode 100644 index 0000000..40c31f6 --- /dev/null +++ b/foretrieval/vector_store/milvus.py @@ -0,0 +1,520 @@ +"""MilvusVectorStore — Milvus Lite backend for FORetrieval. + +Milvus does not support native multi-vector (multi-dimensional) storage like +Qdrant's MAX_SIM. Instead, this implementation uses a two-collection layout +that closely mirrors the RAG_Orch orchestrator in origin/Adrien/Docker-DB: + + __pages — one row per page; vector = mean-pooled page embedding + __tokens — one row per query-token; grouped by page_id + +Search is a two-stage process: + 1. Candidate retrieval: ANN search against __pages using the mean-pooled + query vector. Retrieves candidate_limit pages. + 2. Late-interaction reranking: for each query token, search the __tokens + collection filtered to candidate page IDs. Aggregate max scores per + page across all query tokens → approximate MAX_SIM score. + 3. Return top-k pages sorted by aggregated score. + +This closely approximates Qdrant's MAX_SIM while being compatible with Milvus's +FLOAT_VECTOR type (which does not support multi-dimensional vectors). + +Milvus Lite (file-based, included in pymilvus>=2.4) is used for local/dev usage. +Production deployment uses the same API but with a network URI. + +Heatmap / fetch_vector +---------------------- +fetch_vector() retrieves all token rows for a given page_id from __tokens and +reconstructs the (n_tokens, dim) tensor, enabling the same heatmap path as +Qdrant and local backends. + +Storage layout +-------------- + //milvus.db — Milvus Lite file + +No additional sidecar files for vectors (embed_id_to_extra etc. still managed +by ColPaliModel). +""" + +from __future__ import annotations + +import json +import logging +import uuid +from pathlib import Path +from typing import Any, ClassVar, Dict, List, Optional, Tuple + +import torch + +from .base import ( + MultiVectorQuery, + SearchHit, + StoredPoint, + VectorStore, +) + +logger = logging.getLogger(__name__) + +_SUFFIX_PAGES = "__pages" +_SUFFIX_TOKENS = "__tokens" +_PAGE_VECTOR_FIELD = "page_vector" +_TOKEN_VECTOR_FIELD = "token_vector" +_TOKEN_GROUP_FIELD = "page_id" +_PAYLOAD_FIELD = "payload_json" +_PRIMARY_KEY_MAX_LEN = 64 +_PAYLOAD_JSON_MAX_LEN = 65535 + +# Default number of candidate pages to fetch before late-interaction rerank +_DEFAULT_CANDIDATE_LIMIT = 64 + +try: + from pymilvus import DataType, MilvusClient + _MILVUS_AVAILABLE = True +except ImportError: + _MILVUS_AVAILABLE = False + + +def _require_milvus() -> None: + if not _MILVUS_AVAILABLE: + raise RuntimeError( + "The Milvus storage backend requires the pymilvus package.\n" + "Install it with: pip install \"foretrieval[milvus]\"\n" + "or: uv add foretrieval --extra milvus" + ) + + +def _page_id_str(point_id: int) -> str: + """Convert integer point_id to the string key used as Milvus page_id.""" + return str(point_id) + + +def _mean_pool(vectors: torch.Tensor) -> List[float]: + """Return the mean of a 2-D tensor as a Python list (for Milvus insert).""" + return vectors.float().mean(dim=0).tolist() + + +def _serialize_payload(payload: Dict[str, Any]) -> str: + return json.dumps(payload)[:_PAYLOAD_JSON_MAX_LEN] + + +def _deserialize_payload(raw: Optional[str]) -> Dict[str, Any]: + if not raw: + return {} + try: + return json.loads(raw) + except Exception: + return {} + + +def _collection_names(index_name: str) -> Tuple[str, str]: + return f"{index_name}{_SUFFIX_PAGES}", f"{index_name}{_SUFFIX_TOKENS}" + + +class MilvusVectorStore(VectorStore): + """Two-collection Milvus store using approximate late-interaction scoring.""" + + backend_name: ClassVar[str] = "milvus" + supports_multivector_native: ClassVar[bool] = False # uses approximation + + def __init__(self, candidate_limit: int = _DEFAULT_CANDIDATE_LIMIT) -> None: + self._client: Optional["MilvusClient"] = None + self._index_name: Optional[str] = None + self._db_path: Optional[Path] = None + self._candidate_limit = candidate_limit + + # ------------------------------------------------------------------ + # Lifecycle + # ------------------------------------------------------------------ + + def open( + self, + index_name: str, + index_root: Path, + *, + create: bool, + dim: Optional[int] = None, + ) -> None: + _require_milvus() + self._index_name = index_name + index_dir = Path(index_root) / index_name + index_dir.mkdir(parents=True, exist_ok=True) + self._db_path = index_dir / "milvus.db" + self._client = MilvusClient(uri=str(self._db_path)) + + if create and dim is not None and not self.collection_exists(): + self.create_collection(dim) + + def close(self) -> None: + self._client = None + + # ------------------------------------------------------------------ + # Collection management + # ------------------------------------------------------------------ + + def collection_exists(self) -> bool: + if self._client is None or self._index_name is None: + return False + page_col, _ = _collection_names(self._index_name) + return page_col in self._client.list_collections() + + def create_collection(self, dim: int) -> None: + if self._client is None or self._index_name is None: + raise RuntimeError("MilvusVectorStore.open() must be called first.") + page_col, token_col = _collection_names(self._index_name) + self._create_page_collection(page_col, dim) + self._create_token_collection(token_col, dim) + self._load_collections() + + def _create_page_collection(self, collection_name: str, dim: int) -> None: + if collection_name in self._client.list_collections(): + return + schema = MilvusClient.create_schema( + auto_id=False, enable_dynamic_field=False + ) + schema.add_field( + field_name="id", + datatype=DataType.VARCHAR, + is_primary=True, + max_length=_PRIMARY_KEY_MAX_LEN, + ) + schema.add_field( + field_name=_PAGE_VECTOR_FIELD, + datatype=DataType.FLOAT_VECTOR, + dim=dim, + ) + schema.add_field( + field_name=_PAYLOAD_FIELD, + datatype=DataType.VARCHAR, + max_length=_PAYLOAD_JSON_MAX_LEN, + ) + index_params = self._client.prepare_index_params() + index_params.add_index( + field_name=_PAGE_VECTOR_FIELD, + index_type="AUTOINDEX", + metric_type="COSINE", + ) + self._client.create_collection( + collection_name=collection_name, + schema=schema, + index_params=index_params, + ) + + def _create_token_collection(self, collection_name: str, dim: int) -> None: + if collection_name in self._client.list_collections(): + return + schema = MilvusClient.create_schema( + auto_id=False, enable_dynamic_field=False + ) + schema.add_field( + field_name="id", + datatype=DataType.VARCHAR, + is_primary=True, + max_length=_PRIMARY_KEY_MAX_LEN, + ) + schema.add_field( + field_name=_TOKEN_GROUP_FIELD, + datatype=DataType.VARCHAR, + max_length=_PRIMARY_KEY_MAX_LEN, + ) + schema.add_field( + field_name=_TOKEN_VECTOR_FIELD, + datatype=DataType.FLOAT_VECTOR, + dim=dim, + ) + index_params = self._client.prepare_index_params() + index_params.add_index( + field_name=_TOKEN_VECTOR_FIELD, + index_type="AUTOINDEX", + metric_type="COSINE", + ) + self._client.create_collection( + collection_name=collection_name, + schema=schema, + index_params=index_params, + ) + + def _load_collections(self) -> None: + if self._client is None or self._index_name is None: + return + page_col, token_col = _collection_names(self._index_name) + for col in (page_col, token_col): + if col in self._client.list_collections(): + self._client.load_collection(collection_name=col) + + # ------------------------------------------------------------------ + # Write + # ------------------------------------------------------------------ + + def upsert(self, points: List[StoredPoint]) -> None: + if self._client is None or self._index_name is None: + raise RuntimeError("MilvusVectorStore.open() must be called first.") + page_col, token_col = _collection_names(self._index_name) + self._load_collections() + + page_rows: List[Dict[str, Any]] = [] + token_rows: List[Dict[str, Any]] = [] + + for sp in points: + page_id_str = _page_id_str(sp.point_id) + vectors = sp.vector.float() # (n_tokens, dim) + + page_rows.append( + { + "id": page_id_str, + _PAGE_VECTOR_FIELD: _mean_pool(vectors), + _PAYLOAD_FIELD: _serialize_payload(sp.payload), + } + ) + for vec in vectors: + token_rows.append( + { + "id": str(uuid.uuid4()), + _TOKEN_GROUP_FIELD: page_id_str, + _TOKEN_VECTOR_FIELD: vec.tolist(), + } + ) + + if page_rows: + self._client.upsert(collection_name=page_col, data=page_rows) + if token_rows: + self._client.upsert(collection_name=token_col, data=token_rows) + + def point_exists(self, point_id: int) -> bool: + if self._client is None or self._index_name is None: + return False + if not self.collection_exists(): + return False + page_col, _ = _collection_names(self._index_name) + self._load_collections() + result = self._client.get( + collection_name=page_col, + ids=[_page_id_str(point_id)], + output_fields=["id"], + ) + return bool(result) + + # ------------------------------------------------------------------ + # Read + # ------------------------------------------------------------------ + + def search( + self, + query: MultiVectorQuery, + k: int, + ) -> List[SearchHit]: + if self._client is None or self._index_name is None: + raise RuntimeError("MilvusVectorStore.open() must be called first.") + self._load_collections() + + q_vectors = query.vectors.float() # (n_query_tokens, dim) + filter_expr = self._build_filter_expr(query.filter_metadata) + + candidate_ids, candidate_payloads = self._fetch_candidates( + q_vectors, k, filter_expr + ) + if not candidate_ids: + return [] + + scores = self._late_interaction_rerank(q_vectors, candidate_ids) + + ranked = sorted(scores.items(), key=lambda item: item[1], reverse=True)[:k] + + return [ + SearchHit( + point_id=int(page_id_str), + score=float(score), + payload=candidate_payloads.get(page_id_str, {}), + ) + for page_id_str, score in ranked + ] + + def _fetch_candidates( + self, + q_vectors: torch.Tensor, + k: int, + filter_expr: Optional[str], + ) -> Tuple[List[str], Dict[str, Dict[str, Any]]]: + page_col, _ = _collection_names(self._index_name) + pooled_q = _mean_pool(q_vectors) + candidate_limit = max(k * 10, self._candidate_limit) + + search_kwargs: Dict[str, Any] = dict( + collection_name=page_col, + data=[pooled_q], + anns_field=_PAGE_VECTOR_FIELD, + limit=candidate_limit, + output_fields=[_PAYLOAD_FIELD], + search_params={"metric_type": "COSINE"}, + ) + if filter_expr: + search_kwargs["filter"] = filter_expr + + results = self._client.search(**search_kwargs) + rows = results[0] if results else [] + + candidate_ids: List[str] = [] + candidate_payloads: Dict[str, Dict[str, Any]] = {} + for row in rows: + page_id = str(row.get("id", "")) + entity = row.get("entity") or {} + if not page_id: + continue + candidate_ids.append(page_id) + candidate_payloads[page_id] = _deserialize_payload( + entity.get(_PAYLOAD_FIELD) + ) + return candidate_ids, candidate_payloads + + def _late_interaction_rerank( + self, + q_vectors: torch.Tensor, + candidate_ids: List[str], + ) -> Dict[str, float]: + _, token_col = _collection_names(self._index_name) + quoted = ", ".join(f'"{pid}"' for pid in candidate_ids) + filter_expr = f"{_TOKEN_GROUP_FIELD} in [{quoted}]" + + aggregated: Dict[str, float] = {pid: 0.0 for pid in candidate_ids} + + for q_vec in q_vectors: + results = self._client.search( + collection_name=token_col, + data=[q_vec.tolist()], + anns_field=_TOKEN_VECTOR_FIELD, + filter=filter_expr, + limit=min(len(candidate_ids), 16384), + output_fields=[_TOKEN_GROUP_FIELD], + search_params={"metric_type": "COSINE"}, + group_by_field=_TOKEN_GROUP_FIELD, + ) + for row in (results[0] if results else []): + entity = row.get("entity") or {} + pid = str(entity.get(_TOKEN_GROUP_FIELD, "")) + if pid: + aggregated[pid] = aggregated.get(pid, 0.0) + float( + row.get("distance", 0.0) + ) + return aggregated + + def fetch_vector(self, point_id: int) -> Optional[torch.Tensor]: + """Reconstruct the multi-vector tensor from stored token rows.""" + if self._client is None or self._index_name is None: + return None + _, token_col = _collection_names(self._index_name) + self._load_collections() + page_id_str = _page_id_str(point_id) + + rows = self._client.query( + collection_name=token_col, + filter=f'{_TOKEN_GROUP_FIELD} == "{page_id_str}"', + output_fields=[_TOKEN_VECTOR_FIELD], + limit=16384, + ) + if not rows: + return None + token_vecs = [row[_TOKEN_VECTOR_FIELD] for row in rows] + return torch.tensor(token_vecs) # (n_tokens, dim) + + # ------------------------------------------------------------------ + # Filter helper + # ------------------------------------------------------------------ + + def _build_filter_expr( + self, filter_metadata: Optional[Dict[str, Any]] + ) -> Optional[str]: + """Build a Milvus filter expression from a key/value metadata dict. + + Metadata values are stored inside the JSON payload field, so filtering + is done by checking the payload fields decoded from the payload_json + column. However, Milvus does not support JSON sub-field filtering on + VARCHAR columns natively without dynamic fields. + + Strategy: since we store the full payload as JSON in _PAYLOAD_FIELD we + cannot use structured sub-field filters. The metadata filtering is + therefore implemented as a post-filter on the Python side: + candidates are fetched without a server-side filter, then filtered + locally by deserialising the payload JSON. + """ + # Note: server-side metadata filtering is not available in this layout + # because payload is stored as a serialised JSON string in a VARCHAR + # column (no dynamic field enabled). We return None here and apply + # the filter in _apply_metadata_filter() after candidate retrieval. + return None + + def _filter_candidates_by_metadata( + self, + candidate_ids: List[str], + candidate_payloads: Dict[str, Dict[str, Any]], + filter_metadata: Dict[str, Any], + ) -> Tuple[List[str], Dict[str, Dict[str, Any]]]: + """Post-filter candidates by metadata values in payload dicts.""" + from ..utils import _value_match + from ..models_metadata import MetadataFilter + + f = ( + filter_metadata + if isinstance(filter_metadata, MetadataFilter) + else MetadataFilter(**filter_metadata) + ) + + filtered_ids = [] + for pid in candidate_ids: + payload = candidate_payloads.get(pid, {}) + # Metadata lives under "metadata" key inside payload + meta = payload.get("metadata", payload) + if _value_match(meta, f): + filtered_ids.append(pid) + + return filtered_ids, { + pid: candidate_payloads[pid] + for pid in filtered_ids + if pid in candidate_payloads + } + + # Override search to apply metadata post-filter + def search( # type: ignore[override] # noqa: F811 + self, + query: MultiVectorQuery, + k: int, + ) -> List[SearchHit]: + if self._client is None or self._index_name is None: + raise RuntimeError("MilvusVectorStore.open() must be called first.") + self._load_collections() + + q_vectors = query.vectors.float() + + candidate_ids, candidate_payloads = self._fetch_candidates( + q_vectors, k, filter_expr=None + ) + if not candidate_ids: + return [] + + # Apply metadata filter post-retrieval if requested + if query.filter_metadata: + candidate_ids, candidate_payloads = self._filter_candidates_by_metadata( + candidate_ids, candidate_payloads, query.filter_metadata + ) + if not candidate_ids: + logger.warning( + "Metadata filter matched no candidates — returning empty results." + ) + return [] + + scores = self._late_interaction_rerank(q_vectors, candidate_ids) + ranked = sorted(scores.items(), key=lambda item: item[1], reverse=True)[:k] + + return [ + SearchHit( + point_id=int(page_id_str), + score=float(score), + payload=candidate_payloads.get(page_id_str, {}), + ) + for page_id_str, score in ranked + ] + + # ------------------------------------------------------------------ + # Client accessor + # ------------------------------------------------------------------ + + @property + def client(self) -> Optional["MilvusClient"]: + return self._client diff --git a/foretrieval/vector_store/qdrant.py b/foretrieval/vector_store/qdrant.py new file mode 100644 index 0000000..47a4444 --- /dev/null +++ b/foretrieval/vector_store/qdrant.py @@ -0,0 +1,239 @@ +"""QdrantVectorStore — embedded Qdrant backend for FORetrieval. + +Uses a local QdrantClient(path=...) so no external server is required. +The collection is configured with native multi-vector MAX_SIM scoring, which +gives exact late-interaction ColPali results without any approximation. + +Remote Qdrant (URL-based) is intentionally out of scope for now; it will be +handled by a future RemoteVectorStore that wraps an HTTP client to a dedicated +DB server. + +Metadata filtering +------------------ +Filters are translated from the generic dict representation into Qdrant's +Filter(must=[FieldCondition(key="metadata.", match=MatchValue(v))]) form. +Values may be strings, ints, or lists (via MetadataFilter.regex / range fields). + +Storage layout +-------------- +Vectors and payloads live inside the embedded Qdrant database at: + //qdrant/ + +No additional sidecar files are needed for the vector data itself (unlike local). +The embed_id_to_extra, doc_ids_to_file_names, metadata, and index_config sidecars +are still managed by ColPaliModel directly. +""" + +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Any, ClassVar, Dict, List, Optional + +import torch + +from .base import ( + MultiVectorQuery, + SearchHit, + StoredPoint, + VectorStore, +) + +logger = logging.getLogger(__name__) + +try: + from qdrant_client import QdrantClient + from qdrant_client.models import ( + Distance, + FieldCondition, + Filter, + MatchValue, + MultiVectorComparator, + MultiVectorConfig, + PointStruct, + VectorParams, + ) + _QDRANT_AVAILABLE = True +except ImportError: + _QDRANT_AVAILABLE = False + + +def _require_qdrant() -> None: + if not _QDRANT_AVAILABLE: + raise RuntimeError( + "The Qdrant storage backend requires the qdrant-client package.\n" + "Install it with: pip install \"foretrieval[qdrant]\"\n" + "or: uv add foretrieval --extra qdrant" + ) + + +class QdrantVectorStore(VectorStore): + """Embedded Qdrant vector store with native multi-vector MAX_SIM scoring.""" + + backend_name: ClassVar[str] = "qdrant" + supports_multivector_native: ClassVar[bool] = True + + def __init__(self) -> None: + self._client: Optional["QdrantClient"] = None + self._collection_name: Optional[str] = None + self._index_root: Optional[Path] = None + + # ------------------------------------------------------------------ + # Lifecycle + # ------------------------------------------------------------------ + + def open( + self, + index_name: str, + index_root: Path, + *, + create: bool, + dim: Optional[int] = None, + ) -> None: + _require_qdrant() + self._collection_name = index_name + self._index_root = Path(index_root) + qdrant_path = self._index_root / index_name / "qdrant" + qdrant_path.mkdir(parents=True, exist_ok=True) + self._client = QdrantClient(path=str(qdrant_path)) + + if create and dim is not None and not self._client.collection_exists(index_name): + self.create_collection(dim) + + def close(self) -> None: + self._client = None + + # ------------------------------------------------------------------ + # Collection management + # ------------------------------------------------------------------ + + def collection_exists(self) -> bool: + if self._client is None or self._collection_name is None: + return False + return self._client.collection_exists(self._collection_name) + + def create_collection(self, dim: int) -> None: + if self._client is None or self._collection_name is None: + raise RuntimeError("QdrantVectorStore.open() must be called first.") + if self._client.collection_exists(self._collection_name): + return + self._client.create_collection( + collection_name=self._collection_name, + vectors_config=VectorParams( + size=dim, + distance=Distance.COSINE, + multivector_config=MultiVectorConfig( + comparator=MultiVectorComparator.MAX_SIM, + ), + ), + ) + + # ------------------------------------------------------------------ + # Write + # ------------------------------------------------------------------ + + def upsert(self, points: List[StoredPoint]) -> None: + if self._client is None or self._collection_name is None: + raise RuntimeError("QdrantVectorStore.open() must be called first.") + qdrant_points = [ + PointStruct( + id=sp.point_id, + vector=sp.vector.float().numpy().tolist(), + payload=sp.payload, + ) + for sp in points + ] + self._client.upsert( + collection_name=self._collection_name, + points=qdrant_points, + ) + + def point_exists(self, point_id: int) -> bool: + if self._client is None or self._collection_name is None: + return False + if not self._client.collection_exists(self._collection_name): + return False + found = self._client.retrieve( + collection_name=self._collection_name, + ids=[point_id], + with_payload=False, + with_vectors=False, + ) + return bool(found) + + # ------------------------------------------------------------------ + # Read + # ------------------------------------------------------------------ + + def search( + self, + query: MultiVectorQuery, + k: int, + ) -> List[SearchHit]: + if self._client is None or self._collection_name is None: + raise RuntimeError("QdrantVectorStore.open() must be called first.") + + qfilter = self._build_filter(query.filter_metadata) + + response = self._client.query_points( + collection_name=self._collection_name, + query=query.vectors.float().numpy().tolist(), + query_filter=qfilter, + limit=k, + with_payload=True, + with_vectors=False, + ) + + points = response.points if hasattr(response, "points") else response + + return [ + SearchHit( + point_id=int(p.id), + score=float(p.score), + payload=p.payload or {}, + ) + for p in points + ] + + def fetch_vector(self, point_id: int) -> Optional[torch.Tensor]: + if self._client is None or self._collection_name is None: + return None + retrieved = self._client.retrieve( + collection_name=self._collection_name, + ids=[point_id], + with_payload=False, + with_vectors=True, + ) + if not retrieved: + return None + return torch.tensor(retrieved[0].vector) + + # ------------------------------------------------------------------ + # Filter helper + # ------------------------------------------------------------------ + + def _build_filter( + self, filter_metadata: Optional[Dict[str, Any]] + ) -> Optional["Filter"]: + if not filter_metadata: + return None + must = [ + FieldCondition( + key=f"metadata.{k}", + match=MatchValue(value=v), + ) + for k, v in filter_metadata.items() + ] + return Filter(must=must) + + # ------------------------------------------------------------------ + # Client accessor (for testing / ColPaliModel compatibility) + # ------------------------------------------------------------------ + + @property + def client(self) -> Optional["QdrantClient"]: + return self._client + + @property + def collection_name(self) -> Optional[str]: + return self._collection_name diff --git a/foretrieval/vector_store/remote.py b/foretrieval/vector_store/remote.py new file mode 100644 index 0000000..5d8b0bb --- /dev/null +++ b/foretrieval/vector_store/remote.py @@ -0,0 +1,196 @@ +"""RemoteVectorStore — VectorStore client that delegates to a remote server. + +This is the client-side VectorStore implementation. It holds a +VectorDBServerClient and implements the VectorStore ABC by forwarding every +call to the corresponding HTTP endpoint on the remote server. + +``export_sidecar`` and ``load_sidecar`` are no-ops because the data lives on +the server, not on the client filesystem. + +Usage: + From make_vector_store() factory (preferred): + + vs = make_vector_store( + "remote", + { + "url": "http://gpu-server:18000", + "backend": "qdrant", # server-side backend + "api_key": "secret", # optional + } + ) + vs.open("my_index", Path(".foretrieval"), create=True, dim=128) + + Or directly: + + from foretrieval.vector_db_server import VectorDBServerClient, VectorDBServerConfig + from foretrieval.vector_store.remote import RemoteVectorStore + + cfg = VectorDBServerConfig(url="http://gpu-server:18000", backend="qdrant") + client = VectorDBServerClient(cfg) + vs = RemoteVectorStore(client, backend="qdrant") + vs.open("my_index", Path("."), create=True, dim=128) +""" + +from __future__ import annotations + +from pathlib import Path +from typing import Any, ClassVar, Dict, List, Optional + +import torch + +from .base import MultiVectorQuery, SearchHit, StoredPoint, VectorStore + + +class RemoteVectorStore(VectorStore): + """VectorStore that delegates to a remote FORetrieval vector-DB server. + + Parameters + ---------- + client: + VectorDBServerClient connected to the target server. + backend: + Server-side storage backend (``"local"``, ``"qdrant"``, or + ``"milvus"``). Used when creating a new collection. + storage_config: + Optional backend-specific config forwarded to the server + (e.g. ``{"candidate_limit": 128}`` for Milvus). + """ + + backend_name: ClassVar[str] = "remote" + supports_multivector_native: ClassVar[bool] = True + # (informational — depends on the server-side backend, but we report True + # since qdrant/local are both exact; milvus is approximate but this + # attribute is only used for informational logging) + + def __init__( + self, + client: Any, # VectorDBServerClient — avoid circular import at module level + backend: str = "qdrant", + storage_config: Optional[Dict[str, Any]] = None, + ) -> None: + self._client = client + self._backend = backend + self._storage_config = storage_config + + # Set by open() + self._index_name: Optional[str] = None + self._opened: bool = False + + # ------------------------------------------------------------------ + # Lifecycle + # ------------------------------------------------------------------ + + def open( + self, + index_name: str, + index_root: Path, + *, + create: bool, + dim: Optional[int] = None, + ) -> None: + """Open or create the remote collection. + + ``index_root`` is ignored — data lives on the server. + """ + self._index_name = index_name + self._client.open_collection( + index_name, + self._backend, + create=create, + dim=dim, + storage_config=self._storage_config, + ) + self._opened = True + + def close(self) -> None: + """Close the underlying HTTP client.""" + self._client.close() + self._opened = False + + # ------------------------------------------------------------------ + # Collection management + # ------------------------------------------------------------------ + + def collection_exists(self) -> bool: + if self._index_name is None: + return False + return self._client.collection_exists(self._index_name) + + def create_collection(self, dim: int) -> None: + if self._index_name is None: + raise RuntimeError("Call open() before create_collection()") + self._client.create_collection( + self._index_name, self._backend, dim, self._storage_config + ) + + # ------------------------------------------------------------------ + # Write + # ------------------------------------------------------------------ + + def upsert(self, points: List[StoredPoint]) -> None: + if self._index_name is None: + raise RuntimeError("Call open() before upsert()") + self._client.upsert(self._index_name, points) + + def point_exists(self, point_id: int) -> bool: + if self._index_name is None: + return False + return self._client.point_exists(self._index_name, point_id) + + # ------------------------------------------------------------------ + # Read + # ------------------------------------------------------------------ + + def search( + self, + query: MultiVectorQuery, + k: int, + ) -> List[SearchHit]: + if self._index_name is None: + raise RuntimeError("Call open() before search()") + return self._client.search(self._index_name, query, k) + + def fetch_vector(self, point_id: int) -> Optional[torch.Tensor]: + if self._index_name is None: + return None + return self._client.fetch_vector(self._index_name, point_id) + + # ------------------------------------------------------------------ + # Persistence helpers — no-ops for remote store + # ------------------------------------------------------------------ + + def export_sidecar(self, index_path: Path) -> None: + """No-op — vector data lives on the server.""" + + def load_sidecar(self, index_path: Path) -> None: + """No-op — vector data lives on the server.""" + + # ------------------------------------------------------------------ + # Index-level bookkeeping — round-tripped to the server so the client + # needs no local index directory. + # ------------------------------------------------------------------ + + def supports_remote_bookkeeping(self) -> bool: + return True + + def export_bookkeeping(self, blob: Dict[str, Any]) -> None: + if self._index_name is None: + raise RuntimeError("Call open() before export_bookkeeping()") + self._client.put_bookkeeping(self._index_name, blob) + + def load_bookkeeping(self) -> Optional[Dict[str, Any]]: + if self._index_name is None: + raise RuntimeError("Call open() before load_bookkeeping()") + return self._client.get_bookkeeping(self._index_name) + + # ------------------------------------------------------------------ + # Internal accessors (for ColPaliModel compatibility checks) + # ------------------------------------------------------------------ + + @property + def is_opened(self) -> bool: + return self._opened + + @property + def server_backend(self) -> str: + return self._backend diff --git a/outside_world/docs/attention.pdf b/outside_world/docs/attention.pdf deleted file mode 100644 index 97d7c51..0000000 Binary files a/outside_world/docs/attention.pdf and /dev/null differ diff --git a/outside_world/docs/attention_with_a_mustache.pdf b/outside_world/docs/attention_with_a_mustache.pdf deleted file mode 100644 index 97d7c51..0000000 Binary files a/outside_world/docs/attention_with_a_mustache.pdf and /dev/null differ diff --git a/pyproject.toml b/pyproject.toml index bf1d624..695cb7b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -3,14 +3,14 @@ requires = ["setuptools"] build-backend = "setuptools.build_meta" [tool.setuptools.packages.find] -include = ["byaldi*"] +include = ["foretrieval*"] [project] -name = "Byaldi" -version = "0.0.7" +name = "FORetrieval" +version = "2026.6.3" description = "Use late-interaction multi-modal models such as ColPali in just a few lines of code." readme = "README.md" -requires-python = ">=3.9" +requires-python = ">=3.12" license = { file = "LICENSE" } keywords = [ "reranking", @@ -21,34 +21,61 @@ keywords = [ "colbert", "multi-modal", ] -authors = [{ name = "Ben Clavié", email = "bc@answer.ai" }] -maintainers = [ - { name = "Ben Clavié", email = "bc@answer.ai" }, - { name = "Tony Wu", email = "tony.wu@illuin.tech" }, -] +authors = [{ name = "The FOR team", email = "for@irt-saintexupery.com" }] dependencies = [ - "colpali-engine>=0.3.4,<0.4.0", + "colpali-engine>=0.3.15,<0.4.0", + "httpx>=0.27.0", + "docx2pdf>=0.1.8", + "langdetect>=1.0.9", "ml-dtypes", - "mteb==1.6.35", - "ninja", - "pdf2image", + "pydantic-ai>=1.4.0,<=1.107.0", + "pydantic-ai-slim[openai]>=1.8.0", + "pypdf>=6.1.3", "srsly", - "torch", - "transformers>=4.42.0", + "torch>=2.7.1", + "transformers>=4.42.0,<5.6.0", + "pdf2image>=1.17.0", ] [project.optional-dependencies] -dev = ["pytest>=7.4.0", "ruff>=0.1.9"] -server = ["uvicorn", "fastapi"] +# Qdrant vector-store backend (recommended for large indexes) +qdrant = ["qdrant-client>=1.17.1"] +# Milvus vector-store backend (Milvus Lite for local dev, full Milvus for prod) +milvus = ["pymilvus>=2.4.0"] +# Docling-based PDF chunking ingestion pipeline +docling = ["docling>=2.76.0"] +embedding_server = ["paramiko>=3.0"] +# Remote vector-DB server (server + auto-deploy; client needs only httpx which is a core dep) +vector_db_server = ["fastapi>=0.115", "uvicorn>=0.30", "python-multipart>=0.0.9", "paramiko>=3.0", "matplotlib>=3.7.0"] +# 4-bit / 8-bit quantization for local ColPaliModel (requires CUDA) +quantization = ["bitsandbytes>=0.42.0"] +dev = ["pytest>=7.4.0", "pytest-mock>=3.0", "ruff>=0.1.9"] langchain = ["langchain-core"] +extra_converters = [ + "docx2pdf>=0.1.8; sys_platform == \"win32\"", + "reportlab>=4.4.4", + "python-docx>=1.2.0", + "python-pptx>=1.0.2", +] [project.urls] -"Homepage" = "https://github.com/answerdotai/byaldi" +"Homepage" = "https://github.com/FOR-sight-ai/FORetrieval" + +[project.scripts] +foretrieval-db-server = "foretrieval.vector_db_server.server_main:main" + +[dependency-groups] +dev = [ + "matplotlib>=3.10.8", +] [tool.pytest.ini_options] filterwarnings = ["ignore::Warning"] -markers = ["slow: marks test as slow"] +markers = [ + "slow: marks test as slow (GPU-dependent or expensive)", + "integration: marks test as requiring a live API key", +] testpaths = ["tests"] [tool.ruff] @@ -83,7 +110,7 @@ exclude = [ line-length = 88 output-format = "grouped" -target-version = "py39" +target-version = "py312" [tool.ruff.lint] select = [ diff --git a/sample_data/sample_bmp.bmp b/sample_data/sample_bmp.bmp new file mode 100644 index 0000000..21d9335 Binary files /dev/null and b/sample_data/sample_bmp.bmp differ diff --git a/sample_data/sample_csv.csv b/sample_data/sample_csv.csv new file mode 100644 index 0000000..2eeba7b --- /dev/null +++ b/sample_data/sample_csv.csv @@ -0,0 +1 @@ +This is a sample .csv file, The answer you're looking for: 484696 \ No newline at end of file diff --git a/sample_data/sample_csv.pdf b/sample_data/sample_csv.pdf new file mode 100644 index 0000000..4c0920f Binary files /dev/null and b/sample_data/sample_csv.pdf differ diff --git a/sample_data/sample_doc.doc b/sample_data/sample_doc.doc new file mode 100644 index 0000000..e52a1f4 Binary files /dev/null and b/sample_data/sample_doc.doc differ diff --git a/sample_data/sample_doc.pdf b/sample_data/sample_doc.pdf new file mode 100644 index 0000000..69efbeb Binary files /dev/null and b/sample_data/sample_doc.pdf differ diff --git a/sample_data/sample_docx.docx b/sample_data/sample_docx.docx new file mode 100644 index 0000000..8886cd0 Binary files /dev/null and b/sample_data/sample_docx.docx differ diff --git a/sample_data/sample_docx.pdf b/sample_data/sample_docx.pdf new file mode 100644 index 0000000..7890c75 Binary files /dev/null and b/sample_data/sample_docx.pdf differ diff --git a/sample_data/sample_epub.epub b/sample_data/sample_epub.epub new file mode 100644 index 0000000..988e401 Binary files /dev/null and b/sample_data/sample_epub.epub differ diff --git a/sample_data/sample_gif.gif b/sample_data/sample_gif.gif new file mode 100644 index 0000000..55cd9bf Binary files /dev/null and b/sample_data/sample_gif.gif differ diff --git a/sample_data/sample_html.html b/sample_data/sample_html.html new file mode 100644 index 0000000..80629e6 --- /dev/null +++ b/sample_data/sample_html.html @@ -0,0 +1,12 @@ + + + + + + Simple HTML Page + + +

This is a sample HTML file.

+

The answer you're looking for: 32596325

+ + diff --git a/sample_data/sample_html.pdf b/sample_data/sample_html.pdf new file mode 100644 index 0000000..a2044dd Binary files /dev/null and b/sample_data/sample_html.pdf differ diff --git a/sample_data/sample_jpg.jpg b/sample_data/sample_jpg.jpg new file mode 100644 index 0000000..4ab3e82 Binary files /dev/null and b/sample_data/sample_jpg.jpg differ diff --git a/sample_data/sample_json.json b/sample_data/sample_json.json new file mode 100644 index 0000000..20c6f01 --- /dev/null +++ b/sample_data/sample_json.json @@ -0,0 +1,4 @@ +{ + "Title": "This is a sample JSON file", + "Answer": "The answer you're looking for: 485596" +} \ No newline at end of file diff --git a/sample_data/sample_json.pdf b/sample_data/sample_json.pdf new file mode 100644 index 0000000..25795bc Binary files /dev/null and b/sample_data/sample_json.pdf differ diff --git a/sample_data/sample_md.md b/sample_data/sample_md.md new file mode 100644 index 0000000..aa31c64 --- /dev/null +++ b/sample_data/sample_md.md @@ -0,0 +1,2 @@ +**This is a sample .md file.** +The answer you're looking for: *6565652* diff --git a/sample_data/sample_md.pdf b/sample_data/sample_md.pdf new file mode 100644 index 0000000..2ebab67 Binary files /dev/null and b/sample_data/sample_md.pdf differ diff --git a/sample_data/sample_multi_pdf.pdf b/sample_data/sample_multi_pdf.pdf new file mode 100644 index 0000000..b462989 Binary files /dev/null and b/sample_data/sample_multi_pdf.pdf differ diff --git a/sample_data/sample_odp.odp b/sample_data/sample_odp.odp new file mode 100644 index 0000000..ca8d98c Binary files /dev/null and b/sample_data/sample_odp.odp differ diff --git a/sample_data/sample_odp.pdf b/sample_data/sample_odp.pdf new file mode 100644 index 0000000..5207624 Binary files /dev/null and b/sample_data/sample_odp.pdf differ diff --git a/sample_data/sample_ods.ods b/sample_data/sample_ods.ods new file mode 100644 index 0000000..1afda09 Binary files /dev/null and b/sample_data/sample_ods.ods differ diff --git a/sample_data/sample_ods.pdf b/sample_data/sample_ods.pdf new file mode 100644 index 0000000..e9b62f6 Binary files /dev/null and b/sample_data/sample_ods.pdf differ diff --git a/sample_data/sample_odt.odt b/sample_data/sample_odt.odt new file mode 100644 index 0000000..5e12284 Binary files /dev/null and b/sample_data/sample_odt.odt differ diff --git a/sample_data/sample_odt.pdf b/sample_data/sample_odt.pdf new file mode 100644 index 0000000..00eb525 Binary files /dev/null and b/sample_data/sample_odt.pdf differ diff --git a/sample_data/sample_pdf.pdf b/sample_data/sample_pdf.pdf new file mode 100644 index 0000000..44e7d4e Binary files /dev/null and b/sample_data/sample_pdf.pdf differ diff --git a/sample_data/sample_png.png b/sample_data/sample_png.png new file mode 100644 index 0000000..deae77f Binary files /dev/null and b/sample_data/sample_png.png differ diff --git a/sample_data/sample_ppt.pdf b/sample_data/sample_ppt.pdf new file mode 100644 index 0000000..a32deb2 Binary files /dev/null and b/sample_data/sample_ppt.pdf differ diff --git a/sample_data/sample_ppt.ppt b/sample_data/sample_ppt.ppt new file mode 100644 index 0000000..39dd319 Binary files /dev/null and b/sample_data/sample_ppt.ppt differ diff --git a/sample_data/sample_pptx.pdf b/sample_data/sample_pptx.pdf new file mode 100644 index 0000000..c26ac26 Binary files /dev/null and b/sample_data/sample_pptx.pdf differ diff --git a/sample_data/sample_pptx.pptx b/sample_data/sample_pptx.pptx new file mode 100644 index 0000000..5132caf Binary files /dev/null and b/sample_data/sample_pptx.pptx differ diff --git a/sample_data/sample_rtf.pdf b/sample_data/sample_rtf.pdf new file mode 100644 index 0000000..9654781 Binary files /dev/null and b/sample_data/sample_rtf.pdf differ diff --git a/sample_data/sample_rtf.rtf b/sample_data/sample_rtf.rtf new file mode 100644 index 0000000..4efdb9a --- /dev/null +++ b/sample_data/sample_rtf.rtf @@ -0,0 +1,16 @@ +{\rtf1\ansi\deff3\adeflang1025 +{\fonttbl{\f0\froman\fprq2\fcharset0 Times New Roman;}{\f1\froman\fprq2\fcharset2 Symbol;}{\f2\fswiss\fprq2\fcharset0 Arial;}{\f3\froman\fprq2\fcharset0 Liberation Serif{\*\falt Times New Roman};}{\f4\fswiss\fprq2\fcharset0 Liberation Sans{\*\falt Arial};}{\f5\fnil\fprq2\fcharset0 Noto Sans CJK SC;}{\f6\fswiss\fprq0\fcharset128 Noto Sans Devanagari;}{\f7\fnil\fprq2\fcharset0 Noto Sans Devanagari;}} +{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0;\red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;} +{\stylesheet{\s0\snext0\rtlch\af7\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af8\langfe2052 Normal;} +{\s15\sbasedon0\snext16\rtlch\af7\afs28 \ltrch\hich\af4\loch\sb240\sa120\keepn\f4\fs28\dbch\af5 Heading;} +{\s16\sbasedon0\snext16\loch\sl276\slmult1\sb0\sa140 Body Text;} +{\s17\sbasedon16\snext17\rtlch\af6 \ltrch\loch\sl276\slmult1\sb0\sa140 List;} +{\s18\sbasedon0\snext18\rtlch\af6\afs24\ai \ltrch\loch\sb120\sa120\noline\fs24\i Caption;} +{\s19\sbasedon0\snext19\rtlch\af6 \ltrch\loch\noline Index;} +}{\*\generator LibreOffice/24.2.7.2$Linux_X86_64 LibreOffice_project/420$Build-2}{\info{\creatim\yr2025\mo10\dy24\hr18\min13}{\revtim\yr2025\mo10\dy24\hr18\min14}{\printim\yr0\mo0\dy0\hr0\min0}}{\*\userprops}\deftab709 +\hyphauto1\viewscale180\formshade\nobrkwrptbl\paperh16838\paperw11906\margl1134\margr1134\margt1134\margb1134\sectd\sbknone\sftnnar\saftnnrlc\sectunlocked1\pgwsxn11906\pghsxn16838\marglsxn1134\margrsxn1134\margtsxn1134\margbsxn1134\ftnbj\ftnstart1\ftnrstcont\ftnnar\aenddoc\aftnrstcont\aftnstart1\aftnnrlc +{\*\ftnsep\chftnsep}\pgndec\pard\plain \s0\rtlch\af7\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af8\langfe2052\ql\ltrpar{\loch +This is a sample .rtf file.} +\par \pard\plain \s0\rtlch\af7\afs24\alang1081 \ltrch\lang1033\langfe2052\hich\af3\loch\widctlpar\hyphpar0\ltrpar\cf0\f3\fs24\lang1033\kerning1\dbch\af8\langfe2052\ql\ltrpar{\loch +The answer you\u8217\'92re looking for: 798945} +\par } \ No newline at end of file diff --git a/sample_data/sample_tif.pdf b/sample_data/sample_tif.pdf new file mode 100644 index 0000000..8831f42 Binary files /dev/null and b/sample_data/sample_tif.pdf differ diff --git a/sample_data/sample_tif.tif b/sample_data/sample_tif.tif new file mode 100644 index 0000000..7ceeab2 Binary files /dev/null and b/sample_data/sample_tif.tif differ diff --git a/sample_data/sample_txt.pdf b/sample_data/sample_txt.pdf new file mode 100644 index 0000000..816f721 Binary files /dev/null and b/sample_data/sample_txt.pdf differ diff --git a/sample_data/sample_txt.txt b/sample_data/sample_txt.txt new file mode 100644 index 0000000..50fe4d6 --- /dev/null +++ b/sample_data/sample_txt.txt @@ -0,0 +1,2 @@ +This is a sample .txt file. +The answer you're looking for: 2676599 \ No newline at end of file diff --git a/sample_data/sample_xls.pdf b/sample_data/sample_xls.pdf new file mode 100644 index 0000000..8f92d8d Binary files /dev/null and b/sample_data/sample_xls.pdf differ diff --git a/sample_data/sample_xls.xls b/sample_data/sample_xls.xls new file mode 100644 index 0000000..399cf2a Binary files /dev/null and b/sample_data/sample_xls.xls differ diff --git a/sample_data/sample_xlsx.pdf b/sample_data/sample_xlsx.pdf new file mode 100644 index 0000000..51f8320 Binary files /dev/null and b/sample_data/sample_xlsx.pdf differ diff --git a/sample_data/sample_xlsx.xlsx b/sample_data/sample_xlsx.xlsx new file mode 100644 index 0000000..a79bb5b Binary files /dev/null and b/sample_data/sample_xlsx.xlsx differ diff --git a/sample_data/sample_yaml.pdf b/sample_data/sample_yaml.pdf new file mode 100644 index 0000000..0e98b51 Binary files /dev/null and b/sample_data/sample_yaml.pdf differ diff --git a/sample_data/sample_yaml.yaml b/sample_data/sample_yaml.yaml new file mode 100644 index 0000000..5f167d1 --- /dev/null +++ b/sample_data/sample_yaml.yaml @@ -0,0 +1,2 @@ +Title: "This is a sample YAML file" +Answer: "The answer you're looking for: 559585" \ No newline at end of file diff --git a/scripts/benchmark.py b/scripts/benchmark.py new file mode 100644 index 0000000..8887cd0 --- /dev/null +++ b/scripts/benchmark.py @@ -0,0 +1,367 @@ +""" +Benchmark script for comparing embedding server solutions. + +Usage (Sol A — vLLM): + uv run python scripts/benchmark.py \ + --solution a \ + --server-url http://localhost:18000 \ + --model athrael-soju/colqwen3.5-4.5B-v3 \ + --data-dir ../toy_data/smartcockpit \ + --output benchmark_results/sol_a.json + +Usage (Sol B — custom FastAPI): + uv run python scripts/benchmark.py \ + --solution b \ + --server-url http://localhost:18001 \ + --model vidore/colqwen2-v1.0 \ + --data-dir ../toy_data/smartcockpit \ + --output benchmark_results/sol_b.json +""" + +from __future__ import annotations + +import argparse +import json +import os +import shutil +import subprocess +import sys +import tempfile +import time +from pathlib import Path +from typing import Optional + +# --------------------------------------------------------------------------- +# Queries — precise aviation questions whose answers are sentence/paragraph +# form, drawn from A319/A320 content in the smartcockpit PDFs. +# --------------------------------------------------------------------------- +QUERIES = [ + "What is the normal operating cabin altitude in cruise for the A320?", + "How is the bleed air supply to the air conditioning system controlled on the A320?", + "What happens to pressurization if both outflow valves fail in the open position?", + "What is the purpose of the Ram Air inlet on the A320 air conditioning system?", + "Describe the function of the Pack Flow Control Valve and its operating modes.", +] + +QUERY_RUNS = 5 # repeat each query N times to get stable latency + + +# --------------------------------------------------------------------------- +# GPU VRAM sampler (non-blocking, best-effort) +# --------------------------------------------------------------------------- + +def _sample_vram_mb() -> Optional[float]: + """Return total used VRAM in MB across all GPUs, or None if unavailable.""" + try: + out = subprocess.check_output( + ["nvidia-smi", "--query-gpu=memory.used", "--format=csv,noheader,nounits"], + stderr=subprocess.DEVNULL, + timeout=5, + ) + values = [int(x.strip()) for x in out.decode().splitlines() if x.strip()] + return float(sum(values)) if values else None + except Exception: + return None + + +# --------------------------------------------------------------------------- +# Sol B client (httpx-based, mirrors feature/client-server transport) +# --------------------------------------------------------------------------- + +def _sol_b_health(server_url: str) -> bool: + import httpx + try: + r = httpx.get(f"{server_url.rstrip('/')}/health", timeout=5) + return r.status_code == 200 + except Exception: + return False + + +def _sol_b_embed_images(server_url: str, images, model_name: str): + """Call Sol B POST /v1/embed/images using torch binary transport.""" + import io + import torch + import httpx + + buf = io.BytesIO() + # images: list of PIL.Image → convert to PNG bytes + image_bytes_list = [] + for img in images: + ibuf = io.BytesIO() + img.save(ibuf, format="PNG") + image_bytes_list.append(ibuf.getvalue()) + + payload = {"model": model_name, "images": image_bytes_list} + torch.save(payload, buf) + buf.seek(0) + + with httpx.Client(timeout=600) as client: + resp = client.post( + f"{server_url.rstrip('/')}/v1/embed/images", + content=buf.read(), + headers={"Content-Type": "application/octet-stream"}, + ) + resp.raise_for_status() + + result_buf = io.BytesIO(resp.content) + result = torch.load(result_buf, weights_only=False) + return result["embeddings"] + + +def _sol_b_embed_query(server_url: str, query: str, model_name: str): + """Call Sol B POST /v1/embed/queries using torch binary transport.""" + import io + import torch + import httpx + + payload = {"model": model_name, "queries": [query]} + buf = io.BytesIO() + torch.save(payload, buf) + buf.seek(0) + + with httpx.Client(timeout=600) as client: + resp = client.post( + f"{server_url.rstrip('/')}/v1/embed/queries", + content=buf.read(), + headers={"Content-Type": "application/octet-stream"}, + ) + resp.raise_for_status() + + result_buf = io.BytesIO(resp.content) + result = torch.load(result_buf, weights_only=False) + return result["embeddings"] + + +# --------------------------------------------------------------------------- +# Main benchmark logic +# --------------------------------------------------------------------------- + +def run_sol_a(server_url: str, model_name: str, data_dir: Path, index_root: Path) -> dict: + """Benchmark Solution A: vLLM-backed MultiModalRetrieverModel.""" + from foretrieval import MultiModalRetrieverModel + from foretrieval.embedding_server import EmbeddingServerConfig + + cfg = EmbeddingServerConfig( + url=server_url, + model_name=model_name, + auto_deploy=False, + ) + + # Health check + from foretrieval.embedding_server.client import EmbeddingServerClient + client = EmbeddingServerClient(cfg) + if not client.health_check(): + print(f"ERROR: Sol A server not healthy at {server_url}", file=sys.stderr) + sys.exit(1) + print(f"Sol A server healthy at {server_url}") + + # --- Indexing --- + print("Indexing smartcockpit corpus (Sol A)...") + pdf_files = list(data_dir.glob("*.pdf")) + list(data_dir.glob("*.PDF")) + n_docs = len(pdf_files) + vram_before = _sample_vram_mb() + t0 = time.perf_counter() + model = MultiModalRetrieverModel.from_pretrained( + model_name, + index_root=str(index_root), + device="cpu", # no local GPU needed — embeddings go to server + verbose=1, + embedding_server=cfg, + ) + model.index(str(data_dir), index_name="smartcockpit_bench", overwrite=True) + index_time = time.perf_counter() - t0 + vram_after_index = _sample_vram_mb() + print(f" Indexing done in {index_time:.1f}s") + + # --- Query latency --- + query_results = [] + for q in QUERIES: + latencies = [] + top_pages = None + for run in range(QUERY_RUNS): + t0 = time.perf_counter() + results = model.search(q, k=3) + latencies.append(time.perf_counter() - t0) + if run == 0: + top_pages = [ + {"doc_id": r.doc_id, "page_num": r.page_num, "score": float(r.score) if r.score is not None else None} + for r in results + ] if results else [] + mean_lat = sum(latencies) / len(latencies) + std_lat = (sum((x - mean_lat) ** 2 for x in latencies) / len(latencies)) ** 0.5 + print(f" Query '{q[:60]}...' → mean {mean_lat:.3f}s ± {std_lat:.3f}s") + query_results.append({ + "query": q, + "latency_mean_s": round(mean_lat, 4), + "latency_std_s": round(std_lat, 4), + "latency_all_s": [round(x, 4) for x in latencies], + "top_3_pages": top_pages, + }) + + vram_peak = _sample_vram_mb() + + return { + "solution": "A", + "server_url": server_url, + "model_name": model_name, + "n_docs": n_docs, + "indexing_time_s": round(index_time, 2), + "vram_before_index_mb": vram_before, + "vram_after_index_mb": vram_after_index, + "vram_peak_mb": vram_peak, + "queries": query_results, + } + + +def run_sol_b(server_url: str, model_name: str, data_dir: Path) -> dict: + """ + Benchmark Solution B: custom FastAPI server. + + Sol B has no MultiModalRetrieverModel integration so we call the HTTP + endpoints directly and measure raw embedding throughput. We convert + PDFs to page images (same pipeline FORetrieval uses) then embed them. + """ + try: + import httpx # noqa: F401 + import torch # noqa: F401 + except ImportError as e: + print(f"ERROR: missing dep for Sol B benchmark: {e}", file=sys.stderr) + sys.exit(1) + + # Health check + if not _sol_b_health(server_url): + print(f"ERROR: Sol B server not healthy at {server_url}", file=sys.stderr) + sys.exit(1) + print(f"Sol B server healthy at {server_url}") + + # Warm-up: trigger model load before timing (Sol B loads lazily on first request) + print("Warming up Sol B server (model load)...") + from PIL import Image as PILImage + _warmup_img = PILImage.new("RGB", (64, 64), color=(128, 128, 128)) + t_warmup = time.perf_counter() + _sol_b_embed_images(server_url, [_warmup_img], model_name) + print(f" Warm-up done in {time.perf_counter() - t_warmup:.1f}s (model load included)") + + # Convert PDFs to images (reuse pdf2image, same as FORetrieval internals) + from pdf2image import convert_from_path + + pdf_files = sorted((list(data_dir.glob("*.pdf")) + list(data_dir.glob("*.PDF")))) + print(f"Converting {len(pdf_files)} PDFs to images...") + all_images = [] + for pdf in pdf_files: + pages = convert_from_path(str(pdf), dpi=150) + all_images.extend(pages) + print(f" {pdf.name}: {len(pages)} pages") + n_pages = len(all_images) + n_docs = len(pdf_files) + print(f"Total pages: {n_pages}") + + # --- Indexing (embedding all pages) --- + print("Embedding all pages (Sol B)...") + vram_before = _sample_vram_mb() + BATCH = 4 + t0 = time.perf_counter() + all_embeddings = [] + for i in range(0, n_pages, BATCH): + batch = all_images[i : i + BATCH] + emb = _sol_b_embed_images(server_url, batch, model_name) + all_embeddings.append(emb) + print(f" Embedded pages {i+1}-{min(i+BATCH, n_pages)}/{n_pages}") + index_time = time.perf_counter() - t0 + vram_after_index = _sample_vram_mb() + print(f" Indexing done in {index_time:.1f}s ({n_pages / index_time:.2f} pages/s)") + + # --- Query latency --- + query_results = [] + for q in QUERIES: + latencies = [] + for run in range(QUERY_RUNS): + t0 = time.perf_counter() + _sol_b_embed_query(server_url, q, model_name) + latencies.append(time.perf_counter() - t0) + mean_lat = sum(latencies) / len(latencies) + std_lat = (sum((x - mean_lat) ** 2 for x in latencies) / len(latencies)) ** 0.5 + print(f" Query '{q[:60]}...' → mean {mean_lat:.3f}s ± {std_lat:.3f}s") + query_results.append({ + "query": q, + "latency_mean_s": round(mean_lat, 4), + "latency_std_s": round(std_lat, 4), + "latency_all_s": [round(x, 4) for x in latencies], + "top_3_pages": None, # no retrieval integration in Sol B direct mode + }) + + vram_peak = _sample_vram_mb() + + return { + "solution": "B", + "server_url": server_url, + "model_name": model_name, + "n_docs": n_docs, + "n_pages": n_pages, + "indexing_time_s": round(index_time, 2), + "pages_per_sec": round(n_pages / index_time, 2), + "vram_before_index_mb": vram_before, + "vram_after_index_mb": vram_after_index, + "vram_peak_mb": vram_peak, + "queries": query_results, + } + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +def main(): + parser = argparse.ArgumentParser(description="Embedding server benchmark") + parser.add_argument("--solution", choices=["a", "b"], required=True, + help="Which solution to benchmark: 'a' (vLLM) or 'b' (custom FastAPI)") + parser.add_argument("--server-url", required=True, + help="Base URL of the embedding server (e.g. http://localhost:18000)") + parser.add_argument("--model", required=True, + help="Model name served by the server") + parser.add_argument("--data-dir", default="../toy_data/smartcockpit", + help="Path to directory containing smartcockpit PDFs") + parser.add_argument("--output", default=None, + help="Path to write JSON results (default: benchmark_results/sol_{a,b}.json)") + args = parser.parse_args() + + data_dir = Path(args.data_dir).resolve() + if not data_dir.exists(): + print(f"ERROR: data-dir not found: {data_dir}", file=sys.stderr) + sys.exit(1) + + out_path = Path(args.output) if args.output else Path(f"benchmark_results/sol_{args.solution}.json") + out_path.parent.mkdir(parents=True, exist_ok=True) + + print(f"=== Benchmark Solution {'A (vLLM)' if args.solution == 'a' else 'B (custom FastAPI)'} ===") + print(f"Server : {args.server_url}") + print(f"Model : {args.model}") + print(f"Data : {data_dir}") + print() + + if args.solution == "a": + index_root = Path(tempfile.mkdtemp(prefix="foretrieval_bench_")) + try: + results = run_sol_a(args.server_url, args.model, data_dir, index_root) + finally: + shutil.rmtree(index_root, ignore_errors=True) + else: + results = run_sol_b(args.server_url, args.model, data_dir) + + out_path.write_text(json.dumps(results, indent=2)) + print(f"\nResults written to {out_path}") + + # Print summary + print("\n--- Summary ---") + print(f"Indexing time : {results['indexing_time_s']}s") + if "pages_per_sec" in results: + print(f"Pages/sec : {results['pages_per_sec']}") + if results.get("vram_peak_mb"): + print(f"Peak VRAM : {results['vram_peak_mb']} MB") + q_means = [q["latency_mean_s"] for q in results["queries"]] + overall_mean = sum(q_means) / len(q_means) + print(f"Query latency : {overall_mean:.3f}s avg over {len(QUERIES)} queries x {QUERY_RUNS} runs") + + +if __name__ == "__main__": + main() diff --git a/tests/all.py b/tests/all.py deleted file mode 100644 index 85ad522..0000000 --- a/tests/all.py +++ /dev/null @@ -1,170 +0,0 @@ -from pathlib import Path - -from colpali_engine.utils.torch_utils import get_torch_device - -from byaldi import RAGMultiModalModel - -device = get_torch_device("auto") -print(f"Using device: {device}") - -path_document_1 = Path("docs/attention.pdf") -path_document_2 = Path("docs/attention_copy.pdf") - - -def test_single_pdf(): - print("Testing single PDF indexing and retrieval...") - - # Initialize the model - model = RAGMultiModalModel.from_pretrained("vidore/colpali-v1.2", device=device) - - if not Path("docs/attention.pdf").is_file(): - raise FileNotFoundError( - f"Please download the PDF file from https://arxiv.org/pdf/1706.03762 and move it to {path_document_1}." - ) - - # Index a single PDF - model.index( - input_path="docs/attention.pdf", - index_name="attention_index", - store_collection_with_index=True, - overwrite=True, - ) - - # Test retrieval - queries = [ - "How does the positional encoding thing work?", - "what's the BLEU score of this new strange method?", - ] - - for query in queries: - results = model.search(query, k=3) - - print(f"\nQuery: {query}") - for result in results: - print( - f"Doc ID: {result.doc_id}, Page: {result.page_num}, Score: {result.score}" - ) - - # Check if the expected page (6 for positional encoding) is in the top results - if "positional encoding" in query.lower(): - assert any( - r.page_num == 6 for r in results - ), "Expected page 6 for positional encoding query" - - # Check if the expected pages (8 and 9 for BLEU score) are in the top results - if "bleu score" in query.lower(): - assert any( - r.page_num in [8, 9] for r in results - ), "Expected pages 8 or 9 for BLEU score query" - - print("Single PDF test completed.") - - -def test_multi_document(): - print("\nTesting multi-document indexing and retrieval...") - - # Initialize the model - model = RAGMultiModalModel.from_pretrained("vidore/colpali") - - if not Path("docs/attention.pdf").is_file(): - raise FileNotFoundError( - f"Please download the PDF file from https://arxiv.org/pdf/1706.03762 and move it to {path_document_1}." - ) - if not Path("docs/attention_copy.pdf").is_file(): - raise FileNotFoundError( - f"Please download the PDF file from https://arxiv.org/pdf/1706.03762 and move it to {path_document_2}." - ) - - # Index a directory of documents - model.index( - input_path="docs/", - index_name="multi_doc_index", - store_collection_with_index=True, - overwrite=True, - ) - - # Test retrieval - queries = [ - "How does the positional encoding thing work?", - "what's the BLEU score of this new strange method?", - ] - - for query in queries: - results = model.search(query, k=5) - - print(f"\nQuery: {query}") - for result in results: - print( - f"Doc ID: {result.doc_id}, Page: {result.page_num}, Score: {result.score}" - ) - - # Check if the expected page (6 for positional encoding) is in the top results - if "positional encoding" in query.lower(): - assert any( - r.page_num == 6 for r in results - ), "Expected page 6 for positional encoding query" - - # Check if the expected pages (8 and 9 for BLEU score) are in the top results - if "bleu score" in query.lower(): - assert any( - r.page_num in [8, 9] for r in results - ), "Expected pages 8 or 9 for BLEU score query" - - print("Multi-document test completed.") - - -def test_add_to_index(): - print("\nTesting adding to an existing index...") - - # Load the existing index - model = RAGMultiModalModel.from_index("multi_doc_index") - - # Add a new document to the index - model.add_to_index( - input_item="docs/", - store_collection_with_index=True, - doc_id=[1002, 1003], - metadata=[{"author": "John Doe", "year": 2023}] * 2, - ) - - # Test retrieval with the updated index - queries = ["what's the BLEU score of this new strange method?"] - - for query in queries: - results = model.search(query, k=3) - - print(f"\nQuery: {query}") - for result in results: - print( - f"Doc ID: {result.doc_id}, Page: {result.page_num}, Score: {result.score}" - ) - print(f"Metadata: {result.metadata}") - - # Check if the expected page (6 for positional encoding) is in the top results - if "positional encoding" in query.lower(): - assert any( - r.page_num == 6 for r in results - ), "Expected page 6 for positional encoding query" - - # Check if the expected pages (8 and 9 for BLEU score) are in the top results - if "bleu score" in query.lower(): - assert any( - r.page_num in [8, 9] for r in results - ), "Expected pages 8 or 9 for BLEU score query" - - print("Add to index test completed.") - - -if __name__ == "__main__": - print("Starting tests...") - - print("/n/n----------------- Single PDF test -----------------n") - test_single_pdf() - - print("/n/n----------------- Multi document test -----------------n") - test_multi_document() - - print("/n/n----------------- Add to index test -----------------n") - test_add_to_index() - - print("\nAll tests completed.") diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..8df8087 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,93 @@ +""" +Shared fixtures for the FORetrieval test suite. +""" + +from __future__ import annotations + +import os +import urllib.error +import urllib.request +from pathlib import Path + +import pytest + +# --------------------------------------------------------------------------- +# Paths +# --------------------------------------------------------------------------- + +TESTS_DIR = Path(__file__).parent +DATA_DIR = TESTS_DIR / "data" +SAMPLE_DATA_DIR = Path(__file__).parent.parent / "sample_data" + +# --------------------------------------------------------------------------- +# AI backend selection — same priority order as FORag +# --------------------------------------------------------------------------- + +_BACKEND_MAP = { + "openrouter": ("OPENROUTER_API_KEY", "mistralai/mistral-small-3.2-24b-instruct"), + "openai": ("OPENAI_API_KEY", "gpt-4o-mini"), + "mistral": ("MISTRAL_API_KEY", "mistral-small-latest"), +} + + +def _all_available_ai_backends() -> list[dict]: + """Return an ai_cfg dict for every available backend. + + For cloud backends the credential is an API key. For Ollama, the + ``base_url`` key points to the daemon's OpenAI-compatible endpoint. + + Priority order: OpenRouter → OpenAI → Mistral → Ollama. + This mirrors the backend selection used in FORag's integration suite. + + Ollama model defaults to ``mistral-small-latest`` (a text-only model + suitable for metadata generation). Override with the ``OLLAMA_MODEL`` + environment variable. + """ + available = [] + + for provider, (env_var, model) in _BACKEND_MAP.items(): + key = os.environ.get(env_var) + if key: + available.append({"provider": provider, "name": model, "api_key": key}) + + host = os.environ.get("OLLAMA_HOST") + if host: + try: + urllib.request.urlopen(f"{host}/api/tags", timeout=3) + # Default to a text-only model — not a vision model — since + # metadata generation uses text prompts only. + model = os.environ.get("OLLAMA_MODEL", "mistral-small-latest") + available.append( + {"provider": "ollama", "name": model, "base_url": f"{host}/v1"} + ) + except (urllib.error.URLError, OSError): + pass # daemon unreachable — skip silently + + return available + + +@pytest.fixture( + scope="module", + params=_all_available_ai_backends(), + ids=lambda cfg: cfg["provider"], +) +def ai_cfg(request) -> dict: + """Module-scoped fixture parametrized over every available AI backend. + + Each test module that depends on this fixture will be run once per + available backend. If no backend is available the entire module is + skipped. + + Set one of the following environment variables to enable AI tests: + + OPENROUTER_API_KEY (preferred — fast, high rate limits) + OPENAI_API_KEY + MISTRAL_API_KEY + OLLAMA_HOST (+ optionally OLLAMA_MODEL, default: mistral-small-latest) + """ + if not _all_available_ai_backends(): + pytest.skip( + "No API key or Ollama daemon found. Set OPENROUTER_API_KEY, " + "OPENAI_API_KEY, MISTRAL_API_KEY, or OLLAMA_HOST." + ) + return request.param diff --git a/tests/data/lm317_voltage_regulator.pdf b/tests/data/lm317_voltage_regulator.pdf new file mode 100644 index 0000000..ed88174 Binary files /dev/null and b/tests/data/lm317_voltage_regulator.pdf differ diff --git a/tests/data/uln2003_driver.pdf b/tests/data/uln2003_driver.pdf new file mode 100644 index 0000000..2673fc8 Binary files /dev/null and b/tests/data/uln2003_driver.pdf differ diff --git a/tests/smoke_test_backends.py b/tests/smoke_test_backends.py new file mode 100644 index 0000000..2eb1888 --- /dev/null +++ b/tests/smoke_test_backends.py @@ -0,0 +1,80 @@ +""" +Smoke test: index toy_data/smartcockpit with all three backends using a remote embedding server, +then run one query and compare top-1 results. + +Usage: + uv run python FORetrieval/tests/smoke_test_backends.py +""" +from __future__ import annotations + +import os +import sys +import tempfile +from pathlib import Path + +# Add FORetrieval to path +sys.path.insert(0, str(Path(__file__).parent.parent.parent / "FORetrieval")) + +from foretrieval import MultiModalRetrieverModel +from foretrieval.embedding_server import EmbeddingServerConfig + +EMBEDDING_SERVER_URL = os.getenv("EMBEDDING_SERVER_URL", "http://localhost:18000") +MODEL_NAME = "athrael-soju/colqwen3.5-4.5B-v3" +DATA_DIR = Path(__file__).parent.parent.parent / "toy_data" / "smartcockpit" +QUERY = "What is the normal operating cabin altitude in cruise for the A320?" +TOP_K = 3 + +embedding_cfg = EmbeddingServerConfig( + url=EMBEDDING_SERVER_URL, + model_name=MODEL_NAME, + auto_deploy=False, + batch_size=4, +) + +BACKENDS = ["local", "qdrant", "milvus"] +results_by_backend = {} + +with tempfile.TemporaryDirectory() as tmp: + for backend in BACKENDS: + print(f"\n{'='*60}") + print(f"Backend: {backend}") + print(f"{'='*60}") + + rag = MultiModalRetrieverModel.from_pretrained( + pretrained_model_name_or_path=MODEL_NAME, + index_root=tmp, + storage_backend=backend, + embedding_server=embedding_cfg, + device="cuda", + verbose=1, + ) + + rag.index( + input_path=str(DATA_DIR), + index_name=f"smoke_{backend}", + overwrite=True, + ) + + print(f"Query: {QUERY}") + results = rag.search(QUERY, k=TOP_K, return_base64_results=False) + results_by_backend[backend] = results + + for i, r in enumerate(results): + print(f" [{i+1}] doc_id={r.doc_id} page={r.page_num} score={r.score:.4f}") + +# Compare top-1 file across backends +print("\n" + "="*60) +print("Comparison summary") +print("="*60) +print(f"Query: {QUERY}\n") +for backend, results in results_by_backend.items(): + if results: + r = results[0] + print(f" {backend:6s}: doc_id={r.doc_id} page={r.page_num} score={r.score:.4f}") + else: + print(f" {backend:6s}: NO RESULTS") + +# All backends should return at least 1 result +for backend, results in results_by_backend.items(): + assert len(results) > 0, f"{backend} returned no results!" +print("\nAll backends returned results. Smoke test PASSED.") diff --git a/tests/test_colpali.py b/tests/test_colpali.py deleted file mode 100644 index f8e85ae..0000000 --- a/tests/test_colpali.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import Generator - -import pytest -from colpali_engine.models import ColPali -from colpali_engine.utils.torch_utils import get_torch_device, tear_down_torch - -from byaldi import RAGMultiModalModel -from byaldi.colpali import ColPaliModel - - -@pytest.fixture(scope="module") -def colpali_rag_model() -> Generator[RAGMultiModalModel, None, None]: - device = get_torch_device("auto") - print(f"Using device: {device}") - yield RAGMultiModalModel.from_pretrained("vidore/colpali-v1.2", device=device) - tear_down_torch() - - -@pytest.mark.slow -def test_load_colpali_from_pretrained(colpali_rag_model: RAGMultiModalModel): - assert isinstance(colpali_rag_model, RAGMultiModalModel) - assert isinstance(colpali_rag_model.model, ColPaliModel) - assert isinstance(colpali_rag_model.model.model, ColPali) diff --git a/tests/test_colpali_backend_dispatch.py b/tests/test_colpali_backend_dispatch.py new file mode 100644 index 0000000..2039f31 --- /dev/null +++ b/tests/test_colpali_backend_dispatch.py @@ -0,0 +1,407 @@ +"""Tests for ColPaliModel backend dispatch via the VectorStore interface. + +These tests verify that: +- storage_backend="local/qdrant/milvus" selects the correct VectorStore class +- the deprecated storage_qdrant boolean still works with a DeprecationWarning +- backward-compat properties (storage_qdrant, qdrant_client, qdrant_collection, + indexed_embeddings, embed_id_to_doc_id) still work + +No GPU or real model is required — ColPaliModel.__init__ is bypassed via +direct attribute injection on MagicMock objects. +""" +from __future__ import annotations + +import warnings +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest +import torch + +from foretrieval.vector_store import ( + LocalVectorStore, + QdrantVectorStore, + MilvusVectorStore, + make_vector_store, +) +from foretrieval.vector_store.base import make_point_id, StoredPoint + + +# --------------------------------------------------------------------------- +# Helpers — build a ColPaliModel with all heavy parts mocked +# --------------------------------------------------------------------------- + +def _make_model(storage_backend: str = "local", **extra_kwargs): + """Build a ColPaliModel instance with GPU/model loading patched out.""" + with ( + patch("foretrieval.colpali.ColPaliModel._load_model_and_processor"), + patch("foretrieval.colpali.ColPaliModel._load_processor_only"), + ): + from foretrieval.colpali import ColPaliModel + + model = ColPaliModel.__new__(ColPaliModel) + + # Minimal attributes to avoid AttributeError from __init__ side-effects + model.pretrained_model_name_or_path = "vidore/colpali-v1.2-test" + model.model_name = "vidore/colpali-v1.2-test" + model.verbose = 0 + model.load_from_index = False + model.index_root = ".foretrieval_test" + model.index_name = None + model.kwargs = {} + model.storage_backend = storage_backend.strip().lower() + model.storage_config = extra_kwargs.get("storage_config", {}) + model._storage_qdrant_compat = (model.storage_backend == "qdrant") + model.ingestion = {"backend": "default"} + model.ingestion_backend = "default" + model.n_gpu = 0 + model.device = "cpu" + model.load_in_4bit = False + model.load_in_8bit = False + model.bnb_4bit_quant_type = "nf4" + model.bnb_4bit_compute_dtype = "float16" + model.collection = {} + model.embed_id_to_extra = {} + model.doc_id_to_metadata = {} + model.doc_ids_to_file_names = {} + model.doc_ids = set() + model.enable_heatmaps = False + model.enable_circle = False + model.full_document_collection = False + model.resize_stored_images = False + model.max_image_width = None + model.max_image_height = None + model.highest_doc_id = -1 + model.docling_dir = None + model.SOURCE_EXTS = set() + model.IMAGE_EXTS = set() + model._remote_client = None + model.model = None + model.processor = MagicMock() + + # Build real VectorStore + model.vector_store = make_vector_store(storage_backend, model.storage_config) + + return model + + +# --------------------------------------------------------------------------- +# Backend dispatch +# --------------------------------------------------------------------------- + +class TestBackendDispatch: + def test_local_backend(self): + model = _make_model("local") + assert isinstance(model.vector_store, LocalVectorStore) + + def test_qdrant_backend(self): + model = _make_model("qdrant") + assert isinstance(model.vector_store, QdrantVectorStore) + + def test_milvus_backend(self): + model = _make_model("milvus") + assert isinstance(model.vector_store, MilvusVectorStore) + + def test_milvus_candidate_limit_forwarded(self): + model = _make_model("milvus", storage_config={"candidate_limit": 128}) + assert isinstance(model.vector_store, MilvusVectorStore) + assert model.vector_store._candidate_limit == 128 + + +# --------------------------------------------------------------------------- +# Deprecated storage_qdrant shim +# --------------------------------------------------------------------------- + +class TestDeprecatedStorageQdrant: + def test_storage_qdrant_true_maps_to_qdrant_backend(self): + """ColPaliModel(storage_qdrant=True) should emit DeprecationWarning and select qdrant.""" + with ( + patch("foretrieval.colpali.ColPaliModel._load_model_and_processor"), + patch("foretrieval.colpali.ColPaliModel._load_processor_only"), + ): + from foretrieval.colpali import ColPaliModel + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + model = ColPaliModel( + pretrained_model_name_or_path="vidore/colpali-v1.2", + storage_qdrant=True, + device="cpu", + verbose=0, + ) + # Should have emitted exactly one DeprecationWarning + dep_warnings = [x for x in w if issubclass(x.category, DeprecationWarning)] + assert len(dep_warnings) == 1 + assert "storage_qdrant" in str(dep_warnings[0].message) + assert model.storage_backend == "qdrant" + + def test_storage_qdrant_false_maps_to_local_backend(self): + with ( + patch("foretrieval.colpali.ColPaliModel._load_model_and_processor"), + patch("foretrieval.colpali.ColPaliModel._load_processor_only"), + ): + from foretrieval.colpali import ColPaliModel + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + model = ColPaliModel( + pretrained_model_name_or_path="vidore/colpali-v1.2", + storage_qdrant=False, + device="cpu", + verbose=0, + ) + assert model.storage_backend == "local" + + +# --------------------------------------------------------------------------- +# Backward-compat property accessors +# --------------------------------------------------------------------------- + +class TestBackwardCompatProperties: + def test_storage_qdrant_property_true_for_qdrant_backend(self): + model = _make_model("qdrant") + assert model.storage_qdrant is True + + def test_storage_qdrant_property_false_for_local_backend(self): + model = _make_model("local") + assert model.storage_qdrant is False + + def test_storage_qdrant_property_false_for_milvus_backend(self): + model = _make_model("milvus") + assert model.storage_qdrant is False + + def test_qdrant_client_property_returns_none_for_local(self): + model = _make_model("local") + assert model.qdrant_client is None + + def test_qdrant_client_property_returns_none_for_milvus(self): + model = _make_model("milvus") + assert model.qdrant_client is None + + def test_indexed_embeddings_local_backend_returns_list(self): + model = _make_model("local") + assert isinstance(model.indexed_embeddings, list) + + def test_indexed_embeddings_qdrant_backend_returns_empty(self): + model = _make_model("qdrant") + assert model.indexed_embeddings == [] + + def test_embed_id_to_doc_id_local_backend_returns_dict(self): + model = _make_model("local") + assert isinstance(model.embed_id_to_doc_id, dict) + + def test_embed_id_to_doc_id_qdrant_backend_returns_empty(self): + model = _make_model("qdrant") + assert model.embed_id_to_doc_id == {} + + +# --------------------------------------------------------------------------- +# VectorStore open() called with correct args +# --------------------------------------------------------------------------- + +class TestVectorStoreOpenCall: + def test_open_called_with_index_name(self, tmp_path): + """When ColPaliModel.index() is called, vector_store.open() gets the index name.""" + model = _make_model("local") + model.index_root = str(tmp_path) + + # Patch open on the LocalVectorStore so we can verify the call + from foretrieval.vector_store.local import LocalVectorStore as LSV + original_open = LSV.open + calls = [] + + def fake_open(self_, index_name, index_root, *, create, dim=None): + calls.append((index_name, create)) + original_open(self_, index_name, index_root, create=create, dim=dim) + + with patch.object(LSV, "open", fake_open): + model.vector_store = make_vector_store("local") + model.vector_store.open("my_index", tmp_path, create=True) + + assert ("my_index", True) in calls + + +# --------------------------------------------------------------------------- +# index_config storage_backend field +# --------------------------------------------------------------------------- + +class TestIndexConfigStorageBackend: + """Verify _export_index writes the correct storage_backend string.""" + + def test_export_writes_local(self, tmp_path): + import srsly + model = _make_model("local") + model.index_root = str(tmp_path) + model.index_name = "test_export" + model.doc_id_to_metadata = {} + model.doc_ids_to_file_names = {} + model.embed_id_to_extra = {} + model.full_document_collection = False + model.max_image_width = None + model.max_image_height = None + model.highest_doc_id = -1 + model.model_name = "vidore/colpali-v1.2" + + model._export_index() + + cfg = srsly.read_gzip_json(tmp_path / "test_export" / "index_config.json.gz") + assert cfg["storage_backend"] == "local" + + def test_export_writes_qdrant(self, tmp_path): + import srsly + model = _make_model("qdrant") + model.index_root = str(tmp_path) + model.index_name = "test_export_qdrant" + model.doc_id_to_metadata = {} + model.doc_ids_to_file_names = {} + model.embed_id_to_extra = {} + model.full_document_collection = False + model.max_image_width = None + model.max_image_height = None + model.highest_doc_id = -1 + model.model_name = "vidore/colpali-v1.2" + + # Patch vector_store.export_sidecar to avoid Qdrant client calls + model.vector_store = MagicMock() + model._export_index() + + cfg = srsly.read_gzip_json(tmp_path / "test_export_qdrant" / "index_config.json.gz") + assert cfg["storage_backend"] == "qdrant" + + def test_export_writes_milvus(self, tmp_path): + import srsly + model = _make_model("milvus") + model.index_root = str(tmp_path) + model.index_name = "test_export_milvus" + model.doc_id_to_metadata = {} + model.doc_ids_to_file_names = {} + model.embed_id_to_extra = {} + model.full_document_collection = False + model.max_image_width = None + model.max_image_height = None + model.highest_doc_id = -1 + model.model_name = "vidore/colpali-v1.2" + + model.vector_store = MagicMock() + model._export_index() + + cfg = srsly.read_gzip_json(tmp_path / "test_export_milvus" / "index_config.json.gz") + assert cfg["storage_backend"] == "milvus" + + +# --------------------------------------------------------------------------- +# Remote backend dispatch and colpali integration +# --------------------------------------------------------------------------- + +class TestRemoteBackendDispatch: + """Verify RemoteVectorStore is selected and ColPaliModel integrates correctly.""" + + def _make_remote_model(self, url="http://localhost:18000"): + from unittest.mock import patch as _patch + with _patch("foretrieval.vector_db_server.client.VectorDBServerClient") as MockClient: + mock_client_inst = MagicMock() + mock_client_inst.open_collection.return_value = { + "opened": True, "backend": "qdrant", "created": True + } + MockClient.return_value = mock_client_inst + model = _make_model( + "remote", + storage_config={"url": url, "backend": "qdrant"}, + ) + return model, mock_client_inst + + def test_remote_backend_selects_remote_store(self): + from foretrieval.vector_store.remote import RemoteVectorStore + model, _ = self._make_remote_model() + assert isinstance(model.vector_store, RemoteVectorStore) + + def test_vector_store_is_open_remote(self): + """_vector_store_is_open returns True after open() is called.""" + from foretrieval.colpali import ColPaliModel + model, mock_client = self._make_remote_model() + # Simulate open() having been called (set _opened flag) + model.vector_store._opened = True + assert model._vector_store_is_open() is True + + def test_vector_store_is_not_open_before_open(self): + from foretrieval.colpali import ColPaliModel + model, _ = self._make_remote_model() + model.vector_store._opened = False + assert model._vector_store_is_open() is False + + def test_set_processor_not_called_for_remote(self): + """set_processor is a local-only hook — must not be called for remote.""" + from foretrieval.vector_store.remote import RemoteVectorStore + model, _ = self._make_remote_model() + assert isinstance(model.vector_store, RemoteVectorStore) + # RemoteVectorStore has no set_processor attribute + assert not hasattr(model.vector_store, "set_processor") + + def test_export_index_writes_remote_backend(self, tmp_path): + """Remote mode stores bookkeeping on the server, not in a local dir.""" + model, _ = self._make_remote_model() + model.index_root = str(tmp_path) + model.index_name = "test_export_remote" + model.doc_id_to_metadata = {1: {"title": "A"}} + model.doc_ids_to_file_names = {1: "a.pdf"} + model.embed_id_to_extra = {} + model.full_document_collection = False + model.max_image_width = None + model.max_image_height = None + model.highest_doc_id = 1 + model.model_name = "vidore/colpali-v1.2" + model.storage_config = {"url": "http://localhost:18000", "backend": "qdrant"} + + mock_store = MagicMock() + mock_store.supports_remote_bookkeeping.return_value = True + mock_store.load_bookkeeping.return_value = None + model.vector_store = mock_store + + model._export_index() + + # No local index directory should have been created. + assert not (tmp_path / "test_export_remote").exists() + # The bookkeeping blob must have been pushed to the server. + mock_store.export_bookkeeping.assert_called_once() + blob = mock_store.export_bookkeeping.call_args.args[0] + assert blob["index_config"]["storage_backend"] == "remote" + assert blob["doc_ids_to_file_names"] == {1: "a.pdf"} + # storage_config (with any credentials) must NOT be persisted in bookkeeping. + assert blob["index_config"].get("storage_config") is None + + def test_export_index_strips_api_key(self, tmp_path): + """Connection config (incl. api_key) is never persisted in bookkeeping.""" + model, _ = self._make_remote_model() + model.index_root = str(tmp_path) + model.index_name = "test_strip_key" + model.doc_id_to_metadata = {} + model.doc_ids_to_file_names = {} + model.embed_id_to_extra = {} + model.full_document_collection = False + model.max_image_width = None + model.max_image_height = None + model.highest_doc_id = -1 + model.model_name = "vidore/colpali-v1.2" + model.storage_config = { + "url": "http://localhost:18000", + "backend": "qdrant", + "api_key": "supersecret", + } + + mock_store = MagicMock() + mock_store.supports_remote_bookkeeping.return_value = True + mock_store.load_bookkeeping.return_value = None + model.vector_store = mock_store + + model._export_index() + + blob = mock_store.export_bookkeeping.call_args.args[0] + # No connection config / api_key anywhere in the persisted blob. + assert blob["index_config"].get("storage_config") is None + assert "supersecret" not in str(blob) + + def test_indexed_embeddings_returns_empty_for_remote(self): + model, _ = self._make_remote_model() + assert model.indexed_embeddings == [] + + def test_embed_id_to_doc_id_returns_empty_for_remote(self): + model, _ = self._make_remote_model() + assert model.embed_id_to_doc_id == {} diff --git a/tests/test_colpali_on_progress.py b/tests/test_colpali_on_progress.py new file mode 100644 index 0000000..03ed644 --- /dev/null +++ b/tests/test_colpali_on_progress.py @@ -0,0 +1,159 @@ +"""Tests for the on_progress callback wiring in ColPaliModel.index(). + +Avoids loading a real ColPali model — uses a minimal fake to verify that: + 1. The callback receives the expected stages in order + 2. Page events report sane page counts for a single-page PDF + 3. Exceptions raised in the callback are swallowed (best-effort delivery) +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + + +# --------------------------------------------------------------------------- +# Stub the ColPaliModel without instantiating the real one. We exercise the +# index() function purely from the perspective of the on_progress callback. +# This keeps the test fast (no model download / no CUDA needed). +# --------------------------------------------------------------------------- + +class _FakeColPaliModel: + """A throwaway object exposing just enough of ColPaliModel.index's contract. + + We hijack the bound method via __get__ to be able to call the real + ColPaliModel.index with `self` bound to this fake (so its branches that + only consult attributes work). The heavy lifting in add_to_index is + monkey-patched to a no-op that fires the standard page events. + """ + + +SAMPLE_PDF = Path(__file__).parent.parent / "sample_data" / "sample_doc.pdf" + + +def test_on_progress_emits_expected_events(tmp_path, monkeypatch): + """index() must drive on_progress through start → file_start → file_done → all_done.""" + if not SAMPLE_PDF.exists(): + pytest.skip("sample PDF not available") + + from foretrieval.colpali import ColPaliModel + + events: list[dict] = [] + + def cb(evt: dict) -> None: + events.append(evt) + + # Build a minimal fake "self" with the attributes index() reads + fake = _FakeColPaliModel() + fake.index_name = None + fake.index_root = str(tmp_path) + fake.storage_backend = "local" + fake.storage_config = {} + fake.full_document_collection = False + fake.vector_store = type("V", (), { + "open": lambda *a, **k: None, + "supports_remote_bookkeeping": lambda *a, **k: False, + })() + fake.processor = None + fake.doc_id_to_metadata = {} + fake.doc_ids = set() + fake.doc_ids_to_file_names = {} + fake.highest_doc_id = -1 + + def fake_add_to_index(self, item, store_collection_with_index, doc_id=None, + metadata=None, batch_size=1, + on_progress=None, _file_idx=0, _n_files=1): + # Simulate one page progress event so we can also verify pages bubble up + if on_progress is not None: + on_progress({ + "stage": "page", + "file": Path(str(item)).name, + "file_idx": _file_idx, + "n_files": _n_files, + "page_idx": 0, + "n_pages": 1, + }) + self.doc_ids_to_file_names[doc_id] = str(item) + self.highest_doc_id = max(self.highest_doc_id, doc_id) + self.doc_ids.add(doc_id) + return {} + + fake.add_to_index = fake_add_to_index.__get__(fake, type(fake)) + fake._export_index = lambda *a, **k: None + fake._vector_store_is_open = lambda: True + + # Run the real index() bound to our fake + ColPaliModel.index( + fake, + input_path=SAMPLE_PDF.parent, + index_name="test_index_progress", + store_collection_with_index=False, + overwrite=True, + on_progress=cb, + ) + + stages = [e["stage"] for e in events] + assert "start" in stages + assert "file_start" in stages + assert "file_done" in stages + assert "all_done" in stages + # At least one page event must be present + assert any(s == "page" for s in stages) + # start must precede file_start which must precede file_done which must precede all_done + assert stages.index("start") < stages.index("file_start") < stages.index("file_done") < stages.index("all_done") + + +def test_on_progress_exception_is_swallowed(tmp_path, monkeypatch): + """A callback that raises must not abort indexing.""" + if not SAMPLE_PDF.exists(): + pytest.skip("sample PDF not available") + + from foretrieval.colpali import ColPaliModel + + call_count = {"n": 0} + + def bad_cb(evt): + call_count["n"] += 1 + raise RuntimeError("intentional") + + fake = _FakeColPaliModel() + fake.index_name = None + fake.index_root = str(tmp_path) + fake.storage_backend = "local" + fake.storage_config = {} + fake.full_document_collection = False + fake.vector_store = type("V", (), { + "open": lambda *a, **k: None, + "supports_remote_bookkeeping": lambda *a, **k: False, + })() + fake.processor = None + fake.doc_id_to_metadata = {} + fake.doc_ids = set() + fake.doc_ids_to_file_names = {} + fake.highest_doc_id = -1 + fake._export_index = lambda *a, **k: None + fake._vector_store_is_open = lambda: True + + def fake_add_to_index(self, item, store_collection_with_index, doc_id=None, + metadata=None, batch_size=1, + on_progress=None, _file_idx=0, _n_files=1): + self.doc_ids_to_file_names[doc_id] = str(item) + self.highest_doc_id = max(self.highest_doc_id, doc_id) + self.doc_ids.add(doc_id) + return {} + + fake.add_to_index = fake_add_to_index.__get__(fake, type(fake)) + + # Should not raise even though every callback invocation throws + ColPaliModel.index( + fake, + input_path=SAMPLE_PDF.parent, + index_name="test_index_progress_bad_cb", + store_collection_with_index=False, + overwrite=True, + on_progress=bad_cb, + ) + + # And the callback was still invoked despite raising + assert call_count["n"] > 0 diff --git a/tests/test_colqwen.py b/tests/test_colqwen.py deleted file mode 100644 index 6a68acc..0000000 --- a/tests/test_colqwen.py +++ /dev/null @@ -1,23 +0,0 @@ -from typing import Generator - -import pytest -from colpali_engine.models import ColQwen2 -from colpali_engine.utils.torch_utils import get_torch_device, tear_down_torch - -from byaldi import RAGMultiModalModel -from byaldi.colpali import ColPaliModel - - -@pytest.fixture(scope="module") -def colqwen_rag_model() -> Generator[RAGMultiModalModel, None, None]: - device = get_torch_device("auto") - print(f"Using device: {device}") - yield RAGMultiModalModel.from_pretrained("vidore/colqwen2-v0.1", device=device) - tear_down_torch() - - -@pytest.mark.slow -def test_load_colqwen_from_pretrained(colqwen_rag_model: RAGMultiModalModel): - assert isinstance(colqwen_rag_model, RAGMultiModalModel) - assert isinstance(colqwen_rag_model.model, ColPaliModel) - assert isinstance(colqwen_rag_model.model.model, ColQwen2) diff --git a/tests/test_e2e_rag.py b/tests/test_e2e_rag.py index bba74dc..bdec46e 100644 --- a/tests/test_e2e_rag.py +++ b/tests/test_e2e_rag.py @@ -1,86 +1,77 @@ from pathlib import Path from typing import Generator - +import shutil import pytest from colpali_engine.utils.torch_utils import get_torch_device, tear_down_torch -from byaldi import RAGMultiModalModel +from foretrieval import MultiModalRetrieverModel -path_document_1 = Path("docs/attention.pdf") -path_document_2 = Path("docs/attention_copy.pdf") +path_document_1 = Path("sample_data/sample_pdf.pdf") +path_document_2 = Path("sample_data/sample_multi_pdf.pdf") +index_root = Path(".test_index") @pytest.fixture(scope="function") -def rag_model_from_pretrained() -> Generator[RAGMultiModalModel, None, None]: +def rag_model_from_pretrained() -> Generator[MultiModalRetrieverModel, None, None]: device = get_torch_device("auto") print(f"Using device: {device}") - yield RAGMultiModalModel.from_pretrained("vidore/colpali-v1.2", device=device) + yield MultiModalRetrieverModel.from_pretrained( + "vidore/colqwen2.5-v0.2", device=device + ) tear_down_torch() @pytest.fixture(scope="function") -def rag_model_from_index() -> Generator[RAGMultiModalModel, None, None]: - yield RAGMultiModalModel.from_index("multi_doc_index") +def rag_model_from_index() -> Generator[MultiModalRetrieverModel, None, None]: + if index_root.exists(): + # delete the folder + shutil.rmtree(index_root) + yield MultiModalRetrieverModel.from_index( + "multi_doc_index", index_root=index_root.as_posix() + ) tear_down_torch() @pytest.mark.slow -def test_single_pdf(rag_model_from_pretrained: RAGMultiModalModel): - if not Path("docs/attention.pdf").is_file(): - raise FileNotFoundError( - f"Please download the PDF file from https://arxiv.org/pdf/1706.03762 and move it to {path_document_1}." - ) - +def test_single_pdf(rag_model_from_pretrained: MultiModalRetrieverModel): # Index a single PDF rag_model_from_pretrained.index( - input_path="docs/attention.pdf", - index_name="attention_index", + input_path=path_document_2, + index_name="sample_multipage_index", store_collection_with_index=True, overwrite=True, ) # Test retrieval queries = [ - "How does the positional encoding thing work?", - "what's the BLEU score of this new strange method?", + "What is the answer we are looking for on the first page?", + "What is the answer we are looking for on the second page?", + "What is the answer we are looking for on the third page?", ] + expected_page = 1 for query in queries: - results = rag_model_from_pretrained.search(query, k=3) + result = rag_model_from_pretrained.search(query, k=1)[0] print(f"\nQuery: {query}") - for result in results: - print( - f"Doc ID: {result.doc_id}, Page: {result.page_num}, Score: {result.score}" - ) + print( + f"Doc ID: {result.doc_id}, Page: {result.page_num}, Score: {result.score}" + ) - # Check if the expected page (6 for positional encoding) is in the top results - if "positional encoding" in query.lower(): - assert any( - r.page_num == 6 for r in results - ), "Expected page 6 for positional encoding query" + # Check if the expected page is in the top results - # Check if the expected pages (8 and 9 for BLEU score) are in the top results - if "bleu score" in query.lower(): - assert any( - r.page_num in [8, 9] for r in results - ), "Expected pages 8 or 9 for BLEU score query" + assert result.page_num == expected_page, ( + f"Expected page {expected_page} for this query. Got {result.page_num} instead" + ) + expected_page += 1 -@pytest.mark.slow -def test_multi_document(rag_model_from_pretrained: RAGMultiModalModel): - if not Path("docs/attention.pdf").is_file(): - raise FileNotFoundError( - f"Please download the PDF file from https://arxiv.org/pdf/1706.03762 and move it to {path_document_1}." - ) - if not Path("docs/attention_copy.pdf").is_file(): - raise FileNotFoundError( - f"Please download the PDF file from https://arxiv.org/pdf/1706.03762 and move it to {path_document_2}." - ) +@pytest.mark.slow +def test_multi_document(rag_model_from_pretrained: MultiModalRetrieverModel): # Index a directory of documents rag_model_from_pretrained.index( - input_path="docs/", + input_path="sample_data/", index_name="multi_doc_index", store_collection_with_index=True, overwrite=True, @@ -88,66 +79,18 @@ def test_multi_document(rag_model_from_pretrained: RAGMultiModalModel): # Test retrieval queries = [ - "How does the positional encoding thing work?", - "what's the BLEU score of this new strange method?", + "What is the numerical answer we are looking for in the sample xls file?", + "What is the numerical answer we are looking for on the third page of the sample multipage pdf file?", ] for query in queries: - results = rag_model_from_pretrained.search(query, k=5) + result = rag_model_from_pretrained.search(query, k=1)[0] print(f"\nQuery: {query}") - for result in results: - print( - f"Doc ID: {result.doc_id}, Page: {result.page_num}, Score: {result.score}" - ) - - # Check if the expected page (6 for positional encoding) is in the top results - if "positional encoding" in query.lower(): - assert any( - r.page_num == 6 for r in results - ), "Expected page 6 for positional encoding query" - - # Check if the expected pages (8 and 9 for BLEU score) are in the top results - if "bleu score" in query.lower(): - assert any( - r.page_num in [8, 9] for r in results - ), "Expected pages 8 or 9 for BLEU score query" - - -@pytest.mark.skip("This test should be made independent of the previous tests.") -@pytest.mark.slow -def test_add_to_index(rag_model_from_index: RAGMultiModalModel): - # NOTE: This test should run after the test_multi_document test. - - # Add a new document to the index - rag_model_from_index.add_to_index( - input_item="docs/", - store_collection_with_index=True, - doc_id=[1002, 1003], - metadata=[{"author": "John Doe", "year": 2023}] * 2, - ) - - # Test retrieval with the updated index - queries = ["what's the BLEU score of this new strange method?"] - - for query in queries: - results = rag_model_from_index.search(query, k=3) + print( + f"Doc ID: {result.doc_id}, Page: {result.page_num}, Score: {result.score}" + ) - print(f"\nQuery: {query}") - for result in results: - print( - f"Doc ID: {result.doc_id}, Page: {result.page_num}, Score: {result.score}" - ) - print(f"Metadata: {result.metadata}") - - # Check if the expected page (6 for positional encoding) is in the top results - if "positional encoding" in query.lower(): - assert any( - r.page_num == 6 for r in results - ), "Expected page 6 for positional encoding query" - - # Check if the expected pages (8 and 9 for BLEU score) are in the top results - if "bleu score" in query.lower(): - assert any( - r.page_num in [8, 9] for r in results - ), "Expected pages 8 or 9 for BLEU score query" + # Check if the expected page (3) is in the top results + if "third" in query.lower(): + assert result.page_num == 3, "Expected page 3 for multi-page pdf query" diff --git a/tests/test_embedding_server.py b/tests/test_embedding_server.py new file mode 100644 index 0000000..a4e0db2 --- /dev/null +++ b/tests/test_embedding_server.py @@ -0,0 +1,630 @@ +"""Unit tests for the embedding server package. + +All tests are offline — no real server, no GPU, no SSH required. +HTTP and SSH interactions are fully mocked. +""" + +import base64 +import io +import json +from unittest.mock import MagicMock, patch, call + +import httpx +import pytest +import torch +from PIL import Image + +from foretrieval.embedding_server.config import EmbeddingServerConfig +from foretrieval.embedding_server.client import ( + EmbeddingServerClient, + ServerOOMError, + _pil_to_base64, +) +from foretrieval.embedding_server.manager import EmbeddingServerManager + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + +_TEST_SERVER_URL = "http://localhost:8000" +_TEST_MODEL = "athrael-soju/colqwen3.5-4.5B-v3" +_TEST_SSH_HOST = "test-gpu-server" + + +def _make_config(**kwargs) -> EmbeddingServerConfig: + defaults = dict( + url=_TEST_SERVER_URL, + model_name=_TEST_MODEL, + batch_size=4, + ) + defaults.update(kwargs) + return EmbeddingServerConfig(**defaults) + + +def _make_image(w=32, h=32) -> Image.Image: + return Image.new("RGB", (w, h), color=(128, 64, 32)) + + +def _fake_pooling_response(n_items: int, n_tokens: int = 10, dim: int = 128) -> dict: + """Build a fake vLLM /pooling response with n_items token-embed outputs.""" + data = [] + for i in range(n_items): + token_embeds = [[float(i) * 0.1] * dim for _ in range(n_tokens)] + data.append({"index": i, "object": "embedding", "data": token_embeds}) + return {"object": "list", "data": data, "model": "test-model"} + + +def _make_mock_response(status_code: int = 200, json_data: dict = None, text: str = "") -> MagicMock: + """Build a mock httpx.Response.""" + resp = MagicMock(spec=httpx.Response) + resp.status_code = status_code + resp.json.return_value = json_data or {} + resp.text = text + return resp + + +# --------------------------------------------------------------------------- +# EmbeddingServerConfig tests +# --------------------------------------------------------------------------- + +class TestEmbeddingServerConfig: + def test_basic_construction(self): + cfg = _make_config() + assert cfg.url == _TEST_SERVER_URL + assert cfg.model_name == _TEST_MODEL + assert cfg.n_gpus == -1 + assert cfg.batch_size == 4 + + def test_trailing_slash_stripped(self): + cfg = _make_config(url="http://localhost:8000/") + assert cfg.url == "http://localhost:8000" + + def test_n_gpus_zero_invalid(self): + with pytest.raises(Exception): + _make_config(n_gpus=0) + + def test_n_gpus_negative_minus_one_valid(self): + cfg = _make_config(n_gpus=-1) + assert cfg.n_gpus == -1 + + def test_n_gpus_positive_valid(self): + cfg = _make_config(n_gpus=2) + assert cfg.n_gpus == 2 + + def test_auto_deploy_without_ssh_host_raises(self): + with pytest.raises(Exception, match="ssh_host"): + _make_config(auto_deploy=True, ssh_host=None) + + def test_auto_deploy_with_ssh_host_ok(self): + cfg = _make_config(auto_deploy=True, ssh_host=_TEST_SSH_HOST) + assert cfg.auto_deploy is True + + def test_from_dict(self): + d = {"url": "http://host:9000", "model_name": "athrael-soju/colqwen3.5-4.5B-v3"} + cfg = EmbeddingServerConfig.from_dict(d) + assert cfg.url == "http://host:9000" + assert cfg.port == 8000 # default + + # --- model_name validator --- + + def test_incompatible_model_raises(self): + with pytest.raises(ValueError, match="colqwen3"): + _make_config(model_name="vidore/colpali-v1.2") + + def test_colqwen2_model_raises(self): + with pytest.raises(ValueError, match="colqwen3"): + _make_config(model_name="vidore/colqwen2-v1.0") + + def test_colqwen3_model_accepted(self): + cfg = _make_config(model_name="athrael-soju/colqwen3.5-4.5B-v3") + assert "colqwen3" in cfg.model_name.lower() + + def test_colqwen3_variant_accepted(self): + cfg = _make_config(model_name="vidore/colqwen3-v1.0") + assert cfg.model_name == "vidore/colqwen3-v1.0" + + # --- auth / SSL fields --- + + def test_api_key_default_none(self): + cfg = _make_config() + assert cfg.api_key is None + + def test_api_key_set(self): + cfg = _make_config(api_key="secret-token") + assert cfg.api_key == "secret-token" + + def test_verify_ssl_default_true(self): + cfg = _make_config() + assert cfg.verify_ssl is True + + def test_verify_ssl_false(self): + cfg = _make_config(verify_ssl=False) + assert cfg.verify_ssl is False + + +# --------------------------------------------------------------------------- +# EmbeddingServerClient — request format +# --------------------------------------------------------------------------- + +class TestEmbeddingServerClientRequestFormat: + def _make_client(self, **kwargs) -> EmbeddingServerClient: + return EmbeddingServerClient(_make_config(**kwargs)) + + def test_embed_images_sends_correct_payload(self): + client = self._make_client() + images = [_make_image()] + fake_resp = _fake_pooling_response(1) + + mock_response = _make_mock_response(200, fake_resp) + + with patch.object(client._client, "post", return_value=mock_response) as mock_post: + client.embed_images(images) + + mock_post.assert_called_once() + _, kwargs = mock_post.call_args + payload = kwargs["json"] + + assert payload["model"] == "athrael-soju/colqwen3.5-4.5B-v3" + # vLLM >=0.19.0: images sent via messages array (PoolingChatRequest) + assert "messages" in payload + assert len(payload["messages"]) == 1 + content = payload["messages"][0]["content"] + assert len(content) == 1 + assert content[0]["type"] == "image_url" + assert content[0]["image_url"]["url"].startswith("data:image/png;base64,") + + def test_embed_query_sends_text_payload(self): + client = self._make_client() + fake_resp = _fake_pooling_response(1, n_tokens=5) + + mock_response = _make_mock_response(200, fake_resp) + + with patch.object(client._client, "post", return_value=mock_response) as mock_post: + client.embed_query("What is the speed?") + + _, kwargs = mock_post.call_args + payload = kwargs["json"] + assert payload["input"] == "What is the speed?" + assert "task" not in payload + + def test_embed_images_uses_correct_endpoint(self): + client = self._make_client() + mock_response = _make_mock_response(200, _fake_pooling_response(1)) + + with patch.object(client._client, "post", return_value=mock_response) as mock_post: + client.embed_images([_make_image()]) + + url_called = mock_post.call_args[0][0] + assert url_called == f"{_TEST_SERVER_URL}/pooling" + + def test_auth_header_sent_when_api_key_set(self): + """api_key set → Authorization: Bearer header in httpx.Client headers.""" + cfg = _make_config(api_key="my-secret") + client = EmbeddingServerClient(cfg) + assert client._client.headers.get("authorization") == "Bearer my-secret" + + def test_no_auth_header_when_api_key_none(self): + """api_key=None → no Authorization header.""" + cfg = _make_config() + client = EmbeddingServerClient(cfg) + assert "authorization" not in client._client.headers + + def test_ssl_verify_true_by_default(self): + """verify_ssl=True → httpx.Client verifies SSL.""" + cfg = _make_config() + client = EmbeddingServerClient(cfg) + # httpx.Client stores ssl_context; verify=True means it's not disabled + # We check the config was respected via the verify attr on the transport + assert client.config.verify_ssl is True + + def test_ssl_verify_false_passed_to_client(self): + """verify_ssl=False → httpx.Client created with verify=False.""" + cfg = _make_config(verify_ssl=False) + # Should not raise; httpx accepts verify=False + client = EmbeddingServerClient(cfg) + assert client.config.verify_ssl is False + + +# --------------------------------------------------------------------------- +# EmbeddingServerClient — response parsing +# --------------------------------------------------------------------------- + +class TestEmbeddingServerClientResponseParsing: + def _client_with_response(self, response_dict: dict) -> EmbeddingServerClient: + client = EmbeddingServerClient(_make_config()) + mock_response = _make_mock_response(200, response_dict) + client._client = MagicMock() + client._client.post.return_value = mock_response + return client + + def test_embed_images_returns_correct_number_of_tensors(self): + n = 3 + client = self._client_with_response(_fake_pooling_response(n)) + result = client.embed_images([_make_image()] * n) + assert len(result) == n + + def test_embed_images_tensor_shape(self): + n_tokens, dim = 12, 128 + client = self._client_with_response(_fake_pooling_response(1, n_tokens, dim)) + result = client.embed_images([_make_image()]) + assert result[0].shape == (n_tokens, dim) + + def test_embed_images_tensor_dtype_float32(self): + client = self._client_with_response(_fake_pooling_response(1)) + result = client.embed_images([_make_image()]) + assert result[0].dtype == torch.float32 + + def test_embed_query_returns_single_tensor(self): + client = self._client_with_response(_fake_pooling_response(1, n_tokens=7, dim=128)) + result = client.embed_query("hello") + assert len(result) == 1 + assert result[0].shape == (7, 128) + + def test_embed_empty_images_returns_empty(self): + client = EmbeddingServerClient(_make_config()) + result = client.embed_images([]) + assert result == [] + + +# --------------------------------------------------------------------------- +# EmbeddingServerClient — OOM retry +# --------------------------------------------------------------------------- + +class TestEmbeddingServerClientOOMRetry: + def test_oom_halves_batch_size_and_retries(self): + """Server OOM on batch_size=4 → retries with 2 → succeeds.""" + client = EmbeddingServerClient(_make_config(batch_size=4)) + + call_count = {"n": 0} + first_call = {"done": False} + + def fake_post(url, json=None, **kwargs): + call_count["n"] += 1 + if not first_call["done"]: + first_call["done"] = True + return _make_mock_response(500, text="CUDA out of memory trying to allocate tensor") + return _make_mock_response(200, _fake_pooling_response(1)) + + client._client = MagicMock() + client._client.post.side_effect = fake_post + + images = [_make_image()] * 4 + result = client.embed_images(images) + assert len(result) == 4 + assert call_count["n"] > 1 + + def test_oom_at_batch_size_1_raises(self): + """OOM even at batch_size=1 → raises ServerOOMError.""" + client = EmbeddingServerClient(_make_config(batch_size=1)) + + def fake_post(url, json=None, **kwargs): + return _make_mock_response(500, text="CUDA out of memory") + + client._client = MagicMock() + client._client.post.side_effect = fake_post + + with pytest.raises(ServerOOMError): + client.embed_images([_make_image()]) + + def test_non_oom_500_raises_runtime_error(self): + client = EmbeddingServerClient(_make_config()) + + def fake_post(url, json=None, **kwargs): + return _make_mock_response(500, text="Internal server error: model not loaded") + + client._client = MagicMock() + client._client.post.side_effect = fake_post + + with pytest.raises(RuntimeError, match="HTTP 500"): + client.embed_images([_make_image()]) + + def test_connection_error_raises(self): + client = EmbeddingServerClient(_make_config()) + client._client = MagicMock() + client._client.post.side_effect = httpx.ConnectError("refused") + + with pytest.raises(ConnectionError): + client.embed_images([_make_image()]) + + def test_timeout_raises(self): + client = EmbeddingServerClient(_make_config()) + client._client = MagicMock() + client._client.post.side_effect = httpx.TimeoutException("timed out") + + with pytest.raises(TimeoutError): + client.embed_images([_make_image()]) + + +# --------------------------------------------------------------------------- +# EmbeddingServerClient — health check +# --------------------------------------------------------------------------- + +class TestHealthCheck: + def test_health_check_true_on_200(self): + client = EmbeddingServerClient(_make_config()) + mock_resp = _make_mock_response(200) + client._client = MagicMock() + client._client.get.return_value = mock_resp + assert client.health_check() is True + + def test_health_check_false_on_500(self): + client = EmbeddingServerClient(_make_config()) + mock_resp = _make_mock_response(500) + client._client = MagicMock() + client._client.get.return_value = mock_resp + assert client.health_check() is False + + def test_health_check_false_on_connection_error(self): + client = EmbeddingServerClient(_make_config()) + client._client = MagicMock() + client._client.get.side_effect = httpx.ConnectError("refused") + assert client.health_check() is False + + +# --------------------------------------------------------------------------- +# EmbeddingServerManager — deploy logic (SSH mocked) +# --------------------------------------------------------------------------- + +class TestEmbeddingServerManager: + def _make_manager(self, **kwargs) -> EmbeddingServerManager: + defaults = dict(auto_deploy=True, ssh_host=_TEST_SSH_HOST, n_gpus=-1) + defaults.update(kwargs) + cfg = _make_config(**defaults) + mgr = EmbeddingServerManager(cfg) + # Stub the remote-home resolution so tests that mock _run_remote + # don't accidentally trigger a real SSH connection through + # _remote_home() -> sftp.normalize('.'). + mgr._cached_home = "/home/testuser" + return mgr + + def _mock_ssh(self, manager: EmbeddingServerManager, remote_outputs: dict): + def fake_run(cmd, on_line=None): + for key, val in remote_outputs.items(): + if key in cmd: + return val + return ("", "") + manager._run_remote = MagicMock(side_effect=fake_run) + return manager + + def test_deploy_when_no_metadata(self): + """No metadata file → deploy from scratch.""" + mgr = self._make_manager() + self._mock_ssh(mgr, { + "deployment.json": ("__MISSING__", ""), + "nvidia-smi": ("2\n", ""), + "docker pull": ("", ""), + "docker run": ("abc123\n", ""), + }) + + mgr.ensure_deployed() + + calls = [str(c) for c in mgr._run_remote.call_args_list] + assert any("docker pull" in c for c in calls) + assert any("docker run" in c for c in calls) + + def test_deploy_skipped_when_container_running(self): + """Metadata present + container running → no redeploy.""" + mgr = self._make_manager() + metadata = json.dumps({ + "model_name": "athrael-soju/colqwen3.5-4.5B-v3", + "container_name": "foretrieval_embedding_server", + "port": 8000, + }) + self._mock_ssh(mgr, { + "deployment.json": (metadata, ""), + "docker inspect": ("true\n", ""), + }) + + mgr.ensure_deployed() + + calls = [str(c) for c in mgr._run_remote.call_args_list] + assert not any("docker run" in c for c in calls) + + def test_redeploy_when_container_not_running(self): + """Metadata present but container stopped → redeploy.""" + mgr = self._make_manager() + metadata = json.dumps({ + "model_name": "athrael-soju/colqwen3.5-4.5B-v3", + "container_name": "foretrieval_embedding_server", + "port": 8000, + }) + self._mock_ssh(mgr, { + "deployment.json": (metadata, ""), + "docker inspect": ("false\n", ""), + "nvidia-smi": ("2\n", ""), + "docker pull": ("", ""), + "docker run": ("abc123\n", ""), + }) + + mgr.ensure_deployed() + + calls = [str(c) for c in mgr._run_remote.call_args_list] + assert any("docker run" in c for c in calls) + + def test_n_gpus_minus1_uses_all_detected(self): + """n_gpus=-1 → detect GPU count via nvidia-smi.""" + mgr = self._make_manager(n_gpus=-1) + self._mock_ssh(mgr, { + "deployment.json": ("__MISSING__", ""), + "nvidia-smi": ("2\n", ""), + "docker pull": ("", ""), + "docker run": ("abc123\n", ""), + }) + + mgr.ensure_deployed() + + docker_run_calls = [ + str(c) for c in mgr._run_remote.call_args_list + if "docker run" in str(c) + ] + assert docker_run_calls + assert "tensor-parallel-size 2" in docker_run_calls[0] + + def test_n_gpus_explicit_used_directly(self): + """n_gpus=1 → skip nvidia-smi, use 1 directly.""" + mgr = self._make_manager(n_gpus=1) + self._mock_ssh(mgr, { + "deployment.json": ("__MISSING__", ""), + "docker pull": ("", ""), + "docker run": ("abc123\n", ""), + }) + + mgr.ensure_deployed() + + docker_run_calls = [ + str(c) for c in mgr._run_remote.call_args_list + if "docker run" in str(c) + ] + assert "tensor-parallel-size 1" in docker_run_calls[0] + nvidia_calls = [ + c for c in mgr._run_remote.call_args_list + if "nvidia-smi" in str(c) + ] + assert not nvidia_calls + + def test_stop_removes_container_and_metadata(self): + mgr = self._make_manager() + mgr._run_remote = MagicMock(return_value=("", "")) + mgr.stop() + calls = [str(c) for c in mgr._run_remote.call_args_list] + assert any("docker stop" in c for c in calls) + assert any("docker rm" in c for c in calls) + assert any("deployment.json" in c for c in calls) + + def test_missing_paramiko_raises_import_error(self): + mgr = self._make_manager() + with patch.dict("sys.modules", {"paramiko": None}): + with pytest.raises(ImportError, match="paramiko"): + mgr.ensure_deployed() + + def test_redeploy_calls_deploy(self): + mgr = self._make_manager() + mgr._deploy = MagicMock() + with patch.dict("sys.modules", {"paramiko": MagicMock()}): + mgr.redeploy() + mgr._deploy.assert_called_once() + + def test_redeploy_forwards_on_line_callback(self): + mgr = self._make_manager() + mgr._deploy = MagicMock() + cb = MagicMock() + with patch.dict("sys.modules", {"paramiko": MagicMock()}): + mgr.redeploy(on_line=cb) + assert mgr._deploy.call_args.kwargs.get("on_line") is cb + + def test_run_remote_streams_lines_when_callback_given(self): + """When on_line is provided, stdout is read line-by-line.""" + mgr = self._make_manager() + + class _FakeChannel: + def recv_exit_status(self): + return 0 + + class _FakeStdout: + def __init__(self, lines): + self._lines = list(lines) + self.channel = _FakeChannel() + + def readline(self): + if self._lines: + return self._lines.pop(0) + return "" + + def read(self): + return b"" + + fake_stdout = _FakeStdout(["pulling…\n", "complete\n"]) + fake_stderr = MagicMock() + fake_stderr.read.return_value = b"" + fake_ssh = MagicMock() + fake_ssh.exec_command.return_value = (None, fake_stdout, fake_stderr) + mgr._get_ssh = MagicMock(return_value=fake_ssh) + + captured: list[str] = [] + mgr._run_remote("docker pull vllm/vllm-openai", on_line=captured.append) + assert captured == ["pulling…", "complete"] + + def test_get_ssh_delegates_to_open_ssh_client(self): + """_get_ssh must call foretrieval.ssh_utils.open_ssh_client. + + Regression test for the bug where paramiko was given a raw alias + and DNS-failed for entries that only existed in ~/.ssh/config. + """ + mgr = self._make_manager(ssh_user="alice", ssh_key_path="/k") + fake_client = MagicMock() + with patch("foretrieval.ssh_utils.open_ssh_client", + return_value=fake_client) as mock_open: + result = mgr._get_ssh() + mock_open.assert_called_once_with( + ssh_host=_TEST_SSH_HOST, + ssh_user="alice", + ssh_key_path="/k", + ) + assert result is fake_client + + def test_remote_home_uses_sftp_normalize(self): + mgr = self._make_manager() + mgr._cached_home = None # reset the stub from _make_manager + + fake_sftp = MagicMock() + fake_sftp.normalize.return_value = "/home/alice" + fake_ssh = MagicMock() + fake_ssh.open_sftp.return_value = fake_sftp + mgr._get_ssh = MagicMock(return_value=fake_ssh) + + assert mgr._remote_home() == "/home/alice" + fake_sftp.normalize.assert_called_once_with(".") + + def test_metadata_path_is_absolute(self): + mgr = self._make_manager() # _cached_home = "/home/testuser" + path = mgr._metadata_path() + assert path.startswith("/home/testuser/") + assert "~" not in path + assert path.endswith("deployment.json") + + +# --------------------------------------------------------------------------- +# Quantization config (local mode, no GPU needed — just validates ctor) +# --------------------------------------------------------------------------- + +class TestQuantizationConfig: + def test_colpali_model_accepts_quantization_params(self): + """ColPaliModel.__init__ signature accepts load_in_4bit/8bit — smoke test.""" + import inspect + from foretrieval.colpali import ColPaliModel + sig = inspect.signature(ColPaliModel.__init__) + assert "load_in_4bit" in sig.parameters + assert "load_in_8bit" in sig.parameters + assert "bnb_4bit_quant_type" in sig.parameters + assert "bnb_4bit_compute_dtype" in sig.parameters + + def test_retriever_model_accepts_quantization_params(self): + """MultiModalRetrieverModel.from_pretrained accepts quantization params.""" + import inspect + from foretrieval.retriever import MultiModalRetrieverModel + sig = inspect.signature(MultiModalRetrieverModel.from_pretrained) + assert "load_in_4bit" in sig.parameters + assert "load_in_8bit" in sig.parameters + + def test_quantization_without_bitsandbytes_raises(self): + """load_in_4bit=True with missing bitsandbytes → ImportError at load time.""" + from foretrieval.colpali import ColPaliModel + with patch.dict("sys.modules", {"transformers.utils.bitsandbytes": None}): + with patch("builtins.__import__", side_effect=lambda name, *a, **kw: ( + (_ for _ in ()).throw(ImportError("No module named 'bitsandbytes'")) + if name == "bitsandbytes" else __import__(name, *a, **kw) + )): + pass # import-level mock; actual test is integration-only without GPU + + +# --------------------------------------------------------------------------- +# Utility +# --------------------------------------------------------------------------- + +class TestPilToBase64: + def test_returns_valid_base64_png(self): + img = _make_image() + b64 = _pil_to_base64(img) + raw = base64.b64decode(b64) + assert raw[:4] == b"\x89PNG" diff --git a/tests/test_embedding_server_integration.py b/tests/test_embedding_server_integration.py new file mode 100644 index 0000000..72769d4 --- /dev/null +++ b/tests/test_embedding_server_integration.py @@ -0,0 +1,279 @@ +"""Integration tests for the remote embedding server. + +These tests require a live vLLM server and are skipped by default. + +Environment variables +--------------------- +FORETRIEVAL_TEST_SERVER : str + Base URL of the vLLM server to test against, e.g. "http://gpu-server:8000". + Tests are skipped when this variable is not set. +FORETRIEVAL_TEST_SSH_HOST : str + SSH hostname for deployment tests (auto_deploy path), e.g. "gpu-server". + Only needed for the deploy/stop tests. +FORETRIEVAL_TEST_MODEL : str + HuggingFace model ID served by the server. + Defaults to "athrael-soju/colqwen3.5-4.5B-v3". + +Usage +----- + # Run all integration tests against a remote GPU server: + FORETRIEVAL_TEST_SERVER=http://:8000 \ + FORETRIEVAL_TEST_SSH_HOST= \ + uv run pytest tests/test_embedding_server_integration.py -m "integration" -v +""" + +import os + +import pytest +import torch +from PIL import Image + +from foretrieval.embedding_server.client import EmbeddingServerClient +from foretrieval.embedding_server.config import EmbeddingServerConfig +from foretrieval.embedding_server.manager import EmbeddingServerManager + +# --------------------------------------------------------------------------- +# Helpers / fixtures +# --------------------------------------------------------------------------- + +_SERVER_URL = os.environ.get("FORETRIEVAL_TEST_SERVER", "") +_SSH_HOST = os.environ.get("FORETRIEVAL_TEST_SSH_HOST", "") +_MODEL = os.environ.get( + "FORETRIEVAL_TEST_MODEL", "athrael-soju/colqwen3.5-4.5B-v3" +) + +requires_server = pytest.mark.skipif( + not _SERVER_URL, + reason="Set FORETRIEVAL_TEST_SERVER=http://: to run integration tests", +) +requires_ssh = pytest.mark.skipif( + not _SSH_HOST, + reason="Set FORETRIEVAL_TEST_SSH_HOST= to run deploy integration tests", +) + + +@pytest.fixture(scope="module") +def server_config() -> EmbeddingServerConfig: + return EmbeddingServerConfig( + url=_SERVER_URL, + model_name=_MODEL, + batch_size=2, + request_timeout=180, + ) + + +@pytest.fixture(scope="module") +def client(server_config) -> EmbeddingServerClient: + return EmbeddingServerClient(server_config) + + +def _make_image(w: int = 64, h: int = 64) -> Image.Image: + return Image.new("RGB", (w, h), color=(100, 150, 200)) + + +# --------------------------------------------------------------------------- +# Health check +# --------------------------------------------------------------------------- + +@requires_server +@pytest.mark.integration +def test_server_health(client): + assert client.health_check(), ( + f"Server at {_SERVER_URL} is not healthy — is vLLM running?" + ) + + +# --------------------------------------------------------------------------- +# Image embedding +# --------------------------------------------------------------------------- + +@requires_server +@pytest.mark.integration +@pytest.mark.slow +def test_embed_single_image_returns_tensor(client): + imgs = [_make_image()] + result = client.embed_images(imgs) + assert len(result) == 1 + assert isinstance(result[0], torch.Tensor) + assert result[0].ndim == 2 # [n_tokens, dim] + assert result[0].shape[1] == 320 # ColQwen3.5 embed dim + + +@requires_server +@pytest.mark.integration +@pytest.mark.slow +def test_embed_multiple_images_returns_correct_count(client): + n = 3 + imgs = [_make_image()] * n + result = client.embed_images(imgs) + assert len(result) == n + + +@requires_server +@pytest.mark.integration +@pytest.mark.slow +def test_embed_images_tensors_are_on_cpu(client): + result = client.embed_images([_make_image()]) + assert result[0].device.type == "cpu" + + +@requires_server +@pytest.mark.integration +@pytest.mark.slow +def test_embed_images_tensors_differ_across_images(client): + """Different images should yield different embeddings.""" + img_a = Image.new("RGB", (64, 64), color=(255, 0, 0)) + img_b = Image.new("RGB", (64, 64), color=(0, 0, 255)) + emb_a, emb_b = client.embed_images([img_a, img_b]) + assert not torch.allclose(emb_a, emb_b) + + +# --------------------------------------------------------------------------- +# Query embedding +# --------------------------------------------------------------------------- + +@requires_server +@pytest.mark.integration +@pytest.mark.slow +def test_embed_query_returns_single_tensor(client): + result = client.embed_query("What is the maximum speed?") + assert len(result) == 1 + assert isinstance(result[0], torch.Tensor) + assert result[0].ndim == 2 + assert result[0].shape[1] == 320 + + +@requires_server +@pytest.mark.integration +@pytest.mark.slow +def test_embed_query_differs_from_image_embedding(client): + """Query and image embeddings should be in the same space but differ.""" + q_emb = client.embed_query("What is the airspeed?")[0] + img_emb = client.embed_images([_make_image()])[0] + # Both are [n_tokens, 320] — shapes may differ but dtypes should match + assert q_emb.dtype == img_emb.dtype + + +# --------------------------------------------------------------------------- +# Batch size / OOM retry (live server, small batches) +# --------------------------------------------------------------------------- + +@requires_server +@pytest.mark.integration +@pytest.mark.slow +def test_embed_images_batch_larger_than_one(client): + """Server should handle multiple images per request without OOM on RTX4090.""" + imgs = [_make_image(128, 128)] * 4 + result = client.embed_images(imgs) + assert len(result) == 4 + + +@requires_server +@pytest.mark.integration +@pytest.mark.slow +def test_embed_empty_list_returns_empty(client): + result = client.embed_images([]) + assert result == [] + + +# --------------------------------------------------------------------------- +# End-to-end: ColPaliModel with remote client +# --------------------------------------------------------------------------- + +@requires_server +@pytest.mark.integration +@pytest.mark.slow +def test_colpali_model_remote_embed_images(server_config, tmp_path): + """ColPaliModel loads processor only and uses remote client for embeddings.""" + from foretrieval.colpali import ColPaliModel + + model = ColPaliModel.from_pretrained( + pretrained_model_name_or_path=_MODEL, + index_root=str(tmp_path), + embedding_server=server_config, + storage_qdrant=False, # avoid qdrant dep in integration test env + ) + assert model._remote_client is not None + assert model.model is None # no local weights loaded + + # Index a small synthetic image + img = _make_image(64, 64) + model.index_name = "test_remote" + model.highest_doc_id = -1 + model.full_document_collection = False + model.resize_stored_images = False + model.max_image_width = None + model.max_image_height = None + + model._add_to_index( + images=[img], + store_collection_with_index=False, + doc_id=0, + page_ids=[1], + ) + + # Should have one embedding stored + if model.storage_qdrant: + # qdrant path — count points + pass # point insertion verified by no exception + else: + assert len(model.indexed_embeddings) == 1 + assert model.indexed_embeddings[0].shape[1] == 320 # ColQwen3.5 dim + + +@requires_server +@pytest.mark.integration +@pytest.mark.slow +def test_colpali_model_remote_encode_query(server_config, tmp_path): + """_encode_search_query uses remote client in remote mode.""" + from foretrieval.colpali import ColPaliModel + + model = ColPaliModel.from_pretrained( + pretrained_model_name_or_path=_MODEL, + index_root=str(tmp_path), + embedding_server=server_config, + ) + qs = model._encode_search_query("What is the fuel capacity?") + assert len(qs) == 1 + assert isinstance(qs[0], torch.Tensor) + assert qs[0].ndim == 2 + + +# --------------------------------------------------------------------------- +# Deployment manager (requires SSH access) +# --------------------------------------------------------------------------- + +@requires_server +@requires_ssh +@pytest.mark.integration +@pytest.mark.slow +def test_manager_health_check_against_running_server(): + """If server is already running, health check returns True.""" + cfg = EmbeddingServerConfig( + url=_SERVER_URL, + model_name=_MODEL, + ssh_host=_SSH_HOST, + ) + client = EmbeddingServerClient(cfg) + assert client.health_check() + + +@requires_ssh +@pytest.mark.integration +@pytest.mark.slow +def test_manager_ensure_deployed_idempotent(): + """ensure_deployed called twice should not start a second container.""" + cfg = EmbeddingServerConfig( + url=_SERVER_URL or f"http://{_SSH_HOST}:8000", + model_name=_MODEL, + auto_deploy=True, + ssh_host=_SSH_HOST, + n_gpus=-1, + ) + mgr = EmbeddingServerManager(cfg) + # First call: may deploy or detect already running + mgr.ensure_deployed() + # Second call: container already running → no-op + mgr.ensure_deployed() + # Verify container is running + assert mgr._is_container_running() diff --git a/tests/test_init_lazy_import.py b/tests/test_init_lazy_import.py new file mode 100644 index 0000000..56e998b --- /dev/null +++ b/tests/test_init_lazy_import.py @@ -0,0 +1,105 @@ +"""Tests for foretrieval.__init__ — specifically the lazy-import guarantee. + +The vector-DB server is started by Python importing +``foretrieval.vector_db_server.server_main`` which triggers the +top-level ``foretrieval/__init__.py``. If that __init__ eagerly +imports ``colpali.py``, it pulls in ``colpali_engine``, ``transformers`` +and ``torch._dynamo``, which crashes on the CPU-only Docker image with: + + AssertionError: Artifact of type=precompile already registered + in mega-cache artifact factory + +The fix is a lazy ``__getattr__``-based __init__ so the heavy ML stack +is only loaded when the caller explicitly requests it. +""" + +from __future__ import annotations + +import importlib +import sys + +import pytest + + +@pytest.fixture(autouse=True) +def _restore_sys_modules(): + """Snapshot sys.modules before each test and restore it afterward. + + Each test in this file deliberately purges all foretrieval.* entries from + sys.modules to verify lazy-import semantics. Without this fixture the purge + is permanent for the rest of the pytest session, causing later tests to + receive split module identities (OLD class object vs NEW class object) that + break isinstance() checks, patch() targets, and module-global reads. + """ + snapshot = {k: v for k, v in sys.modules.items()} + yield + # Remove any modules that were added during the test (not in snapshot) + for k in [k for k in sys.modules if k not in snapshot]: + del sys.modules[k] + # Restore all snapshot entries, overwriting any re-imported variants + sys.modules.update(snapshot) + + +def test_foretrieval_init_does_not_import_colpali(): + """Importing ``foretrieval`` must NOT pull in colpali as a side-effect. + + This is the regression guard for the vector-DB server startup crash: + the server only needs ``foretrieval.vector_db_server.*`` and must be + able to start without torch / colpali_engine being importable. + """ + # Remove any already-cached foretrieval / colpali modules so the + # test is independent of import order within the test session. + to_remove = [k for k in sys.modules if k.startswith("foretrieval") or "colpali" in k] + for k in to_remove: + sys.modules.pop(k, None) + + colpali_before = {k for k in sys.modules if "colpali" in k} + + import foretrieval # noqa: F401 + + colpali_after = {k for k in sys.modules if "colpali" in k} + + new_colpali = colpali_after - colpali_before + assert not new_colpali, ( + f"Importing 'foretrieval' pulled in colpali modules: {new_colpali}. " + "The __init__.py must use lazy imports so the vector-DB server " + "can start without torch/colpali_engine being available." + ) + + +def test_foretrieval_multimodal_retriever_model_accessible(): + """MultiModalRetrieverModel must still be reachable via the package.""" + # Re-import fresh + to_remove = [k for k in sys.modules if k.startswith("foretrieval")] + for k in to_remove: + sys.modules.pop(k, None) + + import foretrieval + cls = foretrieval.MultiModalRetrieverModel + assert cls is not None + # Confirm it is the real class (not a stub) + from foretrieval.retriever import MultiModalRetrieverModel as Direct + assert cls is Direct + + +def test_foretrieval_ai_metadata_provider_accessible(): + """ai_metadata_provider_factory must still be reachable via the package.""" + to_remove = [k for k in sys.modules if k.startswith("foretrieval")] + for k in to_remove: + sys.modules.pop(k, None) + + import foretrieval + fn = foretrieval.ai_metadata_provider_factory + assert callable(fn) + + +def test_foretrieval_unknown_attr_raises(): + """Accessing a non-existent attribute must raise AttributeError, not hang.""" + to_remove = [k for k in sys.modules if k.startswith("foretrieval")] + for k in to_remove: + sys.modules.pop(k, None) + + import foretrieval + import pytest + with pytest.raises(AttributeError, match="foretrieval"): + _ = foretrieval.this_does_not_exist diff --git a/tests/test_metadata_ai.py b/tests/test_metadata_ai.py new file mode 100644 index 0000000..b496049 --- /dev/null +++ b/tests/test_metadata_ai.py @@ -0,0 +1,273 @@ +""" +Tests for FORetrieval metadata generation WITH an AI provider. + +These tests exercise the ``_provider`` callable returned by +``ai_metadata_provider_factory(ai_cfg)`` when a real LLM is configured. +They require at least one of the following environment variables: + + OPENROUTER_API_KEY (preferred — fast, high rate limits) + Model: mistralai/mistral-small-3.2-24b-instruct + OPENAI_API_KEY Model: gpt-4o-mini + MISTRAL_API_KEY Model: mistral-small-latest + OLLAMA_HOST Ollama daemon URL (e.g. http://localhost:11434) + + OLLAMA_MODEL (default: llava:latest) + +All tests are marked ``@pytest.mark.integration``. + +Design +------ +The AI provider is called **exactly once** (on the LM317 datasheet) and the +result is cached in a module-scoped fixture. All individual tests then assert +on this single cached result. This minimises API calls, cost, and the risk of +hitting rate limits. + +Relationship to short_description / summary / abstract +------------------------------------------------------- +``short_description`` is FORetrieval's equivalent of a document summary or +abstract. It is always an empty string when no AI provider is configured +(tested in test_metadata_no_ai.py), and is filled by the LLM with 1–3 +factual sentences when AI is enabled. The test +``test_ai_short_description_populated`` validates this behaviour. +""" + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from foretrieval.metadata import ai_metadata_provider_factory +from foretrieval.models_metadata import DocMetadata + +# --------------------------------------------------------------------------- +# Paths +# --------------------------------------------------------------------------- + +TESTS_DIR = Path(__file__).parent +DATA_DIR = TESTS_DIR / "data" + +LM317_PDF = DATA_DIR / "lm317_voltage_regulator.pdf" +ULN2003_PDF = DATA_DIR / "uln2003_driver.pdf" + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def ai_provider(ai_cfg): + """Module-scoped AI metadata provider built from the detected backend.""" + return ai_metadata_provider_factory(ai_cfg) + + +@pytest.fixture(scope="module") +def ai_metadata_lm317(ai_provider): + """Single real API call for the LM317 datasheet, cached for the whole module. + + All per-field tests use this fixture so we only pay for one LLM call + regardless of how many tests run. + """ + try: + return ai_provider(LM317_PDF) + except Exception as exc: + # Surfaced as xfail rather than error so the rest of the suite continues + if _is_rate_limit(exc): + pytest.xfail(f"Rate limit hit during AI metadata generation: {exc}") + raise + + +def _is_rate_limit(exc: Exception) -> bool: + """Heuristic: treat common rate-limit / quota errors as xfail.""" + msg = str(exc).lower() + return any( + kw in msg for kw in ("rate limit", "429", "quota", "too many requests") + ) + + +# --------------------------------------------------------------------------- +# Provider construction +# --------------------------------------------------------------------------- + + +@pytest.mark.integration +def test_ai_provider_is_callable(ai_cfg): + """ai_metadata_provider_factory returns a callable given a valid ai_cfg.""" + provider = ai_metadata_provider_factory(ai_cfg) + assert callable(provider) + + +# --------------------------------------------------------------------------- +# Base (non-AI) fields are preserved in the AI result +# --------------------------------------------------------------------------- + + +@pytest.mark.integration +def test_ai_base_fields_preserved(ai_metadata_lm317): + """Non-AI filesystem fields are still present in the AI-enriched result.""" + raw = ai_metadata_lm317 + assert isinstance(raw.get("source_path"), str) and raw["source_path"] + assert raw.get("stem") == "lm317_voltage_regulator" + assert raw.get("ext") == ".pdf" + assert isinstance(raw.get("mime"), str) and raw["mime"] + assert isinstance(raw.get("mtime"), str) and raw["mtime"] + + +@pytest.mark.integration +def test_ai_pdf_page_count_preserved(ai_metadata_lm317): + """page_count extracted from the PDF is not overwritten by AI.""" + raw = ai_metadata_lm317 + assert isinstance(raw.get("page_count"), int) + assert raw["page_count"] > 0 + + +# --------------------------------------------------------------------------- +# AI-enriched fields +# --------------------------------------------------------------------------- + + +@pytest.mark.integration +def test_ai_language_populated(ai_metadata_lm317): + """language is a non-empty string after AI enrichment.""" + raw = ai_metadata_lm317 + lang = raw.get("language") + assert isinstance(lang, str) and len(lang) > 0, ( + f"Expected non-empty language string, got {lang!r}" + ) + + +@pytest.mark.integration +def test_ai_language_is_english(ai_metadata_lm317): + """LM317 datasheet language should be detected as English ('en'). + + Marked xfail in case a different LLM returns a variant code ('en-US', + 'english', etc.) rather than raising a hard failure. + """ + raw = ai_metadata_lm317 + lang = (raw.get("language") or "").lower().strip() + if not lang.startswith("en"): + pytest.xfail( + f"Language was detected as '{lang}' instead of 'en'. " + "This may be acceptable depending on LLM output format." + ) + + +@pytest.mark.integration +def test_ai_tags_non_empty(ai_metadata_lm317): + """tags is a non-empty list after AI enrichment.""" + raw = ai_metadata_lm317 + tags = raw.get("tags") + assert isinstance(tags, list) and len(tags) > 0, ( + f"Expected non-empty tags list, got {tags!r}" + ) + + +@pytest.mark.integration +def test_ai_tags_are_strings(ai_metadata_lm317): + """Every element of tags is a string.""" + raw = ai_metadata_lm317 + for tag in raw.get("tags", []): + assert isinstance(tag, str), f"Non-string tag: {tag!r}" + + +@pytest.mark.integration +def test_ai_tags_normalized_lowercase(ai_metadata_lm317): + """DocMetadata normalises tags to lowercase. + + The DocMetadata field_validator lowercases all tags, so even if the LLM + returns mixed-case tags they are stored correctly. + """ + md = DocMetadata(**ai_metadata_lm317) + for tag in md.tags: + assert tag == tag.lower(), f"Tag not lowercased: {tag!r}" + + +@pytest.mark.integration +def test_ai_document_type_not_unknown(ai_metadata_lm317): + """document_type is classified by the AI (not the default 'unknown').""" + raw = ai_metadata_lm317 + doc_type = raw.get("document_type", "") + assert isinstance(doc_type, str) and doc_type, "document_type is empty" + assert doc_type.lower() != "unknown", ( + f"AI left document_type as 'unknown': {doc_type!r}" + ) + + +@pytest.mark.integration +def test_ai_short_description_populated(ai_metadata_lm317): + """short_description (the summary/abstract equivalent) is filled by the AI. + + This is the key test that validates AI-backed summary generation. + Without AI, short_description is always '' (tested in + test_metadata_no_ai.py::test_no_ai_short_description_empty). + With AI, the LLM produces 1-3 factual sentences — at least 20 characters. + """ + raw = ai_metadata_lm317 + desc = raw.get("short_description", "") + assert isinstance(desc, str), f"short_description is not a string: {desc!r}" + assert len(desc) >= 20, ( + f"short_description is too short (< 20 chars): {desc!r}" + ) + + +# --------------------------------------------------------------------------- +# DocMetadata construction +# --------------------------------------------------------------------------- + + +@pytest.mark.integration +def test_ai_result_valid_doc_metadata(ai_metadata_lm317): + """DocMetadata(**raw) succeeds for the AI-enriched result.""" + md = DocMetadata(**ai_metadata_lm317) + assert md.stem == "lm317_voltage_regulator" + assert md.ext == ".pdf" + assert md.language is not None + assert len(md.tags) > 0 + assert md.short_description # non-empty + + +@pytest.mark.integration +def test_ai_no_hallucinated_author(ai_metadata_lm317): + """author is None or a plausible non-empty string. + + The LM317 datasheet has no embedded PDF author metadata, so the AI + should either return None or a meaningful string — not a hallucinated + generic placeholder. + """ + raw = ai_metadata_lm317 + author = raw.get("author") + if author is not None: + assert isinstance(author, str) and len(author.strip()) > 0, ( + f"author is set but empty/whitespace: {author!r}" + ) + # Sanity: should not be an obvious placeholder + suspicious = {"unknown", "n/a", "none", "anonymous"} + assert author.strip().lower() not in suspicious, ( + f"author looks like a hallucinated placeholder: {author!r}" + ) + + +# --------------------------------------------------------------------------- +# Rate-limit resilience (second provider call) +# --------------------------------------------------------------------------- + + +@pytest.mark.integration +def test_ai_second_document_or_xfail(ai_provider): + """Run the AI provider on ULN2003 datasheet; xfail on rate limit. + + This validates that the provider works for a second document without + exhausting the session-level cache. Rate-limit errors are tolerated + (xfail) since this is an additional call beyond the module fixture. + """ + try: + raw = ai_provider(ULN2003_PDF) + except Exception as exc: + if _is_rate_limit(exc): + pytest.xfail(f"Rate limit hit on second document: {exc}") + raise + + # Basic sanity on the second result + assert raw.get("ext") == ".pdf" + assert isinstance(raw.get("short_description"), str) + DocMetadata(**raw) # must be constructible diff --git a/tests/test_metadata_filter.py b/tests/test_metadata_filter.py new file mode 100644 index 0000000..b75fa6f --- /dev/null +++ b/tests/test_metadata_filter.py @@ -0,0 +1,306 @@ +""" +Tests for MetadataFilter regex support and empty-filter crash fix. + +No GPU, no API key, no network required — all ColPali internals are mocked. +""" + +from __future__ import annotations + +from unittest.mock import MagicMock, patch + +import pytest + +from foretrieval.models_metadata import MetadataFilter +from foretrieval.utils import _value_match + + +# --------------------------------------------------------------------------- +# Fixtures — representative metadata dicts +# --------------------------------------------------------------------------- + + +@pytest.fixture +def meta_pdf(): + """Metadata for a PDF named 'general_datasheet.pdf'.""" + return { + "stem": "general_datasheet", + "ext": ".pdf", + "title": "General Motor Controller Reference Manual", + "author": "Smith, J. and Doe, A.", + "mime": "application/pdf", + "language": None, + "tags": [], + "document_type": "unknown", + "short_description": "", + "mtime": "2025-03-01T10:00:00+00:00", + "page_count": 42, + } + + +@pytest.fixture +def meta_docx(): + """Metadata for a Word document named 'safety_manual.docx'.""" + return { + "stem": "safety_manual", + "ext": ".docx", + "title": "Industrial Safety Procedures", + "author": "Johnson, R.", + "mime": "application/vnd.openxmlformats-officedocument.wordprocessingml.document", + "language": None, + "tags": [], + "document_type": "unknown", + "short_description": "", + "mtime": "2024-11-15T08:30:00+00:00", + "page_count": None, + } + + +# --------------------------------------------------------------------------- +# _value_match — regex tests +# --------------------------------------------------------------------------- + + +class TestRegexMatching: + def test_stem_substring_match(self, meta_pdf): + """regex on stem matches when the pattern is a substring.""" + f = MetadataFilter(regex={"stem": "general"}) + assert _value_match(meta_pdf, f) is True + + def test_stem_no_match(self, meta_pdf): + """regex on stem returns False when pattern is not found.""" + f = MetadataFilter(regex={"stem": "missing"}) + assert _value_match(meta_pdf, f) is False + + def test_title_case_insensitive(self, meta_pdf): + """regex matching is case-insensitive by default.""" + f = MetadataFilter(regex={"title": "GENERAL"}) + assert _value_match(meta_pdf, f) is True + + def test_author_substring(self, meta_pdf): + """regex on author matches a substring of the author string.""" + f = MetadataFilter(regex={"author": "smith"}) + assert _value_match(meta_pdf, f) is True + + def test_author_no_match(self, meta_pdf): + """regex on author returns False when pattern is not in author string.""" + f = MetadataFilter(regex={"author": "nobody"}) + assert _value_match(meta_pdf, f) is False + + def test_alternation(self, meta_pdf): + """regex alternation (|) matches if any alternative is present.""" + f = MetadataFilter(regex={"title": "motor|pump"}) + assert _value_match(meta_pdf, f) is True + + def test_alternation_no_match(self, meta_pdf): + """regex alternation returns False when none of the alternatives match.""" + f = MetadataFilter(regex={"title": "bicycle|aircraft"}) + assert _value_match(meta_pdf, f) is False + + def test_anchored_start_match(self, meta_pdf): + """regex ^ anchor matches at the start of the field value.""" + f = MetadataFilter(regex={"stem": "^general"}) + assert _value_match(meta_pdf, f) is True + + def test_anchored_start_no_match(self, meta_docx): + """regex ^ anchor fails when value does not start with the pattern.""" + f = MetadataFilter(regex={"stem": "^general"}) + assert _value_match(meta_docx, f) is False + + def test_malformed_pattern_no_crash(self, meta_pdf): + """A malformed regex pattern returns False without raising.""" + f = MetadataFilter(regex={"stem": "[invalid"}) + result = _value_match(meta_pdf, f) + assert result is False + + def test_field_not_in_meta_no_match(self, meta_pdf): + """regex on a field absent from metadata returns False.""" + f = MetadataFilter(regex={"nonexistent_field": "anything"}) + assert _value_match(meta_pdf, f) is False + + def test_regex_on_ai_field_language(self, meta_pdf): + """regex works on AI fields like language when present.""" + meta = dict(meta_pdf) + meta["language"] = "en" + f = MetadataFilter(regex={"language": "^en"}) + assert _value_match(meta, f) is True + + def test_regex_on_document_type(self, meta_pdf): + """regex works on document_type.""" + meta = dict(meta_pdf) + meta["document_type"] = "technical note" + f = MetadataFilter(regex={"document_type": "technical"}) + assert _value_match(meta, f) is True + + def test_regex_combined_with_ext_and(self, meta_pdf, meta_docx): + """regex AND ext: only matches when both conditions hold.""" + f = MetadataFilter(ext=".pdf", regex={"stem": "general"}) + assert _value_match(meta_pdf, f) is True # PDF + stem contains "general" + assert _value_match(meta_docx, f) is False # docx fails ext check + + def test_regex_combined_with_ext_or(self, meta_pdf, meta_docx): + """regex OR ext: matches when either condition holds.""" + f = MetadataFilter(ext=".docx", regex={"stem": "general"}, logic="OR") + assert _value_match(meta_pdf, f) is True # stem matches + assert _value_match(meta_docx, f) is True # ext matches + + def test_regex_not_double_processed_by_extra_loop(self, meta_pdf): + """The 'regex' key must not be processed by the extra-field equality loop. + + If it were, 'regex' as a key would be compared against meta['regex'] + which doesn't exist, producing a spurious False check. + """ + f = MetadataFilter(regex={"stem": "general"}) + # Ensure no check is generated for a literal 'regex' key equality + # The filter has one regex condition → exactly one check in the list + # We verify this by asserting the match is True (would be False if + # the extra loop produced a False check for missing 'regex' field). + assert _value_match(meta_pdf, f) is True + + def test_multiple_regex_fields_and(self, meta_pdf): + """Multiple regex entries are all required under AND logic.""" + f = MetadataFilter(regex={"stem": "general", "title": "motor"}) + assert _value_match(meta_pdf, f) is True + + def test_multiple_regex_fields_one_fails(self, meta_pdf): + """Multiple regex entries: one failing under AND logic → False.""" + f = MetadataFilter(regex={"stem": "general", "title": "aircraft"}) + assert _value_match(meta_pdf, f) is False + + def test_multiple_regex_fields_or(self, meta_pdf): + """Multiple regex entries under OR: one match is sufficient.""" + f = MetadataFilter(regex={"stem": "general", "title": "aircraft"}, logic="OR") + assert _value_match(meta_pdf, f) is True + + +# --------------------------------------------------------------------------- +# Empty filter → [] and no crash +# --------------------------------------------------------------------------- + + +class TestEmptyFilterReturnsEmptyList: + """Tests that ColPaliModel.search() returns [] when the filter matches nothing.""" + + def _make_mock_colpali(self): + """Return a minimal mock ColPaliModel with in-memory index state.""" + from foretrieval.colpali import ColPaliModel + + model = MagicMock(spec=ColPaliModel) + model.device = "cpu" + model.verbose = 0 + model.collection = None + model.enable_heatmaps = False + model.enable_circle = False + model.storage_qdrant = False # use local search path + model._encode_search_query = MagicMock(return_value=[MagicMock()]) + + # One document indexed, stored with ext=".pdf" + model.doc_id_to_metadata = { + 0: {"stem": "datasheet", "ext": ".pdf", "title": "Test Doc"} + } + model.embed_id_to_doc_id = {0: {"doc_id": 0, "page_id": 0}} + model.indexed_embeddings = [MagicMock()] # one page embedding + + # processor mock + processor = MagicMock() + processor.process_queries.return_value = { + "input_ids": MagicMock( + __getitem__=lambda self, idx: MagicMock( + detach=lambda: MagicMock( + cpu=lambda: MagicMock(tolist=lambda: [1, 2, 3]) + ) + ) + ) + } + processor.tokenizer.convert_ids_to_tokens.return_value = ["tok1", "tok2", "tok3"] + model.processor = processor + + # model forward pass mock + inner_model = MagicMock() + inner_model.dtype = None + model.model = inner_model + + return model + + def test_filter_no_match_returns_empty_list(self): + """LocalVectorStore.search() returns [] when the metadata filter matches nothing.""" + from foretrieval.vector_store.local import LocalVectorStore + from foretrieval.vector_store.base import MultiVectorQuery, StoredPoint, make_point_id + import torch + from unittest.mock import MagicMock + + store = LocalVectorStore() + + import tempfile, pathlib + with tempfile.TemporaryDirectory() as tmp: + store.open("idx", pathlib.Path(tmp), create=True) + store.set_doc_id_to_metadata({0: {"stem": "datasheet", "ext": ".pdf"}}) + # Insert one embedding + store.upsert([StoredPoint( + point_id=make_point_id(0, 1), + vector=torch.rand(4, 8), + payload={"doc_id": 0, "page_id": 1, "chunk_id": None, "metadata": {}}, + )]) + proc = MagicMock() + store.set_processor(proc) + + # Filter on language=fr which doesn't match → empty result + q = MultiVectorQuery(vectors=torch.rand(2, 8), filter_metadata={"language": "fr"}) + results = store.search(q, k=5) + assert results == [] + + def test_filter_no_match_does_not_raise(self): + """search() must not raise ValueError when filter matches nothing.""" + from foretrieval.vector_store.local import LocalVectorStore + from foretrieval.vector_store.base import MultiVectorQuery, StoredPoint, make_point_id + import torch + from unittest.mock import MagicMock + + store = LocalVectorStore() + import tempfile, pathlib + with tempfile.TemporaryDirectory() as tmp: + store.open("idx", pathlib.Path(tmp), create=True) + store.set_doc_id_to_metadata({0: {"ext": ".pdf"}}) + store.upsert([StoredPoint( + point_id=make_point_id(0, 1), + vector=torch.rand(4, 8), + payload={"doc_id": 0, "page_id": 1, "chunk_id": None, "metadata": {}}, + )]) + proc = MagicMock() + store.set_processor(proc) + q = MultiVectorQuery(vectors=torch.rand(2, 8), filter_metadata={"language": "fr"}) + try: + store.search(q, k=5) + except ValueError as exc: + pytest.fail(f"search raised ValueError when filter matched nothing: {exc}") + + def test_filter_match_still_works(self): + """search() returns results when the filter matches documents.""" + from foretrieval.colpali import ColPaliModel + from unittest.mock import MagicMock + from foretrieval.vector_store.base import SearchHit + from foretrieval.objects import Result + + mock = MagicMock() + mock.collection = {} + mock.enable_heatmaps = False + mock.enable_circle = False + mock.doc_id_to_metadata = {0: {"ext": ".pdf"}} + mock._encode_search_query = MagicMock(return_value=[MagicMock()]) + + hit = SearchHit(point_id=0, score=0.9, payload={"doc_id": 0, "page_id": 1, "chunk_id": None, "metadata": {}}) + mock.vector_store.search.return_value = [hit] + mock.embed_id_to_extra = {} + # Make _hits_to_results and _finalize_results return proper values + expected = [Result(doc_id=0, page_num=1, score=0.9)] + mock._hits_to_results = MagicMock(return_value=expected) + mock._finalize_results = MagicMock(return_value=expected) + + results = ColPaliModel.search( + mock, + query="test", + k=1, + filter_metadata={"ext": ".pdf"}, + return_base64_results=False, + ) + assert isinstance(results, list) + assert len(results) > 0 diff --git a/tests/test_metadata_no_ai.py b/tests/test_metadata_no_ai.py new file mode 100644 index 0000000..46c3a31 --- /dev/null +++ b/tests/test_metadata_no_ai.py @@ -0,0 +1,378 @@ +""" +Tests for FORetrieval metadata generation WITHOUT an AI provider. + +These tests exercise the ``_no_ai_provider`` callable returned by +``ai_metadata_provider_factory(None)``. No API key, no GPU, and no network +access are required — every test runs against real files on disk. + +Metadata fields and their expected values without AI +----------------------------------------------------- ++--------------------+----------------------------------+ +| Field | Expected value (no AI) | ++====================+==================================+ +| source_path | str (absolute path) | +| stem | str (filename without extension) | +| ext | str (starts with '.', lowercase) | +| mime | str | +| mtime | ISO-8601 UTC string | +| page_count | int > 0 (PDFs only) | +| image_width | int > 0 (images only) | +| image_height | int > 0 (images only) | +| author | str or None (from PDF metadata) | +| title | str or None (from PDF metadata) | +| language | None ← not detected without AI | +| tags | [] ← not generated without AI | +| document_type | "unknown" | +| short_description | "" ← the "summary/abstract" | +| | equivalent; empty without AI | ++--------------------+----------------------------------+ + +Note: ``short_description`` is the field that corresponds to a document +summary or abstract. It is empty when no AI provider is configured, and +is filled by the LLM when an AI provider is used (see test_metadata_ai.py). +""" + +from __future__ import annotations + +from datetime import datetime, timezone +from pathlib import Path + +import pytest + +from foretrieval.metadata import ai_metadata_provider_factory +from foretrieval.models_metadata import DocMetadata, build_metadata_list_for_dir + +# --------------------------------------------------------------------------- +# Paths +# --------------------------------------------------------------------------- + +TESTS_DIR = Path(__file__).parent +DATA_DIR = TESTS_DIR / "data" +SAMPLE_DATA_DIR = TESTS_DIR.parent / "sample_data" + +LM317_PDF = DATA_DIR / "lm317_voltage_regulator.pdf" +ULN2003_PDF = DATA_DIR / "uln2003_driver.pdf" +SAMPLE_PNG = SAMPLE_DATA_DIR / "sample_png.png" +SAMPLE_JPG = SAMPLE_DATA_DIR / "sample_jpg.jpg" +SAMPLE_TXT = SAMPLE_DATA_DIR / "sample_txt.txt" +SAMPLE_DOCX = SAMPLE_DATA_DIR / "sample_docx.docx" +SAMPLE_XLSX = SAMPLE_DATA_DIR / "sample_xlsx.xlsx" + + +# --------------------------------------------------------------------------- +# Fixtures +# --------------------------------------------------------------------------- + + +@pytest.fixture(scope="module") +def provider(): + """Module-scoped no-AI metadata provider.""" + return ai_metadata_provider_factory(None) + + +# --------------------------------------------------------------------------- +# Base field tests +# --------------------------------------------------------------------------- + + +def test_no_ai_pdf_base_fields(provider): + """All base filesystem fields are populated for a PDF.""" + raw = provider(LM317_PDF) + + assert isinstance(raw["source_path"], str) + assert raw["source_path"].endswith("lm317_voltage_regulator.pdf") + assert raw["stem"] == "lm317_voltage_regulator" + assert isinstance(raw["mime"], str) and raw["mime"] + assert isinstance(raw["mtime"], str) and raw["mtime"] + + +def test_no_ai_ext_normalized_pdf(provider): + """ext is '.pdf' (lowercase, starts with dot) for a PDF file.""" + raw = provider(LM317_PDF) + assert raw["ext"] == ".pdf" + + +def test_no_ai_ext_normalized_image(provider): + """ext starts with '.' and is lowercase for image files.""" + for path in [SAMPLE_PNG, SAMPLE_JPG]: + raw = provider(path) + assert raw["ext"].startswith("."), f"ext missing leading dot for {path.name}" + assert raw["ext"] == raw["ext"].lower(), f"ext not lowercase for {path.name}" + + +def test_no_ai_mtime_is_valid_iso_utc(provider): + """mtime is a parseable ISO-8601 string with UTC timezone info.""" + raw = provider(LM317_PDF) + mtime_str = raw["mtime"] + # Parse via datetime — should not raise + dt = datetime.fromisoformat(mtime_str.replace("Z", "+00:00")) + assert dt.tzinfo is not None, "mtime must carry timezone info" + + +# --------------------------------------------------------------------------- +# PDF-specific tests +# --------------------------------------------------------------------------- + + +def test_no_ai_pdf_page_count_lm317(provider): + """page_count is a positive integer for the LM317 datasheet.""" + raw = provider(LM317_PDF) + assert isinstance(raw["page_count"], int) + assert raw["page_count"] > 0 + + +def test_no_ai_pdf_page_count_uln2003(provider): + """page_count is a positive integer for the ULN2003 datasheet.""" + raw = provider(ULN2003_PDF) + assert isinstance(raw["page_count"], int) + assert raw["page_count"] > 0 + + +def test_no_ai_non_pdf_page_count_is_none(provider): + """page_count is None for non-PDF files.""" + for path in [SAMPLE_PNG, SAMPLE_TXT, SAMPLE_DOCX]: + raw = provider(path) + assert raw["page_count"] is None, ( + f"Expected page_count=None for {path.name}, got {raw['page_count']}" + ) + + +# --------------------------------------------------------------------------- +# Image dimension tests +# --------------------------------------------------------------------------- + + +def test_no_ai_image_dims_png(provider): + """image_width and image_height are positive ints for a PNG.""" + raw = provider(SAMPLE_PNG) + assert isinstance(raw["image_width"], int) and raw["image_width"] > 0 + assert isinstance(raw["image_height"], int) and raw["image_height"] > 0 + + +def test_no_ai_image_dims_jpg(provider): + """image_width and image_height are positive ints for a JPEG.""" + raw = provider(SAMPLE_JPG) + assert isinstance(raw["image_width"], int) and raw["image_width"] > 0 + assert isinstance(raw["image_height"], int) and raw["image_height"] > 0 + + +def test_no_ai_non_image_dims_are_none(provider): + """image_width and image_height are None for non-image files.""" + for path in [LM317_PDF, SAMPLE_TXT, SAMPLE_DOCX]: + raw = provider(path) + assert raw["image_width"] is None, f"Expected None for {path.name}" + assert raw["image_height"] is None, f"Expected None for {path.name}" + + +# --------------------------------------------------------------------------- +# AI field absence tests (the critical no-AI assertions) +# --------------------------------------------------------------------------- + + +def test_no_ai_language_is_none(provider): + """language is None without AI — language detection requires the LLM.""" + for path in [LM317_PDF, ULN2003_PDF]: + raw = provider(path) + assert raw["language"] is None, ( + f"Expected language=None without AI for {path.name}" + ) + + +def test_no_ai_tags_empty(provider): + """tags is an empty list without AI — tagging requires the LLM.""" + for path in [LM317_PDF, ULN2003_PDF]: + raw = provider(path) + assert raw["tags"] == [], ( + f"Expected tags=[] without AI for {path.name}" + ) + + +def test_no_ai_document_type_unknown(provider): + """document_type is 'unknown' without AI — classification requires the LLM.""" + for path in [LM317_PDF, ULN2003_PDF]: + raw = provider(path) + assert raw["document_type"] == "unknown", ( + f"Expected document_type='unknown' without AI for {path.name}" + ) + + +def test_no_ai_short_description_empty(provider): + """short_description (the summary/abstract equivalent) is empty without AI. + + ``short_description`` is FORetrieval's equivalent of a document + summary or abstract. Without an AI provider it is always an empty + string; the LLM fills it with 1-3 factual sentences when AI is + enabled (see test_metadata_ai.py::test_ai_short_description_populated). + """ + for path in [LM317_PDF, ULN2003_PDF]: + raw = provider(path) + assert raw["short_description"] == "", ( + f"Expected short_description='' without AI for {path.name}" + ) + + +# --------------------------------------------------------------------------- +# DocMetadata construction tests +# --------------------------------------------------------------------------- + + +def test_no_ai_result_valid_doc_metadata_lm317(provider): + """DocMetadata(**raw) succeeds for the LM317 datasheet.""" + raw = provider(LM317_PDF) + md = DocMetadata(**raw) + assert md.stem == "lm317_voltage_regulator" + assert md.ext == ".pdf" + + +def test_no_ai_result_valid_doc_metadata_uln2003(provider): + """DocMetadata(**raw) succeeds for the ULN2003 datasheet.""" + raw = provider(ULN2003_PDF) + md = DocMetadata(**raw) + assert md.ext == ".pdf" + + +def test_no_ai_doc_metadata_tags_normalized(provider): + """DocMetadata normalises tags to lowercase stripped strings.""" + raw = provider(LM317_PDF) + raw["tags"] = [" Python ", "RAG", "FOO"] + md = DocMetadata(**raw) + assert md.tags == ["python", "rag", "foo"] + + +def test_no_ai_doc_metadata_ext_normalized(provider): + """DocMetadata normalises ext: adds leading dot, lowercases.""" + raw = provider(LM317_PDF) + raw["ext"] = "PDF" # deliberate unnormalized value + md = DocMetadata(**raw) + assert md.ext == ".pdf" + + +# --------------------------------------------------------------------------- +# Multiple format smoke tests +# --------------------------------------------------------------------------- + + +@pytest.mark.parametrize( + "path", + [ + SAMPLE_TXT, + SAMPLE_DOCX, + SAMPLE_XLSX, + SAMPLE_PNG, + SAMPLE_JPG, + ], + ids=["txt", "docx", "xlsx", "png", "jpg"], +) +def test_no_ai_multiple_formats_no_crash(provider, path): + """provider() returns a valid dict for all common file types without crashing.""" + raw = provider(path) + assert isinstance(raw, dict) + # Must always contain these base keys + for key in ("source_path", "stem", "ext", "mime", "mtime"): + assert key in raw, f"Missing key '{key}' for {path.name}" + # Must be constructible as DocMetadata + DocMetadata(**raw) + + +# --------------------------------------------------------------------------- +# build_metadata_list_for_dir tests +# --------------------------------------------------------------------------- + + +def test_build_metadata_list_for_dir_length(provider): + """Result list has the same length as the recursive file enumeration used by index().""" + items = sorted( + (p for p in DATA_DIR.rglob("*") if p.is_file()), + key=lambda p: p.relative_to(DATA_DIR), + ) + md_list = build_metadata_list_for_dir(DATA_DIR, provider) + assert len(md_list) == len(items) + + +def test_build_metadata_list_for_dir_all_doc_metadata(provider): + """Every entry in the result list is a DocMetadata instance (no None placeholders).""" + md_list = build_metadata_list_for_dir(DATA_DIR, provider) + for md in md_list: + # All entries are files (directories are excluded); provider always + # returns metadata for PDF files in DATA_DIR. + assert isinstance(md, DocMetadata), ( + f"Expected DocMetadata for all entries, got {type(md)}" + ) + + +def test_build_metadata_list_for_dir_order_stable(provider): + """Two consecutive calls return stems in the same order (sort is deterministic).""" + stems_first = [ + md.stem for md in build_metadata_list_for_dir(DATA_DIR, provider) + if md is not None + ] + stems_second = [ + md.stem for md in build_metadata_list_for_dir(DATA_DIR, provider) + if md is not None + ] + assert stems_first == stems_second, ( + "build_metadata_list_for_dir returned different ordering on consecutive calls" + ) + + +def test_build_metadata_list_for_dir_stems_match_sorted_filenames(provider): + """Stems in the result list match the stems of recursively-sorted filenames. + + Validates that the list is aligned with ColPaliModel.index() which iterates + files via rglob("*") sorted by relative path. + """ + sorted_files = sorted( + (p for p in DATA_DIR.rglob("*") if p.is_file()), + key=lambda p: p.relative_to(DATA_DIR), + ) + md_list = [ + md for md in build_metadata_list_for_dir(DATA_DIR, provider) + if md is not None + ] + assert len(md_list) == len(sorted_files) + for expected_path, md in zip(sorted_files, md_list): + assert md.stem == expected_path.stem, ( + f"Stem mismatch: expected '{expected_path.stem}', got '{md.stem}'" + ) + + +def test_build_metadata_list_for_dir_recursive_alignment(provider, tmp_path): + """metadata list is aligned with index()'s rglob enumeration for nested dirs. + + This is a regression test for the bug where build_metadata_list_for_dir used + non-recursive iterdir() while index() used recursive rglob(), causing a count + mismatch and ValueError when add_metadata=True on a multi-level dataset. + """ + # Create a nested directory structure: + # tmp_path/ + # a.pdf (top-level file) + # sub/ + # b.pdf + # deep/ + # c.pdf + (tmp_path / "a.pdf").write_bytes(b"%PDF-1.4 fake") + (tmp_path / "sub").mkdir() + (tmp_path / "sub" / "b.pdf").write_bytes(b"%PDF-1.4 fake") + (tmp_path / "sub" / "deep").mkdir() + (tmp_path / "sub" / "deep" / "c.pdf").write_bytes(b"%PDF-1.4 fake") + + def simple_provider(path): + return {"stem": path.stem, "ext": path.suffix} + + md_list = build_metadata_list_for_dir(tmp_path, simple_provider) + + # index() would enumerate: a.pdf, sub/b.pdf, sub/deep/c.pdf (sorted by relative path) + expected_files = sorted( + (p for p in tmp_path.rglob("*") if p.is_file()), + key=lambda p: p.relative_to(tmp_path), + ) + assert len(md_list) == len(expected_files) == 3, ( + f"Expected 3 entries, got {len(md_list)}" + ) + for i, (expected_path, md) in enumerate(zip(expected_files, md_list)): + assert md is not None, f"Entry {i} ({expected_path.name}) should not be None" + assert md.stem == expected_path.stem, ( + f"Entry {i}: stem mismatch — expected '{expected_path.stem}', got '{md.stem}'" + ) + + diff --git a/tests/test_qdrant.py b/tests/test_qdrant.py new file mode 100644 index 0000000..8321049 --- /dev/null +++ b/tests/test_qdrant.py @@ -0,0 +1,445 @@ +""" +Tests for the Qdrant storage backend and the docling ingestion guard. + +Since Qdrant logic now lives in foretrieval/vector_store/qdrant.py, the +low-level tests (make_point_id, filter building, upsert shape, etc.) live in +test_vector_store_qdrant.py. This file retains the tests that exercise +ColPaliModel-level behaviour: storage_backend selection, index config +serialization, search dispatch, and the docling guard. + +The slow integration test (marked @pytest.mark.slow) is preserved unchanged +from the original test_qdrant.py. +""" + +from __future__ import annotations + +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + +from foretrieval.colpali import ColPaliModel, _DOCLING_AVAILABLE +from foretrieval.vector_store import LocalVectorStore, QdrantVectorStore, make_point_id +from foretrieval.vector_store.qdrant import _QDRANT_AVAILABLE + +DATA_DIR = Path(__file__).parent / "data" + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _make_mock_model(storage_backend: str = "qdrant") -> ColPaliModel: + """Build a ColPaliModel with all heavy attributes mocked away.""" + from foretrieval.vector_store import make_vector_store + model = MagicMock() + model.storage_backend = storage_backend + model.storage_qdrant = (storage_backend == "qdrant") + model.index_name = "test_index" + model.index_root = ".foretrieval_test" + model.doc_id_to_metadata = {} + model.doc_ids_to_file_names = {} + model.collection = {} + model.embed_id_to_extra = {} + model.device = "cpu" + model.verbose = 0 + model.processor = MagicMock() + model.full_document_collection = False + model.max_image_width = None + model.max_image_height = None + model.highest_doc_id = -1 + model.model_name = "stub" + model.vector_store = make_vector_store(storage_backend) + return model + + +# --------------------------------------------------------------------------- +# Docling ingestion guard +# --------------------------------------------------------------------------- + +class TestDoclingGuard: + def test_docling_not_available_raises_on_init(self): + """When docling is absent, requesting docling backend raises RuntimeError.""" + with ( + patch("foretrieval.colpali._DOCLING_AVAILABLE", False), + patch("foretrieval.colpali.ColPaliModel._load_model_and_processor"), + patch("foretrieval.colpali.ColPaliModel._load_index_state"), + ): + with pytest.raises(RuntimeError, match="docling"): + ColPaliModel( + pretrained_model_name_or_path="colqwen2-stub", + ingestion={"backend": "docling"}, + index_root="/tmp", + ) + + def test_docling_not_available_message_contains_install_hint(self): + """RuntimeError for missing docling contains pip install hint.""" + with ( + patch("foretrieval.colpali._DOCLING_AVAILABLE", False), + patch("foretrieval.colpali.ColPaliModel._load_model_and_processor"), + patch("foretrieval.colpali.ColPaliModel._load_index_state"), + ): + with pytest.raises(RuntimeError) as exc_info: + ColPaliModel( + pretrained_model_name_or_path="colqwen2-stub", + ingestion={"backend": "docling"}, + index_root="/tmp", + ) + assert "foretrieval[docling]" in str(exc_info.value) + + +# --------------------------------------------------------------------------- +# make_point_id (now in vector_store.base, still accessible publicly) +# --------------------------------------------------------------------------- + +class TestMakePointId: + def test_basic(self): + result = make_point_id(doc_id=1, page_id=2, chunk_id=3) + expected = 1 * 10_000_000 + 2 * 10_000 + 3 + assert result == expected + + def test_none_chunk_uses_zero(self): + result_none = make_point_id(doc_id=1, page_id=2, chunk_id=None) + result_zero = make_point_id(doc_id=1, page_id=2, chunk_id=0) + assert result_none == result_zero + + def test_uniqueness(self): + ids = { + make_point_id(d, p, c) + for d, p, c in [(0, 0, 0), (0, 0, 1), (0, 1, 0), (1, 0, 0)] + } + assert len(ids) == 4 + + def test_deterministic(self): + a = make_point_id(5, 3, 7) + b = make_point_id(5, 3, 7) + assert a == b + + +# --------------------------------------------------------------------------- +# QdrantVectorStore._build_filter (moved from ColPaliModel._build_qdrant_filter) +# --------------------------------------------------------------------------- + +@pytest.mark.skipif(not _QDRANT_AVAILABLE, reason="qdrant-client not installed") +class TestBuildQdrantFilter: + def _store(self) -> QdrantVectorStore: + store = QdrantVectorStore.__new__(QdrantVectorStore) + store._client = MagicMock() + store._collection_name = "test_col" + store._index_root = Path("/tmp") + return store + + def test_none_returns_none(self): + assert self._store()._build_filter(None) is None + + def test_empty_dict_returns_none(self): + assert self._store()._build_filter({}) is None + + def test_single_field_returns_filter(self): + from qdrant_client.models import Filter, FieldCondition + result = self._store()._build_filter({"ext": ".pdf"}) + assert isinstance(result, Filter) + assert len(result.must) == 1 + cond = result.must[0] + assert isinstance(cond, FieldCondition) + assert cond.key == "metadata.ext" + assert cond.match.value == ".pdf" + + def test_multiple_fields_returns_multiple_conditions(self): + from qdrant_client.models import Filter + result = self._store()._build_filter({"ext": ".pdf", "language": "en"}) + assert isinstance(result, Filter) + assert len(result.must) == 2 + keys = {c.key for c in result.must} + assert "metadata.ext" in keys + assert "metadata.language" in keys + + +# --------------------------------------------------------------------------- +# Search dispatch — now goes through vector_store.search() +# --------------------------------------------------------------------------- + +class TestSearchDispatch: + def test_zero_k_returns_empty_immediately(self): + """search() returns [] when k < 1 without calling any search backend.""" + mock = _make_mock_model("local") + mock._encode_search_query = MagicMock() + result = ColPaliModel.search(mock, query="test", k=0) + assert result == [] + mock._encode_search_query.assert_not_called() + + def test_search_delegates_to_vector_store(self): + """search() calls self.vector_store.search() regardless of backend.""" + mock = _make_mock_model("local") + mock.vector_store = MagicMock() + mock.vector_store.search.return_value = [] + mock._encode_search_query = MagicMock(return_value=[MagicMock()]) + ColPaliModel.search(mock, query="test", k=3) + mock.vector_store.search.assert_called_once() + + def test_search_qdrant_backend_uses_qdrant_store(self): + """ColPaliModel with qdrant backend delegates to QdrantVectorStore.""" + mock = _make_mock_model("qdrant") + assert isinstance(mock.vector_store, QdrantVectorStore) + + def test_search_local_backend_uses_local_store(self): + """ColPaliModel with local backend delegates to LocalVectorStore.""" + mock = _make_mock_model("local") + assert isinstance(mock.vector_store, LocalVectorStore) + + +# --------------------------------------------------------------------------- +# Empty filter → empty results (through LocalVectorStore) +# --------------------------------------------------------------------------- + +class TestEmptyFilterLocal: + def test_local_empty_filter_returns_empty_list(self, tmp_path): + """When LocalVectorStore metadata filter matches nothing, search returns [].""" + from foretrieval.vector_store.base import MultiVectorQuery + import torch + + store = LocalVectorStore() + store.open("idx", tmp_path, create=True) + # Empty metadata map → filter matches nothing + store.set_doc_id_to_metadata({}) + proc = MagicMock() + proc.score.return_value = torch.tensor([[]]) + store.set_processor(proc) + + q = MultiVectorQuery( + vectors=torch.rand(2, 8), + filter_metadata={"language": "fr"}, + ) + result = store.search(q, k=5) + assert result == [] + + def test_local_empty_filter_does_not_raise(self, tmp_path): + """search() must not raise ValueError when filter matches nothing.""" + from foretrieval.vector_store.base import MultiVectorQuery + import torch + + store = LocalVectorStore() + store.open("idx", tmp_path, create=True) + store.set_doc_id_to_metadata({}) + proc = MagicMock() + store.set_processor(proc) + + q = MultiVectorQuery( + vectors=torch.rand(2, 8), + filter_metadata={"language": "fr"}, + ) + try: + store.search(q, k=5) + except ValueError as exc: + pytest.fail(f"search raised ValueError on empty filter: {exc}") + + +# --------------------------------------------------------------------------- +# storage_backend serialization — index_config.json.gz +# --------------------------------------------------------------------------- + +class TestStorageBackendSerialization: + def test_qdrant_backend_written_to_config(self, tmp_path): + """_export_index writes storage_backend='qdrant'.""" + import srsly + mock = _make_mock_model("qdrant") + mock.index_root = str(tmp_path) + mock.index_name = "test_idx" + mock.full_document_collection = False + mock.highest_doc_id = 0 + mock.max_image_width = None + mock.max_image_height = None + mock.embed_id_to_extra = {} + mock.doc_ids_to_file_names = {} + mock.doc_id_to_metadata = {} + mock.collection = {} + mock.vector_store = MagicMock() + + ColPaliModel._export_index(mock) + + config = srsly.read_gzip_json(tmp_path / "test_idx" / "index_config.json.gz") + assert config["storage_backend"] == "qdrant" + + def test_local_backend_written_to_config(self, tmp_path): + """_export_index writes storage_backend='local'.""" + import srsly + mock = _make_mock_model("local") + mock.index_root = str(tmp_path) + mock.index_name = "test_idx" + mock.full_document_collection = False + mock.highest_doc_id = 0 + mock.max_image_width = None + mock.max_image_height = None + mock.embed_id_to_extra = {} + mock.doc_ids_to_file_names = {} + mock.doc_id_to_metadata = {} + mock.collection = {} + mock.vector_store = MagicMock() + + ColPaliModel._export_index(mock) + + config = srsly.read_gzip_json(tmp_path / "test_idx" / "index_config.json.gz") + assert config["storage_backend"] == "local" + + def test_milvus_backend_written_to_config(self, tmp_path): + """_export_index writes storage_backend='milvus'.""" + import srsly + mock = _make_mock_model("milvus") + mock.index_root = str(tmp_path) + mock.index_name = "test_idx" + mock.full_document_collection = False + mock.highest_doc_id = 0 + mock.max_image_width = None + mock.max_image_height = None + mock.embed_id_to_extra = {} + mock.doc_ids_to_file_names = {} + mock.doc_id_to_metadata = {} + mock.collection = {} + mock.vector_store = MagicMock() + + ColPaliModel._export_index(mock) + + config = srsly.read_gzip_json(tmp_path / "test_idx" / "index_config.json.gz") + assert config["storage_backend"] == "milvus" + + def test_from_index_reads_qdrant_backend(self, tmp_path): + """from_index() passes storage_backend='qdrant' when config says so.""" + import srsly, torch + + idx_path = tmp_path / "my_index" + idx_path.mkdir() + srsly.write_gzip_json( + idx_path / "index_config.json.gz", + { + "model_name": "vidore/colqwen2.5-v0.2", + "storage_backend": "qdrant", + "full_document_collection": False, + "highest_doc_id": 0, + "resize_stored_images": False, + "max_image_width": None, + "max_image_height": None, + }, + ) + torch.save({}, idx_path / "embed_id_to_extra.pt") + srsly.write_gzip_json(idx_path / "doc_ids_to_file_names.json.gz", {}) + srsly.write_gzip_json(idx_path / "metadata.json.gz", {}) + + with patch("foretrieval.colpali.ColPaliModel.__init__", return_value=None) as mock_init: + try: + ColPaliModel.from_index( + index_path=str(idx_path.name), + index_root=str(tmp_path), + device="cpu", + ) + except Exception: + pass + + if mock_init.called: + call_kwargs = mock_init.call_args.kwargs + assert call_kwargs.get("storage_backend") == "qdrant" + + def test_from_index_reads_local_backend(self, tmp_path): + """from_index() passes storage_backend='local' when config says so.""" + import srsly, torch + + idx_path = tmp_path / "local_index" + idx_path.mkdir() + srsly.write_gzip_json( + idx_path / "index_config.json.gz", + { + "model_name": "vidore/colqwen2.5-v0.2", + "storage_backend": "local", + "full_document_collection": False, + "highest_doc_id": 0, + "resize_stored_images": False, + "max_image_width": None, + "max_image_height": None, + }, + ) + torch.save({}, idx_path / "embed_id_to_extra.pt") + srsly.write_gzip_json(idx_path / "doc_ids_to_file_names.json.gz", {}) + srsly.write_gzip_json(idx_path / "metadata.json.gz", {}) + + with patch("foretrieval.colpali.ColPaliModel.__init__", return_value=None) as mock_init: + try: + ColPaliModel.from_index( + index_path=str(idx_path.name), + index_root=str(tmp_path), + device="cpu", + ) + except Exception: + pass + + if mock_init.called: + call_kwargs = mock_init.call_args.kwargs + assert call_kwargs.get("storage_backend") == "local" + + +# --------------------------------------------------------------------------- +# Qdrant optional-dependency guard (now on QdrantVectorStore) +# --------------------------------------------------------------------------- + +class TestQdrantOptionalDepGuard: + def test_missing_qdrant_raises_on_open(self, tmp_path): + """When qdrant-client is absent, QdrantVectorStore.open() raises RuntimeError.""" + with patch("foretrieval.vector_store.qdrant._QDRANT_AVAILABLE", False): + store = QdrantVectorStore() + with pytest.raises(RuntimeError, match="qdrant-client"): + store.open("idx", tmp_path, create=True, dim=8) + + def test_missing_qdrant_message_contains_install_hint(self, tmp_path): + with patch("foretrieval.vector_store.qdrant._QDRANT_AVAILABLE", False): + store = QdrantVectorStore() + with pytest.raises(RuntimeError) as exc_info: + store.open("idx", tmp_path, create=True, dim=8) + assert "foretrieval[qdrant]" in str(exc_info.value) + + +# --------------------------------------------------------------------------- +# Integration test — full Qdrant index + search (requires GPU + qdrant) +# --------------------------------------------------------------------------- + +@pytest.mark.slow +@pytest.mark.integration +def test_qdrant_index_and_search(tmp_path): + """Full index → from_index → search cycle using the Qdrant backend. + + Requires: + - A compatible GPU (CUDA sm_70+) + - qdrant-client installed (pip install foretrieval[qdrant]) + - test PDFs in tests/data/ + """ + if not _QDRANT_AVAILABLE: + pytest.skip("qdrant-client not installed; install with: pip install foretrieval[qdrant]") + + from foretrieval import MultiModalRetrieverModel + + retriever = MultiModalRetrieverModel.from_pretrained( + pretrained_model_name_or_path="vidore/colqwen2.5-v0.2", + index_root=str(tmp_path), + storage_backend="qdrant", + device=None, # auto-detect + verbose=0, + ) + + retriever.index( + input_path=str(DATA_DIR), + index_name="qdrant_integration_test", + store_collection_with_index=False, + overwrite=True, + ) + + retriever2 = MultiModalRetrieverModel.from_index( + index_path="qdrant_integration_test", + index_root=str(tmp_path), + device=None, + ) + + results = retriever2.search( + "maximum output current", k=1, return_base64_results=False + ) + + assert len(results) >= 1 + assert results[0].score is not None + assert results[0].doc_id is not None + assert results[0].page_num is not None diff --git a/tests/test_recursive_indexing.py b/tests/test_recursive_indexing.py new file mode 100644 index 0000000..0f92ba2 --- /dev/null +++ b/tests/test_recursive_indexing.py @@ -0,0 +1,280 @@ +"""Tests for recursive directory indexing in ColPaliModel. + +Verifies that index(), _process_directory(), and update_index_from_folder() +traverse subdirectories recursively using rglob instead of iterdir. + +No GPU or real model required — ColPaliModel is built with mocked loading +and _process_and_add_to_index is patched to avoid actual embedding work. +""" +from __future__ import annotations + +from pathlib import Path +from unittest.mock import MagicMock, patch, call + +import pytest + +from foretrieval.vector_store import make_vector_store + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _make_model(tmp_path: Path, storage_backend: str = "local"): + """Build a minimal ColPaliModel with GPU/model loading mocked.""" + from foretrieval.colpali import ColPaliModel + + model = ColPaliModel.__new__(ColPaliModel) + model.pretrained_model_name_or_path = "vidore/colpali-v1.2-test" + model.model_name = "vidore/colpali-v1.2-test" + model.verbose = 0 + model.load_from_index = False + model.index_root = str(tmp_path / "index_root") + model.index_name = None + model.kwargs = {} + model.storage_backend = storage_backend + model.storage_config = {} + model._storage_qdrant_compat = False + model.ingestion = {"backend": "default"} + model.ingestion_backend = "default" + model.n_gpu = 0 + model.device = "cpu" + model.load_in_4bit = False + model.load_in_8bit = False + model.bnb_4bit_quant_type = "nf4" + model.bnb_4bit_compute_dtype = "float16" + model.collection = {} + model.embed_id_to_extra = {} + model.doc_id_to_metadata = {} + model.doc_ids_to_file_names = {} + model.doc_ids = set() + model.enable_heatmaps = False + model.enable_circle = False + model.full_document_collection = False + model.resize_stored_images = False + model.max_image_width = None + model.max_image_height = None + model.highest_doc_id = -1 + model.docling_dir = None + model.SOURCE_EXTS = {".docx", ".txt", ".png", ".jpg"} + model.IMAGE_EXTS = {".png", ".jpg", ".jpeg"} + model._remote_client = None + model.model = None + model.processor = MagicMock() + model.vector_store = make_vector_store(storage_backend, {}) + return model + + +def _make_nested_corpus(base: Path) -> list[Path]: + """Create a nested directory of dummy files and return their paths sorted.""" + # base/ + # a.pdf + # sub1/ + # b.pdf + # sub2/ + # c.pdf + (base / "sub1" / "sub2").mkdir(parents=True) + files = [ + base / "a.pdf", + base / "sub1" / "b.pdf", + base / "sub1" / "sub2" / "c.pdf", + ] + for f in files: + f.write_bytes(b"%PDF-1.4 dummy") + return sorted(files, key=lambda p: p.relative_to(base)) + + +# --------------------------------------------------------------------------- +# index() — recursive traversal +# --------------------------------------------------------------------------- + +class TestIndexRecursive: + def test_index_visits_all_nested_files(self, tmp_path): + """index() must find files in subdirectories via rglob.""" + corpus = tmp_path / "corpus" + corpus.mkdir() + expected_files = _make_nested_corpus(corpus) + + model = _make_model(tmp_path) + visited = [] + + def fake_process(item, *args, **kwargs): + visited.append(Path(item)) + model.doc_ids_to_file_names[kwargs.get("doc_id", len(visited))] = str(item) + model.doc_ids.add(kwargs.get("doc_id", len(visited))) + model.highest_doc_id = max(model.highest_doc_id, kwargs.get("doc_id", len(visited))) + return item + + with ( + patch.object(model, "_process_and_add_to_index", side_effect=fake_process), + patch.object(model, "_export_index"), + patch.object(model.vector_store, "open"), + patch.object(model.vector_store, "set_processor", create=True), + patch.object(model.vector_store, "set_doc_id_to_metadata", create=True), + ): + model.index( + input_path=corpus, + index_name="nested_test", + overwrite=False, + ) + + assert sorted(visited, key=lambda p: p.relative_to(corpus)) == expected_files + + def test_index_flat_dir_unchanged_behavior(self, tmp_path): + """Flat directory: rglob and old iterdir produce identical file sets.""" + corpus = tmp_path / "flat" + corpus.mkdir() + files = [] + for name in ["x.pdf", "y.pdf", "z.pdf"]: + f = corpus / name + f.write_bytes(b"%PDF-1.4 dummy") + files.append(f) + files = sorted(files, key=lambda p: p.relative_to(corpus)) + + model = _make_model(tmp_path) + visited = [] + + def fake_process(item, *args, **kwargs): + visited.append(Path(item)) + model.doc_ids_to_file_names[kwargs.get("doc_id", len(visited))] = str(item) + model.doc_ids.add(kwargs.get("doc_id", len(visited))) + model.highest_doc_id = max(model.highest_doc_id, kwargs.get("doc_id", len(visited))) + return item + + with ( + patch.object(model, "_process_and_add_to_index", side_effect=fake_process), + patch.object(model, "_export_index"), + patch.object(model.vector_store, "open"), + patch.object(model.vector_store, "set_processor", create=True), + patch.object(model.vector_store, "set_doc_id_to_metadata", create=True), + ): + model.index(input_path=corpus, index_name="flat_test", overwrite=False) + + assert sorted(visited, key=lambda p: p.relative_to(corpus)) == files + + def test_index_overwrite_false_no_existing_index_creates_new(self, tmp_path): + """overwrite=False with no existing index directory must proceed normally.""" + corpus = tmp_path / "corpus" + corpus.mkdir() + (corpus / "doc.pdf").write_bytes(b"%PDF-1.4 dummy") + + model = _make_model(tmp_path) + visited = [] + + def fake_process(item, *args, **kwargs): + visited.append(Path(item)) + model.doc_ids_to_file_names[kwargs.get("doc_id", 0)] = str(item) + model.doc_ids.add(kwargs.get("doc_id", 0)) + model.highest_doc_id = max(model.highest_doc_id, kwargs.get("doc_id", 0)) + return item + + with ( + patch.object(model, "_process_and_add_to_index", side_effect=fake_process), + patch.object(model, "_export_index"), + patch.object(model.vector_store, "open"), + patch.object(model.vector_store, "set_processor", create=True), + patch.object(model.vector_store, "set_doc_id_to_metadata", create=True), + ): + # Must NOT raise and must NOT return None + result = model.index( + input_path=corpus, + index_name="brand_new_index", + overwrite=False, + ) + + assert result is not None, "index() returned None — did not create a new index" + assert len(visited) == 1 + + +# --------------------------------------------------------------------------- +# _process_directory() — recursive traversal +# --------------------------------------------------------------------------- + +class TestProcessDirectoryRecursive: + def test_process_directory_visits_nested_files(self, tmp_path): + """_process_directory() must recurse into subdirectories.""" + directory = tmp_path / "docs" + directory.mkdir() + expected_files = _make_nested_corpus(directory) + + model = _make_model(tmp_path) + model.index_name = "test_index" + visited = [] + + def fake_process(item, *args, **kwargs): + visited.append(Path(item)) + return item + + with patch.object(model, "_process_and_add_to_index", side_effect=fake_process): + model._process_directory( + directory=directory, + store_collection_with_index=False, + base_doc_id=0, + metadata=None, + batch_size=1, + ) + + assert sorted(visited, key=lambda p: p.relative_to(directory)) == expected_files + + +# --------------------------------------------------------------------------- +# update_index_from_folder() — recursive traversal +# --------------------------------------------------------------------------- + +class TestUpdateIndexFromFolderRecursive: + def test_update_visits_nested_new_files(self, tmp_path): + """update_index_from_folder() must recurse into subdirectories.""" + folder = tmp_path / "docs" + folder.mkdir() + expected_files = _make_nested_corpus(folder) + + model = _make_model(tmp_path) + model.index_name = "test_index" + model.doc_ids_to_file_names = {} + model.doc_ids = set() + model.highest_doc_id = -1 + visited = [] + + def fake_process(item, *args, **kwargs): + visited.append(Path(item)) + model.doc_ids_to_file_names[kwargs.get("doc_id", len(visited))] = str(item) + model.doc_ids.add(kwargs.get("doc_id", len(visited))) + model.highest_doc_id = max(model.highest_doc_id, kwargs.get("doc_id", len(visited))) + return item + + with ( + patch.object(model, "_process_and_add_to_index", side_effect=fake_process), + patch.object(model, "_export_index"), + ): + model.update_index_from_folder(folder=folder) + + assert sorted(visited, key=lambda p: p.relative_to(folder)) == expected_files + + def test_update_skips_already_indexed_nested_files(self, tmp_path): + """update_index_from_folder() must skip already-indexed files in subdirs.""" + folder = tmp_path / "docs" + folder.mkdir() + (folder / "sub").mkdir() + already_indexed = folder / "sub" / "old.pdf" + new_file = folder / "sub" / "new.pdf" + already_indexed.write_bytes(b"%PDF-1.4 old") + new_file.write_bytes(b"%PDF-1.4 new") + + model = _make_model(tmp_path) + model.index_name = "test_index" + model.doc_ids_to_file_names = {0: str(already_indexed.resolve())} + model.doc_ids = {0} + model.highest_doc_id = 0 + visited = [] + + def fake_process(item, *args, **kwargs): + visited.append(Path(item)) + return item + + with ( + patch.object(model, "_process_and_add_to_index", side_effect=fake_process), + patch.object(model, "_export_index"), + ): + model.update_index_from_folder(folder=folder) + + assert visited == [new_file], f"Expected only new.pdf, got {visited}" diff --git a/tests/test_ssh_utils.py b/tests/test_ssh_utils.py new file mode 100644 index 0000000..f559273 --- /dev/null +++ b/tests/test_ssh_utils.py @@ -0,0 +1,182 @@ +"""Tests for foretrieval.ssh_utils.open_ssh_client. + +All tests mock paramiko.SSHClient.connect so nothing touches the network. +The goal is to confirm that ~/.ssh/config directives are honoured the +same way as the OpenSSH CLI. +""" + +from __future__ import annotations + +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + + +# --------------------------------------------------------------------------- +# Fixtures / helpers +# --------------------------------------------------------------------------- + +def _write_ssh_config(tmp_path: Path, content: str) -> Path: + """Write an SSH config to a temp file and return its path.""" + p = tmp_path / "ssh_config" + p.write_text(content) + return p + + +def _captured_connect_kwargs(mock_client_class): + """Return the kwargs of the most recent SSHClient.connect call.""" + instance = mock_client_class.return_value + return instance.connect.call_args.kwargs + + +# --------------------------------------------------------------------------- +# Basic resolution +# --------------------------------------------------------------------------- + +class TestHostAliasResolution: + def test_alias_resolves_to_hostname(self, tmp_path): + cfg = _write_ssh_config(tmp_path, """ +Host pf01 + HostName pf01.example.com + User alice + Port 2222 +""") + from foretrieval import ssh_utils + with patch("paramiko.SSHClient") as MockClient: + ssh_utils.open_ssh_client("pf01", ssh_config_path=cfg) + kw = _captured_connect_kwargs(MockClient) + assert kw["hostname"] == "pf01.example.com" + assert kw["port"] == 2222 + assert kw["username"] == "alice" + + def test_no_config_falls_back_to_direct(self, tmp_path): + """Missing config file → direct connect with the raw host string.""" + from foretrieval import ssh_utils + with patch("paramiko.SSHClient") as MockClient: + ssh_utils.open_ssh_client( + "real.example.com", ssh_config_path=tmp_path / "missing" + ) + kw = _captured_connect_kwargs(MockClient) + assert kw["hostname"] == "real.example.com" + assert kw["port"] == 22 + + def test_no_match_falls_back_to_direct(self, tmp_path): + cfg = _write_ssh_config(tmp_path, """ +Host someotheralias + HostName irrelevant +""") + from foretrieval import ssh_utils + with patch("paramiko.SSHClient") as MockClient: + ssh_utils.open_ssh_client("pf01", ssh_config_path=cfg) + kw = _captured_connect_kwargs(MockClient) + assert kw["hostname"] == "pf01" + + +# --------------------------------------------------------------------------- +# Override precedence +# --------------------------------------------------------------------------- + +class TestOverrides: + def test_user_override_beats_ssh_config(self, tmp_path): + cfg = _write_ssh_config(tmp_path, """ +Host pf01 + HostName pf01.example.com + User alice +""") + from foretrieval import ssh_utils + with patch("paramiko.SSHClient") as MockClient: + ssh_utils.open_ssh_client( + "pf01", ssh_user="override_user", ssh_config_path=cfg, + ) + kw = _captured_connect_kwargs(MockClient) + assert kw["username"] == "override_user" + + def test_key_path_override_beats_identityfile(self, tmp_path): + cfg = _write_ssh_config(tmp_path, """ +Host pf01 + HostName pf01.example.com + IdentityFile ~/.ssh/should_be_ignored +""") + from foretrieval import ssh_utils + with patch("paramiko.SSHClient") as MockClient: + ssh_utils.open_ssh_client( + "pf01", + ssh_key_path="/explicit/key/path", + ssh_config_path=cfg, + ) + kw = _captured_connect_kwargs(MockClient) + assert kw["key_filename"] == "/explicit/key/path" + + +# --------------------------------------------------------------------------- +# ProxyCommand / ProxyJump +# --------------------------------------------------------------------------- + +class TestProxy: + def test_proxycommand_creates_sock(self, tmp_path): + cfg = _write_ssh_config(tmp_path, """ +Host pf01 + HostName pf01.example.com + User alice + ProxyCommand ssh -W %h:%p bastion +""") + from foretrieval import ssh_utils + with patch("paramiko.SSHClient") as MockClient, \ + patch("paramiko.ProxyCommand") as MockPC: + ssh_utils.open_ssh_client("pf01", ssh_config_path=cfg) + # ProxyCommand instance was constructed and passed as 'sock' + kw = _captured_connect_kwargs(MockClient) + assert "sock" in kw + # The expanded command was used + MockPC.assert_called_once() + expanded_cmd = MockPC.call_args.args[0] + assert "pf01.example.com" in expanded_cmd + assert ":22" in expanded_cmd + assert "bastion" in expanded_cmd + + def test_proxyjump_translates_to_proxycommand(self, tmp_path): + cfg = _write_ssh_config(tmp_path, """ +Host pf01 + HostName pf01.example.com + ProxyJump bastion.example.com +""") + from foretrieval import ssh_utils + with patch("paramiko.SSHClient") as MockClient, \ + patch("paramiko.ProxyCommand") as MockPC: + ssh_utils.open_ssh_client("pf01", ssh_config_path=cfg) + kw = _captured_connect_kwargs(MockClient) + assert "sock" in kw + MockPC.assert_called_once() + cmd = MockPC.call_args.args[0] + assert "ssh -W" in cmd + assert "bastion.example.com" in cmd + + def test_no_proxy_no_sock(self, tmp_path): + cfg = _write_ssh_config(tmp_path, """ +Host pf01 + HostName pf01.example.com +""") + from foretrieval import ssh_utils + with patch("paramiko.SSHClient") as MockClient: + ssh_utils.open_ssh_client("pf01", ssh_config_path=cfg) + kw = _captured_connect_kwargs(MockClient) + assert "sock" not in kw + + +# --------------------------------------------------------------------------- +# Failure modes +# --------------------------------------------------------------------------- + +class TestFailureModes: + def test_malformed_ssh_config_falls_back_to_direct(self, tmp_path): + cfg = _write_ssh_config(tmp_path, "this is not valid ssh config syntax{{{") + # paramiko.SSHConfig is permissive — write something that *does* + # crash its parser. Easiest: simulate the parse exception. + from foretrieval import ssh_utils + with patch("paramiko.SSHConfig") as MockCfg, \ + patch("paramiko.SSHClient") as MockClient: + MockCfg.return_value.parse.side_effect = RuntimeError("bad syntax") + ssh_utils.open_ssh_client("pf01", ssh_config_path=cfg) + kw = _captured_connect_kwargs(MockClient) + assert kw["hostname"] == "pf01" diff --git a/tests/test_task10_bugfixes.py b/tests/test_task10_bugfixes.py new file mode 100644 index 0000000..df7df6b --- /dev/null +++ b/tests/test_task10_bugfixes.py @@ -0,0 +1,296 @@ +"""Regression tests for task_10 bug fixes. + +Bug 1: build_metadata_list_for_dir recursive alignment — covered in test_metadata_no_ai.py +Bug 2: cleanup on index failure +Bug 3: doc_ids/highest_doc_id derived from doc_ids_to_file_names (not doc_id_to_metadata) +Bug 4: matplotlib.colormaps replaces deprecated cm.get_cmap +""" +from __future__ import annotations + +import shutil +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _make_model(storage_backend: str = "local", index_root: str = "/tmp/foretrieval_test"): + """Minimal ColPaliModel with GPU / model loading patched out.""" + with ( + patch("foretrieval.colpali.ColPaliModel._load_model_and_processor"), + patch("foretrieval.colpali.ColPaliModel._load_processor_only"), + ): + from foretrieval.colpali import ColPaliModel + from foretrieval.vector_store.local import LocalVectorStore + + model = ColPaliModel.__new__(ColPaliModel) + model.pretrained_model_name_or_path = "vidore/colpali-v1.2-test" + model.model_name = "vidore/colpali-v1.2-test" + model.verbose = 0 + model.load_from_index = False + model.index_root = index_root + model.index_name = None + model.kwargs = {} + model.storage_backend = storage_backend + model.storage_config = {} + model._storage_qdrant_compat = False + model.ingestion = {"backend": "default"} + model.ingestion_backend = "default" + model.n_gpu = 0 + model.device = "cpu" + model.load_in_4bit = False + model.load_in_8bit = False + model.bnb_4bit_quant_type = "nf4" + model.bnb_4bit_compute_dtype = "float16" + model.collection = {} + model.embed_id_to_extra = {} + model.doc_id_to_metadata = {} + model.doc_ids_to_file_names = {} + model.doc_ids = set() + model.enable_heatmaps = False + model.enable_circle = False + model.full_document_collection = False + model.resize_stored_images = False + model.max_image_width = None + model.max_image_height = None + model.highest_doc_id = -1 + model.docling_dir = None + model.SOURCE_EXTS = set() + model.IMAGE_EXTS = set() + model._remote_client = None + model.model = None + model.processor = MagicMock() + model.vector_store = LocalVectorStore() + model.index_description = "" + return model + + +# --------------------------------------------------------------------------- +# Bug 3: load without metadata yields correct doc_ids / highest_doc_id +# --------------------------------------------------------------------------- + +class TestLoadWithoutMetadata: + """Regression: doc_ids / highest_doc_id are derived from doc_ids_to_file_names.""" + + def _build_index_dir(self, tmp_path: Path, with_metadata: bool) -> Path: + """Write minimal index sidecars to tmp_path.""" + import srsly, torch + + idx = tmp_path / "myindex" + idx.mkdir() + + # index_config.json.gz + srsly.write_gzip_json(idx / "index_config.json.gz", { + "model_name": "vidore/colpali-v1.2-test", + "full_document_collection": False, + "highest_doc_id": 2, + "resize_stored_images": False, + "max_image_width": None, + "max_image_height": None, + "library_version": "0.0.0", + "storage_backend": "local", + "storage_config": None, + "description": "", + }) + + # doc_ids_to_file_names.json.gz — ALWAYS present + srsly.write_gzip_json(idx / "doc_ids_to_file_names.json.gz", { + "0": "/data/doc_a.pdf", + "1": "/data/doc_b.pdf", + "2": "/data/doc_c.pdf", + }) + + # embed_id_to_doc_id.json.gz + srsly.write_gzip_json(idx / "embed_id_to_doc_id.json.gz", {}) + + # embed_id_to_extra.pt + torch.save({}, idx / "embed_id_to_extra.pt") + + if with_metadata: + srsly.write_gzip_json(idx / "metadata.json.gz", { + "0": {"stem": "doc_a", "ext": ".pdf"}, + "1": {"stem": "doc_b", "ext": ".pdf"}, + "2": {"stem": "doc_c", "ext": ".pdf"}, + }) + # No metadata.json.gz when with_metadata=False + + return idx + + def test_load_without_metadata_doc_ids_correct(self, tmp_path): + """doc_ids is populated from doc_ids_to_file_names when metadata absent.""" + idx = self._build_index_dir(tmp_path, with_metadata=False) + model = _make_model(index_root=str(tmp_path)) + + model._load_local_sidecars(idx) + + # Simulate _load_index_state final derivation + id_source = model.doc_ids_to_file_names or model.doc_id_to_metadata + model.highest_doc_id = max(id_source.keys(), default=-1) + model.doc_ids = set(id_source.keys()) + + assert model.doc_ids == {0, 1, 2}, ( + f"Expected {{0, 1, 2}}, got {model.doc_ids}" + ) + assert model.highest_doc_id == 2, ( + f"Expected 2, got {model.highest_doc_id}" + ) + assert model.doc_id_to_metadata == {}, "Metadata should be empty" + + def test_load_with_metadata_doc_ids_correct(self, tmp_path): + """doc_ids is populated correctly when metadata present.""" + idx = self._build_index_dir(tmp_path, with_metadata=True) + model = _make_model(index_root=str(tmp_path)) + + model._load_local_sidecars(idx) + + id_source = model.doc_ids_to_file_names or model.doc_id_to_metadata + model.highest_doc_id = max(id_source.keys(), default=-1) + model.doc_ids = set(id_source.keys()) + + assert model.doc_ids == {0, 1, 2} + assert model.highest_doc_id == 2 + + def test_load_local_sidecars_no_metadata(self, tmp_path): + """_load_local_sidecars: doc_id_to_metadata is {} and doc_ids_to_file_names populated.""" + idx = self._build_index_dir(tmp_path, with_metadata=False) + model = _make_model(index_root=str(tmp_path)) + + model._load_local_sidecars(idx) + + assert model.doc_id_to_metadata == {} + assert set(model.doc_ids_to_file_names.keys()) == {0, 1, 2} + + def test_apply_bookkeeping_blob_without_metadata(self): + """Remote path: _apply_bookkeeping_blob derives doc_ids from doc_ids_to_file_names.""" + model = _make_model(storage_backend="remote") + + blob = { + "index_config": { + "model_name": "vidore/colpali-v1.2-test", + "full_document_collection": False, + "highest_doc_id": 2, + "resize_stored_images": False, + "max_image_width": None, + "max_image_height": None, + "description": "", + }, + "embed_id_to_extra": {}, + "doc_ids_to_file_names": { + "0": "/data/doc_a.pdf", + "1": "/data/doc_b.pdf", + "2": "/data/doc_c.pdf", + }, + "doc_id_to_metadata": {}, # empty: index built without add_metadata + } + + model._apply_bookkeeping_blob(blob) + + assert model.doc_ids == {0, 1, 2}, ( + f"Expected {{0, 1, 2}}, got {model.doc_ids}" + ) + assert model.highest_doc_id == 2 + + +# --------------------------------------------------------------------------- +# Bug 2: cleanup on index failure +# --------------------------------------------------------------------------- + +class TestIndexCleanup: + """Regression: _cleanup_failed_index removes partial artefacts.""" + + def test_cleanup_removes_local_index_dir(self, tmp_path): + """_cleanup_failed_index removes the local index directory.""" + index_name = "partial_index" + index_root = tmp_path + index_path = index_root / index_name + index_path.mkdir() + (index_path / "index_config.json.gz").write_bytes(b"fake") + + model = _make_model(index_root=str(index_root)) + model.index_name = index_name + model.highest_doc_id = 0 + model.doc_ids = {0} + + model._cleanup_failed_index(index_name) + + assert not index_path.exists(), "Partial index directory should be removed" + assert model.index_name is None + assert model.highest_doc_id == -1 + assert model.doc_ids == set() + + def test_cleanup_tolerates_missing_dir(self, tmp_path): + """_cleanup_failed_index does not raise if the directory doesn't exist.""" + model = _make_model(index_root=str(tmp_path)) + # Should not raise + model._cleanup_failed_index("nonexistent_index") + + def test_index_raises_cleans_up_on_metadata_mismatch(self, tmp_path): + """index() cleans up when metadata list length doesn't match file count.""" + from foretrieval.models_metadata import DocMetadata + from foretrieval.vector_store.local import LocalVectorStore + + # Create a nested data directory with 2 files + data_dir = tmp_path / "docs" + data_dir.mkdir() + (data_dir / "a.pdf").write_bytes(b"%PDF") + subdir = data_dir / "sub" + subdir.mkdir() + (subdir / "b.pdf").write_bytes(b"%PDF") + + model = _make_model(index_root=str(tmp_path)) + model.vector_store = LocalVectorStore() + + # Provide metadata with wrong length (1 instead of 2) + wrong_metadata = [DocMetadata(stem="only_one", ext=".pdf")] + + with pytest.raises(ValueError, match="metadata entries"): + model.index( + input_path=str(data_dir), + index_name="test_cleanup", + metadata=wrong_metadata, + ) + + # Index directory should be cleaned up + index_path = tmp_path / "test_cleanup" + assert not index_path.exists(), ( + "Partial index directory should be removed after failed indexing" + ) + + +# --------------------------------------------------------------------------- +# Bug 4: matplotlib.colormaps replaces deprecated cm.get_cmap +# --------------------------------------------------------------------------- + +class TestHeatmapColormap: + """Regression: heatmap_overlay_base64 works with matplotlib >= 3.9.""" + + def test_heatmap_overlay_base64_returns_string(self): + """heatmap_overlay_base64 returns a base64 string without AttributeError.""" + import numpy as np + from PIL import Image + from foretrieval.plot_utils import heatmap_overlay_base64 + + img = Image.fromarray( + (np.ones((32, 32, 3)) * 200).astype("uint8") + ) + heat = (np.ones((32, 32)) * 128).astype("uint8") + result = heatmap_overlay_base64(img, heat, cmap="jet") + assert isinstance(result, str) and len(result) > 0 + + def test_heatmap_overlay_base64_different_cmaps(self): + """Several colormaps all work without AttributeError.""" + import numpy as np + from PIL import Image + from foretrieval.plot_utils import heatmap_overlay_base64 + + img = Image.fromarray( + (np.ones((16, 16, 3)) * 100).astype("uint8") + ) + heat = (np.ones((16, 16)) * 50).astype("uint8") + for cmap in ("jet", "viridis", "plasma", "hot"): + result = heatmap_overlay_base64(img, heat, cmap=cmap) + assert isinstance(result, str), f"cmap={cmap} returned non-string" diff --git a/tests/test_task11_features.py b/tests/test_task11_features.py new file mode 100644 index 0000000..1a24eda --- /dev/null +++ b/tests/test_task11_features.py @@ -0,0 +1,130 @@ +"""Unit tests for task_11 Feature 3: circle center alignment with heatmap. + +Verifies that draw_circle_on_max_patch applies patch growth before argmax +so the circle center aligns with the same heat region as heatmap_overlay_base64. +""" +from __future__ import annotations + +import numpy as np +import torch +import pytest +from PIL import Image + +from foretrieval.plot_utils import ( + draw_circle_on_max_patch, + grow_heatmap_patches_torch, + heatmap_overlay_base64, +) + + +def _synthetic_image(w: int = 100, h: int = 80) -> Image.Image: + """Create a plain grey synthetic image.""" + arr = np.full((h, w, 3), 128, dtype=np.uint8) + return Image.fromarray(arr, "RGB") + + +def _heat_with_peak(Hp: int, Wp: int, peak_r: int, peak_c: int) -> torch.Tensor: + """Create a heat grid with a clear peak at (peak_r, peak_c).""" + heat = torch.zeros(Hp, Wp) + heat[peak_r, peak_c] = 10.0 + return heat + + +class TestCircleGrowthAlignment: + """draw_circle_on_max_patch with patch_grow_pct=300,grow_mode='mean' + should find the same argmax as the grown heat used by heatmap_overlay_base64. + """ + + def test_grow_heatmap_patches_torch_no_op_at_100(self): + heat = torch.tensor([[1.0, 0.5], [0.2, 0.8]]) + out = grow_heatmap_patches_torch(heat, patch_grow_pct=100.0) + assert torch.allclose(out, heat) + + def test_grow_heatmap_patches_torch_grow_mean(self): + heat = torch.zeros(5, 5) + heat[2, 2] = 10.0 + grown = grow_heatmap_patches_torch(heat, patch_grow_pct=300.0, grow_mode="mean") + # 300% → radius=2 → 5×5 avg_pool. Peak should spread. + assert grown.shape == (5, 5) + assert grown.max() < 10.0 # mean dilutes the peak + assert grown[2, 2] > 0.0 # center still non-zero + + def test_circle_argmax_matches_grown_heat_argmax(self): + """When peak is clear, circle should center on same patch as heatmap peak.""" + Hp, Wp = 8, 8 + # Put strong peak at (3,5) + heat = _heat_with_peak(Hp, Wp, peak_r=3, peak_c=5) + + # Grown heat argmax + grown = grow_heatmap_patches_torch(heat.clone(), patch_grow_pct=300.0, grow_mode="mean") + flat_grown = int(torch.argmax(grown.flatten())) + r_grown = flat_grown // Wp + c_grown = flat_grown % Wp + + # draw_circle_on_max_patch with same grow + img = _synthetic_image() + W, H = img.size + patch_w = W / float(Wp) + patch_h = H / float(Hp) + expected_cx = (c_grown + 0.5) * patch_w + expected_cy = (r_grown + 0.5) * patch_h + + # Verify grow function defined before draw_circle_on_max_patch (import order) + from foretrieval import plot_utils + import inspect + src = inspect.getsource(plot_utils) + grow_pos = src.index("def grow_heatmap_patches_torch") + circle_pos = src.index("def draw_circle_on_max_patch") + assert grow_pos < circle_pos, "grow_heatmap_patches_torch must be defined before draw_circle_on_max_patch" + + def test_draw_circle_returns_rgb_image(self): + img = _synthetic_image() + heat = _heat_with_peak(8, 8, 3, 5) + result = draw_circle_on_max_patch(img, heat, patch_grow_pct=300.0, grow_mode="mean") + assert isinstance(result, Image.Image) + assert result.mode == "RGB" + assert result.size == img.size + + def test_draw_circle_no_grow_still_works(self): + """Default grow (100%) should work as before.""" + img = _synthetic_image() + heat = _heat_with_peak(8, 8, 3, 5) + result = draw_circle_on_max_patch(img, heat) + assert isinstance(result, Image.Image) + assert result.mode == "RGB" + + def test_draw_circle_modified_image(self): + """Circle is actually drawn — result differs from input.""" + img = _synthetic_image() + heat = _heat_with_peak(8, 8, 3, 5) + result = draw_circle_on_max_patch(img, heat, patch_grow_pct=300.0, grow_mode="mean") + assert np.array(result).shape == np.array(img).shape + # Image should differ (circle pixels added) + diff = np.abs(np.array(result).astype(float) - np.array(img).astype(float)) + assert diff.max() > 0, "Result should differ from input (circle drawn)" + + +class TestHeatmapAndCircleConsistency: + """Ensure circle and heatmap use the same heat processing path.""" + + def test_heatmap_overlay_returns_nonempty_base64(self): + img = _synthetic_image() + heat = _heat_with_peak(8, 8, 3, 5) + b64 = heatmap_overlay_base64( + img, heat, patch_grow_pct=300.0, grow_mode="mean" + ) + assert isinstance(b64, str) + assert len(b64) > 100 + + def test_colpali_circle_call_uses_grow_params(self): + """Smoke-test that the colpali call site now passes grow params. + + We can't import ColPaliModel without GPU weights, but we can verify + the source code of colpali.py contains the new call signature. + """ + from pathlib import Path + colpali_src = (Path(__file__).parent.parent / "foretrieval" / "colpali.py").read_text() + assert "patch_grow_pct=300.0" in colpali_src, \ + "colpali.py circle call site should pass patch_grow_pct=300.0" + assert "grow_mode=\"mean\"" in colpali_src, \ + "colpali.py circle call site should pass grow_mode='mean'" diff --git a/tests/test_vector_db_server_admin.py b/tests/test_vector_db_server_admin.py new file mode 100644 index 0000000..5b4a698 --- /dev/null +++ b/tests/test_vector_db_server_admin.py @@ -0,0 +1,266 @@ +"""Tests for the vector-DB server admin endpoints. + +Covers: +- /v1/admin/indexes +- /v1/admin/data_folders +- 60 s TTL cache + invalidation on data-plane writes +- Auth gating (inherits the shared middleware) +- Symlink jail / containment +""" + +from __future__ import annotations + +import json +import os +import tempfile +import time +from pathlib import Path + +import pytest +from fastapi.testclient import TestClient + +# Patch FOR_DB_DATA_DIR before importing the server (mirrors the existing +# test_vector_db_server_app.py pattern). +_TMP = tempfile.mkdtemp(prefix="foretrieval_db_admin_test_") + +import foretrieval.vector_db_server.server as _server_mod + +_server_mod._DATA_DIR = Path(_TMP) +_server_mod._API_KEY = None +_server_mod._registry.clear() +_server_mod._locks.clear() +_server_mod._SIZE_CACHE.clear() + +from foretrieval.vector_db_server.server import app # noqa: E402 + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _mk_index_dir(name: str, n_files: int = 3, size_per_file: int = 100) -> Path: + """Create a fake index directory under _DATA_DIR.""" + p = _server_mod._DATA_DIR / name + p.mkdir(parents=True, exist_ok=True) + # Mark as an index by adding the server-side sentinel (written by _write_meta). + (p / "index.json").write_text('{"backend": "qdrant", "storage_config": null}') + for i in range(n_files): + (p / f"data_{i}.bin").write_bytes(b"x" * size_per_file) + return p + + +def _mk_data_dir(name: str, n_files: int = 2, size_per_file: int = 50) -> Path: + """Create a plain folder (NOT an index) under _DATA_DIR.""" + p = _server_mod._DATA_DIR / name + p.mkdir(parents=True, exist_ok=True) + for i in range(n_files): + (p / f"file_{i}.bin").write_bytes(b"y" * size_per_file) + return p + + +@pytest.fixture(autouse=True) +def _clean_state(): + _server_mod._registry.clear() + _server_mod._locks.clear() + _server_mod._SIZE_CACHE.clear() + # Wipe data dir between tests + if _server_mod._DATA_DIR.exists(): + for entry in list(_server_mod._DATA_DIR.iterdir()): + if entry.is_dir(): + import shutil + shutil.rmtree(entry, ignore_errors=True) + else: + entry.unlink(missing_ok=True) + yield + _server_mod._registry.clear() + _server_mod._locks.clear() + _server_mod._SIZE_CACHE.clear() + + +@pytest.fixture() +def client(): + return TestClient(app) + + +# --------------------------------------------------------------------------- +# /v1/admin/indexes +# --------------------------------------------------------------------------- + +class TestAdminIndexes: + def test_empty_data_dir(self, client): + # Nothing under data_dir + r = client.get("/v1/admin/indexes") + assert r.status_code == 200 + payload = r.json() + assert payload["items"] == [] + assert payload["count"] == 0 + assert payload["data_dir"] == str(_server_mod._DATA_DIR) + + def test_lists_index_dirs(self, client): + _mk_index_dir("alpha", n_files=4, size_per_file=200) + _mk_index_dir("beta", n_files=1, size_per_file=10) + # A data-only folder should NOT appear in /indexes + _mk_data_dir("gamma_data") + + r = client.get("/v1/admin/indexes") + assert r.status_code == 200 + items = {it["name"]: it for it in r.json()["items"]} + assert set(items) == {"alpha", "beta"} + assert items["alpha"]["n_files"] >= 4 # data files + index_config marker + assert items["alpha"]["size_bytes"] > 0 + assert items["beta"]["size_bytes"] > 0 + assert "modified" in items["alpha"] + + +# --------------------------------------------------------------------------- +# /v1/admin/data_folders +# --------------------------------------------------------------------------- + +class TestAdminDataFolders: + def test_lists_every_subdir_with_is_index_flag(self, client): + _mk_index_dir("alpha") + _mk_data_dir("uploads") + + r = client.get("/v1/admin/data_folders") + assert r.status_code == 200 + items = {it["name"]: it for it in r.json()["items"]} + assert set(items) == {"alpha", "uploads"} + assert items["alpha"]["is_index"] is True + assert items["uploads"]["is_index"] is False + + def test_empty(self, client): + r = client.get("/v1/admin/data_folders") + assert r.status_code == 200 + assert r.json()["count"] == 0 + + +# --------------------------------------------------------------------------- +# Auth gating +# --------------------------------------------------------------------------- + +class TestAdminAuth: + def test_no_token_no_auth(self, client): + _mk_index_dir("alpha") + # _API_KEY is None — no auth required + r = client.get("/v1/admin/indexes") + assert r.status_code == 200 + + def test_with_token_missing_header(self, client): + _server_mod._API_KEY = "secret" + try: + r = client.get("/v1/admin/indexes") + assert r.status_code == 401 + finally: + _server_mod._API_KEY = None + + def test_with_token_wrong(self, client): + _server_mod._API_KEY = "secret" + try: + r = client.get( + "/v1/admin/indexes", + headers={"Authorization": "Bearer wrong"}, + ) + assert r.status_code == 401 + finally: + _server_mod._API_KEY = None + + def test_with_token_correct(self, client): + _mk_index_dir("alpha") + _server_mod._API_KEY = "secret" + try: + r = client.get( + "/v1/admin/indexes", + headers={"Authorization": "Bearer secret"}, + ) + assert r.status_code == 200 + assert any(it["name"] == "alpha" for it in r.json()["items"]) + finally: + _server_mod._API_KEY = None + + +# --------------------------------------------------------------------------- +# TTL cache + invalidation +# --------------------------------------------------------------------------- + +class TestSizeCache: + def test_cache_hit_within_ttl(self, client, monkeypatch): + _mk_index_dir("alpha", n_files=2, size_per_file=100) + + call_count = {"n": 0} + real = _server_mod._dir_stats_sync + + def spy(path): + call_count["n"] += 1 + return real(path) + + monkeypatch.setattr(_server_mod, "_dir_stats_sync", spy) + + client.get("/v1/admin/indexes") + first = call_count["n"] + client.get("/v1/admin/indexes") + second = call_count["n"] + + # Second call must hit the cache — no additional walks + assert second == first + + def test_cache_miss_after_ttl_expiry(self, client, monkeypatch): + _mk_index_dir("alpha", n_files=2, size_per_file=100) + monkeypatch.setattr(_server_mod, "_CACHE_TTL", 0.0) + + call_count = {"n": 0} + real = _server_mod._dir_stats_sync + + def spy(path): + call_count["n"] += 1 + return real(path) + + monkeypatch.setattr(_server_mod, "_dir_stats_sync", spy) + + client.get("/v1/admin/indexes") + first = call_count["n"] + client.get("/v1/admin/indexes") + second = call_count["n"] + + # TTL is 0 → every request must re-walk + assert second == 2 * first + + def test_invalidation_on_delete(self, client): + # Manually populate cache then trigger DELETE on a (non-existent) collection; + # the cache for that path should be cleared. + idx = _mk_index_dir("alpha", n_files=2, size_per_file=100) + client.get("/v1/admin/indexes") + assert str(idx) in _server_mod._SIZE_CACHE + # The delete endpoint always invalidates and returns {"deleted": True}. + r = client.delete("/v1/collection/alpha") + assert r.status_code == 200 + assert str(idx) not in _server_mod._SIZE_CACHE + + +# --------------------------------------------------------------------------- +# Symlink jail +# --------------------------------------------------------------------------- + +class TestSymlinkContainment: + def test_symlinked_dir_outside_data_dir_is_skipped(self, client, tmp_path): + # Create a real dir OUTSIDE _DATA_DIR + outside = tmp_path / "outside" + outside.mkdir() + (outside / "evil.bin").write_bytes(b"z" * 10000) + + # Symlink INSIDE _DATA_DIR pointing outside + link = _server_mod._DATA_DIR / "linked" + try: + os.symlink(outside, link) + except (NotImplementedError, OSError): + pytest.skip("symlinks not supported on this platform") + + # Also a legitimate index alongside + _mk_index_dir("real_index") + + r = client.get("/v1/admin/data_folders") + assert r.status_code == 200 + names = {it["name"] for it in r.json()["items"]} + # The symlink target's content must not be reported via the link. + # The directory entry itself may appear, but with size_bytes == 0 because + # we resolve and check containment in the walk. + assert "real_index" in names diff --git a/tests/test_vector_db_server_app.py b/tests/test_vector_db_server_app.py new file mode 100644 index 0000000..6fa5367 --- /dev/null +++ b/tests/test_vector_db_server_app.py @@ -0,0 +1,318 @@ +"""Tests for the FastAPI vector-DB server app. + +Uses FastAPI's TestClient for in-process HTTP calls — no real server needed. +All storage is local (temp dir) to keep tests fast and self-contained. +""" + +import os +import tempfile +from unittest.mock import patch + +import pytest +import torch +from fastapi.testclient import TestClient + +# ── patch FOR_DB_DATA_DIR before importing the server ────────────────────── +_TMP = tempfile.mkdtemp(prefix="foretrieval_db_test_") + +# We patch the module-level constants before the app is created. +import foretrieval.vector_db_server.server as _server_mod + +_server_mod._DATA_DIR = __import__("pathlib").Path(_TMP) +_server_mod._API_KEY = None # auth off by default +_server_mod._registry.clear() +_server_mod._locks.clear() + +from foretrieval.vector_db_server.server import app # noqa: E402 +from foretrieval.vector_db_server.client import _dumps, _loads +from foretrieval.vector_store.base import make_point_id + + +@pytest.fixture(autouse=True) +def _clean_registry(): + """Clear in-memory registry and lock state between tests.""" + _server_mod._registry.clear() + _server_mod._locks.clear() + yield + _server_mod._registry.clear() + _server_mod._locks.clear() + + +@pytest.fixture() +def client(): + return TestClient(app) + + +# --------------------------------------------------------------------------- +# /health +# --------------------------------------------------------------------------- + +class TestHealth: + def test_health_ok(self, client): + r = client.get("/health") + assert r.status_code == 200 + assert r.json()["status"] == "ok" + + +# --------------------------------------------------------------------------- +# Auth middleware +# --------------------------------------------------------------------------- + +class TestAuth: + def test_missing_auth_header_returns_401(self, client): + _server_mod._API_KEY = "secret" + try: + r = client.get("/v1/collection/myidx/exists") + assert r.status_code == 401 + finally: + _server_mod._API_KEY = None + + def test_wrong_token_returns_401(self, client): + _server_mod._API_KEY = "secret" + try: + r = client.get( + "/v1/collection/myidx/exists", + headers={"Authorization": "Bearer wrongtoken"}, + ) + assert r.status_code == 401 + finally: + _server_mod._API_KEY = None + + def test_correct_token_accepted(self, client): + _server_mod._API_KEY = "secret" + try: + r = client.get( + "/v1/collection/myidx/exists", + headers={"Authorization": "Bearer secret"}, + ) + assert r.status_code == 200 + finally: + _server_mod._API_KEY = None + + def test_health_exempt_from_auth(self, client): + _server_mod._API_KEY = "secret" + try: + r = client.get("/health") + assert r.status_code == 200 + finally: + _server_mod._API_KEY = None + + +# --------------------------------------------------------------------------- +# Collection management +# --------------------------------------------------------------------------- + +class TestCollectionManagement: + def test_collection_does_not_exist(self, client): + r = client.get("/v1/collection/noexist/exists") + assert r.status_code == 200 + assert r.json()["exists"] is False + + def test_open_creates_collection(self, client): + r = client.post( + "/v1/collection/open", + json={"index_name": "test_open", "backend": "local", "create": True, "dim": 8}, + ) + assert r.status_code == 200 + body = r.json() + assert body["opened"] is True + assert body["created"] is True + + def test_open_existing_collection(self, client): + client.post( + "/v1/collection/open", + json={"index_name": "test_reopen", "backend": "local", "create": True, "dim": 8}, + ) + r = client.post( + "/v1/collection/open", + json={"index_name": "test_reopen", "backend": "local", "create": False}, + ) + assert r.status_code == 200 + assert r.json()["created"] is False + + def test_open_nonexistent_without_create_returns_404(self, client): + r = client.post( + "/v1/collection/open", + json={"index_name": "missing", "backend": "local", "create": False}, + ) + assert r.status_code == 404 + + def test_create_collection_endpoint(self, client): + r = client.post( + "/v1/collection", + json={"index_name": "via_create", "backend": "local", "dim": 8}, + ) + assert r.status_code == 200 + assert r.json()["created"] is True + + def test_collection_exists_after_create(self, client): + client.post( + "/v1/collection", + json={"index_name": "check_exists", "backend": "local", "dim": 8}, + ) + r = client.get("/v1/collection/check_exists/exists") + assert r.json()["exists"] is True + + +# --------------------------------------------------------------------------- +# Upsert → search → fetch_vector full round-trip +# --------------------------------------------------------------------------- + +class TestUpsertSearchFetch: + def _open(self, client, name="rt_test", backend="local", dim=8): + client.post( + "/v1/collection/open", + json={"index_name": name, "backend": backend, "create": True, "dim": dim}, + ) + + def _upsert_points(self, client, name, n=3, dim=8): + points = [] + for i in range(n): + pid = make_point_id(1, i) + points.append({ + "point_id": pid, + "vector": torch.randn(5, dim), + "payload": {"doc_id": 1, "page_id": i}, + }) + body = _dumps(points) + return client.post( + f"/v1/upsert/{name}", + content=body, + headers={"Content-Type": "application/octet-stream"}, + ) + + def test_upsert_returns_count(self, client): + self._open(client) + r = self._upsert_points(client, "rt_test", n=2) + assert r.status_code == 200 + assert r.json()["upserted"] == 2 + + def test_point_exists_after_upsert(self, client): + self._open(client, name="pe_test") + self._upsert_points(client, "pe_test", n=1) + pid = make_point_id(1, 0) + r = client.get(f"/v1/point/pe_test/{pid}/exists") + assert r.json()["exists"] is True + + def test_point_not_exists_before_upsert(self, client): + self._open(client, name="pne_test") + r = client.get(f"/v1/point/pne_test/9999999/exists") + assert r.json()["exists"] is False + + def test_search_returns_hits(self, client): + self._open(client, name="srch_test") + self._upsert_points(client, "srch_test", n=3) + + query_vec = torch.randn(4, 8) + wire = _dumps({"vectors": query_vec, "filter_metadata": None, "k": 2}) + r = client.post( + "/v1/search/srch_test", + content=wire, + headers={"Content-Type": "application/octet-stream"}, + ) + assert r.status_code == 200 + hits = _loads(r.content) + assert len(hits) <= 2 + for h in hits: + assert "point_id" in h + assert "score" in h + + def test_fetch_vector_returns_tensor(self, client): + self._open(client, name="fv_test") + self._upsert_points(client, "fv_test", n=1) + pid = make_point_id(1, 0) + r = client.get(f"/v1/vector/fv_test/{pid}") + assert r.status_code == 200 + tensor = _loads(r.content) + assert tensor.shape[-1] == 8 + + def test_fetch_vector_missing_returns_404(self, client): + self._open(client, name="fv_missing") + r = client.get("/v1/vector/fv_missing/9999999") + assert r.status_code == 404 + + def test_search_on_unknown_collection_returns_404(self, client): + wire = _dumps({"vectors": torch.randn(2, 8), "filter_metadata": None, "k": 3}) + r = client.post( + "/v1/search/no_such_col", + content=wire, + headers={"Content-Type": "application/octet-stream"}, + ) + assert r.status_code == 404 + + +# --------------------------------------------------------------------------- +# Delete collection +# --------------------------------------------------------------------------- + +class TestDeleteCollection: + def test_delete_removes_collection(self, client): + client.post( + "/v1/collection/open", + json={"index_name": "to_delete", "backend": "local", "create": True, "dim": 8}, + ) + r = client.delete("/v1/collection/to_delete") + assert r.status_code == 200 + assert r.json()["deleted"] is True + + r2 = client.get("/v1/collection/to_delete/exists") + assert r2.json()["exists"] is False + + def test_delete_nonexistent_ok(self, client): + r = client.delete("/v1/collection/ghost_col") + assert r.status_code == 200 + + +# --------------------------------------------------------------------------- +# Bookkeeping (server-side ColPali index metadata) +# --------------------------------------------------------------------------- + +class TestBookkeeping: + def _blob(self): + return { + "index_config": { + "model_name": "vidore/colqwen-test", + "highest_doc_id": 4, + "description": "demo", + }, + "embed_id_to_extra": {0: {"orig_size": (10, 20)}}, + "doc_ids_to_file_names": {1: "a.pdf"}, + "doc_id_to_metadata": {1: {"title": "A"}}, + } + + def test_get_missing_returns_404(self, client): + r = client.get("/v1/collection/bk_missing/bookkeeping") + assert r.status_code == 404 + + def test_put_then_get_roundtrip(self, client): + blob = self._blob() + r = client.put( + "/v1/collection/bk_rt/bookkeeping", + content=_dumps(blob), + headers={"Content-Type": "application/octet-stream"}, + ) + assert r.status_code == 200 + assert r.json()["stored"] is True + + r2 = client.get("/v1/collection/bk_rt/bookkeeping") + assert r2.status_code == 200 + loaded = _loads(r2.content) + assert loaded["index_config"]["model_name"] == "vidore/colqwen-test" + assert loaded["doc_ids_to_file_names"][1] == "a.pdf" + assert loaded["doc_id_to_metadata"][1]["title"] == "A" + + def test_put_overwrites(self, client): + client.put( + "/v1/collection/bk_ow/bookkeeping", + content=_dumps(self._blob()), + headers={"Content-Type": "application/octet-stream"}, + ) + updated = self._blob() + updated["index_config"]["description"] = "v2" + client.put( + "/v1/collection/bk_ow/bookkeeping", + content=_dumps(updated), + headers={"Content-Type": "application/octet-stream"}, + ) + r = client.get("/v1/collection/bk_ow/bookkeeping") + assert _loads(r.content)["index_config"]["description"] == "v2" diff --git a/tests/test_vector_db_server_client.py b/tests/test_vector_db_server_client.py new file mode 100644 index 0000000..832608c --- /dev/null +++ b/tests/test_vector_db_server_client.py @@ -0,0 +1,293 @@ +"""Tests for VectorDBServerClient — HTTP interactions mocked via MagicMock.""" + +import io +from unittest.mock import MagicMock, patch +import pytest +import torch + +from foretrieval.vector_db_server.client import VectorDBServerClient, _dumps, _loads +from foretrieval.vector_db_server.config import VectorDBServerConfig +from foretrieval.vector_store.base import MultiVectorQuery, StoredPoint, make_point_id + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _make_config(**kwargs) -> VectorDBServerConfig: + defaults = {"url": "http://localhost:18000", "backend": "qdrant"} + return VectorDBServerConfig(**{**defaults, **kwargs}) + + +def _make_client(**cfg_kwargs) -> VectorDBServerClient: + return VectorDBServerClient(_make_config(**cfg_kwargs)) + + +def _make_mock_json_response(status_code: int, data: dict): + resp = MagicMock() + resp.status_code = status_code + resp.json.return_value = data + resp.text = str(data) + return resp + + +def _make_mock_bytes_response(status_code: int, content: bytes): + resp = MagicMock() + resp.status_code = status_code + resp.content = content + resp.text = "" + resp.json.side_effect = Exception("not json") + return resp + + +def _make_point(doc_id: int = 1, page_id: int = 0, dim: int = 4) -> StoredPoint: + pid = make_point_id(doc_id, page_id) + return StoredPoint( + point_id=pid, + vector=torch.randn(3, dim), + payload={"doc_id": doc_id, "page_id": page_id}, + ) + + +# --------------------------------------------------------------------------- +# Codec +# --------------------------------------------------------------------------- + +class TestCodec: + def test_roundtrip_tensor(self): + t = torch.randn(5, 8) + assert torch.allclose(t, _loads(_dumps(t))) + + def test_roundtrip_list_of_dicts(self): + data = [{"point_id": 1, "vector": torch.randn(3, 4), "payload": {"a": 1}}] + result = _loads(_dumps(data)) + assert result[0]["point_id"] == 1 + assert torch.allclose(result[0]["vector"], data[0]["vector"]) + + +# --------------------------------------------------------------------------- +# Auth header +# --------------------------------------------------------------------------- + +class TestAuthHeader: + def test_api_key_added_to_headers(self): + client = _make_client(api_key="mysecret") + assert "Authorization" in client._client.headers + assert client._client.headers["Authorization"] == "Bearer mysecret" + + def test_no_api_key_no_auth_header(self): + client = _make_client() + assert "Authorization" not in client._client.headers + + +# --------------------------------------------------------------------------- +# Health check +# --------------------------------------------------------------------------- + +class TestHealthCheck: + def test_health_ok(self): + client = _make_client() + client._client.get = MagicMock( + return_value=_make_mock_json_response(200, {"status": "ok"}) + ) + assert client.health_check() is True + + def test_health_non_200(self): + client = _make_client() + client._client.get = MagicMock( + return_value=_make_mock_json_response(503, {}) + ) + assert client.health_check() is False + + def test_health_connection_error(self): + import httpx + client = _make_client() + client._client.get = MagicMock(side_effect=httpx.ConnectError("refused")) + assert client.health_check() is False + + +# --------------------------------------------------------------------------- +# collection_exists +# --------------------------------------------------------------------------- + +class TestCollectionExists: + def test_exists_true(self): + client = _make_client() + client._client.get = MagicMock( + return_value=_make_mock_json_response(200, {"exists": True, "backend": "qdrant"}) + ) + assert client.collection_exists("my_index") is True + + def test_exists_false(self): + client = _make_client() + client._client.get = MagicMock( + return_value=_make_mock_json_response(200, {"exists": False, "backend": None}) + ) + assert client.collection_exists("my_index") is False + + +# --------------------------------------------------------------------------- +# open_collection +# --------------------------------------------------------------------------- + +class TestOpenCollection: + def test_open_posts_correct_body(self): + client = _make_client() + expected = {"opened": True, "backend": "qdrant", "created": True} + client._client.post = MagicMock( + return_value=_make_mock_json_response(200, expected) + ) + result = client.open_collection( + "idx", "qdrant", create=True, dim=128, storage_config=None + ) + assert result == expected + call_args = client._client.post.call_args + assert "/v1/collection/open" in call_args.args[0] + body = call_args.kwargs["json"] + assert body["index_name"] == "idx" + assert body["create"] is True + assert body["dim"] == 128 + + +# --------------------------------------------------------------------------- +# upsert +# --------------------------------------------------------------------------- + +class TestUpsert: + def test_upsert_sends_octet_stream(self): + client = _make_client() + resp = _make_mock_json_response(200, {"upserted": 1}) + client._client.post = MagicMock(return_value=resp) + + point = _make_point() + client.upsert("idx", [point]) + + call_args = client._client.post.call_args + assert "upsert/idx" in call_args.args[0] + assert call_args.kwargs["headers"]["Content-Type"] == "application/octet-stream" + # Body is binary — decode and verify it contains the tensor + body_bytes = call_args.kwargs["content"] + decoded = _loads(body_bytes) + assert decoded[0]["point_id"] == point.point_id + + def test_upsert_timeout_raises(self): + import httpx + client = _make_client() + client._client.post = MagicMock(side_effect=httpx.TimeoutException("timeout")) + with pytest.raises(TimeoutError): + client.upsert("idx", [_make_point()]) + + def test_upsert_connection_error_raises(self): + import httpx + client = _make_client() + client._client.post = MagicMock(side_effect=httpx.ConnectError("refused")) + with pytest.raises(ConnectionError): + client.upsert("idx", [_make_point()]) + + +# --------------------------------------------------------------------------- +# point_exists +# --------------------------------------------------------------------------- + +class TestPointExists: + def test_point_exists_true(self): + client = _make_client() + client._client.get = MagicMock( + return_value=_make_mock_json_response(200, {"exists": True}) + ) + assert client.point_exists("idx", 42) is True + + def test_point_exists_false(self): + client = _make_client() + client._client.get = MagicMock( + return_value=_make_mock_json_response(200, {"exists": False}) + ) + assert client.point_exists("idx", 42) is False + + +# --------------------------------------------------------------------------- +# search +# --------------------------------------------------------------------------- + +class TestSearch: + def test_search_sends_query_and_parses_hits(self): + client = _make_client() + hits_wire = [ + {"point_id": 10000, "score": 9.5, "payload": {"doc_id": 1, "page_id": 0}} + ] + resp = _make_mock_bytes_response(200, _dumps(hits_wire)) + client._client.post = MagicMock(return_value=resp) + + query = MultiVectorQuery(vectors=torch.randn(5, 4)) + hits = client.search("idx", query, k=3) + + assert len(hits) == 1 + assert hits[0].point_id == 10000 + assert hits[0].score == pytest.approx(9.5) + + # Verify request body contains k and vectors + body_bytes = client._client.post.call_args.kwargs["content"] + wire = _loads(body_bytes) + assert wire["k"] == 3 + assert wire["filter_metadata"] is None + + +# --------------------------------------------------------------------------- +# fetch_vector +# --------------------------------------------------------------------------- + +class TestFetchVector: + def test_fetch_returns_tensor(self): + client = _make_client() + tensor = torch.randn(3, 4) + resp = _make_mock_bytes_response(200, _dumps(tensor)) + client._client.get = MagicMock(return_value=resp) + + result = client.fetch_vector("idx", 12345) + assert result is not None + assert torch.allclose(result, tensor) + + def test_fetch_returns_none_on_404(self): + client = _make_client() + resp = MagicMock() + resp.status_code = 404 + client._client.get = MagicMock(return_value=resp) + + result = client.fetch_vector("idx", 99999) + assert result is None + + +# --------------------------------------------------------------------------- +# Bookkeeping +# --------------------------------------------------------------------------- + +class TestBookkeeping: + def test_put_bookkeeping_sends_octet_stream(self): + client = _make_client() + resp = _make_mock_json_response(200, {"stored": True}) + client._client.put = MagicMock(return_value=resp) + + blob = {"index_config": {"model_name": "m"}, "doc_id_to_metadata": {1: {"t": "x"}}} + client.put_bookkeeping("idx", blob) + + call = client._client.put.call_args + assert call.args[0].endswith("/v1/collection/idx/bookkeeping") + body = call.kwargs["content"] + assert _loads(body)["index_config"]["model_name"] == "m" + + def test_get_bookkeeping_returns_blob(self): + client = _make_client() + blob = {"index_config": {"model_name": "m"}} + resp = _make_mock_bytes_response(200, _dumps(blob)) + client._client.get = MagicMock(return_value=resp) + + out = client.get_bookkeeping("idx") + assert out["index_config"]["model_name"] == "m" + + def test_get_bookkeeping_returns_none_on_404(self): + client = _make_client() + resp = MagicMock() + resp.status_code = 404 + client._client.get = MagicMock(return_value=resp) + + assert client.get_bookkeeping("idx") is None diff --git a/tests/test_vector_db_server_config.py b/tests/test_vector_db_server_config.py new file mode 100644 index 0000000..ce0aad2 --- /dev/null +++ b/tests/test_vector_db_server_config.py @@ -0,0 +1,104 @@ +"""Tests for VectorDBServerConfig — pydantic validators and helpers.""" + +import pytest +from pydantic import ValidationError + +from foretrieval.vector_db_server.config import VectorDBServerConfig + + +def _make_cfg(**kwargs) -> VectorDBServerConfig: + defaults = {"url": "http://localhost:18000", "backend": "qdrant"} + return VectorDBServerConfig(**{**defaults, **kwargs}) + + +class TestURLValidation: + def test_trailing_slash_stripped(self): + cfg = _make_cfg(url="http://db-host:18000/") + assert cfg.url == "http://db-host:18000" + + def test_multiple_trailing_slashes_stripped(self): + cfg = _make_cfg(url="http://db-host:18000///") + assert cfg.url == "http://db-host:18000" + + def test_url_preserved_without_trailing_slash(self): + cfg = _make_cfg(url="http://db-host:18000") + assert cfg.url == "http://db-host:18000" + + +class TestBackendValidation: + def test_valid_backends(self): + for b in ("local", "qdrant", "milvus"): + cfg = _make_cfg(backend=b) + assert cfg.backend == b + + def test_backend_normalised_to_lower(self): + cfg = _make_cfg(backend="Qdrant") + assert cfg.backend == "qdrant" + + def test_invalid_backend_raises(self): + with pytest.raises(ValidationError, match="not a valid server-side backend"): + _make_cfg(backend="redis") + + +class TestPortValidation: + def test_default_port(self): + cfg = _make_cfg() + assert cfg.port == 18000 + + def test_custom_port(self): + cfg = _make_cfg(port=9999) + assert cfg.port == 9999 + + def test_zero_port_raises(self): + with pytest.raises(ValidationError): + _make_cfg(port=0) + + def test_out_of_range_port_raises(self): + with pytest.raises(ValidationError): + _make_cfg(port=99999) + + +class TestAutoDeployValidation: + def test_auto_deploy_requires_ssh_host(self): + with pytest.raises(ValidationError, match="ssh_host is required"): + _make_cfg(auto_deploy=True, ssh_host=None) + + def test_auto_deploy_with_ssh_host_ok(self): + cfg = _make_cfg(auto_deploy=True, ssh_host="gpu-server") + assert cfg.auto_deploy is True + assert cfg.ssh_host == "gpu-server" + + def test_auto_deploy_false_no_ssh_needed(self): + cfg = _make_cfg(auto_deploy=False) + assert cfg.auto_deploy is False + + +class TestDefaults: + def test_api_key_default_none(self): + cfg = _make_cfg() + assert cfg.api_key is None + + def test_verify_ssl_default_true(self): + cfg = _make_cfg() + assert cfg.verify_ssl is True + + def test_request_timeout_default(self): + cfg = _make_cfg() + assert cfg.request_timeout == 120 + + def test_data_dir_default(self): + cfg = _make_cfg() + assert cfg.data_dir == "/var/lib/foretrieval_db" + + +class TestFromDict: + def test_from_dict_basic(self): + cfg = VectorDBServerConfig.from_dict( + {"url": "http://db-server:18000", "backend": "qdrant", "api_key": "tok"} + ) + assert cfg.url == "http://db-server:18000" + assert cfg.api_key == "tok" + + def test_from_dict_missing_url_raises(self): + with pytest.raises((ValidationError, TypeError)): + VectorDBServerConfig.from_dict({"backend": "qdrant"}) diff --git a/tests/test_vector_db_server_manager.py b/tests/test_vector_db_server_manager.py new file mode 100644 index 0000000..ef3c3c3 --- /dev/null +++ b/tests/test_vector_db_server_manager.py @@ -0,0 +1,446 @@ +"""Tests for VectorDBServerManager — SSH/Docker operations mocked via MagicMock.""" + +import json +from unittest.mock import MagicMock, patch +import pytest + +from foretrieval.vector_db_server.config import VectorDBServerConfig +from foretrieval.vector_db_server.manager import VectorDBServerManager, _CONTAINER_NAME + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _make_manager(**cfg_kwargs) -> VectorDBServerManager: + defaults = { + "url": "http://localhost:18000", + "backend": "qdrant", + "ssh_host": "gpu-server", + "auto_deploy": True, + } + cfg = VectorDBServerConfig(**{**defaults, **cfg_kwargs}) + mgr = VectorDBServerManager(cfg) + # Stub the remote-home resolution so tests that mock _run_remote + # don't accidentally trigger a real SSH connection through + # _remote_home() -> sftp.normalize('.'). + mgr._cached_home = "/home/testuser" + # Stub UID/GID so _build_docker_run_cmd tests don't need SSH. + # Tests that specifically exercise the --user flag override this. + mgr._resolve_remote_uid_gid = MagicMock(return_value=(None, None)) + return mgr + + +def _fake_run_factory(responses: dict): + """Return a _run_remote mock whose stdout depends on substrings in the command.""" + def _fake_run(cmd: str, on_line=None) -> tuple[str, str]: + for substring, output in responses.items(): + if substring in cmd: + return output, "" + return "", "" + return _fake_run + + +# --------------------------------------------------------------------------- +# Constructor +# --------------------------------------------------------------------------- + +class TestManagerConstructor: + def test_requires_ssh_host(self): + cfg = VectorDBServerConfig( + url="http://localhost:18000", backend="qdrant", ssh_host=None + ) + with pytest.raises(ValueError, match="ssh_host"): + VectorDBServerManager(cfg) + + def test_paramiko_import_error(self): + mgr = _make_manager() + mgr._run_remote = MagicMock() + with patch.dict("sys.modules", {"paramiko": None}): + with pytest.raises(ImportError, match="paramiko"): + mgr.ensure_deployed() + + +# --------------------------------------------------------------------------- +# ensure_deployed — deploy from scratch +# --------------------------------------------------------------------------- + +class TestDeployFromScratch: + def test_deploys_when_no_metadata(self, tmp_path): + mgr = _make_manager() + + # _run_remote returns __MISSING__ for metadata cat, "true" for docker inspect + calls = [] + def fake_run(cmd: str, on_line=None): + calls.append(cmd) + if "cat" in cmd and "db_deployment" in cmd: + return "__MISSING__", "" + if "docker inspect" in cmd: + return "false", "" + if "nvidia-smi" in cmd: + return "2\n", "" + return "", "" + + mgr._run_remote = MagicMock(side_effect=fake_run) + # Mock _upload_build_context to avoid real filesystem + SSH + mgr._upload_build_context = MagicMock() + + # Stub paramiko import check + with patch.dict("sys.modules", {"paramiko": MagicMock()}): + mgr.ensure_deployed() + + # Should have called upload + docker build + docker run + all_cmds = " ".join(calls) + assert "docker build" in all_cmds + assert "docker run" in all_cmds + + def test_no_op_when_container_running(self): + mgr = _make_manager() + + meta = json.dumps({"deployed_at": "2026-01-01T00:00:00+00:00"}) + + def fake_run(cmd: str, on_line=None): + if "cat" in cmd and "db_deployment" in cmd: + return meta, "" + if "docker inspect" in cmd: + return "true", "" + return "", "" + + mgr._run_remote = MagicMock(side_effect=fake_run) + mgr._upload_build_context = MagicMock() + + with patch.dict("sys.modules", {"paramiko": MagicMock()}): + mgr.ensure_deployed() + + # _upload_build_context should NOT have been called + mgr._upload_build_context.assert_not_called() + + def test_redeploys_when_container_stopped(self): + mgr = _make_manager() + + meta = json.dumps({"deployed_at": "2026-01-01T00:00:00+00:00"}) + calls = [] + + def fake_run(cmd: str, on_line=None): + calls.append(cmd) + if "cat" in cmd and "db_deployment" in cmd: + return meta, "" + if "docker inspect" in cmd: + return "false", "" + return "", "" + + mgr._run_remote = MagicMock(side_effect=fake_run) + mgr._upload_build_context = MagicMock() + + with patch.dict("sys.modules", {"paramiko": MagicMock()}): + mgr.ensure_deployed() + + all_cmds = " ".join(calls) + assert "docker build" in all_cmds + + +# --------------------------------------------------------------------------- +# _build_docker_run_cmd +# --------------------------------------------------------------------------- + +class TestBuildDockerRunCmd: + def test_port_in_command(self): + mgr = _make_manager(port=18000) + cmd = mgr._build_docker_run_cmd() + assert "-p 18000:18000" in cmd + + def test_data_dir_mount(self): + mgr = _make_manager(data_dir="/mnt/data") + cmd = mgr._build_docker_run_cmd() + assert "/mnt/data:/data" in cmd + + def test_api_key_in_env(self): + mgr = _make_manager(api_key="mysecret") + cmd = mgr._build_docker_run_cmd() + assert "FOR_DB_API_KEY=mysecret" in cmd + + def test_no_api_key_not_in_cmd(self): + mgr = _make_manager() + cmd = mgr._build_docker_run_cmd() + assert "FOR_DB_API_KEY" not in cmd + + def test_container_name_in_cmd(self): + mgr = _make_manager() + cmd = mgr._build_docker_run_cmd() + assert f"--name {_CONTAINER_NAME}" in cmd + + def test_user_flag_present_when_uid_resolved(self): + """When UID/GID can be resolved, --user uid:gid must appear.""" + mgr = _make_manager() + mgr._resolve_remote_uid_gid = MagicMock(return_value=(1000, 1001)) + cmd = mgr._build_docker_run_cmd() + assert "--user 1000:1001" in cmd + + def test_user_flag_absent_when_uid_unknown(self): + """When resolution fails (returns None, None), no --user flag.""" + mgr = _make_manager() + # _resolve_remote_uid_gid already returns (None, None) from _make_manager + cmd = mgr._build_docker_run_cmd() + assert "--user" not in cmd + + +# --------------------------------------------------------------------------- +# stop +# --------------------------------------------------------------------------- + +class TestStop: + def test_stop_sends_correct_commands(self): + mgr = _make_manager() + calls = [] + mgr._run_remote = MagicMock(side_effect=lambda cmd, on_line=None: (calls.append(cmd), ("", ""))[1]) + mgr.stop() + all_cmds = " ".join(calls) + assert "docker stop" in all_cmds + assert "docker rm" in all_cmds + assert "db_deployment" in all_cmds + + +# --------------------------------------------------------------------------- +# redeploy + on_line streaming +# --------------------------------------------------------------------------- + +class TestRedeploy: + def test_redeploy_calls_deploy(self): + mgr = _make_manager() + mgr._deploy = MagicMock() + with patch.dict("sys.modules", {"paramiko": MagicMock()}): + mgr.redeploy() + mgr._deploy.assert_called_once() + + def test_redeploy_forwards_on_line_callback(self): + mgr = _make_manager() + mgr._deploy = MagicMock() + cb = MagicMock() + with patch.dict("sys.modules", {"paramiko": MagicMock()}): + mgr.redeploy(on_line=cb) + mgr._deploy.assert_called_once() + # The callback was threaded through + assert mgr._deploy.call_args.kwargs.get("on_line") is cb + + def test_redeploy_unconditionally_redeploys_running_container(self): + """Unlike ensure_deployed, redeploy() always rebuilds.""" + mgr = _make_manager() + + # _is_container_running returns True; ensure_deployed would no-op, + # but redeploy must still trigger _deploy. + mgr._is_container_running = MagicMock(return_value=True) + mgr._read_remote_metadata = MagicMock(return_value={"deployed_at": "x"}) + mgr._deploy = MagicMock() + with patch.dict("sys.modules", {"paramiko": MagicMock()}): + mgr.redeploy() + mgr._deploy.assert_called_once() + + +class TestOnLineStreaming: + def test_run_remote_streams_lines_when_callback_given(self): + """When on_line is provided, stdout is read line-by-line.""" + mgr = _make_manager() + + # Build a fake SSH client whose exec_command returns three lines + class _FakeChannel: + def recv_exit_status(self): + return 0 + + class _FakeStdout: + def __init__(self, lines): + self._lines = list(lines) + self.channel = _FakeChannel() + + def readline(self): + if self._lines: + return self._lines.pop(0) + return "" + + def read(self): + return b"" + + fake_stdout = _FakeStdout(["Step 1/3\n", "Step 2/3\n", "Done\n"]) + fake_stderr = MagicMock() + fake_stderr.read.return_value = b"" + fake_ssh = MagicMock() + fake_ssh.exec_command.return_value = (None, fake_stdout, fake_stderr) + mgr._get_ssh = MagicMock(return_value=fake_ssh) + + captured = [] + stdout, stderr = mgr._run_remote("docker build .", on_line=captured.append) + + assert captured == ["Step 1/3", "Step 2/3", "Done"] + assert "Step 1/3" in stdout + assert stderr == "" + + def test_run_remote_buffered_when_no_callback(self): + """Default behaviour (no on_line) still reads the whole stdout at once.""" + mgr = _make_manager() + + class _FakeChannel: + def recv_exit_status(self): + return 0 + + class _FakeStdout: + channel = _FakeChannel() + def read(self): + return b"full output\n" + + fake_stderr = MagicMock() + fake_stderr.read.return_value = b"" + fake_ssh = MagicMock() + fake_ssh.exec_command.return_value = (None, _FakeStdout(), fake_stderr) + mgr._get_ssh = MagicMock(return_value=fake_ssh) + + stdout, _ = mgr._run_remote("echo hi") + assert "full output" in stdout + + +# --------------------------------------------------------------------------- +# _get_ssh uses ssh_utils.open_ssh_client +# --------------------------------------------------------------------------- + +class TestGetSshUsesSshUtils: + def test_get_ssh_delegates_to_open_ssh_client(self): + """_get_ssh must call foretrieval.ssh_utils.open_ssh_client. + + This is the regression test for the bug where paramiko was + called with a raw alias string and DNS-failed for entries that + only existed in ~/.ssh/config. + """ + mgr = _make_manager(ssh_host="pf01", ssh_user="alice", ssh_key_path="/k") + fake_client = MagicMock() + with patch("foretrieval.ssh_utils.open_ssh_client", + return_value=fake_client) as mock_open: + result = mgr._get_ssh() + mock_open.assert_called_once_with( + ssh_host="pf01", + ssh_user="alice", + ssh_key_path="/k", + ) + assert result is fake_client + + def test_get_ssh_is_cached(self): + mgr = _make_manager() + fake_client = MagicMock() + with patch("foretrieval.ssh_utils.open_ssh_client", + return_value=fake_client) as mock_open: + r1 = mgr._get_ssh() + r2 = mgr._get_ssh() + assert r1 is r2 + assert mock_open.call_count == 1 + + +# --------------------------------------------------------------------------- +# Remote home / metadata path resolution +# --------------------------------------------------------------------------- + +class TestRemoteHome: + def test_remote_home_uses_sftp_normalize(self): + """_remote_home returns the path produced by sftp.normalize('.').""" + mgr = _make_manager() + mgr._cached_home = None # reset the stub from _make_manager + + fake_sftp = MagicMock() + fake_sftp.normalize.return_value = "/home/alice" + fake_ssh = MagicMock() + fake_ssh.open_sftp.return_value = fake_sftp + mgr._get_ssh = MagicMock(return_value=fake_ssh) + + assert mgr._remote_home() == "/home/alice" + fake_sftp.normalize.assert_called_once_with(".") + fake_sftp.close.assert_called_once() + + def test_remote_home_caches_result(self): + mgr = _make_manager() + mgr._cached_home = None + + fake_sftp = MagicMock() + fake_sftp.normalize.return_value = "/home/alice" + fake_ssh = MagicMock() + fake_ssh.open_sftp.return_value = fake_sftp + mgr._get_ssh = MagicMock(return_value=fake_ssh) + + a = mgr._remote_home() + b = mgr._remote_home() + assert a == b == "/home/alice" + fake_ssh.open_sftp.assert_called_once() + + +class TestMetadataPathAbsolute: + def test_metadata_path_is_absolute(self): + mgr = _make_manager() # _cached_home = "/home/testuser" + path = mgr._metadata_path() + assert path.startswith("/home/testuser/") + assert "~" not in path + assert path.endswith("db_deployment.json") + + def test_stop_uses_absolute_metadata_path(self): + """Regression: stop() must rm an absolute path, not '~/...'.""" + mgr = _make_manager() + calls = [] + mgr._run_remote = MagicMock( + side_effect=lambda cmd, on_line=None: (calls.append(cmd), ("", ""))[1] + ) + mgr.stop() + rm_calls = [c for c in calls if c.startswith("rm -f")] + assert rm_calls, "stop() did not call rm -f" + # No tilde in the path passed to rm + assert all("~" not in c for c in rm_calls), rm_calls + assert any("/home/testuser/" in c for c in rm_calls), rm_calls + + +class TestUploadBuildContextAbsolutePath: + def test_sftp_put_uses_absolute_remote_path(self, tmp_path, monkeypatch): + """Regression for the SFTP ENOENT bug. + + The previous implementation passed + f"{'~/foretrieval_db_build'.replace('~', '')}/build_context.tar.gz" + which resolved to '/foretrieval_db_build/...' (root-relative). + The new code must pass an absolute path rooted at the remote home. + """ + from foretrieval.vector_db_server import manager as mod + + mgr = _make_manager() # _cached_home = "/home/testuser" + + # Mock _run_remote (mkdir, tar extract) + runs: list[str] = [] + mgr._run_remote = MagicMock( + side_effect=lambda cmd, on_line=None: (runs.append(cmd), ("", ""))[1] + ) + + # Mock SSH + SFTP + sftp_calls: list[tuple] = [] + + class _FakeSftp: + def put(self, local, remote): + sftp_calls.append(("put", local, remote)) + def close(self): + sftp_calls.append(("close",)) + + fake_sftp = _FakeSftp() + fake_ssh = MagicMock() + fake_ssh.open_sftp.return_value = fake_sftp + mgr._get_ssh = MagicMock(return_value=fake_ssh) + + # Avoid real disk + real tar — short-circuit _upload_build_context's + # source-locating step by pointing it at the actual repo (it exists). + # tarfile is small so let it run. + mgr._upload_build_context() + + # Verify SFTP put destination + put_calls = [c for c in sftp_calls if c[0] == "put"] + assert len(put_calls) == 1 + remote_dest = put_calls[0][2] + assert remote_dest == "/home/testuser/foretrieval_db_build/build_context.tar.gz", remote_dest + + # Verify the mkdir uses the same absolute path + mkdir_calls = [c for c in runs if c.startswith("mkdir -p")] + assert any("/home/testuser/foretrieval_db_build" in c for c in mkdir_calls), mkdir_calls + + # Verify tar extraction targets the same absolute path + tar_calls = [c for c in runs if "tar -xzf" in c] + assert any("/home/testuser/foretrieval_db_build" in c for c in tar_calls), tar_calls + + # _remote_build_dir stashed for _deploy() to reuse + assert mgr._remote_build_dir == "/home/testuser/foretrieval_db_build" diff --git a/tests/test_vector_store_factory.py b/tests/test_vector_store_factory.py new file mode 100644 index 0000000..f983e6a --- /dev/null +++ b/tests/test_vector_store_factory.py @@ -0,0 +1,143 @@ +"""Tests for make_vector_store() factory.""" +from __future__ import annotations + +from unittest.mock import MagicMock, patch +import pytest + +from foretrieval.vector_store.factory import make_vector_store, BACKEND_REGISTRY +from foretrieval.vector_store.local import LocalVectorStore +from foretrieval.vector_store.qdrant import QdrantVectorStore +from foretrieval.vector_store.milvus import MilvusVectorStore +from foretrieval.vector_store.remote import RemoteVectorStore + + +class TestFactory: + def test_local_backend_returns_local_store(self): + vs = make_vector_store("local") + assert isinstance(vs, LocalVectorStore) + + def test_qdrant_backend_returns_qdrant_store(self): + vs = make_vector_store("qdrant") + assert isinstance(vs, QdrantVectorStore) + + def test_milvus_backend_returns_milvus_store(self): + vs = make_vector_store("milvus") + assert isinstance(vs, MilvusVectorStore) + + def test_unknown_backend_raises_value_error(self): + with pytest.raises(ValueError, match="Unknown storage backend"): + make_vector_store("nonexistent") + + def test_error_message_lists_supported_backends(self): + with pytest.raises(ValueError, match="local"): + make_vector_store("bad") + + def test_none_backend_defaults_to_local(self): + vs = make_vector_store(None) + assert isinstance(vs, LocalVectorStore) + + def test_whitespace_and_uppercase_handled(self): + vs = make_vector_store(" LOCAL ") + assert isinstance(vs, LocalVectorStore) + + def test_milvus_candidate_limit_passed_through(self): + vs = make_vector_store("milvus", {"candidate_limit": 128}) + assert isinstance(vs, MilvusVectorStore) + assert vs._candidate_limit == 128 + + def test_backend_registry_contains_all_four(self): + assert "local" in BACKEND_REGISTRY + assert "qdrant" in BACKEND_REGISTRY + assert "milvus" in BACKEND_REGISTRY + assert "remote" in BACKEND_REGISTRY + + def test_each_call_returns_new_instance(self): + a = make_vector_store("local") + b = make_vector_store("local") + assert a is not b + + +class TestRemoteFactory: + """Test make_vector_store("remote", ...) factory path.""" + + def _make_remote(self, extra=None, auto_deploy=False): + cfg = { + "url": "http://localhost:18000", + "backend": "qdrant", + "auto_deploy": auto_deploy, + } + if extra: + cfg.update(extra) + # Patch VectorDBServerClient inside the module where it is imported + with patch( + "foretrieval.vector_db_server.client.VectorDBServerClient" + ): + with patch( + "foretrieval.vector_store.factory._make_remote_vector_store", + ) as mock_factory: + mock_client_inst = MagicMock() + remote_store = RemoteVectorStore(mock_client_inst, backend=cfg.get("backend", "qdrant")) + mock_factory.return_value = remote_store + vs = make_vector_store("remote", cfg) + return vs, mock_client_inst + + def test_remote_backend_returns_remote_store(self): + vs, _ = self._make_remote() + assert isinstance(vs, RemoteVectorStore) + + def test_remote_store_has_correct_server_backend(self): + vs, _ = self._make_remote() + assert vs.server_backend == "qdrant" + + def test_remote_store_milvus_backend(self): + vs, _ = self._make_remote(extra={"backend": "milvus"}) + assert vs.server_backend == "milvus" + + def test_remote_missing_url_raises(self): + with pytest.raises(ValueError, match="url"): + make_vector_store("remote", {"backend": "qdrant"}) + + def test_auto_deploy_calls_manager_and_health_check(self): + with ( + patch("foretrieval.vector_db_server.client.VectorDBServerClient") as MockClient, + patch("foretrieval.vector_db_server.manager.VectorDBServerManager") as MockMgr, + patch("foretrieval.vector_store.factory._wait_for_health") as mock_wait, + ): + mock_client_inst = MagicMock() + MockClient.return_value = mock_client_inst + mock_mgr_inst = MagicMock() + MockMgr.return_value = mock_mgr_inst + + vs = make_vector_store( + "remote", + { + "url": "http://gpu-server:18000", + "backend": "qdrant", + "auto_deploy": True, + "ssh_host": "gpu-server", + }, + ) + mock_mgr_inst.ensure_deployed.assert_called_once() + mock_wait.assert_called_once() + assert isinstance(vs, RemoteVectorStore) + + def test_candidate_limit_forwarded_as_server_storage_config(self): + mock_client_inst = MagicMock() + remote_store = RemoteVectorStore( + mock_client_inst, backend="milvus", + storage_config={"candidate_limit": 128} + ) + with patch( + "foretrieval.vector_store.factory._make_remote_vector_store", + return_value=remote_store, + ): + vs = make_vector_store( + "remote", + { + "url": "http://localhost:18000", + "backend": "milvus", + "candidate_limit": 128, + }, + ) + assert vs._storage_config == {"candidate_limit": 128} + diff --git a/tests/test_vector_store_local.py b/tests/test_vector_store_local.py new file mode 100644 index 0000000..0142c38 --- /dev/null +++ b/tests/test_vector_store_local.py @@ -0,0 +1,258 @@ +"""Tests for LocalVectorStore. + +All tests run without GPU — processor.score() is mocked. +""" +from __future__ import annotations + +import tempfile +from pathlib import Path +from typing import Any +from unittest.mock import MagicMock + +import numpy as np +import pytest +import torch + +from foretrieval.vector_store.base import ( + MultiVectorQuery, + StoredPoint, + make_point_id, +) +from foretrieval.vector_store.local import LocalVectorStore + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _make_store(processor=None) -> LocalVectorStore: + store = LocalVectorStore() + if processor is not None: + store.set_processor(processor) + return store + + +def _dummy_embedding(n_tokens: int = 4, dim: int = 8) -> torch.Tensor: + return torch.rand(n_tokens, dim) + + +def _mock_processor(scores: list[float]): + """Return a processor mock whose score() returns the given scores as a 2-D array.""" + proc = MagicMock() + proc.score.return_value = torch.tensor([scores]) + return proc + + +def _point(doc_id: int, page_id: int, vec: torch.Tensor | None = None) -> StoredPoint: + if vec is None: + vec = _dummy_embedding() + pid = make_point_id(doc_id, page_id) + return StoredPoint( + point_id=pid, + vector=vec, + payload={"doc_id": doc_id, "page_id": page_id, "chunk_id": None, "metadata": {}}, + ) + + +# --------------------------------------------------------------------------- +# Lifecycle +# --------------------------------------------------------------------------- + +class TestLifecycle: + def test_open_does_not_raise(self, tmp_path): + store = LocalVectorStore() + store.open("idx", tmp_path, create=True, dim=8) + + def test_collection_not_exists_before_upsert(self, tmp_path): + store = LocalVectorStore() + store.open("idx", tmp_path, create=True, dim=8) + assert not store.collection_exists() + + def test_collection_exists_after_export(self, tmp_path): + store = LocalVectorStore() + store.open("idx", tmp_path, create=True, dim=8) + store.upsert([_point(0, 1)]) + index_dir = tmp_path / "idx" + index_dir.mkdir(parents=True, exist_ok=True) + store.export_sidecar(index_dir) + assert store.collection_exists() + + +# --------------------------------------------------------------------------- +# Upsert and point_exists +# --------------------------------------------------------------------------- + +class TestUpsert: + def test_upsert_single_point(self, tmp_path): + store = LocalVectorStore() + store.open("idx", tmp_path, create=True) + sp = _point(0, 1) + store.upsert([sp]) + assert store.point_exists(sp.point_id) + + def test_upsert_multiple_points(self, tmp_path): + store = LocalVectorStore() + store.open("idx", tmp_path, create=True) + pts = [_point(i, 1) for i in range(5)] + store.upsert(pts) + for p in pts: + assert store.point_exists(p.point_id) + + def test_point_not_exists_before_upsert(self, tmp_path): + store = LocalVectorStore() + store.open("idx", tmp_path, create=True) + assert not store.point_exists(make_point_id(0, 1)) + + def test_upsert_idempotent_last_write_wins(self, tmp_path): + store = LocalVectorStore() + store.open("idx", tmp_path, create=True) + pid = make_point_id(0, 1) + v1 = torch.zeros(4, 8) + v2 = torch.ones(4, 8) + sp1 = StoredPoint(point_id=pid, vector=v1, payload={"doc_id": 0, "page_id": 1, "chunk_id": None, "metadata": {}}) + sp2 = StoredPoint(point_id=pid, vector=v2, payload={"doc_id": 0, "page_id": 1, "chunk_id": None, "metadata": {}}) + store.upsert([sp1]) + store.upsert([sp2]) + fetched = store.fetch_vector(pid) + assert fetched is not None + assert torch.allclose(fetched, v2) + + +# --------------------------------------------------------------------------- +# Search +# --------------------------------------------------------------------------- + +class TestSearch: + def test_search_returns_k_results(self, tmp_path): + scores = [0.5, 0.8, 0.3] + proc = _mock_processor(scores) + store = _make_store(proc) + store.open("idx", tmp_path, create=True) + for i in range(3): + store.upsert([_point(i, 1)]) + q = MultiVectorQuery(vectors=_dummy_embedding(2, 8)) + results = store.search(q, k=2) + assert len(results) == 2 + + def test_search_sorted_descending_score(self, tmp_path): + scores = [0.3, 0.9, 0.1] + proc = _mock_processor(scores) + store = _make_store(proc) + store.open("idx", tmp_path, create=True) + for i in range(3): + store.upsert([_point(i, 1)]) + q = MultiVectorQuery(vectors=_dummy_embedding(2, 8)) + results = store.search(q, k=3) + assert results[0].score >= results[1].score >= results[2].score + + def test_search_returns_correct_point_ids(self, tmp_path): + scores = [0.1, 0.9, 0.5] + proc = _mock_processor(scores) + store = _make_store(proc) + store.open("idx", tmp_path, create=True) + pts = [_point(i, 1) for i in range(3)] + for p in pts: + store.upsert([p]) + q = MultiVectorQuery(vectors=_dummy_embedding(2, 8)) + results = store.search(q, k=1) + # Best score is index 1 (score 0.9 → doc_id=1) + assert results[0].payload["doc_id"] == 1 + + def test_search_raises_without_processor(self, tmp_path): + store = LocalVectorStore() + store.open("idx", tmp_path, create=True) + store.upsert([_point(0, 1)]) + q = MultiVectorQuery(vectors=_dummy_embedding(2, 8)) + with pytest.raises(RuntimeError, match="set_processor"): + store.search(q, k=1) + + def test_search_empty_store_returns_empty(self, tmp_path): + proc = MagicMock() + store = _make_store(proc) + store.open("idx", tmp_path, create=True) + q = MultiVectorQuery(vectors=_dummy_embedding(2, 8)) + assert store.search(q, k=5) == [] + + def test_search_with_metadata_filter(self, tmp_path): + """Filter should restrict search to matching docs only.""" + # 3 docs: doc 0 has language=en, doc 1 has language=fr, doc 2 has language=en + # We filter on language=fr → only doc 1 should be in the result pool. + # The mock processor returns a single score for one embedding (the filtered one). + store = LocalVectorStore() + store.open("idx", tmp_path, create=True) + store.set_doc_id_to_metadata({ + 0: {"language": "en"}, + 1: {"language": "fr"}, + 2: {"language": "en"}, + }) + for i in range(3): + store.upsert([_point(i, 1)]) + + # After metadata filter only doc 1 is left → processor.score gets 1 embedding + proc_filtered = MagicMock() + proc_filtered.score.return_value = torch.tensor([[0.7]]) + store.set_processor(proc_filtered) + + q = MultiVectorQuery( + vectors=_dummy_embedding(2, 8), + filter_metadata={"language": "fr"}, + ) + results = store.search(q, k=5) + assert len(results) == 1 + assert results[0].payload["doc_id"] == 1 + + +# --------------------------------------------------------------------------- +# fetch_vector +# --------------------------------------------------------------------------- + +class TestFetchVector: + def test_fetch_returns_tensor(self, tmp_path): + store = LocalVectorStore() + store.open("idx", tmp_path, create=True) + vec = _dummy_embedding(4, 8) + sp = _point(0, 1, vec) + store.upsert([sp]) + fetched = store.fetch_vector(sp.point_id) + assert fetched is not None + assert fetched.shape == (4, 8) + assert torch.allclose(fetched, vec) + + def test_fetch_unknown_id_returns_none(self, tmp_path): + store = LocalVectorStore() + store.open("idx", tmp_path, create=True) + assert store.fetch_vector(make_point_id(99, 99)) is None + + +# --------------------------------------------------------------------------- +# Persistence round-trip +# --------------------------------------------------------------------------- + +class TestPersistence: + def test_export_load_round_trip(self, tmp_path): + store1 = LocalVectorStore() + store1.open("idx", tmp_path, create=True) + pts = [_point(i, 1) for i in range(3)] + for p in pts: + store1.upsert([p]) + index_path = tmp_path / "idx" + index_path.mkdir(parents=True, exist_ok=True) + store1.export_sidecar(index_path) + + # Load into a fresh store + proc = MagicMock() + scores = [0.5, 0.8, 0.3] + proc.score.return_value = torch.tensor([scores]) + store2 = LocalVectorStore() + store2.open("idx", tmp_path, create=False) + store2.load_sidecar(index_path) + store2.set_processor(proc) + + # All points should be present + for p in pts: + assert store2.point_exists(p.point_id) + + # Search should work + q = MultiVectorQuery(vectors=_dummy_embedding(2, 8)) + results = store2.search(q, k=2) + assert len(results) == 2 diff --git a/tests/test_vector_store_milvus.py b/tests/test_vector_store_milvus.py new file mode 100644 index 0000000..01699d5 --- /dev/null +++ b/tests/test_vector_store_milvus.py @@ -0,0 +1,339 @@ +"""Tests for MilvusVectorStore. + +Unit tests mock the MilvusClient. +The slow integration test uses Milvus Lite (file-based, no external server needed). +""" +from __future__ import annotations + +import tempfile +import uuid +from pathlib import Path +from unittest.mock import MagicMock, call, patch + +import pytest +import torch + +from foretrieval.vector_store.base import ( + MultiVectorQuery, + StoredPoint, + make_point_id, +) +from foretrieval.vector_store.milvus import ( + MilvusVectorStore, + _MILVUS_AVAILABLE, + _collection_names, + _mean_pool, + _page_id_str, + _deserialize_payload, + _serialize_payload, +) + + +# --------------------------------------------------------------------------- +# Helper builders +# --------------------------------------------------------------------------- + +def _point(doc_id: int, page_id: int, n_tokens: int = 4, dim: int = 8) -> StoredPoint: + pid = make_point_id(doc_id, page_id) + return StoredPoint( + point_id=pid, + vector=torch.rand(n_tokens, dim), + payload={"doc_id": doc_id, "page_id": page_id, "chunk_id": None, "metadata": {}}, + ) + + +def _make_mock_store(index_name: str = "test_idx") -> tuple[MilvusVectorStore, MagicMock]: + store = MilvusVectorStore.__new__(MilvusVectorStore) + mock_client = MagicMock() + mock_client.list_collections.return_value = [] + store._client = mock_client + store._index_name = index_name + store._db_path = Path("/tmp/fake.db") + store._candidate_limit = 64 + return store, mock_client + + +# --------------------------------------------------------------------------- +# Pure helpers +# --------------------------------------------------------------------------- + +class TestHelpers: + def test_mean_pool_correct_shape(self): + t = torch.ones(4, 8) + pool = _mean_pool(t) + assert len(pool) == 8 + + def test_mean_pool_values(self): + t = torch.tensor([[1.0, 2.0], [3.0, 4.0]]) + pool = _mean_pool(t) + assert pool == pytest.approx([2.0, 3.0]) + + def test_serialize_deserialize_roundtrip(self): + payload = {"doc_id": 1, "page_id": 2, "metadata": {"cat": "A"}} + raw = _serialize_payload(payload) + result = _deserialize_payload(raw) + assert result["doc_id"] == 1 + + def test_collection_names_suffix(self): + page_col, token_col = _collection_names("myindex") + assert page_col == "myindex__pages" + assert token_col == "myindex__tokens" + + def test_page_id_str(self): + pid = make_point_id(5, 3) + assert _page_id_str(pid) == str(pid) + + +# --------------------------------------------------------------------------- +# Optional-dependency guard +# --------------------------------------------------------------------------- + +class TestOptionalDepGuard: + def test_missing_milvus_raises_on_open(self, tmp_path): + with patch("foretrieval.vector_store.milvus._MILVUS_AVAILABLE", False): + store = MilvusVectorStore() + with pytest.raises(RuntimeError, match="pymilvus"): + store.open("idx", tmp_path, create=True, dim=8) + + +# --------------------------------------------------------------------------- +# Collection management +# --------------------------------------------------------------------------- + +@pytest.mark.skipif(not _MILVUS_AVAILABLE, reason="pymilvus not installed") +class TestCollectionManagement: + def test_collection_not_exists_empty_list(self): + store, mock_client = _make_mock_store() + mock_client.list_collections.return_value = [] + assert not store.collection_exists() + + def test_collection_exists_when_page_col_present(self): + store, mock_client = _make_mock_store() + page_col, _ = _collection_names("test_idx") + mock_client.list_collections.return_value = [page_col] + assert store.collection_exists() + + def test_create_collection_creates_both_collections(self): + store, mock_client = _make_mock_store() + mock_client.list_collections.return_value = [] + mock_client.create_collection = MagicMock() + mock_client.prepare_index_params.return_value = MagicMock() + store.create_collection(dim=8) + assert mock_client.create_collection.call_count == 2 + + +# --------------------------------------------------------------------------- +# Upsert +# --------------------------------------------------------------------------- + +@pytest.mark.skipif(not _MILVUS_AVAILABLE, reason="pymilvus not installed") +class TestUpsert: + def test_upsert_inserts_page_rows(self): + store, mock_client = _make_mock_store() + mock_client.list_collections.return_value = [ + "test_idx__pages", "test_idx__tokens" + ] + sp = _point(0, 1, n_tokens=3, dim=8) + store.upsert([sp]) + page_col, token_col = _collection_names("test_idx") + upsert_calls = {c.kwargs["collection_name"]: c for c in mock_client.upsert.call_args_list} + assert page_col in upsert_calls + + def test_upsert_inserts_token_rows(self): + store, mock_client = _make_mock_store() + mock_client.list_collections.return_value = [ + "test_idx__pages", "test_idx__tokens" + ] + n_tokens = 5 + sp = _point(0, 1, n_tokens=n_tokens, dim=8) + store.upsert([sp]) + page_col, token_col = _collection_names("test_idx") + upsert_calls = {c.kwargs["collection_name"]: c for c in mock_client.upsert.call_args_list} + assert token_col in upsert_calls + token_rows = upsert_calls[token_col].kwargs["data"] + assert len(token_rows) == n_tokens + + def test_upsert_page_vector_is_mean_pooled(self): + store, mock_client = _make_mock_store() + mock_client.list_collections.return_value = [ + "test_idx__pages", "test_idx__tokens" + ] + vec = torch.ones(3, 4) # all ones → mean pool = [1,1,1,1] + pid = make_point_id(0, 1) + sp = StoredPoint( + point_id=pid, + vector=vec, + payload={"doc_id": 0, "page_id": 1, "chunk_id": None, "metadata": {}}, + ) + store.upsert([sp]) + page_col, _ = _collection_names("test_idx") + upsert_calls = {c.kwargs["collection_name"]: c for c in mock_client.upsert.call_args_list} + page_row = upsert_calls[page_col].kwargs["data"][0] + assert page_row["page_vector"] == pytest.approx([1.0, 1.0, 1.0, 1.0]) + + def test_upsert_token_rows_share_page_id(self): + store, mock_client = _make_mock_store() + mock_client.list_collections.return_value = [ + "test_idx__pages", "test_idx__tokens" + ] + pid = make_point_id(0, 1) + sp = _point(0, 1, n_tokens=3) + store.upsert([sp]) + _, token_col = _collection_names("test_idx") + upsert_calls = {c.kwargs["collection_name"]: c for c in mock_client.upsert.call_args_list} + token_rows = upsert_calls[token_col].kwargs["data"] + page_ids = {row["page_id"] for row in token_rows} + assert page_ids == {_page_id_str(pid)} + + def test_point_exists_true(self): + store, mock_client = _make_mock_store() + mock_client.list_collections.return_value = ["test_idx__pages"] + mock_client.get.return_value = [{"id": "100"}] + assert store.point_exists(100) + + def test_point_exists_false(self): + store, mock_client = _make_mock_store() + mock_client.list_collections.return_value = ["test_idx__pages"] + mock_client.get.return_value = [] + assert not store.point_exists(100) + + +# --------------------------------------------------------------------------- +# Search — two-stage retrieval logic +# --------------------------------------------------------------------------- + +@pytest.mark.skipif(not _MILVUS_AVAILABLE, reason="pymilvus not installed") +class TestSearch: + def _make_store_with_candidates(self, candidate_ids: list[str]) -> tuple[MilvusVectorStore, MagicMock]: + store, mock_client = _make_mock_store() + mock_client.list_collections.return_value = [ + "test_idx__pages", "test_idx__tokens" + ] + + # _fetch_candidates: mock page search + def page_search(**kwargs): + rows = [ + { + "id": cid, + "entity": { + "payload_json": _serialize_payload( + {"doc_id": int(cid) // 10_000_000, "page_id": 1, "metadata": {}} + ) + }, + } + for cid in candidate_ids + ] + return [rows] + + # _late_interaction_rerank: mock token search, return first candidate as best + def token_search(**kwargs): + if not candidate_ids: + return [[]] + rows = [ + { + "entity": {"page_id": candidate_ids[0]}, + "distance": 0.9, + } + ] + return [rows] + + mock_client.search.side_effect = [page_search(**{}), token_search(**{})] + return store, mock_client + + def test_search_returns_search_hits(self): + candidate_ids = [str(make_point_id(i, 1)) for i in range(3)] + store, mock_client = _make_mock_store() + mock_client.list_collections.return_value = ["test_idx__pages", "test_idx__tokens"] + + # Mock page search + page_rows = [ + {"id": cid, "entity": {"payload_json": _serialize_payload({"doc_id": i, "page_id": 1, "metadata": {}})}} + for i, cid in enumerate(candidate_ids) + ] + # Mock token search (one per query token) + token_rows = [ + {"entity": {"page_id": candidate_ids[0]}, "distance": 0.9} + ] + mock_client.search.side_effect = [[page_rows], [token_rows]] + + q = MultiVectorQuery(vectors=torch.rand(1, 8)) + results = store.search(q, k=2) + assert len(results) > 0 + assert all(hasattr(r, "point_id") for r in results) + + def test_search_empty_candidates_returns_empty(self): + store, mock_client = _make_mock_store() + mock_client.list_collections.return_value = ["test_idx__pages", "test_idx__tokens"] + mock_client.search.return_value = [[]] + q = MultiVectorQuery(vectors=torch.rand(1, 8)) + results = store.search(q, k=5) + assert results == [] + + +# --------------------------------------------------------------------------- +# fetch_vector +# --------------------------------------------------------------------------- + +@pytest.mark.skipif(not _MILVUS_AVAILABLE, reason="pymilvus not installed") +class TestFetchVector: + def test_fetch_returns_tensor(self): + store, mock_client = _make_mock_store() + mock_client.list_collections.return_value = ["test_idx__tokens"] + dim = 8 + n_tokens = 3 + token_rows = [{"token_vector": torch.rand(dim).tolist()} for _ in range(n_tokens)] + mock_client.query.return_value = token_rows + vec = store.fetch_vector(make_point_id(0, 1)) + assert vec is not None + assert vec.shape == (n_tokens, dim) + + def test_fetch_returns_none_when_no_rows(self): + store, mock_client = _make_mock_store() + mock_client.list_collections.return_value = ["test_idx__tokens"] + mock_client.query.return_value = [] + assert store.fetch_vector(make_point_id(0, 1)) is None + + +# --------------------------------------------------------------------------- +# Integration test: Milvus Lite (no external server) +# --------------------------------------------------------------------------- + +@pytest.mark.slow +@pytest.mark.skipif(not _MILVUS_AVAILABLE, reason="pymilvus not installed") +class TestMilvusIntegration: + """Full round-trip using Milvus Lite (file-based).""" + + def test_round_trip(self, tmp_path): + dim = 8 + store = MilvusVectorStore() + + # open with create=True but no dim → collection not created yet + store.open("test_rt", tmp_path, create=False) + assert not store.collection_exists() + + # Create the collection explicitly + store.create_collection(dim) + assert store.collection_exists() + + pts = [ + StoredPoint( + point_id=make_point_id(i, 1), + vector=torch.rand(4, dim), + payload={"doc_id": i, "page_id": 1, "chunk_id": None, "metadata": {}}, + ) + for i in range(5) + ] + store.upsert(pts) + + assert store.point_exists(pts[0].point_id) + assert not store.point_exists(make_point_id(99, 99)) + + q = MultiVectorQuery(vectors=torch.rand(2, dim)) + results = store.search(q, k=3) + assert len(results) > 0 + + vec = store.fetch_vector(pts[0].point_id) + assert vec is not None + assert vec.shape[1] == dim + + store.close() diff --git a/tests/test_vector_store_qdrant.py b/tests/test_vector_store_qdrant.py new file mode 100644 index 0000000..3dd447e --- /dev/null +++ b/tests/test_vector_store_qdrant.py @@ -0,0 +1,268 @@ +"""Tests for QdrantVectorStore. + +Unit tests mock the QdrantClient. +The slow integration test runs a real embedded Qdrant (no external server needed). +""" +from __future__ import annotations + +import tempfile +from pathlib import Path +from unittest.mock import MagicMock, patch, PropertyMock + +import pytest +import torch + +from foretrieval.vector_store.base import ( + MultiVectorQuery, + StoredPoint, + make_point_id, +) +from foretrieval.vector_store.qdrant import QdrantVectorStore, _QDRANT_AVAILABLE + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _point(doc_id: int, page_id: int, dim: int = 8) -> StoredPoint: + pid = make_point_id(doc_id, page_id) + return StoredPoint( + point_id=pid, + vector=torch.rand(4, dim), + payload={"doc_id": doc_id, "page_id": page_id, "chunk_id": None, "metadata": {}}, + ) + + +def _make_mock_store() -> tuple[QdrantVectorStore, MagicMock]: + """Return a QdrantVectorStore with a mock client already injected.""" + store = QdrantVectorStore.__new__(QdrantVectorStore) + mock_client = MagicMock() + mock_client.collection_exists.return_value = False + store._client = mock_client + store._collection_name = "test_idx" + store._index_root = Path("/tmp/foretrieval_test") + return store, mock_client + + +# --------------------------------------------------------------------------- +# Optional-dependency guard +# --------------------------------------------------------------------------- + +class TestOptionalDepGuard: + def test_missing_qdrant_raises_on_open(self, tmp_path): + with patch("foretrieval.vector_store.qdrant._QDRANT_AVAILABLE", False): + store = QdrantVectorStore() + with pytest.raises(RuntimeError, match="qdrant-client"): + store.open("idx", tmp_path, create=True, dim=8) + + +# --------------------------------------------------------------------------- +# collection_exists / create_collection +# --------------------------------------------------------------------------- + +@pytest.mark.skipif(not _QDRANT_AVAILABLE, reason="qdrant-client not installed") +class TestCollectionManagement: + def test_collection_exists_false_when_not_created(self): + store, mock_client = _make_mock_store() + mock_client.collection_exists.return_value = False + assert not store.collection_exists() + + def test_collection_exists_true_when_created(self): + store, mock_client = _make_mock_store() + mock_client.collection_exists.return_value = True + assert store.collection_exists() + + def test_create_collection_calls_client(self): + store, mock_client = _make_mock_store() + mock_client.collection_exists.return_value = False + store.create_collection(dim=128) + mock_client.create_collection.assert_called_once() + call_kwargs = mock_client.create_collection.call_args[1] + assert call_kwargs["collection_name"] == "test_idx" + + def test_create_collection_multivector_config(self): + """Collection must be created with MultiVectorConfig(MAX_SIM).""" + from qdrant_client.models import MultiVectorComparator + store, mock_client = _make_mock_store() + mock_client.collection_exists.return_value = False + store.create_collection(dim=128) + call_kwargs = mock_client.create_collection.call_args[1] + vc = call_kwargs["vectors_config"] + assert vc.multivector_config.comparator == MultiVectorComparator.MAX_SIM + + def test_create_collection_noop_if_already_exists(self): + store, mock_client = _make_mock_store() + mock_client.collection_exists.return_value = True + store.create_collection(dim=128) + mock_client.create_collection.assert_not_called() + + +# --------------------------------------------------------------------------- +# Upsert +# --------------------------------------------------------------------------- + +@pytest.mark.skipif(not _QDRANT_AVAILABLE, reason="qdrant-client not installed") +class TestUpsert: + def test_upsert_calls_client_upsert(self): + store, mock_client = _make_mock_store() + sp = _point(0, 1) + store.upsert([sp]) + mock_client.upsert.assert_called_once() + call_kwargs = mock_client.upsert.call_args[1] + assert call_kwargs["collection_name"] == "test_idx" + assert len(call_kwargs["points"]) == 1 + + def test_upsert_payload_contains_doc_page(self): + store, mock_client = _make_mock_store() + sp = _point(5, 3) + store.upsert([sp]) + point = mock_client.upsert.call_args[1]["points"][0] + assert point.payload["doc_id"] == 5 + assert point.payload["page_id"] == 3 + + def test_upsert_vector_is_list_of_floats(self): + store, mock_client = _make_mock_store() + sp = _point(0, 1) + store.upsert([sp]) + point = mock_client.upsert.call_args[1]["points"][0] + # Multivector: list of lists + assert isinstance(point.vector, list) + assert all(isinstance(row, list) for row in point.vector) + + def test_point_exists_returns_true(self): + store, mock_client = _make_mock_store() + mock_client.collection_exists.return_value = True + mock_client.retrieve.return_value = [MagicMock()] + assert store.point_exists(make_point_id(0, 1)) + + def test_point_exists_returns_false(self): + store, mock_client = _make_mock_store() + mock_client.collection_exists.return_value = True + mock_client.retrieve.return_value = [] + assert not store.point_exists(make_point_id(0, 1)) + + +# --------------------------------------------------------------------------- +# Search +# --------------------------------------------------------------------------- + +@pytest.mark.skipif(not _QDRANT_AVAILABLE, reason="qdrant-client not installed") +class TestSearch: + def _make_hit(self, pid, score, doc_id, page_id): + hit = MagicMock() + hit.id = pid + hit.score = score + hit.payload = {"doc_id": doc_id, "page_id": page_id, "chunk_id": None, "metadata": {}} + return hit + + def test_search_returns_search_hits(self): + store, mock_client = _make_mock_store() + hits = [self._make_hit(100, 0.9, 0, 1), self._make_hit(200, 0.5, 1, 1)] + response = MagicMock() + response.points = hits + mock_client.query_points.return_value = response + + q = MultiVectorQuery(vectors=torch.rand(2, 8)) + results = store.search(q, k=2) + assert len(results) == 2 + assert results[0].point_id == 100 + assert results[0].score == pytest.approx(0.9) + + def test_search_passes_k_to_client(self): + store, mock_client = _make_mock_store() + response = MagicMock() + response.points = [] + mock_client.query_points.return_value = response + q = MultiVectorQuery(vectors=torch.rand(2, 8)) + store.search(q, k=7) + call_kwargs = mock_client.query_points.call_args[1] + assert call_kwargs["limit"] == 7 + + def test_search_with_metadata_filter_passes_filter(self): + from qdrant_client.models import Filter + store, mock_client = _make_mock_store() + response = MagicMock() + response.points = [] + mock_client.query_points.return_value = response + q = MultiVectorQuery(vectors=torch.rand(2, 8), filter_metadata={"category": "A"}) + store.search(q, k=5) + call_kwargs = mock_client.query_points.call_args[1] + assert call_kwargs["query_filter"] is not None + assert isinstance(call_kwargs["query_filter"], Filter) + + def test_search_no_filter_passes_none(self): + store, mock_client = _make_mock_store() + response = MagicMock() + response.points = [] + mock_client.query_points.return_value = response + q = MultiVectorQuery(vectors=torch.rand(2, 8)) + store.search(q, k=5) + call_kwargs = mock_client.query_points.call_args[1] + assert call_kwargs["query_filter"] is None + + +# --------------------------------------------------------------------------- +# fetch_vector +# --------------------------------------------------------------------------- + +@pytest.mark.skipif(not _QDRANT_AVAILABLE, reason="qdrant-client not installed") +class TestFetchVector: + def test_fetch_returns_tensor(self): + store, mock_client = _make_mock_store() + vec_data = [[0.1, 0.2, 0.3, 0.4]] * 4 + retrieved = MagicMock() + retrieved.vector = vec_data + mock_client.retrieve.return_value = [retrieved] + result = store.fetch_vector(make_point_id(0, 1)) + assert result is not None + assert isinstance(result, torch.Tensor) + + def test_fetch_returns_none_when_not_found(self): + store, mock_client = _make_mock_store() + mock_client.retrieve.return_value = [] + assert store.fetch_vector(make_point_id(99, 99)) is None + + +# --------------------------------------------------------------------------- +# Integration test: real embedded Qdrant +# --------------------------------------------------------------------------- + +@pytest.mark.slow +@pytest.mark.skipif(not _QDRANT_AVAILABLE, reason="qdrant-client not installed") +class TestQdrantIntegration: + """Full round-trip: open → upsert → search → fetch_vector → close.""" + + def test_round_trip(self, tmp_path): + dim = 8 + store = QdrantVectorStore() + + # Open with create=False first to test empty state + store.open("test_rt", tmp_path, create=False) + assert not store.collection_exists() + + # Create collection explicitly + store.create_collection(dim) + assert store.collection_exists() + + pts = [ + StoredPoint( + point_id=make_point_id(i, 1), + vector=torch.rand(4, dim), + payload={"doc_id": i, "page_id": 1, "chunk_id": None, "metadata": {}}, + ) + for i in range(5) + ] + store.upsert(pts) + + assert store.point_exists(pts[0].point_id) + assert not store.point_exists(make_point_id(99, 99)) + + q = MultiVectorQuery(vectors=torch.rand(2, dim)) + results = store.search(q, k=3) + assert len(results) == 3 + + vec = store.fetch_vector(pts[0].point_id) + assert vec is not None + assert vec.shape[1] == dim + + store.close() diff --git a/tests/test_vector_store_remote.py b/tests/test_vector_store_remote.py new file mode 100644 index 0000000..5d5addb --- /dev/null +++ b/tests/test_vector_store_remote.py @@ -0,0 +1,208 @@ +"""Tests for RemoteVectorStore — all HTTP calls mocked via a MagicMock client.""" + +from pathlib import Path +from unittest.mock import MagicMock +import pytest +import torch + +from foretrieval.vector_store.remote import RemoteVectorStore +from foretrieval.vector_store.base import MultiVectorQuery, SearchHit, StoredPoint, make_point_id + + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _make_mock_client() -> MagicMock: + client = MagicMock() + client.open_collection.return_value = {"opened": True, "backend": "qdrant", "created": True} + client.collection_exists.return_value = True + client.point_exists.return_value = False + client.search.return_value = [] + client.fetch_vector.return_value = None + return client + + +def _make_store(backend="qdrant") -> tuple[RemoteVectorStore, MagicMock]: + client = _make_mock_client() + store = RemoteVectorStore(client, backend=backend) + return store, client + + +def _make_point(doc_id=1, page_id=0, dim=8) -> StoredPoint: + return StoredPoint( + point_id=make_point_id(doc_id, page_id), + vector=torch.randn(3, dim), + payload={"doc_id": doc_id, "page_id": page_id}, + ) + + +# --------------------------------------------------------------------------- +# Lifecycle +# --------------------------------------------------------------------------- + +class TestLifecycle: + def test_open_calls_client(self): + store, client = _make_store() + store.open("idx", Path("."), create=True, dim=128) + client.open_collection.assert_called_once_with( + "idx", "qdrant", create=True, dim=128, storage_config=None + ) + + def test_is_opened_after_open(self): + store, _ = _make_store() + assert not store.is_opened + store.open("idx", Path("."), create=True, dim=128) + assert store.is_opened + + def test_close_marks_not_opened(self): + store, _ = _make_store() + store.open("idx", Path("."), create=True, dim=128) + store.close() + assert not store.is_opened + + def test_backend_name_class_attr(self): + assert RemoteVectorStore.backend_name == "remote" + + +# --------------------------------------------------------------------------- +# collection_exists +# --------------------------------------------------------------------------- + +class TestCollectionExists: + def test_returns_false_before_open(self): + store, client = _make_store() + assert store.collection_exists() is False + + def test_delegates_to_client(self): + store, client = _make_store() + store.open("idx", Path("."), create=True) + client.collection_exists.return_value = True + assert store.collection_exists() is True + client.collection_exists.assert_called_with("idx") + + +# --------------------------------------------------------------------------- +# upsert / point_exists +# --------------------------------------------------------------------------- + +class TestWrite: + def test_upsert_delegates_to_client(self): + store, client = _make_store() + store.open("idx", Path("."), create=True) + pts = [_make_point()] + store.upsert(pts) + client.upsert.assert_called_once_with("idx", pts) + + def test_point_exists_delegates(self): + store, client = _make_store() + store.open("idx", Path("."), create=True) + client.point_exists.return_value = True + pid = make_point_id(1, 0) + assert store.point_exists(pid) is True + client.point_exists.assert_called_with("idx", pid) + + def test_point_exists_returns_false_before_open(self): + store, _ = _make_store() + assert store.point_exists(12345) is False + + def test_upsert_raises_without_open(self): + store, _ = _make_store() + with pytest.raises(RuntimeError, match="open"): + store.upsert([_make_point()]) + + +# --------------------------------------------------------------------------- +# search +# --------------------------------------------------------------------------- + +class TestSearch: + def test_search_delegates_to_client(self): + store, client = _make_store() + store.open("idx", Path("."), create=True) + hit = SearchHit(point_id=10000, score=9.5, payload={"doc_id": 1, "page_id": 0}) + client.search.return_value = [hit] + + query = MultiVectorQuery(vectors=torch.randn(4, 8)) + results = store.search(query, k=3) + + client.search.assert_called_once_with("idx", query, 3) + assert len(results) == 1 + assert results[0].score == pytest.approx(9.5) + + def test_search_raises_without_open(self): + store, _ = _make_store() + with pytest.raises(RuntimeError, match="open"): + store.search(MultiVectorQuery(vectors=torch.randn(2, 8)), k=5) + + +# --------------------------------------------------------------------------- +# fetch_vector +# --------------------------------------------------------------------------- + +class TestFetchVector: + def test_fetch_returns_tensor(self): + store, client = _make_store() + store.open("idx", Path("."), create=True) + t = torch.randn(3, 8) + client.fetch_vector.return_value = t + result = store.fetch_vector(make_point_id(1, 0)) + assert result is t + + def test_fetch_returns_none_on_miss(self): + store, client = _make_store() + store.open("idx", Path("."), create=True) + client.fetch_vector.return_value = None + assert store.fetch_vector(99999) is None + + def test_fetch_returns_none_before_open(self): + store, _ = _make_store() + assert store.fetch_vector(12345) is None + + +# --------------------------------------------------------------------------- +# Persistence no-ops +# --------------------------------------------------------------------------- + +class TestPersistenceNoOps: + def test_export_sidecar_noop(self, tmp_path): + store, _ = _make_store() + store.open("idx", Path("."), create=True) + # Should not raise or create any files + store.export_sidecar(tmp_path) + assert list(tmp_path.iterdir()) == [] + + def test_load_sidecar_noop(self, tmp_path): + store, _ = _make_store() + store.open("idx", Path("."), create=True) + store.load_sidecar(tmp_path) # no error + + +# --------------------------------------------------------------------------- +# Bookkeeping (server-side, replaces local sidecars in remote mode) +# --------------------------------------------------------------------------- + +class TestBookkeeping: + def test_supports_remote_bookkeeping(self): + store, _ = _make_store() + assert store.supports_remote_bookkeeping() is True + + def test_export_bookkeeping_delegates(self): + store, client = _make_store() + store.open("idx", Path("."), create=True) + blob = {"index_config": {"model_name": "m"}} + store.export_bookkeeping(blob) + client.put_bookkeeping.assert_called_once_with("idx", blob) + + def test_load_bookkeeping_delegates(self): + store, client = _make_store() + store.open("idx", Path("."), create=True) + client.get_bookkeeping.return_value = {"index_config": {"model_name": "m"}} + out = store.load_bookkeeping() + client.get_bookkeeping.assert_called_once_with("idx") + assert out["index_config"]["model_name"] == "m" + + def test_export_bookkeeping_before_open_raises(self): + store, _ = _make_store() + with pytest.raises(RuntimeError): + store.export_bookkeeping({})