diff --git a/bitagent_subnet-main/.circleci/config.yml b/bitagent_subnet-main/.circleci/config.yml new file mode 100644 index 0000000000000000000000000000000000000000..0473afe6f31db161ddb32904aa5a4c90a39f9fef --- /dev/null +++ b/bitagent_subnet-main/.circleci/config.yml @@ -0,0 +1,168 @@ +version: 2.1 + +orbs: + python: circleci/python@2.1.1 + python-lib: dialogue/python-lib@0.1.55 + # coveralls: coveralls/coveralls@1.0.6 + +jobs: + black: + resource_class: small + parameters: + python-version: + type: string + docker: + - image: cimg/python:<< parameters.python-version >> + + steps: + - checkout + + - restore_cache: + name: Restore cached black venv + keys: + - v1-pypi-py-black-<< parameters.python-version >> + + - run: + name: Update & Activate black venv + command: | + python -m venv env/ + . env/bin/activate + python -m pip install --upgrade pip + pip install black + + - save_cache: + name: Save cached black venv + paths: + - "env/" + key: v1-pypi-py-black-<< parameters.python-version >> + + - run: + name: Black format check + command: | + . env/bin/activate + black --line-length 79 --exclude '(env|venv|.eggs)' --check . + + pylint: + resource_class: small + parameters: + python-version: + type: string + docker: + - image: cimg/python:<< parameters.python-version >> + + steps: + - checkout + + - run: + name: Install Pylint + command: | + python -m venv env/ + . env/bin/activate + pip install pylint + + - run: + name: Pylint check + command: | + . env/bin/activate + pylint --fail-on=W,E,F --exit-zero ./ + + check_compatibility: + parameters: + python_version: + type: string + docker: + - image: cimg/python:3.10 + steps: + - checkout + - run: + name: Check if requirements files have changed + command: ./scripts/check_requirements_changes.sh + - run: + name: Install dependencies and Check compatibility + command: | + if [ "$REQUIREMENTS_CHANGED" == "true" ]; then + sudo apt-get update + sudo apt-get install -y jq curl + ./scripts/check_compatibility.sh << parameters.python_version >> + else + echo "Skipping compatibility checks..." + fi + + build: + resource_class: medium + parallelism: 2 + parameters: + python-version: + type: string + docker: + - image: cimg/python:<< parameters.python-version >> + + steps: + - checkout + + - restore_cache: + name: Restore cached venv + keys: + - v1-pypi-py<< parameters.python-version >>-{{ checksum "requirements.txt" }} + - v1-pypi-py<< parameters.python-version >> + + - run: + name: Update & Activate venv + command: | + python -m venv env/ + . env/bin/activate + python -m pip install --upgrade pip + + - save_cache: + name: Save cached venv + paths: + - "env/" + key: v1-pypi-py<< parameters.python-version >>-{{ checksum "requirements.txt" }} + + - run: + name: Install Bittensor Subnet Template + command: | + . env/bin/activate + pip install -e . + + - store_test_results: + path: test-results + - store_artifacts: + path: test-results + + coveralls: + docker: + - image: cimg/python:3.10 + steps: + - run: + name: Combine Coverage + command: | + pip3 install --upgrade coveralls + coveralls --finish --rcfile .coveragerc || echo "Failed to upload coverage" + +workflows: + compatibility_checks: + jobs: + - check_compatibility: + python_version: "3.8" + name: check-compatibility-3.8 + - check_compatibility: + python_version: "3.9" + name: check-compatibility-3.9 + - check_compatibility: + python_version: "3.10" + name: check-compatibility-3.10 + - check_compatibility: + python_version: "3.11" + name: check-compatibility-3.11 + + pr-requirements: + jobs: + - black: + python-version: "3.8.12" + - pylint: + python-version: "3.8.12" + - build: + matrix: + parameters: + python-version: ["3.9.13", "3.10.6", "3.11.4"] diff --git a/bitagent_subnet-main/.gitignore b/bitagent_subnet-main/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..afcea06eae622d9bca2b23e4b503b7becd4e2aa3 --- /dev/null +++ b/bitagent_subnet-main/.gitignore @@ -0,0 +1,184 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +.venvsglang +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +testing/ +env/ +app.config.js +mnemonics.txt +run_it_all.sh + +wandb +TODO +bitagent.data* +.vscode +repo +.cometml-runs +old.* + +node_modules +package-lock.json +package.json +*net.py +*.old + +notebooks +Notebooks diff --git a/bitagent_subnet-main/LICENSE b/bitagent_subnet-main/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..9410f0f22058bdad104b8aca6beea58d20b8bd05 --- /dev/null +++ b/bitagent_subnet-main/LICENSE @@ -0,0 +1,22 @@ +MIT License + +Copyright (c) 2023 Opentensor +Copyright (c) 2023 RogueTensor + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/bitagent_subnet-main/README.md b/bitagent_subnet-main/README.md new file mode 100644 index 0000000000000000000000000000000000000000..f3679dd53a2d4fc93bbc3867fde18c4b6e6332dc --- /dev/null +++ b/bitagent_subnet-main/README.md @@ -0,0 +1,474 @@ +
+ +# **BitAgent Subnet (#20) on Bittensor** +[![Discord Chat](https://img.shields.io/discord/308323056592486420.svg)](https://discord.com/channels/799672011265015819/1175085112703078400) +[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) + +--- + +## Agency for Your World Through Natural Language + +**Communications:** [BitAgent Discord](https://discord.com/channels/799672011265015819/1194736998250975332)\ +**Downstream Applications:** [GoGoAgent](https://gogoagent.ai/) • [MSP Tech](https://msptech.ai) +
+ +--- +- [Introduction](#introduction) +- [Get Running](#get-running) + - [BitAgent](#bitagent) + - [Validator](#validator) + - [Dependencies](#dependencies) + - [Installation](#installation) + - [vLLM Setup for Validators](#vllm-setup-for-validators) + - [sglang Setup for Validators](#sglang-setup-for-validators) + - [Recommended Startup](#recommended-startup) + - [Alternative Startup](#alternative-startup) + - [Verify Validator is Working](#verify-validator-is-working) + - [Hardware Requirements](#validator-hardware-requirements) + - [Miner](#miner) + - [Hardware Requirements](#miner-hardware-requirements) + - [Default Miner](#default-miner) + - [Miner Emissions](#miner-emissions) + - [Miner Considerations](#miner-considerations) + - [Example Task](#example-task) + - [Miner Feedback](#miner-feedback) + - [Advanced](#advanced) +- [FAQ](#faq) +- [License](#license) + +## Introduction + +**Quick Pitch**: BitAgent revolutionizes how you manage tasks and workflows across platforms, merging the capabilities of large language models (LLMs) with the convenience of your favorite apps such as web browsers, Discord, and custom integrations. BitAgent empowers users to seamlessly integrate intelligent agents, providing personalized assistance and integrated task automation. + +**Key Objective** - provide intelligent agency to simplify and automate tasks in your day-to-day + +**GoGoAgent - Our Application** - [https://gogoagent.ai](https://gogoagent.ai) \ +**MSPTech - Real world business case** - [https://MSPTech.ai](https://msptech.ai) + +**Key Features** +- Working our way up the [Berkeley Function Calling Leaderboard](https://gorilla.cs.berkeley.edu/leaderboard.html#leaderboard) (BFCL) +- No API / subscription requirements +- Run light models (8B parameter) for huge impact +- FINETUNED MODEL evaluation of tool calling language model fine tunes +- MINER HOSTED evaluation of miners running tool calling language models allowing applications to scale on top of SN20 +- Miner's receive [transparent feedback](#miner-feedback) +- And a BONUS for getting this far - are you tired of waiting for registration slots? Check out [register.sh](./scripts/register.sh) + +--- + +## Get Running + +- BitAgent is a competitive subnet, meaning miners succeed and fail based on how well they perform on tasks. +- **Make sure to test your miner on Testnet 76 before ever considering registering for Subnet 20.** +- Newly registered miners will start at the median score per validator and go up or down depending on their performance. +- Before getting too far, please make sure you've looked over the [Bittensor documentation](https://docs.bittensor.com/) for your needs. +- The min compute requirements are [noted below for Validators](#hardware-requirements). +- See [FAQ](#faq) for a few more details related to computing requirements for validators and miners. +- The minimum requirements for a miner are determined by the resources needed to run a competitive and performant tool calling LLM. + +### BitAgent +This repository requires python 3.10 or higher. +To install and get running, simply clone this repository and install the requirements. +```bash +git clone https://github.com/RogueTensor/bitagent_subnet +cd bitagent_subnet +# at this point, it's recommended that you use a venv, but not required; the next two lines are venv specific +python -m venv .venv #replace .venv with the name you'd like to use for your primary venv +source ./.venv/bin/activate +python -m pip install -e . +``` + +Then make sure to register your intended wallet (coldkey, hotkey) to Subnet 20: +```bash +btcli subnet register --wallet.path --wallet.name $coldkey --wallet.hotkey $hotkey --subtensor.network finney --netuid 20 +``` + +### Validator + +#### Dependencies + +You must have the following things: + +- System with at least 48gb of VRAM +- Python >=3.10 +- Docker with [gpu support](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) + +#### Installation + +Ensure that you have Docker with GPU support, you can choose to follow either of the instructions: + +- [Official Guide](https://docs.nvidia.com/datacenter/cloud-native/container-toolkit/latest/install-guide.html) +- [Quick and Dirty Stack Overflow Guide](https://stackoverflow.com/questions/75118992/docker-error-response-from-daemon-could-not-select-device-driver-with-capab) + + +Install [PM2](https://pm2.io/docs/runtime/guide/installation/) and the [`jq` package](https://jqlang.github.io/jq/) on your system.\ + **On Linux**: + ```bash + sudo apt update && sudo apt install jq && sudo apt install npm && sudo npm install pm2 -g && pm2 update + ``` + **On Mac OS** + ```bash + brew update && brew install jq && brew install npm && sudo npm install pm2 -g && pm2 update + ``` + +#### vLLM Setup for Validators + +Validators must spin-up their own LLM (specifically mistral 7B). +Note: Previously we ran the LLM's inside the validator code with the transformer package, however we pivoted away from that due to the inefficiency of running the model using vanilla transformers. Hosting the models using llama.cpp, oobabooga, vllm, TGI, are much better options as they provide additional functionality. + +To run with vLLM you can do the following: + +```bash +sudo docker run -d -p 8000:8000 --gpus all --ipc host --name mistral-instruct docker.io/vllm/vllm-openai:latest --model thesven/Mistral-7B-Instruct-v0.3-GPTQ --max-model-len 8912 --quantization gptq --dtype half --gpu-memory-utilization 0.45 +``` + +This will run the LLM on port 8000. To change the port, change the host port for this parameter up above `-p :`. And use `--openai-api-base http://localhost:/v1` in your params to point to the vLLM model for SN20. + +#### sglang Setup for Validators + +You'll need to create a virtual env and install the requirements for sglang: +```bash +python3 -m venv .venvsglang +# note to change cu121 in this path according to this page: https://docs.flashinfer.ai/installation.html +./.venvsglang/bin/pip install flashinfer -i https://flashinfer.ai/whl/cu121/torch2.4/ +./.venvsglang/bin/pip install -r requirements.sglang.txt +``` + +**Test that it's working with:** +``` +.venvsglang/bin/python -m sglang.launch_server --model-path Salesforce/xLAM-7b-r --port 8028 --host 0.0.0.0 --mem-fraction-static 0.40 +``` + +You should not run out of memory and it should eventually show that the Salesforce model loaded correclty. + +#### Recommended Startup + +Make sure you do the [vLLM setup](#vllm-setup-for-validators) above and the [sglang setup](#sglang-setup-for-validators) above. + +```bash +# for mainnet with AUTO UPDATES (recommended) +pm2 start run.sh --name bitagent_validators_autoupdate -- --wallet.path --wallet.name --wallet.hotkey --netuid 20 +``` + +Double check everything is working by following [these steps](#verify-validator-is-working). + +#### Alternative Startup + +Make sure you do the [vLLM setup](#vllm-setup-for-validators) above and the [sglang setup](#sglang-setup-for-validators) above. + +```bash +# for testnet +python3 neurons/validator.py --netuid 76 --subtensor.network test --wallet.path --wallet.name --wallet.hotkey + +# for mainnet +pm2 start neurons/validator.py --interpreter python3 -- --netuid 20 --subtensor.network --wallet.path --wallet.name --wallet.hotkey --axon.port +``` + +Double check everything is working by following [these steps](#verify-validator-is-working). + +#### Verify Validator is Working + +After you've launched and pm2 is running, here's what to expect:\ +- You'll see a LOT (one per mind) of IsAlive() queries like this:\ + ```bash + 1|bitagent | 2024-11-17 23:25:59.156 | TRACE | bittensor:loggingmachine.py:432 | dendrite | <-- | 3354 B | IsAlive | 5GbnkQJ6zfsWa9iX4ZtwKccXZv4s8MTt2LSQFmS8CMgjkSgx | 213.180.0.45:20019 | 200 | Success + 1|bitagent | 2024-11-17 23:26:04.135 | TRACE | bittensor:loggingmachine.py:432 | dendrite | <-- | 3327 B | IsAlive | 5E7eqUChR4WUnRwNAUXRNUZhhjEzTfdeGAvDyf99aygVGYBJ | 176.55.1.98:8091 | 408 | Request timeout after 5.0 seconds + 1|bitagent | 2024-11-17 23:26:04.180 | TRACE | bittensor:loggingmachine.py:432 | dendrite | <-- | 3331 B | IsAlive | 5EHQoRqwMHG3QVVpsSZBPHJD87SEwGn6FhTSR3LCj8XiHVUC | 109.206.196.130:8888 | 408 | Request timeout after 5.0 seconds + ``` +- After the IsAlive() queries, you'll start to see QueryTask queries followed by QueryResult queries, like these:\ + ```bash + 1|bitagent | 2024-11-17 23:53:20.322 | ERROR | bittensor:loggingmachine.py:457 | - ContentTypeError#aefadd84-8586-4faa-9206-e048c2b85114: 404, message='Attempt to decode JSON with unexpected mimetype: text/html', url='http://52.220.128.145:32222/QueryTask' - + 1|bitagent | 2024-11-17 23:53:20.323 | TRACE | bittensor:loggingmachine.py:432 | dendrite | <-- | 27205 B | QueryTask | 5GjGiziPatj7mf4is5JaDPJq4jbPnagoeiSHe4TfFERafM7X | 52.220.128.145:32222 | 422 | Failed to parse response: 404, message='Attempt to decode JSON with unexpected mimetype: text/html', url='http://52.220.128.145:32222/QueryTask' + 1|bitagent | 2024-11-17 23:53:21.708 | TRACE | bittensor:loggingmachine.py:432 | dendrite | <-- | 27522 B | QueryTask | 5GbnkQJ6zfsWa9iX4ZtwKccXZv4s8MTt2LSQFmS8CMgjkSgx | 213.180.0.45:20019 | 500 | Internal Server Error #b36dc761-1035-44d0-b88d-24fe9ccc7e1e + 1|bitagent | 2024-11-17 23:53:21.806 | TRACE | bittensor:loggingmachine.py:432 | dendrite | --> | 5418 B | QueryResult | 5GbnkQJ6zfsWa9iX4ZtwKccXZv4s8MTt2LSQFmS8CMgjkSgx | 213.180.0.45:20019 | 0 | Success + 1|bitagent | 2024-11-17 23:53:23.200 | TRACE | bittensor:loggingmachine.py:432 | dendrite | <-- | 5578 B | QueryResult | 5GbnkQJ6zfsWa9iX4ZtwKccXZv4s8MTt2LSQFmS8CMgjkSgx | 213.180.0.45:20019 | 200 | Success + + ``` +- These logs above let you know that the ONLINE / MINER HOSTED querying is working. +- Finally, you'll want to check the miners' HF (hugging face) models are being evaluated OFFLINE. +- You'll want to check your `pm2 log | grep OFFLINE` output for lines like these (from testnet):\ + ```bash + 1|bitagent | 2024-11-17 23:26:07.154 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: Starting offline mode for competition 1-1 + 1|bitagent | 2024-11-17 23:26:08.831 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: Starting offline task + 1|bitagent | 2024-11-17 23:26:12.529 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: Miner HF model names: [None, 'Salesforce/xLAM-7b-r'] + 1|bitagent | 2024-11-17 23:26:12.529 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: Unique miner HF model names: ['Salesforce/xLAM-7b-r'] + 1|bitagent | 2024-11-17 23:26:12.529 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: Generating tasks + 1|bitagent | 2024-11-17 23:28:21.793 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: Generated 1000 tasks of 1000 total + 1|bitagent | 2024-11-17 23:28:21.793 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: Running tasks for model Salesforce/xLAM-7b-r + 1|bitagent | 2024-11-17 23:28:21.939 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: Starting server for model Salesforce/xLAM-7b-r + 1|bitagent | 2024-11-17 23:28:21.941 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: Started server for model Salesforce/xLAM-7b-r, waiting for it to start on port 8028 (could take several minutes) + 1|bitagent | 2024-11-17 23:29:25.469 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: Server for model Salesforce/xLAM-7b-r started + 1|bitagent | 2024-11-17 23:29:25.470 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: Getting LLM responses for model Salesforce/xLAM-7b-r + 1|bitagent | 2024-11-17 23:38:54.257 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: Got 1000 LLM responses for model: Salesforce/xLAM-7b-r + 1|bitagent | 2024-11-17 23:38:54.258 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: Terminating server for model: Salesforce/xLAM-7b-r + 1|bitagent | 2024-11-17 23:38:54.965 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: Terminated server for model: Salesforce/xLAM-7b-r + 1|bitagent | 2024-11-17 23:38:55.030 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: Processing rewards for model: Salesforce/xLAM-7b-r, for miners: [160] + 1|bitagent | 2024-11-17 23:38:58.530 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE Scattered rewards: [np.float64(0.16442660138718893)] + 1|bitagent | 2024-11-17 23:38:58.531 | DEBUG | bittensor:loggingmachine.py:437 | Updated moving avg OFFLINE scores for Competition 1-1: [-0.5 -0.5 -0.5 -0.5 -0.5 -0.5 + 1|bitagent | 2024-11-17 23:38:58.533 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: Deleting model from HF cache: Salesforce/xLAM-7b-r + 1|bitagent | 2024-11-17 23:39:01.198 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: Model 'Salesforce/xLAM-7b-r' has been removed from the cache. + 1|bitagent | 2024-11-17 23:39:01.199 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: Finished processing offline tasks + 1|bitagent | 2024-11-17 23:39:02.765 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: Starting offline mode for competition 1-1 + 1|bitagent | 2024-11-17 23:39:03.147 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: Starting offline task + 1|bitagent | 2024-11-17 23:39:03.638 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: Miner HF model names: [None] + 1|bitagent | 2024-11-17 23:39:03.638 | DEBUG | bittensor:loggingmachine.py:437 | OFFLINE: No unique miner HF model names to evaluate in OFFLINE mode + ``` +- If you're seeing all of this output, your validator is working! + +#### Validator Hardware Requirements + +Validators have hardware requirements. Two LLMS are needed to be run simultaneously: + - 1st LLM `thesven/Mistral-7B-Instruct-v0.3-GPTQ` can run off of 10GB to 20GB of VRAM - this model is used to alter tasks before going out to miners. + - 2nd LLM is each miner's tool calling model fetched from Hugging Face, one at a time to be evaluated OFFLINE for FINETUNED SUBMISSION and takes up 20GB to 30GB of VRAM. + +### Miner +If you just want to run the miner without the [script](./scripts/setup_and_run.sh) or are connecting to mainnet: +```bash +# for testing (use testnet 76) +python3 neurons/miner.py --netuid 76 --subtensor.network test --wallet.path --wallet.name --wallet.hotkey +# for mainnet +pm2 start neurons/miner.py --interpreter python3 -- + --netuid 20 + --subtensor.network + --neuron.device cuda # could be cuda:0, cuda:1 depending on which GPU device + --wallet.path # 8.2.0 has a bug that requires wallet path to be provided + --wallet.name # Must be created using the bittensor-cli + --wallet.hotkey # Must be created using the bittensor-cli + --miner-hf-model-name-to-submit Salesforce/xLAM-7b-r # submit your own fine tune with this param + --hf-model-name-to-run Salesforce/xLAM-7b-r # run the best tool calling LLM you can + --openai-api-base http://localhost:8000/v1 # point to your vllm instance of the model you are running + --logging.debug # Run in debug mode, alternatively --logging.trace for trace mode + --log_level trace # for trace logs + --axon.port # VERY IMPORTANT: set the port to be one of the open TCP ports on your machine + +``` +#### Miner Hardware Requirements +Miners will need to run a top tool calling LLM or a fine-tune of their own, needing a GPU with 20GB to 30GB of VRAM. + +#### Default Miner +The default miner is all you need with these modifications: +1) `--miner-hf-model-name-to-submit` - set this to the HF model path and repo name from Hugging Face (HF). \ + Example: `--miner-hf-model-name-to-submit Salesforce/xLAM-7b-r` +2) `--hf-model-name-to-run` - this is the model the miner is running to respond to queries that are sent to the miner. \ + Example: `--hf-model-name-to-run Salesforce/xLAM-7b-r` +3) `--openai-api-base` - this sets the vLLM endpoint that's running your local model. \ + Example: `--openai-api-base http://localhost:8000/v1` + +See [Miner Configuration Considerations](#miner-configuration-considerations) for common areas miners should look to improve. + +#### Miner Emissions + +Miner emissions are composed of both MINER-HOSTED and FINETUNED SUBMISSION evaluation: +- 20% of the miner's score is determined by the persistent availability of the miners and their response to on-demand queries This is MINER-HOSTED evaluation of the miner. +- 80% is determined by bi-weekly challenges in which the miner submits their latest huggingface model and Validators load the model on their machine to evaluate. This is FINETUNED SUBMISSION evaluation. This 80% portion serves as a delayed incentive mechanism, meaning it is always based on miner/model performance from the PREVIOUS competition. + +Both MINER-HOSTED and FINETUNED SUBMISSION tasks are evaluated against modifications of these datasets: +- Berkeley Function Calling tasks +- Glaive Function Calling tasks +- BitAgent Function calling tasks + +The Bi-weekly challenge is to finetune an 8B model (or less) to perform well on the tool calling tasks and perform well on the [BFCL Leaderboard](https://gorilla.cs.berkeley.edu/leaderboard.html). Miners must publish their model to HuggingFace and update their `--miner-hf-model-name-to-subnet` parameter when starting/restarting their miner - see [Default Miner](#default-miner) + +#### Miner Registration Considerations + +Due to the delayed incentive mechanism of finetuned model evaluation, miners are not recommended to register during the middle of a competition. This is because miners registering mid-competition will not have a score from the prior competition, making them unable to benefit from the 80% incentive calculation. +- It is recommended that miners register on the day a competition ends (prior to the actual time of competition close). The competitions end on the midnight (00:00) UTC between Monday and Tuesday every two weeks starting from 11-5-2024. +- Registering on the competition end date (within 16 hours of the deadline) ensures that the miner's 16-hour immunity will be used for model submission grading and scoring in the current competition. Additionally, while the miner is still immune, the competition will roll over into the next cycle, and the miner's score will be finalized for the incentive calculation for the entire next competition cycle. +- On the day of the competition's end, registration slots are expected to be extremely competitive. Due to substrate constraints, at most three miners can register per hour. If you're receiving error messages while registering, this is why and you will need to keep trying. + +#### Miner Configuration Considerations +The default miner is all you need, just make sure you update the parameters described in [Default Miner](#default-miner). +For your consideration: +1) Use vLLM as a fast inference runner for your tool calling LLM. Check [this](https://docs.vllm.ai/en/v0.6.0/getting_started/quickstart.html#openai-compatible-server) out to stand up an openAI compliant vLLM instance. +2) Use pm2 to launch your miner for easy management and reconfiguration as needed. +3) We use [SGLang](https://sgl-project.github.io/start/install.html) to run your hugging face models, please make sure your model loads with SGLang. +4) Don't make it obvious to other miners where your HuggingFace submission is, manage this discretely. + + +#### Example Task +Here's an example task you can expect your model to see in FINETUNED SUBMISSION mode as well as your local miner to see in MINER-HOSTED mode: + +You'll receive messages like this: +```baseh +[{"content":"What is the discounted price of the jacket, given it was originally $200 and there is a 20% reduction?","role":"user"}] +``` +and Tools like this: +```bash +[{"arguments":{"discount_percentage":{"required":true,"type":"number","description":"The percentage discount to be applied"}, +"original_price":{"description":"The original price of the item","required":true,"type":"number"}}, +"description":"Calculate the discounted price of an item based on the original price and discount percentage","name":"calculate_discount"}, +{"arguments":{"pod_name":{"description":"The name of the pod to be restarted","required":true,"type":"str"}}, +"description":"A function to restart a given pod, useful for deployment and testing.","name":"restart_pod"},...] +``` + +In response your model should return the function call like this:\ +`calculate_discount(discount_percentation=..., original_price=...)` + +The model is responsible for returning a function call like above with the right function name, the correct function argument names and values, being sure to set any required arguments appropriately. + +#### Miner Feedback +As a miner, you receive tasks, you get rewarded, but on most subnets, you do not know what you're being graded on. +BitAgent (SN20) offers transparent feedback (in debug logging mode), so you know what you're up against. + +Here's an example of a well performed task: +![miner feedback - good example](./docs/examples/output_to_miner.png) + +Here's an example of a poorly performed task: +![miner feedback - bad example](./docs/examples/bad_output_to_miner.png) + +Additionally, we send all queries and results to Wandb: +- WandB Testnet - https://wandb.ai/bitagentsn20/testnet +- WandB Mainnet - https://wandb.ai/bitagentsn20/mainnet + +### Advanced +If you have a need to create and fund wallets for your own testing ... + +After getting the [subtensor package started and a subnet up and running](./docs/running_on_staging.md) (for staging/local) - you can use this [script](./scripts/setup_and_run.sh) to: +- create wallets (for owner, validators, miners), +- fund those wallets with the right amount of tao, +- register wallets on the local subnet, +- start miners and validators + +```bash +./scripts/setup_and_run.sh +``` +You can use several flags to configure: +- the number of miners or validators it sets up, +- whether it funds wallets, +- or if it registers wallets, +- or just launches a miner +```bash +bitagent_subnet$ ./scripts/setup_and_run.sh --help + +Creates wallets for the subnet (owner, validators, miners), funds them, registers them, then starts them. + +usage: ./scripts/setup_and_run.sh --num_validators num --num_miners num --subnet_prefix string + + --num_validators num number of validators to launch + (default: 1) + --num_miners num number of miners to launch + (default: 2) + --subnet_prefix string the prefix of the subnet wallets + (default: local_subnet_testing_bitagent) + --skip-wallet skip wallet creation + (default: run wallet creation) + --skip-faucet skip wallet funding + (default: fund wallets) + --skip-subnet skip subnet creation + (default: create subnet) + --skip-reg skip all registration to the subnet + (default: register wallets) + --skip-val-reg skip validator registration to the subnet + (default: register validator) + --skip-miner-reg skip miner registration to the subnet + (default: register miner) + --skip-launch skip validator and miner launching on the subnet + (default: launch validators and miners) + --skip-launch_v skip validator launching on the subnet + (default: launch validators) + --only-launch skip everything but launching + (default: do everything) + --test-net do the same things, but for testnet + (default: false, local) + --main-net do the same things, but for mainnet + (default: false, local) + --netuid the netuid to work with + (default: 1 for local, change if main or test) + +Example: ./scripts/setup_and_run.sh --only-launch +This will skip everything and just launch the already registered and funded validators and miners +``` + +--- + +## FAQ +**Q: How much GPU (VRAM) and RAM do I need to run a validator and/or miner?** \ +A: Validators need a GPU and require a minimum of 48 GBs of VRAM with performant CPU. Miners are left to their own setup, but should be aware that the more capable tool calling LLMs require a decent amount of VRAM (common configurations: a 3090 (with 24GB VRAM) is capable enough for the smaller (~8B params) models we require). + +**Q: Are there any required subscriptions or paid APIs?** \ +A: No - no subs, no external companies, in fact we'd rather the community build amazing AI capabilities than relying on corporations. + +**Q: What LLM should I use?** \ +A: This is where the miner needs to experiment some and test and fine-tune different LLM models to find what accomplishes the tasks most successfully. Have a look at models in the Salesforce xLAM family as good starting points. + +**Q: Validators are running miner-submitted HF models, will validators require `trust_remote_code`?** \ +A: No, we require that no setup scripts or any code be necessary for running the models. + +**Q: I started my miner and I am not receiving any tasks.** \ +A: There are a few things to check: +- Is your axon port, as reported on the metagraph correct (you can check taostats or metagraph)? +- Is your axon port open and reachable from a system in the real world (like where the validators are)? +- Do you have Trace logging on to see the dendrite requests and Debug logging on to see the task results? +- Make sure your IsAlive() forward is returning True and wait an hour for that to update in the validator's cache. +- Make sure there isn't a stale process that is preventing your new miner process from starting up on the intended port. + +**Q: What about model copying?** \ +A: https://discord.com/channels/799672011265015819/1194736998250975332/1302870011362279514 + +**Q: My model is not being evaluated OFFLINE for FINETUNED SUBMISSION and is receiving a score of 0.** \ +A: There are a few things to check: +- Is your model licensed under the apache-2.0 license? +- Is your model size less than 10B parameters? We are looking for 8B params or less models. +- Is your model name properly set in the Hugging Face? + +**Q: I'm getting a wallet path error, like: `KeyFileError: Keyfile at: ${HOME}/~/.bittensor/wallets/...`** \ +A: There is a bug in 8.2.0 that is setting the wallet path incorrectly, so you may need to fix this by adding this parameter to your start command: \ + `--wallet.path ~/.bittensor/wallets` + +**Q: I have a complicated CUDA Device setup and need to use a specific GPU device as a validator running the FINETUNED models:** \ +A: We provide two parameters for this: \ + `--neuron.visible_devices`\ + `--neuron.device`\ +Example usage: To use the 2nd CUDA Device, you would add these to your parameters: \ + `--neuron.visible_devices 1 --neuron.device cuda:0` + +**Q: My validator is running out of GPU memory when loading OFFLINE models via sglang.** \ +A: You can use this parameter: `--validator-hf-server-mem-fraction-static` to increase or decrease the amount of the GPU VRAM to use.\ +It defaults to 0.55, just over half of the VRAM. + +**Q: My vLLM or other inference instance is not served on 8000, how do I change this?**\ +A: We provide a parameter `--openai-api-base`\ +It defaults to this: `http://localhost:8000/v1`, updated as needed by passing the `--openai-api-base` parameter to your start command. + +**Q: My vTrust is low and it looks like I'm not setting OFFLINE weights.**\ +A: Please test your sglang setup - check [here](#sglang-setup-for-validators). + +**Q: I'm validating and seeing errors like:** +- TimeoutError +- ClientConnectorError \ + +A: These are responses likely during the IsAlive() query, they are just letting you know that the miner is not responding or connecting in time. + +**Q: My validator is hanging, just printing out "Validator running ..."**\ +A: There are a few things to check:\ +- Make sure your vLLM is running with the required LLM from [vLLM Setup](#vllm-setup-for-validators) +- You may not see much unless you turn on some logging, you can add this to your params to see more details:\ + `--log_level trace --logging.trace --logging.debug` +- Check your storage, make sure you didn't run out:\ + `df -h` +- If all else fails, [reach out](https://discord.com/channels/799672011265015819/1194736998250975332) + + +--- + +## License +This repository is licensed under the MIT License. +```text +# The MIT License (MIT) +# Copyright © 2023 Yuma Rao +# Copyright © 2023 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +``` diff --git a/bitagent_subnet-main/bitagent/__init__.py b/bitagent_subnet-main/bitagent/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8c1bed6e944cd02d1f80dd760dc38b4ac3802776 --- /dev/null +++ b/bitagent_subnet-main/bitagent/__init__.py @@ -0,0 +1,11 @@ +# not used for weight versioning +__version__ = "1.0.8" +version_split = __version__.split(".") +__spec_version__ = ( + (1000 * int(version_split[0])) + + (10 * int(version_split[1])) + + (1 * int(version_split[2])) +) + +# Import all submodules. +from . import protocol diff --git a/bitagent_subnet-main/bitagent/criteria/__init__.py b/bitagent_subnet-main/bitagent/criteria/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b75fa6c1732c73a2c29c7a9122da12eec914d159 --- /dev/null +++ b/bitagent_subnet-main/bitagent/criteria/__init__.py @@ -0,0 +1 @@ +from .criterion import * diff --git a/bitagent_subnet-main/bitagent/criteria/criterion.py b/bitagent_subnet-main/bitagent/criteria/criterion.py new file mode 100644 index 0000000000000000000000000000000000000000..ce5f8b79b091e6a5da9d3d3d321adc0bc265a546 --- /dev/null +++ b/bitagent_subnet-main/bitagent/criteria/criterion.py @@ -0,0 +1,95 @@ +# The MIT License (MIT) +# Copyright © 2023 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import ast +import bittensor as bt +from pprint import pformat +from typing import Callable, List, Tuple +from bitagent.criteria.utils import bad_message +from bitagent.criteria.default_criteria import * +from bitagent.criteria.tool_call_criteria import * + +# building block for the criteria used to evaluate the miner's response +class Criterion(): + name: str + desc: str + eval_fx: Callable + + def __init__(self, name: str, desc: str, eval_fx: Callable, eval_args=[]) -> None: + self.name = name + self.desc = desc + self.eval_fx = eval_fx + self.eval_args = eval_args + + def clean_response(self,response): + # TODO check multiple functions for parallel, when the response is a list [fx1(), fx2()] + response = response.strip() + if "[" in response[0] and "]" in response[-1]: + response = response[1:-1] + + try: + ast.parse(response.strip()) + except: + # if it not a parsable function, then it's potentially an irrelevance call + response = "" + + return response.strip() + + def evaluate(self, task, validator, synapse: bt.Synapse) -> Tuple[float, float, str]: + try: + # make sure the tool response converts nicely to an ast + synapse.response = self.clean_response(synapse.response) + try: + ast.parse(synapse.response) + except: + reward = -0.5 + max_reward = 1.0 + feedback = bad_message(f"Your response: {synapse.response} was not parsable") + return reward, max_reward, feedback + + # actually do the evaluation + reward, max_reward, feedback = self.eval_fx(task, validator, synapse, *self.eval_args) + except Exception as e: + #bt.logging.error(f"Exception was raised during criteria evaluation: {e}") + reward = -0.5 + max_reward = 1.0 + feedback = bad_message(f"Exception while processing your response, please check format per protocol - {e}") + feedback = f"[bold blue]{self.name}[/bold blue]\n" + feedback + return reward, max_reward, feedback + + def __repr__(self): + return pformat(vars(self), indent=4, width=1) + +# Function Call +def tool_call_criteria(expected_response: dict) -> List[Criterion]: + return [ + Criterion(name="Return correct function format", desc="", eval_fx=correct_tool_call_function_format), + Criterion(name="Return correct function name", desc="", eval_fx=correct_tool_call_function_name, eval_args=[expected_response]), + Criterion(name="Return function with correct argument names", desc="", eval_fx=correct_tool_argument_names, eval_args=[expected_response]), + Criterion(name="Return function with correct argument values", desc="", eval_fx=correct_tool_argument_values, eval_args=[expected_response]), + ] + +def irrelevant_tool_call_criteria() -> List[Criterion]: + return [ + Criterion(name="Return valid function call for irrelevant tool", desc="", eval_fx=correct_irrelevant_tool_call), + ] + +# simple, defaults +default_criteria = [ + Criterion(name="Does not error", desc="", eval_fx=does_not_error), + Criterion(name="Does not take a long time", desc="", eval_fx=does_not_take_a_long_time), +] \ No newline at end of file diff --git a/bitagent_subnet-main/bitagent/criteria/default_criteria.py b/bitagent_subnet-main/bitagent/criteria/default_criteria.py new file mode 100644 index 0000000000000000000000000000000000000000..a2efebb771e0e4dd7189abfb3c98ed6754171fc7 --- /dev/null +++ b/bitagent_subnet-main/bitagent/criteria/default_criteria.py @@ -0,0 +1,59 @@ +# The MIT License (MIT) +# Copyright © 2023 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import bittensor as bt +from typing import Tuple +from bitagent.criteria.utils import good_message, bad_message, received_reward_template + +# CRITERION: successful call to miner +def does_not_error(task, validator, synapse: bt.Synapse) -> Tuple[float, float, str]: + max_reward = 0.25 + a_status_code = synapse.axon.status_code + d_status_code = synapse.dendrite.status_code + reward = 0.0 + if a_status_code == 200 and d_status_code == 200: + reward = max_reward + feedback = good_message("You successfully responded to the request.") + else: + feedback = bad_message("You failed to respond correctly to the request.") + if d_status_code == 408: + feedback += "You timed out and will fail the remainder of the criteria." + feedback += f" Status Code: {a_status_code}/{d_status_code}" + + return reward, max_reward, feedback + received_reward_template.format(reward, max_reward) + +# CRITERION: reward speedy response +def does_not_take_a_long_time(task, validator, synapse: bt.Synapse) -> Tuple[float, float, str]: + max_reward = 0.5 + process_time = synapse.dendrite.process_time + if not process_time: + feedback = f"You likely ran into an error processing this task and failed to respond appropriately." + reward = 0 + return reward, max_reward, bad_message(feedback) + received_reward_template.format(reward,max_reward) + + feedback = f"You responded to the request in {process_time}." + reward = 0.0 + if process_time <= task.timeout/1.75: + reward = max_reward + return reward, max_reward, good_message(feedback) + received_reward_template.format(reward,max_reward) + if process_time <= task.timeout/1.25: + reward = max_reward/2 + return reward, max_reward, good_message(feedback, color="yellow") + received_reward_template.format(reward,max_reward) + if process_time <= task.timeout: + reward = max_reward/5 + return reward, max_reward, bad_message(feedback, color="yellow") + received_reward_template.format(reward,max_reward) + return reward, max_reward, bad_message(feedback) + received_reward_template.format(reward,max_reward) \ No newline at end of file diff --git a/bitagent_subnet-main/bitagent/criteria/tool_call_criteria.py b/bitagent_subnet-main/bitagent/criteria/tool_call_criteria.py new file mode 100644 index 0000000000000000000000000000000000000000..0b21abc454c131e19353204fc9aefad8513cd1c0 --- /dev/null +++ b/bitagent_subnet-main/bitagent/criteria/tool_call_criteria.py @@ -0,0 +1,422 @@ +# The MIT License (MIT) +# Copyright 2024 RogueTensor +# Copyright 2024 TheIntern + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +import ast +import bittensor as bt +from typing import Tuple +from bitagent.criteria.utils import good_message, bad_message, received_reward_template +from functools import lru_cache + +# just checking if the function can be parsed by ast +def correct_tool_call_function_format(task, validator, synapse: bt.Synapse) -> Tuple[float, float, str]: + max_reward = 1.0 + reward = 1.0 + + try: + ast.parse(synapse.response) + except Exception as e: + reward = -1.0 + feedback = bad_message(f"Your response was not in the correct format - {e}") + return reward, max_reward, feedback+received_reward_template.format(reward, max_reward) + + feedback = good_message(f"Your response was in the correct format.") + return reward, max_reward, feedback+received_reward_template.format(reward, max_reward) + +@lru_cache(maxsize=128) +def extract_function_name_and_params(response: str): + if response == "": + return "", [], {} + + node = ast.parse(response , mode="eval") + + # Walk through the AST to extract the function name + class FunctionNameExtractor(ast.NodeVisitor): + def __init__(self): + self.function_name = None + + def visit_Call(self, node): + # Check if the node is a function call + if isinstance(node.func, ast.Attribute): # Handles dot notation (e.g., module.function) + parts = [] + current = node.func + while isinstance(current, ast.Attribute): + parts.append(current.attr) + current = current.value + if isinstance(current, ast.Name): + parts.append(current.id) + # Join the parts in reverse to get the full function name + self.function_name = '.'.join(reversed(parts)) + elif isinstance(node.func, ast.Name): # Handles simple function names (e.g., functionName) + self.function_name = node.func.id + # No need to visit further + self.generic_visit(node) + + extractor = FunctionNameExtractor() + extractor.visit(node) + function_name = extractor.function_name + + param_names = [kw.arg for kw in node.body.keywords] + if param_names: + param_values = [ast.literal_eval(kw.value) for kw in node.body.keywords] + else: + param_values = [] + + param_values_dict = {} + for i,param_name in enumerate(param_names): + param_values_dict[param_name] = param_values[i] + + return function_name, param_names, param_values_dict + +# just checking if the function name is correct +def correct_tool_call_function_name(task, validator, synapse: bt.Synapse, expected_response: dict) -> Tuple[float, float, str]: + max_reward = 3.0 + reward = 3.0 + + function_name, _, _ = extract_function_name_and_params(synapse.response) + expected_function_name = expected_response['name'] + + if function_name.strip() == expected_function_name.strip(): + feedback = good_message(f"Your function name matches the expected function name.") + return reward, max_reward, feedback+received_reward_template.format(reward, max_reward) + else: + reward = -0.5 + feedback = bad_message(f"Your function name does not match the expected function name.") + return reward, max_reward, feedback+received_reward_template.format(reward, max_reward) + +# comparing just the argument names +# looking for required arguments and that they are present +def correct_tool_argument_names(task, validator, synapse: bt.Synapse, expected_response: dict) -> Tuple[float, float, str]: + max_reward = 1.0 + reward = max_reward + feedback_parts = [] + + # MINER response + function_name, function_args, _ = extract_function_name_and_params(synapse.response) + expected_args = set(expected_response['arguments'].keys()) + function_args_set = set(function_args) + + # no args + if not expected_args and not function_args and function_name: + feedback_parts.append(good_message("Function has no arguments, good job")) + return reward, max_reward, ''.join(feedback_parts) + received_reward_template.format(reward, max_reward) + + required_args = get_required_args(task, expected_response) + + for arg in required_args: + if arg in function_args_set: + feedback_parts.append(good_message(f"Your function has the required argument: {arg}")) + else: + reward -= max_reward/len(required_args) + feedback_parts.append(bad_message(f"Your function is missing the required argument: {arg}")) + + return reward, max_reward, '\n'.join(feedback_parts) + received_reward_template.format(reward, max_reward) + +def correct_tool_argument_values(task, validator, synapse: bt.Synapse, expected_response: dict) -> Tuple[float, float, str]: + max_reward = 3.0 + reward = 0.0 + feedback = "" + + # MINER response + function_name, function_args, function_values = extract_function_name_and_params(synapse.response) + expected_args = set(expected_response['arguments'].keys()) # Convert to set for O(1) lookups + function_args_set = set(function_args) # Convert to set for O(1) lookups + + # no args + if not expected_args and not function_args and function_name: + reward = max_reward + feedback = good_message("Function has no arguments, good job") + return reward, max_reward, feedback+received_reward_template.format(reward, max_reward) + + required_args = get_required_args(task, expected_response) + is_flipped = is_distance_calculation_with_flipped_args(function_name, task.synapse.messages) + + # Check if this is a distance calculation with flipped arguments + for arg in required_args: + if arg in function_args_set: + correct_values = get_correct_values_for_arg(arg, expected_response, is_flipped) + + if "is_ground_truth" in expected_response and function_values[arg] in correct_values: + reward += max_reward/max(len(function_args),len(expected_args)) + feedback += good_message(f"Your function has the required value for argument: {arg}") + "\n" + elif function_values[arg] == correct_values: + reward += max_reward/max(len(function_args),len(expected_args)) + feedback += good_message(f"Your function has the required value for argument: {arg}") + "\n" + else: + reward -= 0 + feedback += bad_message(f"Your function has the incorrect value for argument: {arg}") + "\n" + else: + reward -= max_reward/len(required_args) + feedback += bad_message(f"Your function is missing the required argument: {arg}") + "\n" + + return reward, max_reward, feedback+received_reward_template.format(reward, max_reward) + +def correct_irrelevant_tool_call(task, validator, synapse: bt.Synapse) -> Tuple[float, float, str]: + max_reward = 3.0 + reward = 3.0 + + if synapse.response.strip() != "": + reward = -0.5 + feedback = bad_message(f"Your response (`{synapse.response}`) was not empty, expected an empty response to be returned.") + return reward, max_reward, feedback+received_reward_template.format(reward, max_reward) + + feedback = good_message(f"You responded with the expected response.") + return reward, max_reward, feedback+received_reward_template.format(reward, max_reward) + + +# Examples: +synapse_response1 = 'calculate_gpa(grades=["A", "B", "A", "C"], credit_hours=[3, 4, 3, 2])' +synapse_response2 = 'calculate_gpa(credit_hours=[3, 4, 3, 2], grades=["A", "B", "A", "C"])' +expected_response = {'name': 'calculate_gpa', 'arguments': {'grades': ['A', 'B', 'A', 'C'], 'credit_hours': [3, 4, 3, 2]}} + +import unittest +from typing import List + +class MockSynapse: + from bitagent.schemas.tool import Tool + response: str + tools: List[Tool] = [Tool(name="calculate_gpa", description="Calculate the GPA of a student", arguments={"grades": {"type": "list", "required": True}, "credit_hours": {"type": "list", "required": True}})] + + def __init__(self, response: str): + self.response = response + +class MockTask: + synapse: MockSynapse + + def __init__(self, synapse: MockSynapse): + self.synapse = synapse + +class TestToolCallCriteria(unittest.TestCase): + + def setUp(self): + self.validator = "" + + def test_correct_tool_call_function_format(self): + # Test valid function format + synapse = MockSynapse(response="calculate_gpa(grades=['A'], credit_hours=[3])") + task = MockTask(synapse=synapse) + reward, max_reward, feedback = correct_tool_call_function_format(task, self.validator, synapse) + self.assertEqual(reward, 1.0) + self.assertEqual(max_reward, 1.0) + self.assertTrue("was in the correct format" in feedback.lower()) + + # Test invalid function format + synapse = MockSynapse(response="invalid(function syntax") + task = MockTask(synapse=synapse) + reward, max_reward, feedback = correct_tool_call_function_format(task, self.validator, synapse) + self.assertEqual(reward, -1.0) + self.assertEqual(max_reward, 1.0) + self.assertTrue("not in the correct format" in feedback.lower()) + + # Test json response + synapse = MockSynapse(response='{"name": "calculate_gpa", "arguments": {"grades": ["A"], "credit_hours": [3]}}') + task = MockTask(synapse=synapse) + reward, max_reward, feedback = correct_tool_call_function_format(task, self.validator, synapse) + self.assertEqual(reward, 1.0) + self.assertEqual(max_reward, 1.0) + self.assertTrue("was in the correct format" in feedback.lower()) + + def test_extract_function_name_and_params(self): + # Test basic function extraction + response = "calculate_gpa(grades=['A'], credit_hours=[3])" + name, params, values = extract_function_name_and_params(response) + self.assertEqual(name, "calculate_gpa") + self.assertEqual(params, ["grades", "credit_hours"]) + self.assertEqual(values, {"grades": ["A"], "credit_hours": [3]}) + + # Test empty response + name, params, values = extract_function_name_and_params("") + self.assertEqual(name, "") + self.assertEqual(params, []) + self.assertEqual(values, {}) + + # Test function with dot notation + response = "math.sqrt(value=16)" + name, params, values = extract_function_name_and_params(response) + self.assertEqual(name, "math.sqrt") + self.assertEqual(params, ["value"]) + self.assertEqual(values, {"value": 16}) + + # Test function with dot notation and no value + response = "math.sqrt()" + name, params, values = extract_function_name_and_params(response) + self.assertEqual(name, "math.sqrt") + self.assertEqual(params, []) + self.assertEqual(values, {}) + + def test_correct_irrelevant_tool_call(self): + # Test empty response (correct) + synapse = MockSynapse(response="") + task = MockTask(synapse=synapse) + reward, max_reward, feedback = correct_irrelevant_tool_call(task, self.validator, synapse) + self.assertEqual(reward, 3.0) + self.assertEqual(max_reward, 3.0) + self.assertTrue("expected response" in feedback.lower()) + + # Test non-empty response (incorrect) + synapse = MockSynapse(response="some_function()") + task = MockTask(synapse=synapse) + reward, max_reward, feedback = correct_irrelevant_tool_call(task, self.validator, synapse) + self.assertEqual(reward, -0.5) + self.assertEqual(max_reward, 3.0) + self.assertTrue("not empty" in feedback.lower()) + + def test_correct_tool_call_function_name(self): + # Test correct function name + synapse = MockSynapse(response="calculate_gpa(grades=['A'])") + task = MockTask(synapse=synapse) + expected = {"name": "calculate_gpa", "arguments": {"grades": ["A"]}} + reward, max_reward, feedback = correct_tool_call_function_name(task, self.validator, synapse, expected) + self.assertEqual(reward, 3.0) + self.assertEqual(max_reward, 3.0) + self.assertTrue("matches the expected function name" in feedback.lower()) + + # Test incorrect function name + synapse = MockSynapse(response="wrong_function(grades=['A'])") + task = MockTask(synapse=synapse) + reward, max_reward, feedback = correct_tool_call_function_name(task, self.validator, synapse, expected) + self.assertEqual(reward, -0.5) + self.assertEqual(max_reward, 3.0) + self.assertTrue("not match" in feedback.lower()) + + def test_correct_tool_argument_names(self): + # Test no expected arguments + synapse = MockSynapse(response="calculate_gpa()") + task = MockTask(synapse=synapse) + expected = {"name": "calculate_gpa", "arguments": {}} + reward, max_reward, feedback = correct_tool_argument_names(task, self.validator, synapse, expected) + self.assertEqual(reward, 1.0) + self.assertEqual(max_reward, 1.0) + self.assertTrue("no arguments, good job" in feedback.lower()) + + # Test no expected arguments, but pass in arguments anyway + synapse = MockSynapse(response="calculate_gpa(grades=['A'])") + task = MockTask(synapse=synapse) + expected = {"name": "calculate_gpa", "arguments": {}} + reward, max_reward, feedback = correct_tool_argument_names(task, self.validator, synapse, expected) + self.assertEqual(reward, 0.0) + self.assertEqual(max_reward, 1.0) + self.assertTrue("expects no arguments" in feedback.lower()) + + # Test correct argument names + synapse = MockSynapse(response="calculate_gpa(grades=['A'], credit_hours=[3])") + task = MockTask(synapse=synapse) + expected = {"name": "calculate_gpa", "arguments": {"grades": ["A"], "credit_hours": [3]}} + reward, max_reward, feedback = correct_tool_argument_names(task, self.validator, synapse, expected) + self.assertEqual(reward, 1.0) + self.assertEqual(max_reward, 1.0) + self.assertEqual(feedback.lower().count("has the required argument"), 2) + + # Test correct argument names plus an incorrect argument + synapse = MockSynapse(response="calculate_gpa(grades=['A'], credit_hours=[3], extra_arg=1)") + task = MockTask(synapse=synapse) + expected = {"name": "calculate_gpa", "arguments": {"grades": ["A"], "credit_hours": [3]}} + reward, max_reward, feedback = correct_tool_argument_names(task, self.validator, synapse, expected) + self.assertEqual(reward, 1.0) + self.assertEqual(max_reward, 1.0) + self.assertEqual(feedback.lower().count("has the required argument"), 2) + + # Test correct argument names out of order + synapse = MockSynapse(response="calculate_gpa(credit_hours=[3], grades=['A'])") + task = MockTask(synapse=synapse) + expected = {"name": "calculate_gpa", "arguments": {"grades": ["A"], "credit_hours": [3]}} + reward, max_reward, feedback = correct_tool_argument_names(task, self.validator, synapse, expected) + self.assertEqual(reward, 1.0) + self.assertEqual(max_reward, 1.0) + self.assertEqual(feedback.lower().count("has the required argument"), 2) + + # Test missing argument + synapse = MockSynapse(response="calculate_gpa(grades=['A'])") + task = MockTask(synapse=synapse) + reward, max_reward, feedback = correct_tool_argument_names(task, self.validator, synapse, expected) + self.assertEqual(reward, 0.0) + self.assertEqual(max_reward, 1.0) + self.assertEqual(feedback.lower().count("has the required argument"), 1) + self.assertEqual(feedback.lower().count("missing the required argument"), 1) + + def test_correct_tool_argument_values(self): + # Test correct argument values + synapse = MockSynapse(response="calculate_gpa(grades=['A'], credit_hours=[3])") + task = MockTask(synapse=synapse) + expected = {"name": "calculate_gpa", "arguments": {"grades": ["A"], "credit_hours": [3]}} + reward, max_reward, feedback = correct_tool_argument_values(task, self.validator, synapse, expected) + self.assertEqual(reward, 3.0) + self.assertEqual(max_reward, 3.0) + self.assertEqual(feedback.lower().count("has the required value for argument"), 2) + + # Test correct argument values out of order + synapse = MockSynapse(response="calculate_gpa(credit_hours=[3], grades=['A'])") + task = MockTask(synapse=synapse) + expected = {"name": "calculate_gpa", "arguments": {"grades": ["A"], "credit_hours": [3]}} + reward, max_reward, feedback = correct_tool_argument_values(task, self.validator, synapse, expected) + self.assertEqual(reward, 3.0) + self.assertEqual(max_reward, 3.0) + self.assertEqual(feedback.lower().count("has the required value for argument"), 2) + + # Test incorrect argument values + synapse = MockSynapse(response="calculate_gpa(grades=['B'], credit_hours=[4])") + task = MockTask(synapse=synapse) + reward, max_reward, feedback = correct_tool_argument_values(task, self.validator, synapse, expected) + self.assertEqual(reward, -3.0) + self.assertEqual(max_reward, 3.0) + self.assertEqual(feedback.lower().count("has the incorrect value for argument"), 2) + + # Test incorrect argument values out of order + synapse = MockSynapse(response="calculate_gpa(credit_hours=[4], grades=['B'])") + task = MockTask(synapse=synapse) + reward, max_reward, feedback = correct_tool_argument_values(task, self.validator, synapse, expected) + self.assertEqual(reward, -3.0) + self.assertEqual(max_reward, 3.0) + self.assertEqual(feedback.lower().count("has the incorrect value for argument"), 2) + + # Test incorrect value types + synapse = MockSynapse(response="calculate_gpa(grades='A', credit_hours=3)") + task = MockTask(synapse=synapse) + reward, max_reward, feedback = correct_tool_argument_values(task, self.validator, synapse, expected) + self.assertEqual(reward, -3.0) + self.assertEqual(max_reward, 3.0) + self.assertEqual(feedback.lower().count("has the incorrect value for argument"), 2) + +if __name__ == '__main__': + # Run all tests in this file + # You can run this file directly with: python -m bitagent.criteria.tool_call_criteria + # Or run all tests with: python -m pytest bitagent/criteria/tool_call_criteria.py + unittest.main(verbosity=2) + +def get_required_args(task, expected_response: dict) -> set: + """Helper function to get required arguments.""" + if "is_ground_truth" in expected_response: + return {arg for arg in expected_response['arguments'] if expected_response['arguments'][arg] != [""]} + + expected_tool = next((tool for tool in task.synapse.tools if tool.name == expected_response['name']), None) + if not expected_tool: + return set() + return {k for k in expected_tool.arguments if expected_tool.arguments[k].get('required', False)} + +def is_distance_calculation_with_flipped_args(function_name: str, messages) -> bool: + """Helper function to check if this is a distance calculation with flipped arguments.""" + return function_name == "calculate_distance" and "flipped" in str(messages).lower() + +def get_correct_values_for_arg(arg: str, expected_response: dict, is_flipped: bool) -> list: + """Helper function to get correct values for an argument, handling flipped cases.""" + if not is_flipped: + return expected_response['arguments'][arg] + + if arg == "origin": + return expected_response['arguments'].get("destination", expected_response['arguments'][arg]) + elif arg == "destination": + return expected_response['arguments'].get("origin", expected_response['arguments'][arg]) + return expected_response['arguments'][arg] diff --git a/bitagent_subnet-main/bitagent/criteria/utils.py b/bitagent_subnet-main/bitagent/criteria/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..4b0a36c33356324c539c9ad4c3719e66f05f44b3 --- /dev/null +++ b/bitagent_subnet-main/bitagent/criteria/utils.py @@ -0,0 +1,25 @@ +# The MIT License (MIT) +# Copyright © 2023 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +received_reward_template="\nYou received {} of {} reward." + +def bad_message(text: str, color: str="red") -> str: + return f":cross_mark: [{color}]{text}[/{color}]" + +def good_message(text: str, color: str="green") -> str: + return f":white_heavy_check_mark: [{color}]{text}[/{color}]" + diff --git a/bitagent_subnet-main/bitagent/datasources/__init__.py b/bitagent_subnet-main/bitagent/datasources/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..6ac1ccc35ccb2aa0408826edbd74abffa6b54fee --- /dev/null +++ b/bitagent_subnet-main/bitagent/datasources/__init__.py @@ -0,0 +1 @@ +from .tools import * \ No newline at end of file diff --git a/bitagent_subnet-main/bitagent/datasources/loaders.py b/bitagent_subnet-main/bitagent/datasources/loaders.py new file mode 100644 index 0000000000000000000000000000000000000000..14e48857af4765891a01f63f6e2870077086f023 --- /dev/null +++ b/bitagent_subnet-main/bitagent/datasources/loaders.py @@ -0,0 +1,63 @@ +import os +import pandas as pd +import bittensor as bt +from datasets import load_dataset, load_from_disk +from huggingface_hub import snapshot_download + +class ShuffledJSONDatasetIterator: + def __init__(self): + dataframes = [] + + # TODO - other BFCL task types: + # irrelevance and live_irrelevance - answer is NOTHING + # exec_* (simple, multiple, parallel, parallel_multiple) - answer in the file itself + # multi_turn_* - answer in the file itself + # parallel* - answer in the file itself + # rest - maybe later - calls to API that the validator would need to setup + + for filename in ["java", "javascript", "simple", "multiple", "sql", "live_simple", "live_multiple"]: + bfcl_path = "bitagent.data/bfcl/BFCL_v3_{filename}.json" + bfcl_answer_path = "bitagent.data/bfcl/possible_answer/BFCL_v3_{filename}.json" + file_path = bfcl_path.format(filename=filename) + answer_path = bfcl_answer_path.format(filename=filename) + df_data = pd.read_json(file_path, lines=True) + df_answer = pd.read_json(answer_path, lines=True) + df_data['ground_truth'] = df_answer['ground_truth'] + dataframes.append(df_data[['id','question','function','ground_truth']]) + self.all_data = pd.concat(dataframes) + self._shuffle_data() + + def _shuffle_data(self): + self.shuffled_data = self.all_data.sample(frac=1).reset_index(drop=True) + self.index = 0 + + def __iter__(self): + self.index = 0 + return self + + def __next__(self): + if self.index < len(self.shuffled_data): + row = self.shuffled_data.iloc[self.index] + self.index += 1 + return row + else: + self._shuffle_data() # Shuffle and reset index if end is reached + return self.__next__() + +def huggingface_loader(dataset_name, root_data_dir="bitagent.data", split="train", name=None): + bt.logging.debug(f"Loading {dataset_name}") + dataset_dir = f"{root_data_dir}/{dataset_name.replace('/','_')}" + if os.path.exists(f"{dataset_dir}/state.json"): + bt.logging.debug(f"Loading from disk ({dataset_dir}) ...") + ds = load_from_disk(dataset_dir) + else: + bt.logging.debug("Loading from web ...") + ds = load_dataset(dataset_name, split=split, name=name, token=os.getenv("HF_TOKEN", None)) + ds.save_to_disk(dataset_dir) + bt.logging.debug("Loaded.") + return ds + +def load_bfcl_dataset(dataset_name, root_data_dir="bitagent.data", split="train", name=None): + snapshot_download(repo_id=dataset_name, allow_patterns="*.json", repo_type="dataset", local_dir="bitagent.data/bfcl/") + + return ShuffledJSONDatasetIterator() \ No newline at end of file diff --git a/bitagent_subnet-main/bitagent/datasources/tools.py b/bitagent_subnet-main/bitagent/datasources/tools.py new file mode 100644 index 0000000000000000000000000000000000000000..aaa1f6e00f1e67ba55689c860622b46df8b4c6a0 --- /dev/null +++ b/bitagent_subnet-main/bitagent/datasources/tools.py @@ -0,0 +1,194 @@ +import re +import json +import random +import bittensor as bt +from pydantic import BaseModel +from typing import List, Dict, Any +from collections.abc import Iterator +from bitagent.schemas.tool import Tool +from bitagent.schemas.chat import ChatMessage, messages_from_list +from bitagent.datasources.loaders import huggingface_loader, load_bfcl_dataset +from bitagent.helpers.string_parse import parse_multiple_space_sep_json + + +def split_dialogue(text) -> List[ChatMessage]: + # Define a pattern to match the roles and capture messages + pattern = r"(USER|ASSISTANT|TOOL CALL|TOOl RESPONSE): (.*?)(?=\s*(USER|ASSISTANT|TOOL CALL|TOOL RESPONSE):|$)" + + # Find all matches in the text using the pattern + matches = re.findall(pattern, text, re.DOTALL) + + # Create a list of dictionaries based on the matches + dialogue_list = [{"role": role.lower(), "content": message.strip().replace('\'','')} for role, message, _ in matches] + + for message in dialogue_list: + if not message['role']: + raise ValueError("There is a message with no role.") + + return messages_from_list(dialogue_list) + + +def clean_text(text): + text = text.replace("<|endoftext|>", "") + text = text.replace("ASSISTANT: ", "TOOL CALL: ") + text = text.replace("FUNCTION RESPONSE", "TOOL RESPONSE") + text = text.replace(" ", " ") + return text.strip() + +def custom_json_schema_to_pydantic_tool(schema: dict) -> Tool: + tool_name = schema.get("name", "") + tool_description = schema.get("description", "") + + schema_arguments = schema.get("arguments", {}) + parameters = {} + for param_name, param_info in schema_arguments.items(): + parameters[param_name] = { + "required": param_info.get("required", False), + "type": param_info.get("type", ""), + "description": param_info.get("description", ""), + } + + return Tool(name=tool_name, description=tool_description, arguments=parameters) + +def json_schema_to_pydantic_tool(schema: dict) -> Tool: + tool_name = schema.get("name", "") + tool_description = schema.get("description", "") + + schema_parameters = schema.get("parameters", {}) + if not schema_parameters: + schema_parameters = schema.get("arguments", {}) + properties = schema_parameters.get("properties", {}) + required_params = schema_parameters.get("required", []) + if isinstance(required_params, bool): + required_params = list(properties.keys()) if required_params else [] + elif not isinstance(required_params, list): + required_params = [] + parameters = {} + for param_name, param_info in properties.items(): + if param_name == "required": + continue + parameters[param_name] = { + "required": param_name in required_params, + "type": param_info.get("type", ""), + "description": param_info.get("description", ""), + } + return Tool(name=tool_name, description=tool_description, arguments=parameters) + +class ToolCallData(BaseModel): + messages: List[ChatMessage] + tools: list[Tool] + +TYPES = ["str", "int", "dict", "list", "float", "bool", "string", "integer", "number", "boolean", "dictionary", "object"] + +def detect_type(value: Any) -> str: + type_mapping = { + int: 'integer', + float: 'number', + str: 'string', + bool: 'boolean', + list: 'array', + dict: 'object' + } + return type_mapping.get(type(value), 'string') + +def add_extra_arguments(tool_call: Dict[str, Any], tools: List[Tool]): + # Find the tool in the list + tool_name = tool_call['name'] + arguments = tool_call.get('arguments', {}) + + for tool in tools: + if tool.name == tool_name: + for arg_name, arg_value in arguments.items(): + if arg_name not in tool.arguments: + # Detect the type of the argument + arg_type = detect_type(arg_value) + # Add the new argument to the tool's schema + tool.arguments[arg_name] = { + 'required': False, # assume false + 'type': arg_type, + 'description': arg_name + } + break + +class ToolDataset(Iterator): + def __init__(self): + super().__init__() + seed = random.randint(0, 10000) + glaive_ds = huggingface_loader("glaiveai/glaive-function-calling-v2") + bitagent_ds = huggingface_loader("BitAgent/tool_calling") + bfcl_ds = load_bfcl_dataset("gorilla-llm/Berkeley-Function-Calling-Leaderboard") + + self.datasets = { + "glaive": iter(glaive_ds.shuffle(seed=seed)), + "bitagent": iter(bitagent_ds.shuffle(seed=seed)), + "bfcl": iter(bfcl_ds), + } + + def __next__(self) -> ToolCallData: + #bt.logging.debug("Retrieving function call data from dataset...") + count = 0 + while count < 25: + count += 1 + try: + dname, ds = random.choices(list(self.datasets.items()), [5, 5, 10])[0] + data = next(ds) + if dname == "glaive": + system_prompt = data["system"].replace("SYSTEM: ", "") + if "following functions" not in system_prompt: + continue + + chat_history = clean_text(data["chat"]) + tools = parse_multiple_space_sep_json( + system_prompt.replace( + "You are a helpful assistant with access to the following functions. Use them if required - ", + "", + ) + ) + tools = [json_schema_to_pydantic_tool(tool) for tool in tools] + messages = split_dialogue(chat_history) + + # Add arguments that werent defined in schema to the tool + for msg in messages: + if msg.role == "tool call": + tool_call = None + if isinstance(msg.content, str): + tool_call = json.loads(msg.content) + else: + tool_call = msg.content + + add_extra_arguments(tool_call, tools) + + + return ToolCallData(messages=messages, tools=tools) + elif dname == "bitagent": + for key, value in data.items(): + if isinstance(value, str): + data[key] = json.loads(value) + messages = messages_from_list(data["conversation"]) + if isinstance(data["tools"], str): + tools = [ + json_schema_to_pydantic_tool(tool) + for tool in json.loads(data["tools"]) + ] + elif isinstance(data["tools"], list): + tools = [Tool(**tool) for tool in data["tools"]] + else: + raise ValueError(f"Invalid format for tools: {data['tools']}") + for tool in tools: + for arg_name, arg_value in tool.arguments.items(): + if arg_value["type"] not in TYPES: + raise ValueError(f"Inavlid type used type: {arg_value['type']}") + return ToolCallData(messages=messages, tools=tools) + elif dname == "bfcl": + messages = messages_from_list(data["question"][0]) + ground_truth = data['ground_truth'][0] + messages.append(ChatMessage(role="tool call", + content={"is_ground_truth": True, + "name": list(ground_truth.keys())[0], + "arguments": list(ground_truth.values())[0]})) + tools = [json_schema_to_pydantic_tool(tool) for tool in data["function"]] + return ToolCallData(messages=messages, tools=tools) + + except Exception as e: + #bt.logging.debug(f"Issue getting tool call from dataset ... {e}") + pass \ No newline at end of file diff --git a/bitagent_subnet-main/bitagent/helpers/dockers.py b/bitagent_subnet-main/bitagent/helpers/dockers.py new file mode 100644 index 0000000000000000000000000000000000000000..9a0854efebdc7a1d2a60a559c40d39a5647e9054 --- /dev/null +++ b/bitagent_subnet-main/bitagent/helpers/dockers.py @@ -0,0 +1,44 @@ +import os +import docker +import bittensor as bt + +def create_container(container_name, model_name, docker_vllm_port): + container_to_run = "docker.io/vllm/vllm-openai:latest" + + dclient = docker.from_env() + + try: + dclient.containers.get(container_name).remove(force=True) + except: + pass + + # get home directory + home_dir = os.path.expanduser('~') + + bt.logging.debug('starting container') + dclient.containers.run(container_to_run, + f"--model {model_name} --max-model-len 8912 --gpu-memory-utilization 0.9", + name=container_name, + device_requests=[docker.types.DeviceRequest(count=1, capabilities=[["gpu"]])], + detach=True, + volumes={f'{home_dir}/.cache/huggingface': {'bind': '/root/.cache/huggingface', 'mode': 'rw'}}, + ports={'8000/tcp': docker_vllm_port}) + bt.logging.debug('started container') + + return dclient.containers.get(container_name) + +def wait_for_container(openai_client, model_name): + bt.logging.debug('waiting for container') + while True: + try: + openai_client.chat.completions.create( + model=model_name, + messages=[{"role": "user", "content": "Hello!"}] + ) + break + except Exception as e: + #bt.logging.debug(e) + import time + time.sleep(1) + pass + bt.logging.debug('container ready') \ No newline at end of file diff --git a/bitagent_subnet-main/bitagent/helpers/llms.py b/bitagent_subnet-main/bitagent/helpers/llms.py new file mode 100644 index 0000000000000000000000000000000000000000..fd5b57375ee0bfab27ff36a11463319643e0cc48 --- /dev/null +++ b/bitagent_subnet-main/bitagent/helpers/llms.py @@ -0,0 +1,85 @@ +# The MIT License (MIT) +# Copyright 2024 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import bittensor as bt +from openai import OpenAI + +# specifically for the validator +def get_openai_llm(self, hugging_face=False): + if "validator" in self.__class__.__name__.lower() and hugging_face and self.config.validator_hf_server_port: + # stand up a vLLM server on this port for the OFFLINE HF model evals + base_url = f'http://localhost:{self.config.validator_hf_server_port}/v1' + else: + base_url = self.config.openai_api_base + + return OpenAI( + api_key=self.config.openai_api_key, + base_url=base_url + ) + +def system_prompt(tools): + prompt = """You are an expert in composing functions. You are given a question and a set of possible functions. Based on the question, you will need to make one or more function/tool calls to achieve the purpose. + If none of the function can be used, point it out. If the given question lacks the parameters required by the function, also point it out. + You should only return the function call in tools call sections. + + For the calculate_distance function: + When asking for distance FROM A TO B and parameters are flipped: + - Set origin=B (the endpoint) + - Set destination=A (the starting point) + Example: For "distance from Los Angeles TO New York": + - Use origin="New York" (B/endpoint) + - Use destination="Los Angeles" (A/starting point) + + If you decide to invoke any of the function(s), you MUST put it in the format of [func_name1(params_name1="params_string_value1", params_name2=params_value2...), func_name2(params)] + Notice that any values that are strings must be put in quotes like this: "params_string_value1" + You SHOULD NOT include any other text in the response. + Here is a list of functions in JSON format that you can invoke.\n{functions}\n + """ + + return prompt.format(functions=tools) + + +def llm(self, messages, tools, model_name, hugging_face=False,max_new_tokens = 160, temperature=0.7): + prompt = system_prompt(tools) + + try: + #try: + # new_messages = [{"role":"system", "content":prompt}] + messages + # response = get_openai_llm(self, hugging_face).chat.completions.create( + # messages=new_messages, + # max_tokens=max_new_tokens, + # model=model_name, + # temperature=temperature + # ) + #except Exception as e: + # errored b/c the model does not allow system prompts + messages[0].content = prompt + "\n\n" + messages[0].content + response = get_openai_llm(self, hugging_face).chat.completions.create( + messages=messages, + max_tokens=max_new_tokens, + model=model_name, + temperature=temperature + ) + + except Exception as e: + bt.logging.error(f"Error calling to LLM: {e}") + return "" + + if hugging_face: + return response.choices[0].message.content.strip(), response.choices[0].finish_reason + else: + return response.choices[0].message.content.strip() \ No newline at end of file diff --git a/bitagent_subnet-main/bitagent/helpers/logging.py b/bitagent_subnet-main/bitagent/helpers/logging.py new file mode 100644 index 0000000000000000000000000000000000000000..3b9eccb60cf05ef4790e572aab85f9e09300dd1b --- /dev/null +++ b/bitagent_subnet-main/bitagent/helpers/logging.py @@ -0,0 +1,38 @@ +import bittensor as bt +from contextlib import contextmanager +@contextmanager +def temporary_logging_state(new_state): + """ + A context manager to temporarily set Bittensor's logging state. + """ + # Cache the current logging state + current_state = bt.logging.current_state + bt.logging.info(f"OFFLINE: Caching current logging state: {current_state}") + + # Set the new logging state + if new_state == 'Debug': + bt.logging.set_debug() + elif new_state == 'Trace': + bt.logging.set_trace() + elif new_state == 'Warning': + bt.logging.set_warning() + elif new_state == 'Info': + bt.logging.set_info() + else: + bt.logging.set_default() + + try: + yield + finally: + # Restore the original logging state + if current_state.value == 'Debug': + bt.logging.set_debug() + elif current_state.value == 'Trace': + bt.logging.set_trace() + elif current_state.value == 'Warning': + bt.logging.set_warning() + elif current_state.value == 'Info': + bt.logging.set_info() + else: + bt.logging.set_default() + bt.logging.info(f"OFFLINE: Restored logging state to: {current_state}") diff --git a/bitagent_subnet-main/bitagent/helpers/sbert.py b/bitagent_subnet-main/bitagent/helpers/sbert.py new file mode 100644 index 0000000000000000000000000000000000000000..ade709c7a755ec4d1ea6097d91d222889784ec0b --- /dev/null +++ b/bitagent_subnet-main/bitagent/helpers/sbert.py @@ -0,0 +1,45 @@ +from sentence_transformers import SentenceTransformer +import numpy as np +import torch + +class CachedSentenceTransformer(SentenceTransformer): + def __init__(self, model_name_or_path: str): + super().__init__(model_name_or_path) + self.cache = {} # Initialize an empty cache + + def encode(self, sentences, convert_to_tensor=False, **kwargs): + if isinstance(sentences, str): + sentences = [sentences] + + results = [] + sentences_to_encode = [] + original_positions = [] + + cache_key_suffix = "_tensor" if convert_to_tensor else "_array" + + for i, sentence in enumerate(sentences): + cache_key = f"{sentence}" + cache_key_suffix + if cache_key in self.cache: + results.append(self.cache[cache_key]) + else: + sentences_to_encode.append(sentence) + original_positions.append(i) + results.append(None) # Placeholder + + if sentences_to_encode: + encoded = super().encode(sentences_to_encode, convert_to_tensor=convert_to_tensor, **kwargs) + if not isinstance(encoded, list): + encoded = [encoded[i] for i in range(len(sentences_to_encode))] + + for original_pos, sentence, emb in zip(original_positions, sentences_to_encode, encoded): + cache_key = sentence + cache_key_suffix + self.cache[cache_key] = emb + results[original_pos] = emb + + if len(results) == 1: + return results[0] + + if convert_to_tensor: + return torch.stack(results) + else: + return np.array(results) diff --git a/bitagent_subnet-main/bitagent/helpers/string_parse.py b/bitagent_subnet-main/bitagent/helpers/string_parse.py new file mode 100644 index 0000000000000000000000000000000000000000..236da04f7e5bad9e41c09f71f8934e0f8b75d076 --- /dev/null +++ b/bitagent_subnet-main/bitagent/helpers/string_parse.py @@ -0,0 +1,34 @@ +import re +import json + +def extract_text_inside_quotes(s): + match = re.search(r'"(.*?)"', s) + if match: + return match.group(1) # Returns the text inside the first pair of double quotes + else: + return s # Returns the original string if no double quotes are found + +def parse_multiple_space_sep_json(json_str): + """ + Parses a string containing multiple JSON objects separated by whitespace. + + {} {} -> [{},{}] + """ + results = [] + start = 0 + json_str = json_str.strip() # Remove leading and trailing whitespace + while start < len(json_str): + # Find the start of a JSON object + start = json_str.find('{', start) + if start == -1: # No more JSON object + break + try: + obj, index = json.JSONDecoder().raw_decode(json_str[start:]) + results.append(obj) + start += index + while start < len(json_str) and json_str[start] in ' \t\n\r': # Skip whitespace + start += 1 + except json.JSONDecodeError: + # Move start forward and try again + start += 1 + return results diff --git a/bitagent_subnet-main/bitagent/helpers/tool_parsing.py b/bitagent_subnet-main/bitagent/helpers/tool_parsing.py new file mode 100644 index 0000000000000000000000000000000000000000..0023b315bbca0af1fe8d80a8e6101750686c61b7 --- /dev/null +++ b/bitagent_subnet-main/bitagent/helpers/tool_parsing.py @@ -0,0 +1,92 @@ +import bittensor as bt +from typing import Dict, Any, List +from pydantic import ValidationError +from bitagent.schemas.tool import Tool, ToolCall +from bitagent.schemas.chat import ChatMessage, messages_to_list + +# Mapping from type strings to Python types +type_mapping = { + "str": str, + "int": int, + "dict": Dict, + "list": List, + "float": float, + "bool": bool, + "string": str, + "integer": int, + "number": (int, float), # Allow both int and float for 'number' + "boolean": bool, + "array": List, + "dictionary": Dict, + "object": Dict, # Handle nested objects as dictionaries +} + +def validate_tool_call(tool: Tool, tool_call: Dict[str, Any]) -> bool: + try: + # Validate the tool call structure + tool_call_validated = ToolCall(**tool_call) + + # Check if the tool call name matches the tool name + if tool_call_validated.name != tool.name: + #bt.logging.warning(f"Tool name mismatch: {tool_call_validated.name} != {tool.name}") + return False + + if len(tool_call_validated.arguments.keys()) < len([argname for argname, argdict in tool.arguments.items() if argdict['required']]) or len(tool_call_validated.arguments.keys()) > len([argname for argname, argdict in tool.arguments.items()]): + #bt.logging.warning(f"Argument length mismatch") + return False + + # Check arguments + for arg_name, arg_schema in tool.arguments.items(): + if arg_schema['required'] and arg_name not in tool_call_validated.arguments: + #bt.logging.warning(f"Missing required argument: {arg_name}") + return False + if arg_name in tool_call_validated.arguments: + expected_type = type_mapping.get(arg_schema['type']) + if expected_type is None: + #bt.logging.warning(f"Unknown type for argument {arg_name}: {arg_schema['type']}") + return False + + # Handle nested objects + if 'is_ground_truth' in list(tool_call.keys()) and tool_call['is_ground_truth']: + arg_value = tool_call_validated.arguments[arg_name][-1] + else: + arg_value = tool_call_validated.arguments[arg_name] + # convert int to float if expected type is float + if expected_type == float and type(arg_value) == int: + arg_value = float(arg_value) + # convert str to float if expected type is float + if expected_type == float and type(arg_value) == str: + arg_value = float(arg_value) + # convert str to int if expected type is int + if expected_type == int and type(arg_value) == str: + arg_value = int(arg_value) + + if expected_type == dict: + if not isinstance(arg_value, dict): + #bt.logging.warning(f"""Argument {arg_name} has incorrect type. + # Expected {expected_type}, got {type(arg_value)}""") + return False + else: + if not isinstance(arg_value, expected_type): + #bt.logging.warning(f"""Argument {arg_name} has incorrect type. + # Expected {expected_type}, got {type(arg_value)}""") + return False + + # All checks passed + return True + except ValidationError as e: + #bt.logging.warning(f"Validation error: {e}") + return False + +def find_first_tool_call(messages: List[ChatMessage]): + for msg in messages: + if msg.role == 'tool call': + return msg + +def find_msgs_before_tool_call(messages: List[ChatMessage]): + result = [] + for msg in messages: + if msg.role == 'tool call': + break + result.append(msg) + return result \ No newline at end of file diff --git a/bitagent_subnet-main/bitagent/miners/__init__.py b/bitagent_subnet-main/bitagent/miners/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b062a5807b2b410058f365fc43cd18d655fd3f4c --- /dev/null +++ b/bitagent_subnet-main/bitagent/miners/__init__.py @@ -0,0 +1,9 @@ +__version__ = "1.0.0" +version_split = __version__.split(".") +__spec_version__ = ( + (1000 * int(version_split[0])) + + (10 * int(version_split[1])) + + (1 * int(version_split[2])) +) +from . import mock_miner +from . import default_miner \ No newline at end of file diff --git a/bitagent_subnet-main/bitagent/miners/default_miner.py b/bitagent_subnet-main/bitagent/miners/default_miner.py new file mode 100644 index 0000000000000000000000000000000000000000..0b0f441c4e1c09310e8af1d29a1e64a05f173f30 --- /dev/null +++ b/bitagent_subnet-main/bitagent/miners/default_miner.py @@ -0,0 +1,30 @@ +# The MIT License (MIT) +# Copyright © 2024 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import bitagent +from bitagent.helpers.llms import llm + +def miner_init(self, config=None): + self.model_name = self.config.hf_model_name_to_run + self.llm = llm + +def miner_process(self, synapse: bitagent.protocol.QueryTask) -> bitagent.protocol.QueryTask: + llm_response = self.llm(self, synapse.messages, synapse.tools, self.model_name) + synapse.response = llm_response + synapse.hf_run_model_name = self.model_name + + return synapse \ No newline at end of file diff --git a/bitagent_subnet-main/bitagent/miners/mock_miner.py b/bitagent_subnet-main/bitagent/miners/mock_miner.py new file mode 100644 index 0000000000000000000000000000000000000000..b4014e04e3d285cf9f318283d5d01cd74d64fc2f --- /dev/null +++ b/bitagent_subnet-main/bitagent/miners/mock_miner.py @@ -0,0 +1,32 @@ +# The MIT License (MIT) +# Copyright © 2023 Yuma Rao +# Copyright © 2023 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import bitagent + +def miner_init(self, config=None): + + def llm(): + return {"role": "assistant", "content": "I'm the LLM response, b/c I'm mock miner - rahhh!"} + + self.llm = llm + +def miner_process(self, synapse: bitagent.protocol.QueryTask) -> bitagent.protocol.QueryTask: + llm_response = self.llm() + synapse.response = llm_response + + return synapse \ No newline at end of file diff --git a/bitagent_subnet-main/bitagent/protocol.py b/bitagent_subnet-main/bitagent/protocol.py new file mode 100644 index 0000000000000000000000000000000000000000..2108a80e5a17763d99f4a2b0118f2d9ae80f3918 --- /dev/null +++ b/bitagent_subnet-main/bitagent/protocol.py @@ -0,0 +1,66 @@ +# The MIT License (MIT) +# Copyright © 2023 Yuma Rao +# Copyright © 2023 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +from typing import Optional, List +import bittensor as bt +from bitagent.schemas.chat import ChatMessage +from bitagent.schemas.tool import Tool + +class QueryTask(bt.Synapse): + """ + A simple BitAgent protocol representation which uses bt.Synapse as its base. + This protocol helps in handling validator request and miner response communication + + Attributes: + - messages: a list of ChatMessage (see bitagent/schemas) - will be used for every task except Tool Gen + - tools: list of tools {name, description, arguments } in a List of dicts + - repsonse: string (e.g., tool_name(arg1=value1, arg2=value2)) + - hf_run_model_name: string representing the HF model the miner is running + """ + + # Required request input, filled by sending dendrite caller. + tools: List[Tool] = [] + messages: List[ChatMessage] = [] + + # Optional request output, filled by recieving axon. + response: str = "" + hf_run_model_name: str = "N/A" + competition_version: Optional[str] = None + +class QueryResult(bt.Synapse): + """ + Provide feedback on last task request from validator to inform Miner of performance. + This is a one-way request does not require a response. + Attributes: + - results: string of results to be printed to the logs + """ + results: str + +class IsAlive(bt.Synapse): + response: bool = False + +# Validator calls this to get the HF model name that this miner hosts on HF +class GetHFModelName(bt.Synapse): + hf_model_name: Optional[str] = None + +# Validator calls this to have the miner set the TOP HF model for this miner to run +class SetHFModelName(bt.Synapse): + hf_model_name: str + +class GetHFRunModelName(bt.Synapse): + hf_run_model_name: Optional[str] = None diff --git a/bitagent_subnet-main/bitagent/schemas/chat.py b/bitagent_subnet-main/bitagent/schemas/chat.py new file mode 100644 index 0000000000000000000000000000000000000000..a0a26aa1544fd85c259cc3930c2bfff709e4d3ca --- /dev/null +++ b/bitagent_subnet-main/bitagent/schemas/chat.py @@ -0,0 +1,39 @@ +from strenum import StrEnum +from typing import Dict, List +from pydantic import BaseModel, Field + +class ChatRole(StrEnum): + """One of ASSISTANT|USER to identify who the message is coming from.""" + + SYSTEM = "system" + ASSISTANT = "assistant" + USER = "user" + TOOL_CALL = "tool call" + TOOL_RESPONSE = "tool response" + + +class ChatMessage(BaseModel): + """A list of previous messages between the user and the model, meant to give the model conversational context for responding to the user's message.""" + + role: ChatRole = Field( + title="One of the ChatRole's to identify who the message is coming from.", + ) + content: str | dict | list = Field( # TODO the dict/list was added to support json loading the function calls. this should maybe be done inside a ToolMessage type + title="Contents of the chat message.", + ) + + @classmethod + def from_dict(cls, data: Dict[str, str]): + """Create a ChatMessage object from a dictionary.""" + return cls(role=ChatRole(data['role']), content=data['content']) + + def to_dict(self) -> Dict[str, str]: + return {"role": self.role.value, "content": self.content} + + +def messages_from_list(data_list: List[Dict[str, str]]): + messages = [ChatMessage.from_dict(item) for item in data_list] + return messages + +def messages_to_list(messages: List[ChatMessage]): + return [msg.to_dict() for msg in messages] diff --git a/bitagent_subnet-main/bitagent/schemas/tool.py b/bitagent_subnet-main/bitagent/schemas/tool.py new file mode 100644 index 0000000000000000000000000000000000000000..92c33ec2338650349d2eb447a7dbf386b0f5c5ef --- /dev/null +++ b/bitagent_subnet-main/bitagent/schemas/tool.py @@ -0,0 +1,21 @@ +from pydantic import BaseModel +from typing import Dict, Any, List + +class Tool(BaseModel): + """ + Attributes: + - name: str + - description: str + - arguments: dict where the key is the name of the argument and the value is a dict containing the keys (required:bool, type:str, description:str) + """ + name: str + description: str + arguments: Dict[str, Any] + + def to_dict(self): + return self.dict() + + +class ToolCall(BaseModel): + name: str + arguments: Dict[str, Any] \ No newline at end of file diff --git a/bitagent_subnet-main/bitagent/tasks/__init__.py b/bitagent_subnet-main/bitagent/tasks/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..ab01e38fc6b2fd065d62d88c5045355b97f1d691 --- /dev/null +++ b/bitagent_subnet-main/bitagent/tasks/__init__.py @@ -0,0 +1,3 @@ +from .constants import * +from .task import * +from .tool_call_task import * diff --git a/bitagent_subnet-main/bitagent/tasks/constants.py b/bitagent_subnet-main/bitagent/tasks/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..63369fafa715791f7623a1cadfff3c9b57df17c1 --- /dev/null +++ b/bitagent_subnet-main/bitagent/tasks/constants.py @@ -0,0 +1,7 @@ +TASK_FREQUENCY = { + "tool_call": 1, +} + +TASK_WEIGHTS = { + "tool_call": 0.05, +} \ No newline at end of file diff --git a/bitagent_subnet-main/bitagent/tasks/task.py b/bitagent_subnet-main/bitagent/tasks/task.py new file mode 100644 index 0000000000000000000000000000000000000000..ba5d751b967580410c8c4b9e366bea6ccc3aca83 --- /dev/null +++ b/bitagent_subnet-main/bitagent/tasks/task.py @@ -0,0 +1,105 @@ +# The MIT License (MIT) +# Copyright © 2023 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +import random +import bittensor as bt +from pprint import pformat +from typing import List, Tuple +from bitagent.protocol import QueryTask +from bitagent.schemas.tool import Tool +from bitagent.tasks import TASK_FREQUENCY +from bitagent.criteria import Criterion, default_criteria +from bitagent.schemas.chat import ChatMessage, messages_to_list + +# Task() +# combines criterion/criteria with the QueryTask (messages,tools) for eval to form a task for the miner +class Task(): + criteria: List[Criterion] + synapse: QueryTask + + def __init__(self, + name: str, + weight: int = 0.05, + desc: str = "", + timeout: int = 12, + tools: List[Tool] = [], + messages: List[ChatMessage] = [], + criteria: List[Criterion] = default_criteria, + correct_answer: str=None + ) -> None: + + self.name=name + self.mode = "online" + self.weight = weight + self.desc=desc + self.timeout=timeout + self.criteria=criteria + self.messages = messages + self.synapse = QueryTask(messages=messages, tools=tools) + self.correct_answer = correct_answer + + def reward(self, validator, synapse: QueryTask) -> Tuple[float, float, List[str]]: + total_score = 0.0 + total_possible = 0.0 + results = [] + for criterion in self.criteria: + score, max_score, result = criterion.evaluate(self, validator, synapse) + total_score += score + total_possible += max_score + results.append(result) + if self.correct_answer: + correct_answer = self.correct_answer + else: + correct_answer = "N/A" + return [total_score, total_possible, results, correct_answer] + + def __repr__(self): + return pformat(vars(self), indent=4, width=1) + + def toJSON(self): + return { + "weight": self.weight, + "name": self.name, + "mode": self.mode, + "desc": self.desc, + "messages": messages_to_list(self.messages) if isinstance(self.messages, list) else [], + "tools": [tool.to_dict() for tool in self.synapse.tools], + "timeout": self.timeout, + } + +# evaluate task +def evaluate_task(validator, task:Task, synapse:bt.Synapse) -> Tuple[float, float, List[str]]: + return task.reward(validator, synapse) + +# get random task +def get_random_task(validator, offline=False) -> Task: + from bitagent.tasks.tool_call_task import ToolCallTask + task_names = list(TASK_FREQUENCY.keys()) + task_frequencies = list(TASK_FREQUENCY.values()) + choice = random.choices(task_names, weights=task_frequencies)[0] + + for _ in range(100): + try: + match choice: + case "tool_call": + return ToolCallTask(validator=validator, name="Responds with correct function call", offline=offline) + + except Exception as e: + #bt.logging.warning(f'Error getting task (name {choice}): ', e) + #bt.logging.warning(traceback.format_exc()) + pass + + raise Exception("Failed to get task after 100 attempts") \ No newline at end of file diff --git a/bitagent_subnet-main/bitagent/tasks/tool_call_task.py b/bitagent_subnet-main/bitagent/tasks/tool_call_task.py new file mode 100644 index 0000000000000000000000000000000000000000..955deb6b84d787936cd100ce3728ac1531332539 --- /dev/null +++ b/bitagent_subnet-main/bitagent/tasks/tool_call_task.py @@ -0,0 +1,191 @@ +# The MIT License (MIT) +# Copyright © 2023 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +import ast +import json +import random +import bittensor as bt +from bitagent.protocol import QueryTask +from bitagent.tasks import Task +from bitagent.tasks import TASK_WEIGHTS +from bitagent.schemas.chat import messages_to_list +from bitagent.datasources.tools import ToolCallData +from bitagent.helpers.tool_parsing import validate_tool_call, find_msgs_before_tool_call, find_first_tool_call +from bitagent.criteria import default_criteria, tool_call_criteria, irrelevant_tool_call_criteria + +REWRITE_TOOL_USER_PROMPT = """You rewrite questions to make sense when paired with a function call. +The rewritten question will need to be changed to match the argument parameters and values relative to the function name. +You should change the phrasing of the question to be different and keeping aligned with the function name and arguments. +The capitalization of your user prompt rephrasasl should match the exact case of what is expected in the function call. +Your response should be the rewritten question only.\n +Function call:\n`{tool_call}`\n +Question: {user}\n +Modified Question: """ + +class ToolCallTask(Task): + def __init__( + self, + validator, + name: str, + desc: str = "", + offline: bool = False, + ): + super().__init__(name=name, desc=desc) + self.validator = validator + self.timeout = 15.0 + self.name += " - Tool Call" + self.weight = TASK_WEIGHTS["tool_call"] + + if offline: + self.mode = "offline" + messages = None + for _ in range(10): + try: + messages, tools, data = self.generate_task_data() + expected_messages = messages_to_list(data.messages) + expected_tool_call_messages = [em for em in expected_messages if em['role'] == 'tool call'] + if messages[0].role == 'system': + # try again - skip tasks with system prompts + continue + if len(expected_tool_call_messages) > 0: + expected_tool_call_message = expected_tool_call_messages[0]['content'] + else: + #bt.logging.debug(f"Skipping - no tool call message found in expected messages: {expected_messages}") + continue + + if type(expected_tool_call_message) == str: + expected_tool_call = json.loads(expected_tool_call_message) + else: + expected_tool_call = expected_tool_call_message + self.criteria = default_criteria + tool_call_criteria(expected_response=expected_tool_call) + + # 75% of the time do a tool call task with a relevant tool, other times do a tool call with no valid tool option + # irrelevant tool call + if "is_ground_truth" not in expected_tool_call_message and bool(random.random() < 0.25) and len(tools) > 1: + # remove the real tool + expected_tool_call_message_json = json.loads(expected_tool_call_message) + if isinstance(expected_tool_call_message_json, str): + expected_tool_call_message_json = json.loads(expected_tool_call_message_json) + tools = [t for t in tools if t.name != expected_tool_call_message_json['name']] + self.criteria = default_criteria + irrelevant_tool_call_criteria() + + break + + except Exception as e: + bt.logging.debug(f'Exception getting new task - {e} - you may need to CHECK YOUR vLLM docker instance') + pass + if not messages: + raise Exception(f"Failed to generate task data 10 times") + self.messages = messages + self.synapse = QueryTask(messages=messages, tools=tools) + + def generate_task_data(self) -> ToolCallData: + data: ToolCallData = next(self.validator.tool_dataset) + + tool_call = find_first_tool_call(data.messages) + if not tool_call: + # no tool call in the messages, so skip + raise Exception(f"Skipping - no tool call in the messages: {data.messages}") + + # increase number of tools + for _ in range(random.randint(2,4)): + # filter out the tools by name that are already in the data.tools + new_tools = [t for t in next(self.validator.tool_dataset).tools if t.name not in [dt.name for dt in data.tools]] + data.tools = data.tools + new_tools + + # remove all the messages after the first tool call, keeping the assistant + # this reduces the number of messages needing rewording + messages = data.messages + filtered_msgs = [] + seen_tool_call = False + for msg in messages: + filtered_msgs.append(msg) + if seen_tool_call: # want to do break after to include the assistant response + break + if msg.role == 'tool call': + seen_tool_call = True + data.messages = filtered_msgs + + user = data.messages[0].content + + count = 0 + while count < 10: + count += 1 + if find_first_tool_call(data.messages): + tool_call = find_first_tool_call(data.messages).content + try: # check that the tool call can be loaded, and that it's valid + try: + if isinstance(tool_call, str): + new_tool_call = json.dumps(json.loads(tool_call)) + tool_call_dict = json.loads(new_tool_call) + elif isinstance(tool_call, dict): + new_tool_call = tool_call + tool_call_dict = tool_call + else: + raise Exception(f'tool call is not a string or dict: {tool_call}') + + except Exception as e: + # this usually happens when the json is not valid (single vs double quotes) + new_tool_call = json.dumps(ast.literal_eval(tool_call)) + tool_call_dict = ast.literal_eval(tool_call) + # check through all the tools that will be passed to the miner + # find the tool that is THE tool that is expected to be returned + # since it has been rewritten, validate that the tool call is valid/comparable still + #for tool in data.tools: + # if tool.name == tool_call_dict['name']: + # if not validate_tool_call(tool, tool_call_dict): + # raise Exception('The rewritten tool call is not valid') + #bt.logging.debug(f'finished validating tool call: {tool_call_dict}') + except Exception as e: + bt.logging.error(f'An error occured while rewriting the tool call {e} - you may need to CHECK YOUR vLLM docker instance') + count = 11 + continue + + rw_prompt = REWRITE_TOOL_USER_PROMPT.format(tool_call=new_tool_call, user=user) + new_user = self.validator.llm([{"role": "user", "content": rw_prompt}], max_new_tokens=1000, temperature=1) + if not self.check_rewrite_alignment(new_user, user): + raise Exception(f"User rewrite is not in alignment\nOriginal: {user}\n Rewrite: {new_user}") + + data.messages[0].content = new_user + + data = ToolCallData(messages=data.messages, tools=data.tools) + messages_before_call = find_msgs_before_tool_call(data.messages) + + else: + # no tool call in the messages, so skip + raise Exception(f"Skipping - guess there was no tool call in the messages: {data.messages}") + + all_tools = data.tools + random.shuffle(all_tools) + return messages_before_call, all_tools, data + + raise Exception("Skipping - while loop ended without a tool call task") + + def check_rewrite_alignment(self, original: str, rewrite: str) -> bool: + score = self.validator.measure_relevance_of_texts(original, rewrite) + + if score > 0.98: + return False + + if score < 0.2: + return False + + if len(rewrite) > 2 * len(rewrite): + return False + + if len(rewrite) < 0.25 * len(rewrite): + return False + + return True \ No newline at end of file diff --git a/bitagent_subnet-main/bitagent/validator/__init__.py b/bitagent_subnet-main/bitagent/validator/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a7c0d43fcd05b8c137a458109b03466764d498e8 --- /dev/null +++ b/bitagent_subnet-main/bitagent/validator/__init__.py @@ -0,0 +1,10 @@ +__version__ = "1.0.15" +version_split = __version__.split(".") +__spec_version__ = ( + (1000 * int(version_split[0])) + + (10 * int(version_split[1])) + + (1 * int(version_split[2])) +) +from . import reward +from .forward import forward +from .initiation import initiate_validator diff --git a/bitagent_subnet-main/bitagent/validator/constants.py b/bitagent_subnet-main/bitagent/validator/constants.py new file mode 100644 index 0000000000000000000000000000000000000000..5b30678eedd0938f6c246b472074ccc9036cf552 --- /dev/null +++ b/bitagent_subnet-main/bitagent/validator/constants.py @@ -0,0 +1,5 @@ +COMPETITION_PREVIOUS_PREFIX = 1 # This is the previous competition prefix for when we swap to a new competition prefix and need to keep track of the old scores +COMPETITION_PREFIX = 1 +DEPLOYED_DATE = "2024-12-10" +COMPETITION_LENGTH_DAYS = 7 +TESTNET_COMPETITION_LENGTH_DAYS = 1.0/24.0 # every hour \ No newline at end of file diff --git a/bitagent_subnet-main/bitagent/validator/forward.py b/bitagent_subnet-main/bitagent/validator/forward.py new file mode 100644 index 0000000000000000000000000000000000000000..496efd9a851c71755ce35cff2f48d8a62bc87f63 --- /dev/null +++ b/bitagent_subnet-main/bitagent/validator/forward.py @@ -0,0 +1,129 @@ +# The MIT License (MIT) +# Copyright © 2023 Yuma Rao +# Copyright © 2023 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import asyncio +import numpy as np +import bittensor as bt +from bitagent.protocol import QueryTask +from common.utils.uids import get_alive_uids +from common.utils.uids import get_random_uids +from bitagent.tasks.task import get_random_task +from bitagent.validator.offline_task import offline_task +from bitagent.validator.reward import process_rewards_update_scores_and_send_feedback + +async def forward(self, synapse: QueryTask=None) -> QueryTask: + """ + The forward function is called by the validator every time step. + It is responsible for querying the network and scoring the responses. + + Args: + self (:obj:`bittensor.neuron.Neuron`): The neuron object which contains all the necessary state for the validator. + + """ + # complete this first so it's cached for both ONLINE and OFFLINE + get_alive_uids(self) + + # ########################################################### + # OFFLINE TASKING + # ########################################################### + # if all miners have been processed for this competition, then don't run offline mode + self.update_competition_numbers() + + wandb_data = { + "task_name": "offline_model_check", + "task_mode": "offline", + "validator_uid": self.metagraph.hotkeys.index(self.wallet.hotkey.ss58_address), + "val_spec_version": self.spec_version, + "highest_score_for_miners_with_this_validator": self.scores.max(), + "median_score_for_miners_with_this_validator": np.median(self.scores), + "highest_offline_score_for_miners_with_this_validator": self.offline_scores[self.competition_version].max(), + "median_offline_score_for_miners_with_this_validator": np.median(self.offline_scores[self.competition_version]), + "average_offline_score_for_miners_with_this_validator": np.mean(self.offline_scores[self.competition_version]), + "prior_highest_offline_score_for_miners_with_this_validator": self.offline_scores[self.previous_competition_version].max(), + "prior_median_offline_score_for_miners_with_this_validator": np.median(self.offline_scores[self.previous_competition_version]), + "prior_average_offline_score_for_miners_with_this_validator": np.mean(self.offline_scores[self.previous_competition_version]), + "competition_version": self.competition_version, + } + + if self.config.subtensor.network != "test": + if len(self.miners_left_to_score) == 0: + if self.offline_status != "complete": + self.offline_status = "complete" + wandb_data['offline_status'] = self.offline_status + wandb_data['num_miners_left_to_score'] = len(self.miners_left_to_score) + self.log_event(wandb_data) + wandb_data.pop('offline_status') + wandb_data.pop('num_miners_left_to_score') + self.running_offline_mode = False + #bt.logging.debug(f"OFFLINE: No miners left to score for competition {self.competition_version}") + pass + elif not self.running_offline_mode: + bt.logging.debug(f"OFFLINE: Starting offline mode for competition {self.competition_version}") + #bt.logging.debug(f"OFFLINE: Miners left to score: {self.miners_left_to_score}") + self.running_offline_mode = True + self.offline_status = "starting" + wandb_data['offline_status'] = self.offline_status + wandb_data['num_miners_left_to_score'] = len(self.miners_left_to_score) + wandb_data['miners_left_to_score'] = self.miners_left_to_score + self.log_event(wandb_data) + wandb_data.pop('num_miners_left_to_score') + wandb_data.pop('miners_left_to_score') + wandb_data.pop('offline_status') + asyncio.create_task(offline_task(self, wandb_data)) + self.running_offline_mode = False + elif self.running_offline_mode: + #bt.logging.debug(f"OFFLINE: Already running offline mode for competition {self.competition_version}") + #bt.logging.debug(f"OFFLINE: Miners left to score: {self.miners_left_to_score}") + if self.offline_status != "running": + self.offline_status = "running" + wandb_data['offline_status'] = self.offline_status + wandb_data['num_miners_left_to_score'] = len(self.miners_left_to_score) + self.log_event(wandb_data) + wandb_data.pop('num_miners_left_to_score') + wandb_data.pop('offline_status') + pass + else: + bt.logging.debug("OFFLINE: Skipping offline for testnet") + + # ########################################################### + # ONLINE TASKING + # ########################################################### + try: + bt.logging.debug(f"ONLINE: Starting online run") + # check a random sample of miners in online mode + bt.logging.debug(f"ONLINE: Getting random miner uids") + miner_uids = get_random_uids(self, min(self.config.neuron.sample_size, self.metagraph.n.item())) + bt.logging.debug(f"ONLINE: Getting random task") + task = get_random_task(self) + task.mode = "online" + + # send the task to the miners + bt.logging.debug(f"ONLINE: Sending task to miners") + responses = self.dendrite.query( + axons=[self.metagraph.axons[uid] for uid in miner_uids], + synapse=task.synapse, + deserialize=False, + timeout=task.timeout, + ) + + bt.logging.debug(f"ONLINE: Evaluating responses") + await asyncio.create_task(process_rewards_update_scores_and_send_feedback(self, task=task, responses=responses, miner_uids=miner_uids)) + bt.logging.debug(f"ONLINE: Evaluation complete") + + except Exception as e: + bt.logging.debug(f"Error in forward: {e}") diff --git a/bitagent_subnet-main/bitagent/validator/initiation.py b/bitagent_subnet-main/bitagent/validator/initiation.py new file mode 100644 index 0000000000000000000000000000000000000000..40aca6a1e7055fb8fc6ac29c698ee85ddea90004 --- /dev/null +++ b/bitagent_subnet-main/bitagent/validator/initiation.py @@ -0,0 +1,151 @@ +# The MIT License (MIT) +# Copyright © 2023 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import os +import copy +import wandb +import shutil +import bittensor as bt +from datetime import datetime +from bitagent.datasources import ToolDataset +from langchain_openai import ChatOpenAI +from sentence_transformers import util +from bitagent.helpers.sbert import CachedSentenceTransformer + +# setup validator with wandb +# clear out the old wandb dirs if possible +def initiate_validator(self): + + def init_wandb(self, reinit=False): + uid = self.metagraph.hotkeys.index(self.wallet.hotkey.ss58_address) + spec_version = self.spec_version + + """Starts a new wandb run.""" + tags = [ + self.wallet.hotkey.ss58_address, + str(spec_version), + f"netuid_{self.config.netuid}", + ] + + wandb_config = { + key: copy.deepcopy(self.config.get(key, None)) + for key in ("neuron", "reward", "netuid", "wandb") + } + wandb_config["neuron"].pop("full_path", None) + wandb_config["validator_uid"] = uid + + if self.config.netuid == 20: + project_name = "mainnet" + elif self.config.netuid == 76: + project_name = "testnet" # for TN76 + else: + self.wandb = "errored" + return # must be using a local netuid, no need to log to wandb + + try: + self.wandb = wandb.init( + anonymous="allow", + reinit=reinit, + entity='bitagentsn20', + project=project_name, + config=wandb_config, + dir=self.config.neuron.full_path, + tags=tags, + resume='allow', + name=f"{uid}-{spec_version}-{datetime.today().strftime('%Y-%m-%d')}", + ) + bt.logging.success(f"Started a new wandb run {self.wandb.name} ") + except Exception as e: + self.wandb = "errored" + bt.logging.error("Could not connect to wandb ... continuing without it ...") + bt.logging.error(f"WANDB Error: {e}") + + def log_event(event): + #bt.logging.debug("Writing to WandB ....") + + if not self.config.wandb.on: + return + + if not getattr(self, "wandb", None): + clear_wandb_dir(self) + init_wandb(self) + + if self.wandb == "errored": + return + + # Log the event to wandb. + self.wandb.log(event) + #bt.logging.debug("Logged event to WandB ....") + + self.log_event = log_event + + initiate_validator_local(self) + +def clear_wandb_dir(self): + wandb_path = os.path.join(self.config.neuron.full_path, "wandb") + if os.path.exists(wandb_path): + bt.logging.info(f"Clearing WandB directory of old runs") + for item in os.listdir(wandb_path): + item_path = os.path.join(wandb_path, item) + try: + if os.path.islink(item_path): # If it's a symbolic link + os.unlink(item_path) # Remove the symlink + elif os.path.isfile(item_path): # If it's a regular file + os.remove(item_path) + elif os.path.isdir(item_path): # If it's a directory + shutil.rmtree(item_path) + except Exception as e: + bt.logging.warning(f"Failed to remove {item_path}: {e}") + bt.logging.info(f"Cleared WandB directory of old runs") + +# provide some capabilities to the task API (LLM, cossim) +def initiate_validator_local(self): + #bt.logging.info("Initializing Validator - this may take a while (downloading data and models).") + self.tool_dataset = ToolDataset() + #bt.logging.debug("Initializing Validator - this may take a while (downloading data and models) - loading model ...") + self.sentence_transformer = CachedSentenceTransformer('BAAI/bge-small-en-v1.5') + + def llm(messages, max_new_tokens = 160, temperature=0.7): + if isinstance(messages, str): + messages = [{"role":"user","content":messages}] + llm = ChatOpenAI( + openai_api_key=self.config.openai_api_key, + openai_api_base=self.config.openai_api_base, + model_name=self.config.validator_model_name, + max_tokens = max_new_tokens, + temperature = temperature, + ) + return llm.invoke(messages).content.strip() + + self.llm = llm + + #bt.logging.debug("Initializing Validator - this may take a while (downloading data and models) - finished loading model") + + # code to measure the relevance of the response to the question + def measure_relevance_of_texts(text1, text2): + # Encode the texts to get the embeddings + if type(text2) == list: + embeddings = self.sentence_transformer.encode([text1,*text2], convert_to_tensor=True, show_progress_bar=False) + else: + embeddings = self.sentence_transformer.encode([text1,text2], convert_to_tensor=True, show_progress_bar=False) + # Compute the cosine similarity between the embeddings + if type(text2) == list: + return util.pytorch_cos_sim(embeddings[0], embeddings[1:])[0] + else: + return float(util.pytorch_cos_sim(embeddings[0], embeddings[1:])[0][0]) + + self.measure_relevance_of_texts = measure_relevance_of_texts \ No newline at end of file diff --git a/bitagent_subnet-main/bitagent/validator/offline_task.py b/bitagent_subnet-main/bitagent/validator/offline_task.py new file mode 100644 index 0000000000000000000000000000000000000000..51b0f7853e704af1d80aa995285862e9f0e901dd --- /dev/null +++ b/bitagent_subnet-main/bitagent/validator/offline_task.py @@ -0,0 +1,425 @@ +import os +import time +import shutil +import psutil +import asyncio +import requests +import bittensor as bt + +from sglang.utils import ( + terminate_process) +from bitagent.helpers.llms import llm +from huggingface_hub import model_info +from common.utils.uids import get_alive_uids +from bitagent.protocol import GetHFModelName +from bitagent.tasks.task import get_random_task +from common.utils.shell import execute_shell_command +from bitagent.helpers.logging import temporary_logging_state +from bitagent.validator.reward import process_rewards_update_scores_for_many_tasks_and_many_miners + +# TODO overall for tracking, would be nice to track based on hotkey instead of UID +# it's currently handled for uid and new hotkeys taking over a uid, but might be cleaner + +# Delete the model from the huggingface cache when we're done serving it so we don't run out of disk space +def delete_model_from_hf_cache(self, model_name: str): + # Determine the cache directory + cache_dir = os.path.expanduser(self.config.validator_hf_cache_dir) + + # Format the directory name based on the model name + model_cache_dir = os.path.join(cache_dir, f"models--{model_name.replace('/', '--')}") + + # Check if the directory exists and delete it + if os.path.exists(model_cache_dir): + try: + shutil.rmtree(model_cache_dir) + bt.logging.debug(f"OFFLINE: Model has been removed from the HF cache.") + except Exception as e: + bt.logging.error(f"OFFLINE: Error deleting model: from HF cache: {e}") + else: + bt.logging.debug(f"OFFLINE: Model not found in the cache, could not delete") + +# added our own wait for server to check the process itself +# this will check to see if the sglang process crashes due to limited VRAM +def wait_for_server(base_url: str, server_process, timeout: int = None) -> None: + """Wait for the server to be ready by polling the /v1/models endpoint. + + Args: + base_url: The base URL of the server + server_process: The process to terminate if the server is ready + timeout: Maximum time to wait in seconds. None means wait forever. + """ + start_time = time.time() + procutil = psutil.Process(int(server_process.pid)) + while True: + try: + if timeout and time.time() - start_time > timeout: + bt.logging.error(f"OFFLINE: Server did not become ready within timeout period") + raise TimeoutError("Server did not become ready within timeout period") + + # Use psutil to monitor the process + if not procutil.is_running(): # Check if process is still running + bt.logging.error(f"OFFLINE: Server process terminated unexpectedly, check VRAM usage") + raise Exception("Server process terminated unexpectedly, potentially VRAM usage issue") + if server_process.poll() is not None: + bt.logging.error(f"OFFLINE: Server process terminated with code {server_process.poll()}") + raise Exception(f"Server process terminated with code {server_process.poll()}") + + response = requests.get( + f"{base_url}/v1/models", + headers={"Authorization": "Bearer None"}, + ) + if response.status_code == 200: + time.sleep(5) + break + + except requests.exceptions.RequestException: + time.sleep(1) + + +# ########################################################### +# OFFLINE TASKING +# ########################################################### + +# TODO also run the bfcl suite on the validator - but skip the API calls, don't use those at first +# TODO store TOP score from last round and all-time in validator state + +async def offline_task(self, wandb_data): + bt.logging.debug("OFFLINE: Starting offline task") + self.running_offline_mode = True + wandb_data['event_name'] = "offline_task_started" + self.log_event(wandb_data) + + # get all alive miner UIDs to compare against the top scores from the last round + miner_uids = self.miners_left_to_score + + # TODO potentially fetch prompt template from miner too + # Grab all the models that the miners submitted + responses = await self.dendrite.forward( + axons=[self.metagraph.axons[miner_uid] for miner_uid in miner_uids], + synapse=GetHFModelName(), + deserialize=False, + timeout=15.0, + ) + + wandb_data['event_name'] = "GetHFModelName Responses Fetched" + self.log_event(wandb_data) + + # get all the HF model names from the responses + miner_hf_model_names = [response.hf_model_name for response in responses] + bt.logging.debug(f"OFFLINE: Miner HF model names: {len(miner_hf_model_names)}") + + try: + hf_model_name_to_miner_uids = {} + for i,miner_uid in enumerate(miner_uids): + self.offline_model_names[self.competition_version][miner_uid] = responses[i].hf_model_name + if responses[i].hf_model_name is not None: + if responses[i].hf_model_name not in hf_model_name_to_miner_uids: + hf_model_name_to_miner_uids[responses[i].hf_model_name] = [] + hf_model_name_to_miner_uids[responses[i].hf_model_name].append(int(miner_uid)) + + # Group all the models together uniquely and share the same inference server + unique_miner_hf_model_names = [m for m in list(set(miner_hf_model_names)) if m not in [None, ""]] + if len(unique_miner_hf_model_names) == 0: + bt.logging.debug(f"OFFLINE: No unique miner HF model names to evaluate in OFFLINE mode") + for miner_uid in miner_uids: + self.offline_scores[self.competition_version][miner_uid] = 0.0 + wandb_data['event_name'] = "No Unique HF Models" + wandb_data['miners_left_to_score'] = miner_uids + self.log_event(wandb_data) + wandb_data.pop('miners_left_to_score') + self.running_offline_mode = False + return + except Exception as e: + bt.logging.error(f"OFFLINE: Error getting unique miner HF model names: {e}") + wandb_data['event_name'] = "Error Getting Unique HF Models" + wandb_data['error'] = f"{e}" + self.log_event(wandb_data) + wandb_data.pop('error') + self.running_offline_mode = False + return + + bt.logging.debug(f"OFFLINE: Unique miner HF model names: {len(unique_miner_hf_model_names)}") + wandb_data['event_name'] = "Unique HF Model Fetched" + wandb_data['num_unique_hf_models'] = len(unique_miner_hf_model_names) + self.log_event(wandb_data) + wandb_data.pop('num_unique_hf_models') + + # no need to regrade if score exists for the same model + models_to_skip = [] + + for hfmn in unique_miner_hf_model_names: + uids_with_same_model = [] + scores_with_same_model = [] + for k, model_name in self.offline_model_names[self.competition_version].items(): + if model_name == hfmn: + uids_with_same_model.append(k) + scores_with_same_model.append(self.offline_scores[self.competition_version][k]) + + if len(uids_with_same_model) > 0: + max_score_for_model = max(scores_with_same_model) # Calculate max score once + + if max_score_for_model <= 0: + # Skip adding to models_to_skip if max score is zero + continue + + models_to_skip.append(hfmn) # Add only if max_score > 0 + + # Process the miners + the_uids = hf_model_name_to_miner_uids[hfmn] + bt.logging.debug(f"OFFLINE: Found miner with same model, using existing score") + for uid in the_uids: + self.offline_scores[self.competition_version][uid] = max_score_for_model + self.update_offline_scores([max_score_for_model] * len(the_uids), the_uids) + + # skip the models we already have scores for + unique_miner_hf_model_names = [m for m in unique_miner_hf_model_names if m not in models_to_skip] + + if len(unique_miner_hf_model_names) > 0: + bt.logging.debug(f"OFFLINE: Generating tasks") + # Generate a set of tasks to run on all the offline models + num_tasks = 1000 + batch_size = 100 + wandb_data['event_name'] = "Generating Tasks" + self.log_event(wandb_data) + tasks = [] + for i,_ in enumerate(range(0, num_tasks, batch_size)): + #bt.logging.debug(f"OFFLINE: Generating tasks batch {i+1} of {num_tasks // batch_size}") + tasks.extend(await asyncio.gather(*[asyncio.to_thread(get_random_task, self, offline=True) for _ in range(batch_size)])) + #bt.logging.debug(f"OFFLINE: Generated tasks batch {i+1} of {num_tasks // batch_size}") + bt.logging.debug(f"OFFLINE: Generated {len(tasks)} tasks of {num_tasks} total") + wandb_data['event_name'] = "Generated Tasks" + wandb_data['num_tasks'] = len(tasks) + self.log_event(wandb_data) + wandb_data.pop('num_tasks') + + for i,hf_model_name in enumerate(unique_miner_hf_model_names): + bt.logging.debug(f"OFFLINE: Running tasks for model {i+1} of {len(unique_miner_hf_model_names)}") + wandb_data['event_name'] = "Running HF Model" + wandb_data['num_hf_model'] = i + wandb_data['miner_uids'] = hf_model_name_to_miner_uids[hf_model_name] + self.log_event(wandb_data) + wandb_data.pop('miner_uids') + + if hf_model_name is None or hf_model_name == "" or hf_model_name.lower() == "none": + bt.logging.debug(f"OFFLINE: Miner returned empty HF model name ... skipping") + for miner_uid in hf_model_name_to_miner_uids[hf_model_name]: + self.offline_scores[self.competition_version][miner_uid] = 0.0 + wandb_data['event_name'] = "Skipping Empty HF Model" + wandb_data['miner_uids'] = hf_model_name_to_miner_uids[hf_model_name] + self.log_event(wandb_data) + wandb_data.pop('miner_uids') + continue # skip this model + + # Extract the model card data for the model from HF + # ensure logger doesn't print the model name publicly, so restrict to only HF warnings + # Temporarily set logging to WARNING within the context manager + with temporary_logging_state('Warning'): + info = model_info(hf_model_name) + total_size = info.safetensors.total + try: + license = info.card_data['license'] + except Exception: + bt.logging.debug("OFFLINE: No license found for model") + license = 'No license available' + + # confirm model license is apache-2.0 or nc-by-nc-4.0 or mit + # TODO eventually ONLY accept apache-2.0 + if license not in ["apache-2.0", "cc-by-nc-4.0", "mit"]: + bt.logging.debug(f"OFFLINE: Skipping model {i+1} of {len(unique_miner_hf_model_names)} due to license: {license}") + for miner_uid in hf_model_name_to_miner_uids[hf_model_name]: + self.offline_scores[self.competition_version][miner_uid] = 0.0 + wandb_data['event_name'] = "Skipping Model Due to License" + wandb_data['miner_uids'] = hf_model_name_to_miner_uids[hf_model_name] + self.log_event(wandb_data) + wandb_data.pop('miner_uids') + continue + + # confirm model size is less than 10B params (want 8B or less models) + if total_size > 10000000000: + bt.logging.debug(f"OFFLINE: Skipping model {i+1} of {len(unique_miner_hf_model_names)} due to size: {total_size}") + for miner_uid in hf_model_name_to_miner_uids[hf_model_name]: + self.offline_scores[self.competition_version][miner_uid] = 0.0 + wandb_data['event_name'] = "Skipping Model Due to Size" + wandb_data['miner_uids'] = hf_model_name_to_miner_uids[hf_model_name] + self.log_event(wandb_data) + wandb_data.pop('miner_uids') + continue + + bt.logging.debug(f"OFFLINE: Starting server for model {i+1} of {len(unique_miner_hf_model_names)}") + wandb_data['event_name'] = "HF Model Eval Server Starting" + self.log_event(wandb_data) + + # see if we have a snapshot already in the cache + latest_snapshot = None + + try: + # Start the server for the model + try: + cache_dir = os.path.expanduser(self.config.validator_hf_cache_dir) + snapshot_dir = f"{cache_dir}/models--{hf_model_name.replace('/', '--')}/snapshots/" + + # Get all snapshot directories + snapshots = [os.path.join(snapshot_dir, d) for d in os.listdir(snapshot_dir) if os.path.isdir(os.path.join(snapshot_dir, d))] + + # Sort snapshots by creation time (os.path.getctime) or modification time (os.path.getmtime) + latest_snapshot = max(snapshots, key=os.path.getctime) + # TODO if the latest snapshot is older than a week, delete it and download a new one + + except Exception as e: + bt.logging.debug(f"OFFLINE: Error getting latest snapshot") + latest_snapshot = None + + # # either load an existing snapshot or download the model + # if os.path.exists(snapshot_dir) and latest_snapshot: + # model_path = latest_snapshot + # else: + # # need to download from hugging face + model_path = hf_model_name + + server_process = await asyncio.to_thread(execute_shell_command, + f""" + {os.getcwd()}/.venvsglang/bin/python -m sglang.launch_server \ + --model-path {model_path} \ + --port {self.config.validator_hf_server_port} \ + --host 0.0.0.0 \ + --mem-fraction-static {self.config.validator_hf_server_mem_fraction_static} \ + --context-length 25000 + """, + hf_model_name + ) + + bt.logging.debug(f"OFFLINE: Started server for model {i+1} of {len(unique_miner_hf_model_names)}, waiting for it to start on port {self.config.validator_hf_server_port} (could take several minutes)") + try: + await asyncio.wait_for( + asyncio.to_thread(wait_for_server, f"http://localhost:{self.config.validator_hf_server_port}", server_process), + timeout=60*15 # wait up to 15 minutes + ) + bt.logging.debug(f"OFFLINE: Server for model {i+1} of {len(unique_miner_hf_model_names)} started") + wandb_data['event_name'] = "HF Model Eval Server Started" + self.log_event(wandb_data) + except asyncio.TimeoutError as e: + # likely a validator error + bt.logging.error(f"OFFLINE: Timeout waiting for server for model {i+1} of {len(unique_miner_hf_model_names)} to start, skipping") + wandb_data['event_name'] = "Timeout Waiting for HF Model Eval Server" + wandb_data['miner_uids'] = hf_model_name_to_miner_uids[hf_model_name] + self.log_event(wandb_data) + wandb_data.pop('miner_uids') + wandb_data.pop('num_hf_model') + # can't score this model, so skipping it for now, the miner will be tried again if this runs again + continue + except Exception as e: + bt.logging.error(f"OFFLINE: Error waiting for server: {e}, skipping") + wandb_data['event_name'] = "Error Waiting for HF Model Eval Server" + wandb_data['error'] = f"{e}" + wandb_data['miner_uids'] = hf_model_name_to_miner_uids[hf_model_name] + self.log_event(wandb_data) + wandb_data.pop('error') + wandb_data.pop('num_hf_model') + wandb_data.pop('miner_uids') + continue + + except Exception as e: + # likely a validator error + bt.logging.error(f"OFFLINE: Error starting sglang server for model: {i+1} of {len(unique_miner_hf_model_names)}: {e}") + wandb_data['event_name'] = "Error Starting HF Model Eval Server" + wandb_data['error'] = f"{e}" + wandb_data['miner_uids'] = hf_model_name_to_miner_uids[hf_model_name] + self.log_event(wandb_data) + wandb_data.pop('error') + wandb_data.pop('num_hf_model') + wandb_data.pop('miner_uids') + # can't score this model, so skipping it for now, the miner will be tried again if this runs again + # could be an issue with model size + continue + + # get LLM responses + bt.logging.debug(f"OFFLINE: Getting LLM responses for model {i+1} of {len(unique_miner_hf_model_names)}") + wandb_data['event_name'] = "Getting LLM Responses" + self.log_event(wandb_data) + + # at most 5 LLM calls concurrently + sem = asyncio.Semaphore(5) + + async def call_llm_with_semaphore(task): + async with sem: + return await asyncio.to_thread( + llm, self, task.synapse.messages, task.synapse.tools, hf_model_name, hugging_face=True + ) + + llm_responses_and_finishes = await asyncio.gather( + *[call_llm_with_semaphore(task) for task in tasks] + ) + try: + llm_responses = [r[0] for r in llm_responses_and_finishes] + llm_finishes = [r[1] for r in llm_responses_and_finishes] + except Exception as e: + bt.logging.error(f"OFFLINE: Error getting LLM responses: {e}, have to skip this model") + continue + + # TODO actually use the finishes to provide more detail to the miners in wandb + + bt.logging.debug(f"OFFLINE: Got {len(llm_responses)} LLM responses for model: {i+1} of {len(unique_miner_hf_model_names)}") + wandb_data['event_name'] = "Got LLM Responses" + self.log_event(wandb_data) + + # terminate the server after getting all the responses + bt.logging.debug(f"OFFLINE: Terminating server for model: {i+1} of {len(unique_miner_hf_model_names)}") + wandb_data['event_name'] = "HF Model Eval Server Terminating" + self.log_event(wandb_data) + await asyncio.to_thread(terminate_process, server_process) + bt.logging.debug(f"OFFLINE: Terminated server for model: {i+1} of {len(unique_miner_hf_model_names)}") + wandb_data['event_name'] = "HF Model Eval Server Terminated" + self.log_event(wandb_data) + + these_miner_uids = hf_model_name_to_miner_uids[hf_model_name] + responses = [] + for j, llm_response in enumerate(llm_responses): + task = tasks[j] + response = task.synapse.model_copy() + response.response = llm_response.strip() + response.dendrite.process_time = 5.0 # TODO may be useful to test performance of the model itself + response.dendrite.status_code = 200 + response.axon.status_code = 200 + response.competition_version = self.competition_version + responses.append(response) + + # evaluate, track score and add to wandb + # TODO need to see if this SCORE is higher than the all-time top score + # TODO if so, update the all-time top score and model name and reward TOP miners + # TODO if not, then temporal decay of scores + bt.logging.debug(f"OFFLINE: Processing rewards for model: {i+1} of {len(unique_miner_hf_model_names)}, for miners: {these_miner_uids}") + wandb_data['event_name'] = "Processing Rewards" + self.log_event(wandb_data) + + # TODO This is blocking the main loop + # blocking due to await, attempting to remove await and create a task and move on + + #await process_rewards_update_scores_for_many_tasks_and_many_miners(self, tasks=tasks, responses=responses, miner_uids=these_miner_uids, wandb_data=wandb_data) + asyncio.create_task(process_rewards_update_scores_for_many_tasks_and_many_miners(self, tasks=tasks, responses=responses, miner_uids=these_miner_uids, wandb_data=wandb_data)) + + + # remove newly downloaded files from HF cache if were not already in cache + if not latest_snapshot: + bt.logging.debug(f"OFFLINE: Deleting model from HF cache: {i+1} of {len(unique_miner_hf_model_names)}") + wandb_data['event_name'] = "Deleting HF Model from Cache" + self.log_event(wandb_data) + await asyncio.to_thread(delete_model_from_hf_cache, self, hf_model_name) + else: + bt.logging.debug(f"OFFLINE: NOT Deleting model from HF cache: {i+1} of {len(unique_miner_hf_model_names)}") + wandb_data['event_name'] = "NOT Deleting HF Model from Cache - snapshot found, so no new download to revert" + self.log_event(wandb_data) + + # TODO handle temporal decay of scores if no miners outperform all time top score + # TODO handle temporal decay of all scores depending on a) if no new TOP score and b) if new TOP score + wandb_data['event_name'] = "Finished Processing Rewards" + wandb_data['miner_uids'] = these_miner_uids + self.log_event(wandb_data) + wandb_data.pop('num_hf_model') + wandb_data.pop('miner_uids') + + bt.logging.debug(f"OFFLINE: Finished processing offline tasks") + self.running_offline_mode = False + wandb_data['event_name'] = "Finished Processing Offline Tasks" + wandb_data['miner_uids'] = miner_uids + self.log_event(wandb_data) + wandb_data.pop('miner_uids') diff --git a/bitagent_subnet-main/bitagent/validator/reward.py b/bitagent_subnet-main/bitagent/validator/reward.py new file mode 100644 index 0000000000000000000000000000000000000000..80dd19404dd08313f7577fc067d04f86097e616f --- /dev/null +++ b/bitagent_subnet-main/bitagent/validator/reward.py @@ -0,0 +1,266 @@ +# The MIT License (MIT) +# Copyright © 2023 Yuma Rao +# Copyright © 2023 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. +import asyncio +import numpy as np +import bittensor as bt +from typing import List, Any +from rich.console import Console +from bitagent.tasks.task import Task +from bitagent.protocol import QueryResult +from common.base.validator import BaseValidatorNeuron + +rich_console = Console() + +async def send_results_to_miner(validator, result, miner_axon): + # extra transparent details for miners + + # For generated/evaluated tasks, we send the results back to the miner so they know how they did and why + # The dendrite client queries the network to send feedback to the miner + _ = validator.dendrite.query( + # Send the query to selected miner axons in the network. + axons=[miner_axon], + # Construct a query. + synapse=QueryResult(results=result), + # All responses have the deserialize function called on them before returning. + # You are encouraged to define your own deserialization function. + deserialize=False, + timeout=5.0 # quick b/c we are not awaiting a response + ) + +async def evaluate_task(validator, task, response): + try: + return [task.reward(validator, response)] + except Exception as e: + bt.logging.warning(f"An exception calling task.reward: {e}") + +async def return_results(validator, task, miner_uid, reward, response): + # means we got all of the information we need to score the miner and update wandb + if len(reward) == 4: + score, max_possible_score, task_results, correct_answer = reward + # make sure the score is not None + if score and max_possible_score: + normalized_score = score/max_possible_score + + result = f""" +[bold]Task: {task.name}[/bold] +[bold]Messages:[/bold] {task.synapse.messages} +[bold]Tools:[/bold] {[t.name for t in task.synapse.tools]} +[bold]Response:[/bold] `{response.response}` +\n[bold]Results:[/bold]\n +=====================\n"""+"\n".join(task_results) + f""" +[bold]Total reward:[/bold] {score} +[bold]Total possible reward:[/bold] {max_possible_score} +[bold]Normalized reward:[/bold] {normalized_score} +--- +Stats with this validator: +Your Average Score: {validator.scores[miner_uid]} +Highest Score across all miners: {validator.scores.max()} +Median Score across all miners: {np.median(validator.scores)} +Your Offline Model Score for Competition {validator.previous_competition_version}: {validator.offline_scores[validator.previous_competition_version][miner_uid]} +Your Offline Model Score for Competition {validator.competition_version}: {validator.offline_scores[validator.competition_version][miner_uid]}""" +# TODO need to add BFCL scores when we do them + # send results + if task.mode == "online": + await send_results_to_miner(validator, result, validator.metagraph.axons[miner_uid]) + else: + # useful if validators want to see progress or results of offline tasks + # rich_console.print("this is a non-online task") + # rich_console.print(result) + pass + return task_results + return None + elif len(reward) == 2: # skip it + #bt.logging.debug(f"Skipping results for this task b/c Task API seems to have rebooted: {reward[1]}") + #time.sleep(25) + return None + else: + #bt.logging.debug(f"Skipping results for this task b/c not enough information") + #time.sleep(25) + return None + +async def write_to_wandb(validator: BaseValidatorNeuron, task: Task, responses: List[Any], miner_uids: List[int], rewards: List[List[float]], results: List[List[str]]) -> None: + # common wandb setup + try: + messages = task.synapse.messages + tools = task.synapse.tools + task_name = task.name + task_mode = task.mode + except Exception as e: + bt.logging.error("Could not setup common data - ", e) + + for i in range(len(responses)): + response = responses[i] + miner_uid = miner_uids[i] + score,max_possible_score,_,correct_answer = rewards[i][0] + normalized_score = score/max_possible_score + + resp = "None" + try: + resp = response.response + run_model = response.hf_run_model_name + except: + pass + + try: + data = { + "task_name": task_name, + "task_mode": task_mode, + "messages": [{'role': m.role, 'content': m.content} for m in messages], + "tools": [{'name': t.name, 'description': t.description, 'arguments': t.arguments} for t in tools], + "miners_count": len(miner_uids), + "messages_count": len(messages), + "tools_count": len(tools), + "response": resp, + "miner_uid": miner_uids[i], + "score": score, + "normalized_score": normalized_score, + "average_score_for_miner_with_this_validator": validator.scores[miner_uid], + "stake": validator.metagraph.S[miner_uid], + "trust": validator.metagraph.T[miner_uid], + "incentive": validator.metagraph.I[miner_uid], + "consensus": validator.metagraph.C[miner_uid], + "dividends": validator.metagraph.D[miner_uid], + "results": "\n".join(str(item) for item in results[i]) if results[i] else "None", + "dendrite_process_time": response.dendrite.process_time, + "dendrite_status_code": response.dendrite.status_code, + "axon_status_code": response.axon.status_code, + "validator_uid": validator.metagraph.hotkeys.index(validator.wallet.hotkey.ss58_address), + "val_spec_version": validator.spec_version, + "highest_score_for_miners_with_this_validator": validator.scores.max(), + "median_score_for_miners_with_this_validator": np.median(validator.scores), + "offline_score_for_miner_with_this_validator": validator.offline_scores[validator.competition_version][miner_uid], + "highest_offline_score_for_miners_with_this_validator": validator.offline_scores[validator.competition_version].max(), + "median_offline_score_for_miners_with_this_validator": np.median(validator.offline_scores[validator.competition_version]), + "average_offline_score_for_miners_with_this_validator": np.mean(validator.offline_scores[validator.competition_version]), + "prior_highest_offline_score_for_miners_with_this_validator": validator.offline_scores[validator.previous_competition_version].max(), + "prior_median_offline_score_for_miners_with_this_validator": np.median(validator.offline_scores[validator.previous_competition_version]), + "prior_average_offline_score_for_miners_with_this_validator": np.mean(validator.offline_scores[validator.previous_competition_version]), + "competition_version": validator.competition_version, + # TODO add BFCL scores + #"correct_answer": correct_answer, # TODO best way to send this without lookup attack? + } + + try: + #if task.mode == "offline": + # bt.logging.debug(f"OFFLINE Logging to WandB") + #else: + # bt.logging.debug(f"ONLINE Logging to WandB") + validator.log_event(data) + #if task.mode == "offline": + # bt.logging.debug(f"OFFLINE Logged to WandB") + #else: + # bt.logging.debug(f"ONLINE Logged to WandB") + except Exception as e: + bt.logging.warning("WandB failed to log, moving on ... exception: {}".format(e)) + + except Exception as e: + bt.logging.warning("Exception in logging to WandB: {}".format(e)) + +# all of these miners are scored the same way with the same tasks b/c this is scoring offline models +async def process_rewards_update_scores_for_many_tasks_and_many_miners( + validator: BaseValidatorNeuron, tasks: List[Task], responses: List[Any], + miner_uids: List[int], wandb_data: dict +) -> None: + # Gather rewards in parallel + rewards = await asyncio.gather(*[ + evaluate_task(validator, tasks[i], responses[i]) for i in range(len(responses)) + ]) + + try: + scores = [] + miner_tasks = [] # Collect tasks to execute in parallel for each miner + for i, reward in enumerate(rewards): + if len(reward[0]) == 4 and reward[0][0] is not None and reward[0][1] is not None: + scores.append(reward[0][0] / reward[0][1]) + + # Create a coroutine chain for each miner_uid + for miner_uid in miner_uids: + async def process_miner_task(task_idx, miner_uid, reward, response): + # Get the result for this miner + result = await return_results(validator, tasks[task_idx], miner_uid, reward[0], response) + # Write the result to wandb + await write_to_wandb(validator, tasks[task_idx], [response], [miner_uid], rewards, result) + + # Append the task for execution + miner_tasks.append(process_miner_task(i, miner_uid, reward, responses[i])) + else: + # Bad reward, so 0 score + scores.append(0.0) + + # Await all miner-specific tasks concurrently + await asyncio.gather(*miner_tasks) + + except Exception as e: + bt.logging.warning(f"OFFLINE: Error logging reward data: {e}") + wandb_data['event_name'] = "Processing Rewards - Error" + wandb_data['miner_uids'] = miner_uids + wandb_data['error'] = e + validator.log_event(wandb_data) + wandb_data.pop('error') + wandb_data.pop('miner_uids') + + # Compute and log the mean score + score = np.mean(scores) + wandb_data['event_name'] = "Processing Rewards - Score" + wandb_data['score'] = score + wandb_data['miner_uids'] = miner_uids + validator.log_event(wandb_data) + wandb_data.pop('score') + wandb_data.pop('miner_uids') + + # Update scores + validator.update_offline_scores([score] * len(miner_uids), miner_uids) + + return score + +async def process_rewards_update_scores_and_send_feedback(validator: BaseValidatorNeuron, task: Task, responses: List[Any], + miner_uids: List[int]) -> None: + """ + Returns a tensor of rewards for the given query and responses. + + Args: + - task (Task): The task sent to the miner. + - responses (List[float]): A list of responses from the miner. + - miner_uids (List[int]): A list of miner UIDs. The miner at a particular index has a response in responses at the same index. + """ + # run these in parallel but wait for the reuslts b/c we need them downstream + rewards = await asyncio.gather(*[evaluate_task(validator, task, response) for response in responses]) + try: + # track which miner uids are scored for updating the scores + #temp_miner_uids = [miner_uids[i] for i, reward in enumerate(rewards) if len(reward[0]) == 4 and reward[0][0] is not None and reward[0][1] is not None] + scores = [] + results = [] + for i, reward in enumerate(rewards): + if len(reward[0]) == 4 and reward[0][0] is not None and reward[0][1] is not None: + scores.append(reward[0][0]/reward[0][1]) + results.append(await return_results(validator, task, miner_uids[i], reward[0], responses[i])) + else: + # bad reward, so 0 score + scores.append(0.0) + results.append(None) + + await write_to_wandb(validator, task, responses, miner_uids, rewards, results) + + except Exception as e: + bt.logging.warning(f"ONLINE: Error logging reward data: {e}") + + # Update the scores based on the rewards. You may want to define your own update_scores function for custom behavior. + #miner_uids = temp_miner_uids + validator.update_scores(scores, miner_uids, alpha=task.weight) + + return scores \ No newline at end of file diff --git a/bitagent_subnet-main/common/__init__.py b/bitagent_subnet-main/common/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b5e87156991bcde6866cda6a8a15e2acb0400f26 --- /dev/null +++ b/bitagent_subnet-main/common/__init__.py @@ -0,0 +1,29 @@ +# The MIT License (MIT) +# Copyright © 2023 Yuma Rao +# Copyright © 2023 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +# Define the version of the common module. +__version__ = "1.0.15" +version_split = __version__.split(".") +__spec_version__ = ( + (1000 * int(version_split[0])) + + (10 * int(version_split[1])) + + (1 * int(version_split[2])) +) + +# Import all submodules. +from . import base diff --git a/bitagent_subnet-main/common/base/__init__.py b/bitagent_subnet-main/common/base/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/bitagent_subnet-main/common/base/miner.py b/bitagent_subnet-main/common/base/miner.py new file mode 100644 index 0000000000000000000000000000000000000000..148998a44320b2fe0c4a464092fff92ff5e3034c --- /dev/null +++ b/bitagent_subnet-main/common/base/miner.py @@ -0,0 +1,266 @@ +# The MIT License (MIT) +# Copyright © 2023 Yuma Rao +# Copyright © 2023 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import time +import asyncio +import uvicorn +import threading +import traceback + +import bittensor as bt +from bittensor.core.axon import FastAPIThreadedServer # type: ignore + +#from collections import Counter +from common.base.neuron import BaseNeuron + +class BaseMinerNeuron(BaseNeuron): + """ + Base class for Bittensor miners. + """ + + neuron_type: str = "MinerNeuron" + + def __init__(self, config=None): + super().__init__(config=config) + + # Warn if allowing incoming requests from anyone. + if not self.config.blacklist.force_validator_permit: + bt.logging.warning( + "You are allowing non-validators to send requests to your miner. This is a security risk." + ) + if self.config.blacklist.allow_non_registered: + bt.logging.warning( + "You are allowing non-registered entities to send requests to your miner. This is a security risk." + ) + + # The axon handles request processing, allowing validators to send this miner requests. + self.axon = bt.axon( + wallet=self.wallet, + config=self.config, + port=self.config.axon.port, + ip=self.config.axon.ip, + external_ip=self.config.axon.external_ip, + external_port=self.config.axon.external_port, + max_workers=self.config.axon.max_workers + ) + fast_config = uvicorn.Config( + self.axon.app, host="0.0.0.0", port=self.config.axon.port, log_level="trace", loop="asyncio" + ) + self.axon.fast_server = FastAPIThreadedServer(config=fast_config) + + # Attach determiners which functions are called when servicing a request. + bt.logging.info(f"Attaching forward function to miner axon.") + # NOTE - only big change made to common miner - RogueTensor + for forward_capability in self.forward_capabilities: + forward_fn = forward_capability['forward'] + blacklist_fn = forward_capability['blacklist'] + priority_fn = forward_capability['priority'] + self.axon.attach( + forward_fn=forward_fn, + blacklist_fn=blacklist_fn, + priority_fn=priority_fn, + ) + + bt.logging.info(f"Axon created: {self.axon}") + + # Instantiate runners + self.should_exit: bool = False + self.is_running: bool = False + self.thread: threading.Thread = None + self.lock = asyncio.Lock() + + def run(self): + """ + Initiates and manages the main loop for the miner on the Bittensor network. The main loop handles graceful shutdown on keyboard interrupts and logs unforeseen errors. + + This function performs the following primary tasks: + 1. Check for registration on the Bittensor network. + 2. Starts the miner's axon, making it active on the network. + 3. Periodically resynchronizes with the chain; updating the metagraph with the latest network state and setting weights. + + The miner continues its operations until `should_exit` is set to True or an external interruption occurs. + During each epoch of its operation, the miner waits for new blocks on the Bittensor network, updates its + knowledge of the network (metagraph), and sets its weights. This process ensures the miner remains active + and up-to-date with the network's latest state. + + Note: + - The function leverages the global configurations set during the initialization of the miner. + - The miner's axon serves as its interface to the Bittensor network, handling incoming and outgoing requests. + + Raises: + KeyboardInterrupt: If the miner is stopped by a manual interruption. + Exception: For unforeseen errors during the miner's operation, which are logged for diagnosis. + """ + + # Check that miner is registered on the network. + try: + self.sync() + except Exception as e: + bt.logging.error(f"Could not sync, will try again later. Error: {e}") + + # Serve passes the axon information to the network + netuid we are hosting on. + # This will auto-update if the axon port of external ip have changed. + bt.logging.info( + f"Serving miner axon {self.axon} on network: {self.config.subtensor.chain_endpoint} with netuid: {self.config.netuid}" + ) + try: + self.axon.serve(netuid=self.config.netuid, subtensor=self.subtensor) + + # Start starts the miner's axon, making it active on the network. + self.axon.start() + + bt.logging.info(f"Miner starting at block: {self.block}") + except Exception as e: + bt.logging.error(f"Could not start miner, errored: {e}") + + # This loop maintains the miner's operations until intentionally stopped. + try: + while not self.should_exit: + while(self.block - self.last_block_sync < self.config.neuron.epoch_length): + # Wait before checking again. + time.sleep(1) + + # Check if we should exit. + if self.should_exit: + break + + # Sync metagraph + self.sync() + self.step += 1 + + time.sleep(1) + + # If someone intentionally stops the miner, it'll safely terminate operations. + except KeyboardInterrupt: + self.axon.stop() + bt.logging.success("Miner killed by keyboard interrupt.") + exit() + + # In case of unforeseen errors, the miner will log the error and continue operations. + except Exception as e: + bt.logging.error(traceback.format_exc()) + + def run_in_background_thread(self): + """ + Starts the miner's operations in a separate background thread. + This is useful for non-blocking operations. + """ + if not self.is_running: + bt.logging.debug("Starting miner in background thread.") + self.should_exit = False + self.thread = threading.Thread(target=self.run, daemon=True) + self.thread.start() + self.is_running = True + bt.logging.debug("Started") + + def stop_run_thread(self): + """ + Stops the miner's operations that are running in the background thread. + """ + if self.is_running: + bt.logging.debug("Stopping miner in background thread.") + self.should_exit = True + self.thread.join(5) + self.is_running = False + bt.logging.debug("Stopped") + + def __enter__(self): + """ + Starts the miner's operations in a background thread upon entering the context. + This method facilitates the use of the miner in a 'with' statement. + """ + self.run_in_background_thread() + return self + + def __exit__(self, exc_type, exc_value, traceback): + """ + Stops the miner's background operations upon exiting the context. + This method facilitates the use of the miner in a 'with' statement. + + Args: + exc_type: The type of the exception that caused the context to be exited. + None if the context was exited without an exception. + exc_value: The instance of the exception that caused the context to be exited. + None if the context was exited without an exception. + traceback: A traceback object encoding the stack trace. + None if the context was exited without an exception. + """ + self.stop_run_thread() + + def resync_metagraph(self): + """Resyncs the metagraph and updates the hotkeys and moving averages based on the new metagraph.""" + bt.logging.info("resync_metagraph()") + + # Sync the metagraph. + try: + self.metagraph.sync(subtensor=self.subtensor) + self.last_block_sync = self.block + except Exception as e: + bt.logging.error(f"Could not sync with metagraph right now, will try later. Error: {e}") + + # each validator sends their top miner's HF model name to each miner + def get_top_miner_HF_model_name(self): + # miner can specify a HF model name to run + if self.config.hf_model_name_to_run != "none": + return self.config.hf_model_name_to_run + + return "Salesforce/xLAM-7b-r" + + ## TODO might consider selecting the TOP model that each validator votes for + ## if no specific model name is specified, miner will use the top model name from the validators' votes + #if not self.hf_top_model_names or len(list(self.hf_top_model_names.keys())) == 0: + # return self.config.hf_model_name_to_run + #else: + # # get the most common model name + # most_common_model_name = Counter(self.hf_top_model_names).most_common(1)[0][0] + # return most_common_model_name + + + # we might do this in the future, not doing for now + ## validator sends the miner the top model name to run from HF + ## store it + #def save_top_model_from_validator(self, top_hf_model_name, validator_uid): + # # save off the top model from this validator + # bt.logging.debug(f"Saving top HF model name from validator {validator_uid} state - {self.config.neuron.full_path}/miner_state.npz.") + # + # self.hf_top_model_names[validator_uid] = top_hf_model_name + # + # # Save the state of the miner to file. + # np.savez( + # self.config.neuron.full_path + "/miner_state.npz", + # hf_top_model_names=self.hf_top_model_names, + # ) + # + ## load that top model and run with it + #def load_state(self): + # """Loads the state of the miner from a file.""" + # bt.logging.info("Loading miner state.") + # if os.path.exists(self.config.neuron.full_path + "/miner_state.npz"): + # state = np.load(self.config.neuron.full_path + "/miner_state.npz", allow_pickle=True) + # else: + # np.savez( + # self.config.neuron.full_path + "/miner_state.npz", + # hf_top_model_names={}, + # ) + # state = np.load(self.config.neuron.full_path + "/miner_state.npz", allow_pickle=True) + # + # if 'hf_top_model_names' in state: + # loaded_hf_top_model_names = state["hf_top_model_names"] + # self.hf_top_model_names = loaded_hf_top_model_names + # else: + # self.hf_top_model_names = {} \ No newline at end of file diff --git a/bitagent_subnet-main/common/base/neuron.py b/bitagent_subnet-main/common/base/neuron.py new file mode 100644 index 0000000000000000000000000000000000000000..65a4a431173f39a9195133a68f1842cf701ad0ec --- /dev/null +++ b/bitagent_subnet-main/common/base/neuron.py @@ -0,0 +1,187 @@ +# The MIT License (MIT) +# Copyright © 2023 Yuma Rao + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import copy +import bittensor as bt +from abc import ABC, abstractmethod + +# Sync calls set weights and also resyncs the metagraph. +from common.utils.config import check_config, add_args, config +from common.utils.misc import ttl_get_block +from common import __spec_version__ as spec_version + + +class BaseNeuron(ABC): + """ + Base class for Bittensor miners. This class is abstract and should be inherited by a subclass. It contains the core logic for all neurons; validators and miners. + + In addition to creating a wallet, subtensor, and metagraph, this class also handles the synchronization of the network state via a basic checkpointing mechanism based on epoch length. + """ + + neuron_type: str = "BaseNeuron" + + @classmethod + def check_config(cls, config: "bt.Config"): + check_config(cls, config) + + @classmethod + def add_args(cls, parser): + add_args(cls, parser) + + @classmethod + def config(cls): + return config(cls) + + subtensor: "bt.subtensor" + wallet: "bt.wallet" + metagraph: "bt.metagraph" # type: ignore + spec_version: int = spec_version + + @property + def block(self): + return ttl_get_block(self) + + def __init__(self, config=None): + base_config = copy.deepcopy(config or BaseNeuron.config()) + self.config = self.config() + self.config.merge(base_config) + self.check_config(self.config) + + # Set up logging with the provided configuration and directory. + bt.logging(config=self.config, logging_dir=self.config.full_path) + + # If a gpu is required, set the device to cuda:N (e.g. cuda:0) + self.device = self.config.neuron.device + + # Log the configuration for reference. + bt.logging.info(self.config) + + # Build Bittensor objects + # These are core Bittensor classes to interact with the network. + bt.logging.info("Setting up bittensor objects.") + + # The wallet holds the cryptographic key pairs for the miner. + self.wallet = bt.wallet(config=self.config) + bt.logging.info(f"Wallet: {self.wallet}") + + # The subtensor is our connection to the Bittensor blockchain. + try: + while True: + try: + self.subtensor = bt.subtensor(config=self.config) + bt.logging.info(f"Subtensor: {self.subtensor}") + + # The metagraph holds the state of the network, letting us know about other validators and miners. + self.metagraph = self.subtensor.metagraph(self.config.netuid) + bt.logging.info(f"Metagraph: {self.metagraph}") + break + except Exception as e: + bt.logging.error(f"Error trying to connect to subtensor: {e}") + + # Check if the miner is registered on the Bittensor network before proceeding further. + self.check_registered() + + # Each miner gets a unique identity (UID) in the network for differentiation. + self.uid = self.metagraph.hotkeys.index( + self.wallet.hotkey.ss58_address + ) + bt.logging.info( + f"Running neuron on subnet: {self.config.netuid} with uid {self.uid} using network: {self.subtensor.chain_endpoint}" + ) + self.last_block_sync = self.block + self.step = 0 + except Exception as e: + bt.logging.error(f"Error trying to connect to subtensor: {e}") + + @abstractmethod + async def forward(self, synapse: bt.Synapse) -> bt.Synapse: + ... + + @abstractmethod + def run(self): + ... + + def sync(self, save_state=True): + """ + Wrapper for synchronizing the state of the network for the given miner or validator. + """ + try: + # Ensure miner or validator hotkey is still registered on the network. + self.check_registered() + + if self.should_sync_metagraph(): + self.resync_metagraph() + + if self.should_set_weights(): + self.set_weights() + + # Always save state unless during reinitiation. + if save_state: + self.save_state() + except Exception as e: + # Reconnect to subtensor if there is an error. + self.subtensor = bt.subtensor(config=self.config) + bt.logging.error(f"Error trying to sync, reconnected subtensor and skipping this round: {e}") + + def check_registered(self): + # --- Check for registration. + if not self.subtensor.is_hotkey_registered( + netuid=self.config.netuid, + hotkey_ss58=self.wallet.hotkey.ss58_address, + ): + bt.logging.error( + f"Wallet: {self.wallet} is not registered on netuid {self.config.netuid}." + f" Please register the hotkey using `btcli subnets register` before trying again" + ) + exit() + + def should_sync_metagraph(self): + """ + Check if enough epoch blocks have elapsed since the last checkpoint to sync. + """ + bt.logging.debug(f"Checking if ready to resync the metagraph at block {self.block}, last sync at {self.last_block_sync} and given epoch size of {self.config.neuron.epoch_length}: ", self.block - self.last_block_sync > self.config.neuron.epoch_length) + return self.block - self.last_block_sync > self.config.neuron.epoch_length + + def should_set_weights(self) -> bool: + # Don't set weights for miners + if self.config.neuron.type == "miner": + return False + + # Don't set weights on initialization. + if self.step == 0: + return False + + # Check if enough epoch blocks have elapsed since the last epoch. + if self.config.neuron.disable_set_weights: + return False + + # Define appropriate logic for when set weights. + return ( + (self.block - self.metagraph.last_update[self.uid]) + > self.config.neuron.epoch_length + and self.neuron_type != "MinerNeuron" + ) # don't set weights if you're a miner + + def save_state(self): + bt.logging.warning( + "save_state() not implemented for this neuron. You can implement this function to save model checkpoints or other useful data." + ) + + def load_state(self): + bt.logging.warning( + "load_state() not implemented for this neuron. You can implement this function to load model checkpoints or other useful data." + ) diff --git a/bitagent_subnet-main/common/base/validator.py b/bitagent_subnet-main/common/base/validator.py new file mode 100644 index 0000000000000000000000000000000000000000..8622076d1c7332eeffb5b13a597e81e464a7c1bf --- /dev/null +++ b/bitagent_subnet-main/common/base/validator.py @@ -0,0 +1,576 @@ +# The MIT License (MIT) +# Copyright © 2023 Yuma Rao +# Copyright © 2023 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import os +import copy +import asyncio +import threading +import numpy as np +import bittensor as bt +from datetime import datetime, timezone +from scoring_utils import score_spreading +from common.utils.uids import get_alive_uids +from bitagent.validator.constants import DEPLOYED_DATE, COMPETITION_LENGTH_DAYS, TESTNET_COMPETITION_LENGTH_DAYS, COMPETITION_PREFIX, COMPETITION_PREVIOUS_PREFIX +from common.utils.weight_utils import ( + process_weights_for_netuid, + convert_weights_and_uids_for_emit, +) +from typing import List +from traceback import print_exception + +from common.base.neuron import BaseNeuron +from common.utils.uids import check_uid_availability + +class BaseValidatorNeuron(BaseNeuron): + """ + Base class for Bittensor validators. Your validator should inherit from this class. + """ + + neuron_type: str = "ValidatorNeuron" + + def __init__(self, config=None): + super().__init__(config=config) + + # Save a copy of the hotkeys to local memory. + self.hotkeys = copy.deepcopy(self.metagraph.hotkeys) + + # Dendrite lets us send messages to other nodes (axons) in the network. + self.dendrite = bt.dendrite(wallet=self.wallet) + bt.logging.info(f"Dendrite: {self.dendrite}") + + # Set up initial scoring weights for validation + bt.logging.info("Building validation weights.") + self.scores = np.zeros(self.metagraph.n, dtype=np.float32) + self.offline_scores = {} + self.offline_miners_scored = {} + self.offline_model_names = {} + self.running_offline_mode = False + self.offline_status = None + self.regrade_version = 1025 + self.update_competition_numbers() + self.max_div = 0.0006 + self.min_div = 0.0002 + self.state_file_name = "ft_state.npz" + + # Init sync with the network. Updates the metagraph. + if os.path.exists(self.config.neuron.full_path + f"/{self.state_file_name}"): + # if we are booting up and have this file, then we'll want to load it + # otherwise, if we save state, it will overwrite from the sync + self.sync(save_state=False) + else: + # if no state file then we'll create one on init + self.sync() + # Serve axon to enable external connections. + if not self.config.neuron.axon_off: + #self.serve_axon() + pass + else: + bt.logging.warning("axon off, not serving ip to chain.") + + # Create asyncio event loop to manage async tasks. + self.loop = asyncio.get_event_loop() + + # Instantiate runners + self.should_exit: bool = False + self.is_running: bool = False + self.thread: threading.Thread = None + self.lock = asyncio.Lock() + + def serve_axon(self): + """Serve axon to enable external connections.""" + + bt.logging.info("serving ip to chain...") + try: + self.axon = bt.axon(wallet=self.wallet, config=self.config) + + self.axon.attach( + forward_fn=self.forward_fn, + blacklist_fn=self.blacklist_fn, + priority_fn=self.priority_fn, + ) + + try: + self.axon.serve(netuid=self.config.netuid, subtensor=self.subtensor) + self.axon.start() + except Exception as e: + bt.logging.error(f"Failed to serve Axon with exception: {e}") + pass + + except Exception as e: + bt.logging.error( + f"Failed to create Axon initialize with exception: {e}" + ) + pass + + async def concurrent_forward(self): + coroutines = [ + self.forward() + for _ in range(self.config.neuron.num_concurrent_forwards) + ] + await asyncio.gather(*coroutines,return_exceptions=True) + + def run(self): + """ + Initiates and manages the main loop for the miner on the Bittensor network. The main loop handles graceful shutdown on keyboard interrupts and logs unforeseen errors. + + This function performs the following primary tasks: + 1. Check for registration on the Bittensor network. + 2. Continuously forwards queries to the miners on the network, rewarding their responses and updating the scores accordingly. + 3. Periodically resynchronizes with the chain; updating the metagraph with the latest network state and setting weights. + + The essence of the validator's operations is in the forward function, which is called every step. The forward function is responsible for querying the network and scoring the responses. + + Note: + - The function leverages the global configurations set during the initialization of the miner. + - The miner's axon serves as its interface to the Bittensor network, handling incoming and outgoing requests. + + Raises: + KeyboardInterrupt: If the miner is stopped by a manual interruption. + Exception: For unforeseen errors during the miner's operation, which are logged for diagnosis. + """ + + # Check that validator is registered on the network. + self.sync() + bt.logging.info( + f"Running validator on network: {self.config.subtensor.chain_endpoint} with netuid: {self.config.netuid}" + ) + + bt.logging.info(f"Validator starting at block: {self.block}") + # This loop maintains the validator's operations until intentionally stopped. + try: + while True: + try: + bt.logging.info(f"step({self.step}) block({self.block})") + except Exception as e: + bt.logging.error(f"Error logging step and block, likely socket issue, will update next round: {e}") + #if "Broken pipe" in str(e): + # print("======= Exiting due to a broken pipe ========") + # self.axon.stop() + # self.should_exit = True + # exit() + + # Run multiple forwards concurrently. + self.loop.run_until_complete(self.concurrent_forward()) + + # Check if we should exit. + if self.should_exit: + break + + # Sync metagraph and potentially set weights. + try: + self.sync() + except Exception as e: + bt.logging.error(f"Error syncing metagraph during run loop: {e}") + + self.step += 1 + except Exception as e: + bt.logging.error(f"Unexpected error during run: {e}") + + # If someone intentionally stops the validator, it'll safely terminate operations. + except KeyboardInterrupt: + self.axon.stop() + bt.logging.success("Validator killed by keyboard interrupt.") + exit() + + # In case of unforeseen errors, the validator will log the error and continue operations. + except Exception as err: + bt.logging.error("Error during validation", str(err)) + bt.logging.debug( + print_exception(type(err), err, err.__traceback__) + ) + + def run_in_background_thread(self): + """ + Starts the validator's operations in a background thread upon entering the context. + This method facilitates the use of the validator in a 'with' statement. + """ + if not self.is_running: + bt.logging.debug("Starting validator in background thread.") + self.should_exit = False + self.thread = threading.Thread(target=self.run, daemon=True) + self.thread.start() + self.is_running = True + bt.logging.debug("Started") + + def stop_run_thread(self): + """ + Stops the validator's operations that are running in the background thread. + """ + if self.is_running: + bt.logging.debug("Stopping validator in background thread.") + self.should_exit = True + self.thread.join(5) + self.is_running = False + bt.logging.debug("Stopped") + + def __enter__(self): + self.run_in_background_thread() + return self + + def __exit__(self, exc_type, exc_value, traceback): + """ + Stops the validator's background operations upon exiting the context. + This method facilitates the use of the validator in a 'with' statement. + + Args: + exc_type: The type of the exception that caused the context to be exited. + None if the context was exited without an exception. + exc_value: The instance of the exception that caused the context to be exited. + None if the context was exited without an exception. + traceback: A traceback object encoding the stack trace. + None if the context was exited without an exception. + """ + if self.is_running: + bt.logging.debug("Stopping validator in background thread.") + self.should_exit = True + self.thread.join(5) + self.is_running = False + bt.logging.debug("Stopped") + + def set_weights(self): + """ + Sets the validator weights to the metagraph hotkeys based on the scores it has received from the miners. The weights determine the trust and incentive level the validator assigns to miner nodes on the network. + """ + bt.logging.debug(f"set_weights()") + if self.config.subtensor.network == "test": + return # Don't set weights on testnet. + + self.divisions = int(np.floor(self.block / 1000)) + current_odds = (0.2 * self.scores) + (0.8 * self.offline_scores[self.previous_competition_version]) + + # Check if self.scores contains any NaN values and log a warning if it does. + if np.isnan(self.scores).any(): + bt.logging.warning( + f"Scores contain NaN values. This may be due to a lack of responses from miners, or a bug in your reward functions." + ) + # correct validator scores to be 0 + for uid, hotkey in enumerate(self.hotkeys): + if not check_uid_availability(self.metagraph, uid, self.config.neuron.vpermit_tao_limit): + # if validator, set validators scores to 0 + self.scores[uid] = 0 + self.offline_scores[self.previous_competition_version][uid] = 0 + self.offline_scores[self.competition_version][uid] = 0 + self.offline_miners_scored[self.competition_version][self.regrade_version].append(uid) + self.offline_model_names[self.competition_version][uid] = "" + + # always fit scores to weighted curve + weighted_scores = score_spreading(current_odds,self.divisions,self.min_div,self.max_div) + + #bt.logging.info(f"weighted_scores: {weighted_scores}") + + # Calculate the average reward for each uid across non-zero values. + # Replace any NaN values with 0. + norm = np.linalg.norm(weighted_scores, ord=1, axis=0, keepdims=True) + if np.any(norm == 0) or np.isnan(norm).any(): + norm = np.ones_like(norm) + raw_weights = weighted_scores/norm + + # bt.logging.debug("raw_weights: ") + # bt.logging.debug(raw_weights) + # bt.logging.debug("raw_weight_uids: ") + # bt.logging.debug(self.metagraph.uids) + + # Process the raw weights to final_weights via subtensor limitations. + ( + processed_weight_uids, + processed_weights, + ) = process_weights_for_netuid( + uids=self.metagraph.uids, + weights=raw_weights, + netuid=self.config.netuid, + subtensor=self.subtensor, + metagraph=self.metagraph, + ) + # bt.logging.debug("processed_weights: ") + # bt.logging.debug(processed_weights) + # bt.logging.debug("processed_weight_uids: ") + # bt.logging.debug(processed_weight_uids) + + # Convert to uint16 weights and uids. + ( + uint_uids, + uint_weights, + ) = convert_weights_and_uids_for_emit( + uids=processed_weight_uids, weights=processed_weights + ) + + # Set the weights on chain via our subtensor connection. + + result, msg = self.subtensor.set_weights( + wallet=self.wallet, + netuid=self.config.netuid, + uids=uint_uids, + weights=uint_weights, + wait_for_finalization=False, + wait_for_inclusion=False, + version_key=self.spec_version, + ) + if result is True: + bt.logging.info(f"set_weights on chain for version: {self.spec_version} successfully!") + else: + bt.logging.error(f"set_weights failed: {msg}") + + def get_weighted_scores(self): + # scores are largely based on PREVIOUS competition scores + scaled_scores = ((0.2 * self.scores) + (0.8 * self.offline_scores[self.previous_competition_version])) * 5 + exp_scores = np.exp(scaled_scores) + return exp_scores / np.sum(exp_scores) + + def resync_metagraph(self): + """Resyncs the metagraph and updates the hotkeys and moving averages based on the new metagraph.""" + bt.logging.info("resync_metagraph()") + + # Copies state of metagraph before syncing. + previous_metagraph = copy.deepcopy(self.metagraph) + + # Sync the metagraph. + try: + self.metagraph.sync(subtensor=self.subtensor) + self.last_block_sync = self.block + + # Check if the metagraph axon info has changed. + if previous_metagraph.axons == self.metagraph.axons: + bt.logging.debug("Metagraph axons are the same, skipping resync") + return + + bt.logging.info("Metagraph updated, resyncing hotkeys, dendrite pool and moving averages") + # Normalize all hotkeys that have been replaced, and zero out all hotkeys that are no longer available + for uid, hotkey in enumerate(self.hotkeys): + if hotkey != self.metagraph.hotkeys[uid]: + bt.logging.debug(f"RESYNC: hotkey changed for uid: {uid}") + self.scores[uid] = np.median(self.scores) + self.offline_scores[self.previous_competition_version][uid] = 0 + self.offline_scores[self.competition_version][uid] = 0 + if uid in self.offline_miners_scored[self.competition_version][self.regrade_version]: + self.offline_miners_scored[self.competition_version][self.regrade_version].remove(uid) + self.offline_model_names[self.competition_version][uid] = "" + self.offline_model_names[self.previous_competition_version][uid] = "" + + # Check to see if the metagraph has changed size. + # If so, we need to add new hotkeys and moving averages. + if len(self.hotkeys) < len(self.metagraph.hotkeys): + # Update the size of the moving average scores. + new_moving_average = np.zeros((self.metagraph.n)) + min_len = min(len(self.hotkeys), len(self.scores)) + new_moving_average[:min_len] = self.scores[:min_len] + self.scores = new_moving_average + + # previous offline scores + new_moving_average = np.zeros((self.metagraph.n)) + min_len = min(len(self.hotkeys), len(self.offline_scores[self.previous_competition_version])) + new_moving_average[:min_len] = self.offline_scores[self.previous_competition_version][:min_len] + self.offline_scores[self.previous_competition_version] = new_moving_average + + # current offline scores + new_moving_average = np.zeros((self.metagraph.n)) + min_len = min(len(self.hotkeys), len(self.offline_scores[self.competition_version])) + new_moving_average[:min_len] = self.offline_scores[self.competition_version][:min_len] + self.offline_scores[self.competition_version] = new_moving_average + + # Update the hotkeys. + self.hotkeys = copy.deepcopy(self.metagraph.hotkeys) + except Exception as e: + bt.logging.error(f"Could not resync with metagraph right now, will try later. Error: {e}") + + def update_offline_scores(self, rewards: np.ndarray, uids: List[int]): + """Performs exponential moving average on the scores based on the rewards received from the miners.""" + if np.isnan(rewards).any(): + #bt.logging.debug(f"NaN values detected in rewards: {rewards}") + # Replace any NaN values in rewards with 0. + rewards = np.nan_to_num(rewards, nan=0) + + if isinstance(uids, np.ndarray): + uids_array = uids.copy() + else: + uids_array = np.array(uids) + + scattered_rewards: np.ndarray = self.offline_scores[self.competition_version].copy() + scattered_rewards[uids_array] = rewards + + bt.logging.debug(f"OFFLINE Scattered rewards: {rewards}") + + self.offline_scores[self.competition_version]: np.ndarray = scattered_rewards # type: ignore + self.offline_miners_scored[self.competition_version][self.regrade_version].extend([int(x) for x in uids_array]) + bt.logging.debug(f"Updated moving avg OFFLINE scores for Competition {self.competition_version}: {self.offline_scores[self.competition_version]}") + self.save_state() + + def update_scores(self, rewards: np.ndarray, uids: List[int], alpha=None): + """Performs exponential moving average on the scores based on the rewards received from the miners.""" + if np.isnan(rewards).any(): + #bt.logging.debug(f"NaN values detected in rewards: {rewards}") + # Replace any NaN values in rewards with 0. + rewards = np.nan_to_num(rewards, nan=0) + + if isinstance(uids, np.ndarray): + uids_array = uids.copy() + else: + uids_array = np.array(uids) + + scattered_rewards: np.ndarray = self.scores.copy() + scattered_rewards[uids_array] = rewards + + bt.logging.debug(f"ONLINE Scattered rewards: {rewards}") + + # Update scores with rewards produced by this step. + # shape: [ metagraph.n ] + if not alpha: + alpha: float = self.config.neuron.moving_average_alpha + self.scores: np.ndarray = alpha * scattered_rewards + ( 1 - alpha) * self.scores + bt.logging.debug(f"Updated moving avg ONLINE scores: {self.scores}") + + def save_state(self): + """Saves the state of the validator to a file.""" + bt.logging.debug(f"Saving validator state - {self.state_file_name}.") + + # Save the state of the validator to file. + try: + np.savez( + self.config.neuron.full_path + f"/{self.state_file_name}", + step=self.step, + scores=self.scores, + offline_scores=self.offline_scores, + offline_miners_scored=np.array(list(self.offline_miners_scored.items()), dtype=object), + offline_model_names=self.offline_model_names, + hotkeys=self.hotkeys, + allow_pickle=True, + ) + except Exception as e: + bt.logging.error(f"OFFLINE: Error saving validator state: {e}") + + def load_state(self): + """Loads the state of the validator from a file.""" + bt.logging.info("Loading validator state.") + state = np.load(self.config.neuron.full_path + f"/{self.state_file_name}",allow_pickle=True) + bt.logging.debug(f"OFFLINE: LOADING STATE: {state}") + + self.step = state["step"] + if 'hotkeys' in state: + self.hotkeys = state["hotkeys"] + + if 'scores' in state: + loaded_scores = state["scores"] + self.scores[:len(loaded_scores)] = loaded_scores + + if 'offline_scores' in state: + loaded_offline_scores = state["offline_scores"] + if isinstance(loaded_offline_scores, dict): + self.offline_scores = loaded_offline_scores + elif isinstance(loaded_offline_scores, np.ndarray): + self.offline_scores = loaded_offline_scores.item() + else: + bt.logging.error(f"OFFLINE: loaded_offline_scores is not a dict or array, type: {type(loaded_offline_scores)}") + + if self.offline_scores.get(self.previous_competition_version) is None: + self.offline_scores[self.previous_competition_version] = np.zeros(self.metagraph.n, dtype=np.float32) + #for uid in self.metagraph.uids: + # if uid not in self.offline_scores[self.previous_competition_version]: + # self.offline_scores[self.previous_competition_version][uid] = 0 + if 'offline_miners_scored' in state: + loaded_offline_miners_scored = state["offline_miners_scored"] + self.offline_miners_scored = dict(loaded_offline_miners_scored) + + if 'offline_model_names' in state: + loaded_offline_model_names = state["offline_model_names"] + if isinstance(loaded_offline_model_names, dict): + self.offline_model_names = loaded_offline_model_names + elif isinstance(loaded_offline_model_names, np.ndarray): + self.offline_model_names = loaded_offline_model_names.item() + else: + bt.logging.error(f"OFFLINE: loaded_offline_model_names is not a dict or array, type: {type(loaded_offline_model_names)}") + + def update_competition_numbers(self): + try: + # get competition details + competition_start_date = datetime.strptime(DEPLOYED_DATE, "%Y-%m-%d").replace(tzinfo=timezone.utc) + delta = datetime.now(timezone.utc) - competition_start_date + number_of_days_since_start = delta.days + (delta.seconds / (24*3600)) + number_of_competitions_since_start = int(number_of_days_since_start / COMPETITION_LENGTH_DAYS) + if self.config.subtensor.network == "test": + bt.logging.debug(f"OFFLINE TESTNET: using {TESTNET_COMPETITION_LENGTH_DAYS} days per competition") + number_of_competitions_since_start = int(number_of_days_since_start / TESTNET_COMPETITION_LENGTH_DAYS) + + #bt.logging.debug(f"OFFLINE: number_of_competitions_since_start: {number_of_competitions_since_start}") + + if number_of_competitions_since_start < 1: + # we have not completed any competitions with this prefix, so the previous competition number is the last one we completed with the old prefix + largest_previous_competition_number = 0 + # search through all the previous competition numbers to find the largest (most recent) one + for k,_ in self.offline_scores.items(): + if k.startswith(f"{COMPETITION_PREVIOUS_PREFIX}-"): + if int(k.split("-")[1]) > largest_previous_competition_number: + largest_previous_competition_number = int(k.split("-")[1]) + self.previous_competition_version = f"{COMPETITION_PREVIOUS_PREFIX}-{largest_previous_competition_number}" + else: + # we have completed at least one competition with this prefix, so the previous competition number is the last one we completed + self.previous_competition_version = f"{COMPETITION_PREFIX}-{int(number_of_competitions_since_start-1)}" + + if self.offline_scores.get(self.previous_competition_version) is None: + self.offline_scores[self.previous_competition_version] = np.zeros(self.metagraph.n, dtype=np.float32) + + self.competition_version = f"{COMPETITION_PREFIX}-{int(number_of_competitions_since_start)}" + + if self.offline_scores.get(self.competition_version) is None: + self.offline_scores[self.competition_version] = np.zeros(self.metagraph.n, dtype=np.float32) + + # SETUP OFFLINE MINERS SCORED + if self.offline_miners_scored.get(self.competition_version) is None: + self.offline_miners_scored[self.competition_version] = {} + + if not isinstance(self.offline_miners_scored[self.competition_version], dict): + self.offline_miners_scored[self.competition_version] = {} + + if self.offline_miners_scored[self.competition_version].get(self.regrade_version) is None: + self.offline_miners_scored[self.competition_version][self.regrade_version] = [] + + # SETUP OFFLINE MODEL NAMES + if self.offline_model_names.get(self.competition_version) is None: + self.offline_model_names[self.competition_version] = {} + + self.miners_left_to_score = [] + + # if an offline_score is 0 (we should try again), we need to add the miner to the list of miners left to score + # so clear out the offline_miners_scored for this competition, for those miners + for uid in self.offline_miners_scored[self.competition_version][self.regrade_version]: + if self.offline_scores[self.competition_version][uid] <= 0.01: # little wiggle room + #bt.logging.debug(f"OFFLINE: removing miner {uid} from offline_miners_scored for competition {self.competition_version} because score is less than 0.01") + self.offline_miners_scored[self.competition_version][self.regrade_version].remove(uid) + + # add all miners that are alive and not already scored to the list of miners left to score + for uid in get_alive_uids(self): + if uid not in [int(x) for x in self.offline_miners_scored[self.competition_version][self.regrade_version]]: + self.miners_left_to_score.append(int(uid)) + + # if a regrade has been set for the comp, then reset the scores for the miners + #bt.logging.debug(f"OFFLINE: regrade version: {self.regrade_version}") + #bt.logging.debug(f"OFFLINE: regrade check - offline miners scored: {self.offline_miners_scored[self.competition_version][self.regrade_version]}") + #bt.logging.debug(f"OFFLINE: regrade check - offline scores: {self.offline_scores[self.competition_version]}") + for uid,score in enumerate(self.offline_scores[self.competition_version]): + #bt.logging.debug(f"OFFLINE: regrade check for uid: {uid}") + if score > 0.0 and uid not in [int(x) for x in self.offline_miners_scored[self.competition_version][self.regrade_version]]: + #bt.logging.debug(f"OFFLINE: resetting miner {uid}'s score for competition {self.competition_version} for regrade") + self.offline_scores[self.competition_version][uid] = 0.0 + #bt.logging.debug(f"OFFLINE: regrade check for uid done: {uid}") + + # if number of keys in offline_scores is greater than 5, we need to delete the oldest one + # if len(self.offline_scores.keys()) > 6: + # oldest_key = list(self.offline_scores.keys())[0] + # del self.offline_scores[oldest_key] + # del self.offline_miners_scored[oldest_key] + # del self.offline_model_names[oldest_key] + except Exception as e: + bt.logging.error(f"Error updating competition numbers: {e}") diff --git a/bitagent_subnet-main/common/utils/__init__.py b/bitagent_subnet-main/common/utils/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..abbc05ed7f95e76d9c50fbb1b05c33b9cbcefd47 --- /dev/null +++ b/bitagent_subnet-main/common/utils/__init__.py @@ -0,0 +1,4 @@ +from . import config +from . import misc +from . import uids +from . import shell \ No newline at end of file diff --git a/bitagent_subnet-main/common/utils/config.py b/bitagent_subnet-main/common/utils/config.py new file mode 100644 index 0000000000000000000000000000000000000000..8bb00a6ce6969e3d55d7fad97682474111d8a9ae --- /dev/null +++ b/bitagent_subnet-main/common/utils/config.py @@ -0,0 +1,284 @@ +# The MIT License (MIT) +# Copyright © 2023 Yuma Rao +# Copyright © 2023 Opentensor Foundation + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import os +import argparse +import subprocess +import bittensor as bt + +def is_cuda_available(): + try: + output = subprocess.check_output(["nvidia-smi", "-L"], stderr=subprocess.STDOUT) + if "NVIDIA" in output.decode("utf-8"): + return "cuda" + except Exception: + pass + try: + output = subprocess.check_output(["nvcc", "--version"]).decode("utf-8") + if "release" in output: + return "cuda" + except Exception: + pass + return "cpu" + +def check_config(cls, config: "bt.Config"): + r"""Checks/validates the config namespace object.""" + bt.logging.check_config(config) + + full_path = os.path.expanduser( + "{}/{}/{}/netuid{}/{}".format( + config.logging.logging_dir, # TODO: change from ~/.bittensor/miners to ~/.bittensor/neurons + config.wallet.name, + config.wallet.hotkey, + config.netuid, + config.neuron.name, + ) + ) + print("full path:", full_path) + config.neuron.full_path = os.path.expanduser(full_path) + if not os.path.exists(config.neuron.full_path): + os.makedirs(config.neuron.full_path, exist_ok=True) + + #if not config.neuron.dont_save_events: + # # Add custom event logger for the events. + # logger.level("EVENTS", no=38, icon="📝") + # logger.add( + # os.path.join(config.neuron.full_path, "events.log"), + # rotation=config.neuron.events_retention_size, + # serialize=True, + # enqueue=True, + # backtrace=False, + # diagnose=False, + # level="EVENTS", + # format="{time:YYYY-MM-DD at HH:mm:ss} | {level} | {message}", + # ) + + +def add_args(cls, parser): + """ + Adds relevant arguments to the parser for operation. + """ + # Netuid Arg: The netuid of the subnet to connect to. + parser.add_argument("--netuid", type=int, help="Subnet netuid", default=1) + neuron_type = ( + "validator" if "miner" not in cls.__name__.lower() else "miner" + ) + + parser.add_argument( + "--openai-api-key", + type=str, + default="EMPTY", + help="the OpenAI API key defaults to EMPTY" + ) + parser.add_argument( + "--openai-api-base", + type=str, + default="http://localhost:8000/v1", + help="the OpenAI API base url - defaults to a local LLM server (like VLLM)", + ) + parser.add_argument( + "--neuron.name", + type=str, + help="Trials for this neuron go in neuron.root / (wallet_cold - wallet_hot) / neuron.name. ", + default=neuron_type, + ) + + parser.add_argument( + "--neuron.visible_devices", + type=str, + help="Comma separated list of visible cuda devices.", + default="", + ) + + parser.add_argument( + "--neuron.device", + type=str, + help="Device to run on.", + default=is_cuda_available(), + ) + + parser.add_argument( + "--neuron.epoch_length", + type=int, + help="The default epoch length (how often we set weights, measured in 12 second blocks).", + default=100, + ) + + parser.add_argument( + "--neuron.events_retention_size", + type=str, + help="Events retention size.", + default="2 GB", + ) + + parser.add_argument( + "--neuron.dont_save_events", + action="store_true", + help="If set, we dont save events to a log file.", + default=False, + ) + + parser.add_argument( + "--log_level", + type=str, + choices=["trace", "debug", "info"], # Add more levels if needed + help="Logging level to use", + default="info" + ) + + if neuron_type == "validator": + + parser.add_argument( + "--validator-hf-cache-dir", + type=str, + default="~/.cache/huggingface/hub", + help="the directory where the HF models are stored on your system - this is where we delete the models from after we're done serving them", + ) + parser.add_argument( + "--validator-hf-server-port", + type=int, + default=8028, + help="the port of the docker container to run the offline HF model check", + ) + parser.add_argument( + "--validator-hf-server-mem-fraction-static", + type=float, + default=0.40, + help="the fraction of the GPU memory to use for the HF server", + ) + + parser.add_argument( + "--validator-model-name", + type=str, + default="thesven/Mistral-7B-Instruct-v0.3-GPTQ", + help="the OpenAI model name defaults to thesven/Mistral-7B-Instruct-v0.3-GPTQ, the model that the validator uses to rewrite user queries", + ) + + parser.add_argument( + "--neuron.num_concurrent_forwards", + type=int, + help="The number of concurrent forwards running at any time.", + default=1, + ) + + parser.add_argument( + "--neuron.sample_size", + type=int, + help="The number of miners to query in a single step.", + default=10 + ) + + parser.add_argument( + "--neuron.disable_set_weights", + action="store_true", + help="Disables setting weights.", + default=False, + ) + + parser.add_argument( + "--neuron.moving_average_alpha", + type=float, + help="Moving average alpha parameter, how much to add of the new observation.", + default=0.05, + ) + + parser.add_argument( + "--wandb.on", + type=bool, + default=True, + help="Enable wandb logging.", + ) + + parser.add_argument( + "--neuron.axon_off", + "--axon_off", + action="store_true", + # Note: the validator needs to serve an Axon with their IP or they may + # be blacklisted by the firewall of serving peers on the network. + help="Set this flag to not attempt to serve an Axon.", + default=False, + ) + + parser.add_argument( + "--neuron.vpermit_tao_limit", + type=int, + help="The maximum number of TAO allowed to query a validator with a vpermit.", + default=4096, + ) + + else: + # grab the command line arguments to find the netuid and set the default accordingly + # for mainnet (SN20), we'll set to blacklist any validator without a validator permit + # for testnet (SN76), or any other netuid besides 20, we'll allow validators without a permit + # this is b/c our testnet validator does not have enough stake to have a "permit" + args, _ = parser.parse_known_args() + parser.add_argument( + "--blacklist.force_validator_permit", + action="store_true", + help="If set, we will force incoming requests to have a permit.", + default=(args.netuid==20), + ) + + parser.add_argument( + "--blacklist.allow_non_registered", + action="store_true", + help="If set, miners will accept queries from non registered entities. (Dangerous!)", + default=False, + ) + + parser.add_argument( + "--hf-model-name-to-run", + type=str, + default="Salesforce/xLAM-7b-r", + help="the OpenAI model name defaults to Salesforce/xLAM-7b-r" + ) + + parser.add_argument( + "--miner", + type=str, + default="default", + help="Miner to load. Default choices are 'default' and 'mock'. Pass your custom miner name as appropriate." + ) + + parser.add_argument( + "--miner-hf-model-name-to-submit", + type=str, + default="Salesforce/xLAM-7b-r", + help="the HF model name that you've uploaded to the HF hub to be evaluated, will be returned when validator asks for your model to evaluate." + ) + +def config(cls): + """ + Returns the configuration object specific to this miner or validator after adding relevant arguments. + """ + parser = argparse.ArgumentParser() + bt.wallet.add_args(parser) + bt.subtensor.add_args(parser) + bt.logging.add_args(parser) + bt.axon.add_args(parser) + cls.add_args(parser) + args = parser.parse_args() + + # Conditional logging based on the argument + logging_level = args.log_level + if logging_level == "trace": + bt.trace() + elif logging_level == "debug": + bt.debug() + + return bt.config(parser) diff --git a/bitagent_subnet-main/common/utils/misc.py b/bitagent_subnet-main/common/utils/misc.py new file mode 100644 index 0000000000000000000000000000000000000000..80b4e6142580e93704acb1b29667cd656bafd3ea --- /dev/null +++ b/bitagent_subnet-main/common/utils/misc.py @@ -0,0 +1,112 @@ +# The MIT License (MIT) +# Copyright © 2023 Yuma Rao +# Copyright © 2023 Opentensor Foundation + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import time +import math +import hashlib as rpccheckhealth +from math import floor +from typing import Callable, Any +from functools import lru_cache, update_wrapper + + +# LRU Cache with TTL +def ttl_cache(maxsize: int = 128, typed: bool = False, ttl: int = -1): + """ + Decorator that creates a cache of the most recently used function calls with a time-to-live (TTL) feature. + The cache evicts the least recently used entries if the cache exceeds the `maxsize` or if an entry has + been in the cache longer than the `ttl` period. + + Args: + maxsize (int): Maximum size of the cache. Once the cache grows to this size, subsequent entries + replace the least recently used ones. Defaults to 128. + typed (bool): If set to True, arguments of different types will be cached separately. For example, + f(3) and f(3.0) will be treated as distinct calls with distinct results. Defaults to False. + ttl (int): The time-to-live for each cache entry, measured in seconds. If set to a non-positive value, + the TTL is set to a very large number, effectively making the cache entries permanent. Defaults to -1. + + Returns: + Callable: A decorator that can be applied to functions to cache their return values. + + The decorator is useful for caching results of functions that are expensive to compute and are called + with the same arguments frequently within short periods of time. The TTL feature helps in ensuring + that the cached values are not stale. + + Example: + @ttl_cache(ttl=10) + def get_data(param): + # Expensive data retrieval operation + return data + """ + if ttl <= 0: + ttl = 65536 + hash_gen = _ttl_hash_gen(ttl) + + def wrapper(func: Callable) -> Callable: + @lru_cache(maxsize, typed) + def ttl_func(ttl_hash, *args, **kwargs): + return func(*args, **kwargs) + + def wrapped(*args, **kwargs) -> Any: + th = next(hash_gen) + return ttl_func(th, *args, **kwargs) + + return update_wrapper(wrapped, func) + + return wrapper + + +def _ttl_hash_gen(seconds: int): + """ + Internal generator function used by the `ttl_cache` decorator to generate a new hash value at regular + time intervals specified by `seconds`. + + Args: + seconds (int): The number of seconds after which a new hash value will be generated. + + Yields: + int: A hash value that represents the current time interval. + + This generator is used to create time-based hash values that enable the `ttl_cache` to determine + whether cached entries are still valid or if they have expired and should be recalculated. + """ + start_time = time.time() + while True: + yield floor((time.time() - start_time) / seconds) + + +# 12 seconds updating block. +@ttl_cache(maxsize=1, ttl=12) +def ttl_get_block(self) -> int: + """ + Retrieves the current block number from the blockchain. This method is cached with a time-to-live (TTL) + of 12 seconds, meaning that it will only refresh the block number from the blockchain at most every 12 seconds, + reducing the number of calls to the underlying blockchain interface. + + Returns: + int: The current block number on the blockchain. + + This method is useful for applications that need to access the current block number frequently and can + tolerate a delay of up to 12 seconds for the latest information. By using a cache with TTL, the method + efficiently reduces the workload on the blockchain interface. + + Example: + current_block = ttl_get_block(self) + + Note: self here is the miner or validator instance + """ + return self.subtensor.get_current_block() diff --git a/bitagent_subnet-main/common/utils/shell.py b/bitagent_subnet-main/common/utils/shell.py new file mode 100644 index 0000000000000000000000000000000000000000..af1275ee44f0228af5198e88b569312007595611 --- /dev/null +++ b/bitagent_subnet-main/common/utils/shell.py @@ -0,0 +1,47 @@ +import shlex +import subprocess +import bittensor as bt +from threading import Thread + +def execute_shell_command(command: str, model_name: str) -> subprocess.Popen: + """ + Execute a shell command and stream the output to the caller in real-time. + + Args: + command: Shell command as a string (can include \\ line continuations) + Returns: + subprocess.Popen: The process handle for further interaction. + """ + # Replace \ newline with space and split using shlex + command = command.replace("\\\n", " ").replace("\\", " ") + parts = shlex.split(command) # Handles quoted strings correct + + try: + # Run the process + process = subprocess.Popen( + parts, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE + ) + + def stream_output(stream, stream_name): + for line in iter(stream.readline, ''): + line = line.rstrip('\n') + if stream_name == "STDERR": + # log everything except for token generation metrics + if "#new-token" not in line and "Decode batch." not in line: + redacted_line = line.replace(model_name, "[REDACTED]") + bt.logging.debug(f"{stream_name}: {redacted_line}") + + # Uncomment this if you want STDOUT logging as well: + # else: + # print(f"{stream_name}: {line}") + + stream.close() + + # Stream both stdout and stderr + Thread(target=stream_output, args=(process.stdout, "STDOUT")).start() + Thread(target=stream_output, args=(process.stderr, "STDERR")).start() + + return process + except Exception as e: + print(f"Error executing command: {command}. Exception: {e}") + raise diff --git a/bitagent_subnet-main/common/utils/uids.py b/bitagent_subnet-main/common/utils/uids.py new file mode 100644 index 0000000000000000000000000000000000000000..ff433f870e247aa692749ac1cf72e43bec52968c --- /dev/null +++ b/bitagent_subnet-main/common/utils/uids.py @@ -0,0 +1,113 @@ +import random +import numpy as np +import bittensor as bt +from typing import List +from bitagent.protocol import IsAlive +from cachetools import cached, TTLCache + +def check_uid_availability( + metagraph: "bt.metagraph.Metagraph", uid: int, vpermit_tao_limit: int # type: ignore +) -> bool: + """Check if uid is available. The UID should be available if it is serving and has less than vpermit_tao_limit stake + Args: + metagraph (:obj: bt.metagraph.Metagraph): Metagraph object + uid (int): uid to be checked + vpermit_tao_limit (int): Validator permit tao limit + Returns: + bool: True if uid is available, False otherwise + """ + # Filter non serving axons. + #if not metagraph.axons[uid].is_serving: + # return False + # don't hit sn owner + if uid == 0: + return False + # Filter validator permit > 1024 stake. + if metagraph.validator_permit[uid]: + if metagraph.S[uid] > vpermit_tao_limit: + return False + # any miner receiving incentive should be queried + if metagraph.I[uid] > 0: + return True + # Available otherwise. + return True + +# Create a cache with a maximum size of 256 items and a TTL of 1 hour (3600 seconds) +cache = TTLCache(maxsize=256, ttl=3600) + +@cached(cache) +def get_alive_uids(self): + start = 0 + finish = start + 10 + results = [] + # query 10 at a time + while start < len(self.metagraph.axons): + result = self.dendrite.query( + axons=self.metagraph.axons[start:finish], synapse=IsAlive(), deserialize=False, timeout=5.0 + ) + results.extend(result) + start = finish + finish = start + 10 + if finish > len(self.metagraph.axons): + finish = len(self.metagraph.axons) + alive_uids = [uid for uid, response in zip(range(self.metagraph.n.item()), results) if response.response and response.dendrite.status_code == 200] + + # if not alive for querying, they won't get tasks for an hour, set their score to -0.5 + for uid in self.metagraph.uids: + if uid not in alive_uids: + self.offline_scores[self.competition_version][uid] = -0.5 + self.scores[uid] = -0.5 + #bt.logging.debug(f"Found {len(alive_uids)} alive UIDs, caching for 1 hour") + return alive_uids + +def get_random_uids( + self, k: int, exclude: List[int] = None +) -> np.ndarray: + """Returns k available random uids from the metagraph. + Args: + k (int): Number of uids to return. + exclude (List[int]): List of uids to exclude from the random sampling. + Returns: + uids (np.ndarray): Randomly sampled available uids. + Notes: + If `k` is larger than the number of available `uids`, set `k` to the number of available `uids`. + """ + candidate_uids = [] + avail_uids = [] + + for uid in get_alive_uids(self): + uid_is_available = check_uid_availability( + self.metagraph, uid, self.config.neuron.vpermit_tao_limit + ) + uid_is_not_excluded = exclude is None or uid not in exclude + + if uid_is_available: + avail_uids.append(uid) + if uid_is_not_excluded: + candidate_uids.append(uid) + + # Check if candidate_uids contain enough for querying, if not grab all avaliable uids + available_uids = candidate_uids + while True: + try: + if len(candidate_uids) < k: + available_uids += random.sample( + [uid for uid in avail_uids if uid not in candidate_uids], + k - len(candidate_uids), + ) + uids = random.sample(available_uids, k) + return uids + except Exception as e: + #bt.logging.debug(f"Reduced sample size from {k} to {k-1} and trying again.") + k -= 1 + +def get_uid_rank(self, uid: int) -> int: + """Returns the rank of the uid in the metagraph. + Args: + uid (int): uid to get the rank of. + Returns: + rank (int): Rank of the uid in the metagraph. + """ + # Get the rank of the uid in the metagraph. + rank = (-self.metagraph.I).argsort().tolist().index(uid) + return rank diff --git a/bitagent_subnet-main/common/utils/weight_utils.py b/bitagent_subnet-main/common/utils/weight_utils.py new file mode 100644 index 0000000000000000000000000000000000000000..52543f0fa2a2a9c761ffe7baeb05b3efe7f26a24 --- /dev/null +++ b/bitagent_subnet-main/common/utils/weight_utils.py @@ -0,0 +1,216 @@ +import numpy as np +from typing import Tuple, List, Union, Any +import bittensor +from numpy import ndarray, dtype, floating, complexfloating + +U32_MAX = 4294967295 +U16_MAX = 65535 + + +def normalize_max_weight( + x: np.ndarray, limit: float = 0.1 +) -> np.ndarray: + r"""Normalizes the numpy array x so that sum(x) = 1 and the max value is not greater than the limit. + Args: + x (:obj:`np.ndarray`): + Array to be max_value normalized. + limit: float: + Max value after normalization. + Returns: + y (:obj:`np.ndarray`): + Normalized x array. + """ + epsilon = 1e-7 # For numerical stability after normalization + + weights = x.copy() + values = np.sort(weights) + + if x.sum() == 0 or len(x) * limit <= 1: + return np.ones_like(x) / x.size + else: + estimation = values / values.sum() + + if estimation.max() <= limit: + return weights / weights.sum() + + # Find the cumulative sum and sorted array + cumsum = np.cumsum(estimation, 0) + + # Determine the index of cutoff + estimation_sum = np.array( + [(len(values) - i - 1) * estimation[i] for i in range(len(values))] + ) + n_values = (estimation / (estimation_sum + cumsum + epsilon) < limit).sum() + + # Determine the cutoff based on the index + cutoff_scale = (limit * cumsum[n_values - 1] - epsilon) / ( + 1 - (limit * (len(estimation) - n_values)) + ) + cutoff = cutoff_scale * values.sum() + + # Applying the cutoff + weights[weights > cutoff] = cutoff + + y = weights / weights.sum() + + return y + + +def convert_weights_and_uids_for_emit( + uids: np.ndarray, weights: np.ndarray +) -> Tuple[List[int], List[int]]: + r"""Converts weights into integer u32 representation that sum to MAX_INT_WEIGHT. + Args: + uids (:obj:`np.ndarray,`): + Array of uids as destinations for passed weights. + weights (:obj:`np.ndarray,`): + Array of weights. + Returns: + weight_uids (List[int]): + Uids as a list. + weight_vals (List[int]): + Weights as a list. + """ + # Checks. + uids = np.asarray(uids) + weights = np.asarray(weights) + + # Get non-zero weights and corresponding uids + non_zero_weights = weights[weights > 0] + non_zero_weight_uids = uids[weights > 0] + + # Debugging information + bittensor.logging.debug(f"weights: {weights}") + bittensor.logging.debug(f"non_zero_weights: {non_zero_weights}") + bittensor.logging.debug(f"uids: {uids}") + bittensor.logging.debug(f"non_zero_weight_uids: {non_zero_weight_uids}") + + if np.min(weights) < 0: + raise ValueError( + "Passed weight is negative cannot exist on chain {}".format(weights) + ) + if np.min(uids) < 0: + raise ValueError("Passed uid is negative cannot exist on chain {}".format(uids)) + if len(uids) != len(weights): + raise ValueError( + "Passed weights and uids must have the same length, got {} and {}".format( + len(uids), len(weights) + ) + ) + if np.sum(weights) == 0: + bittensor.logging.debug("nothing to set on chain") + return [], [] # Nothing to set on chain. + else: + max_weight = float(np.max(weights)) + weights = [ + float(value) / max_weight for value in weights + ] # max-upscale values (max_weight = 1). + bittensor.logging.debug(f"setting on chain max: {max_weight} and weights: {weights}") + + weight_vals = [] + weight_uids = [] + for i, (weight_i, uid_i) in enumerate(list(zip(weights, uids))): + uint16_val = round( + float(weight_i) * int(U16_MAX) + ) # convert to int representation. + + # Filter zeros + if uint16_val != 0: # Filter zeros + weight_vals.append(uint16_val) + weight_uids.append(uid_i) + bittensor.logging.debug(f"final params: {weight_uids} : {weight_vals}") + return weight_uids, weight_vals + + +def process_weights_for_netuid( + uids, + weights: np.ndarray, + netuid: int, + subtensor: "bittensor.subtensor", + metagraph: "bittensor.metagraph" = None, + exclude_quantile: int = 0, +) -> Union[tuple[ndarray[Any, dtype[Any]], Union[ + Union[ndarray[Any, dtype[floating[Any]]], ndarray[Any, dtype[complexfloating[Any, Any]]]], Any]], tuple[ + ndarray[Any, dtype[Any]], ndarray], tuple[Any, ndarray]]: + bittensor.logging.debug("process_weights_for_netuid()") + bittensor.logging.debug("weights: ") + bittensor.logging.debug(weights) + bittensor.logging.debug("netuid: ") + bittensor.logging.debug(netuid) + bittensor.logging.debug("subtensor: ") + bittensor.logging.debug(subtensor) + bittensor.logging.debug("metagraph: ") + bittensor.logging.debug(metagraph) + + # Get latest metagraph from chain if metagraph is None. + if metagraph is None: + metagraph = subtensor.metagraph(netuid) + + # Cast weights to floats. + if not isinstance(weights, np.ndarray) or weights.dtype != np.float32: + weights = weights.astype(np.float32) + + # Network configuration parameters from an subtensor. + # These parameters determine the range of acceptable weights for each neuron. + quantile = exclude_quantile / U16_MAX + min_allowed_weights = subtensor.min_allowed_weights(netuid=netuid) + max_weight_limit = subtensor.max_weight_limit(netuid=netuid) + bittensor.logging.debug("quantile", quantile) + bittensor.logging.debug("min_allowed_weights", min_allowed_weights) + bittensor.logging.debug("max_weight_limit", max_weight_limit) + + # Find all non zero weights. + non_zero_weight_idx = np.argwhere(weights > 0).squeeze() + non_zero_weight_uids = uids[non_zero_weight_idx] + non_zero_weights = weights[non_zero_weight_idx] + if non_zero_weights.size == 0 or metagraph.n < min_allowed_weights: + bittensor.logging.warning("No non-zero weights returning all ones.") + final_weights = np.ones(metagraph.n) / metagraph.n + bittensor.logging.debug("final_weights", final_weights) + return np.arange(len(final_weights)), final_weights + + elif non_zero_weights.size < min_allowed_weights: + bittensor.logging.warning( + "No non-zero weights less then min allowed weight, returning all ones." + ) + weights = ( + np.ones(metagraph.n) * 1e-5 + ) # creating minimum even non-zero weights + weights[non_zero_weight_idx] += non_zero_weights + bittensor.logging.debug("final_weights", weights) + normalized_weights = normalize_max_weight( + x=weights, limit=max_weight_limit + ) + return np.arange(len(normalized_weights)), normalized_weights + + bittensor.logging.debug("non_zero_weights: ") + bittensor.logging.debug(non_zero_weights) + + # Compute the exclude quantile and find the weights in the lowest quantile + max_exclude = max(0, len(non_zero_weights) - min_allowed_weights) / len( + non_zero_weights + ) + exclude_quantile = min([quantile, max_exclude]) + lowest_quantile = np.quantile(non_zero_weights, exclude_quantile) + bittensor.logging.debug("max_exclude", max_exclude) + bittensor.logging.debug("exclude_quantile: ") + bittensor.logging.debug(exclude_quantile) + bittensor.logging.debug("lowest_quantile: ") + bittensor.logging.debug(lowest_quantile) + + # Exclude all weights below the allowed quantile. + non_zero_weight_uids = non_zero_weight_uids[lowest_quantile <= non_zero_weights] + non_zero_weights = non_zero_weights[lowest_quantile <= non_zero_weights] + bittensor.logging.debug("non_zero_weight_uids: ") + bittensor.logging.debug(non_zero_weight_uids) + bittensor.logging.debug("non_zero_weights: ") + bittensor.logging.debug(non_zero_weights) + + # Normalize weights and return. + normalized_weights = normalize_max_weight( + x=non_zero_weights, limit=max_weight_limit + ) + bittensor.logging.debug("final_weights: ") + bittensor.logging.debug(normalized_weights) + + return non_zero_weight_uids, normalized_weights \ No newline at end of file diff --git a/bitagent_subnet-main/contrib/CODE_REVIEW_DOCS.md b/bitagent_subnet-main/contrib/CODE_REVIEW_DOCS.md new file mode 100644 index 0000000000000000000000000000000000000000..9909606a8995f6702a688ae3f9bfc53fd9b4466c --- /dev/null +++ b/bitagent_subnet-main/contrib/CODE_REVIEW_DOCS.md @@ -0,0 +1,72 @@ +# Code Review +### Conceptual Review + +A review can be a conceptual review, where the reviewer leaves a comment + * `Concept (N)ACK`, meaning "I do (not) agree with the general goal of this pull + request", + * `Approach (N)ACK`, meaning `Concept ACK`, but "I do (not) agree with the + approach of this change". + +A `NACK` needs to include a rationale why the change is not worthwhile. +NACKs without accompanying reasoning may be disregarded. +After conceptual agreement on the change, code review can be provided. A review +begins with `ACK BRANCH_COMMIT`, where `BRANCH_COMMIT` is the top of the PR +branch, followed by a description of how the reviewer did the review. The +following language is used within pull request comments: + + - "I have tested the code", involving change-specific manual testing in + addition to running the unit, functional, or fuzz tests, and in case it is + not obvious how the manual testing was done, it should be described; + - "I have not tested the code, but I have reviewed it and it looks + OK, I agree it can be merged"; + - A "nit" refers to a trivial, often non-blocking issue. + +### Code Review +Project maintainers reserve the right to weigh the opinions of peer reviewers +using common sense judgement and may also weigh based on merit. Reviewers that +have demonstrated a deeper commitment and understanding of the project over time +or who have clear domain expertise may naturally have more weight, as one would +expect in all walks of life. + +Where a patch set affects consensus-critical code, the bar will be much +higher in terms of discussion and peer review requirements, keeping in mind that +mistakes could be very costly to the wider community. This includes refactoring +of consensus-critical code. + +Where a patch set proposes to change the Bittensor consensus, it must have been +discussed extensively on the discord server and other channels, be accompanied by a widely +discussed BIP and have a generally widely perceived technical consensus of being +a worthwhile change based on the judgement of the maintainers. + +### Finding Reviewers + +As most reviewers are themselves developers with their own projects, the review +process can be quite lengthy, and some amount of patience is required. If you find +that you've been waiting for a pull request to be given attention for several +months, there may be a number of reasons for this, some of which you can do something +about: + + - It may be because of a feature freeze due to an upcoming release. During this time, + only bug fixes are taken into consideration. If your pull request is a new feature, + it will not be prioritized until after the release. Wait for the release. + - It may be because the changes you are suggesting do not appeal to people. Rather than + nits and critique, which require effort and means they care enough to spend time on your + contribution, thundering silence is a good sign of widespread (mild) dislike of a given change + (because people don't assume *others* won't actually like the proposal). Don't take + that personally, though! Instead, take another critical look at what you are suggesting + and see if it: changes too much, is too broad, doesn't adhere to the + [developer notes](DEVELOPMENT_WORKFLOW.md), is dangerous or insecure, is messily written, etc. + Identify and address any of the issues you find. Then ask e.g. on IRC if someone could give + their opinion on the concept itself. + - It may be because your code is too complex for all but a few people, and those people + may not have realized your pull request even exists. A great way to find people who + are qualified and care about the code you are touching is the + [Git Blame feature](https://docs.github.com/en/github/managing-files-in-a-repository/managing-files-on-github/tracking-changes-in-a-file). Simply + look up who last modified the code you are changing and see if you can find + them and give them a nudge. Don't be incessant about the nudging, though. + - Finally, if all else fails, ask on IRC or elsewhere for someone to give your pull request + a look. If you think you've been waiting for an unreasonably long time (say, + more than a month) for no particular reason (a few lines changed, etc.), + this is totally fine. Try to return the favor when someone else is asking + for feedback on their code, and the universe balances out. + - Remember that the best thing you can do while waiting is give review to others! \ No newline at end of file diff --git a/bitagent_subnet-main/contrib/CONTRIBUTING.md b/bitagent_subnet-main/contrib/CONTRIBUTING.md new file mode 100644 index 0000000000000000000000000000000000000000..ba33ce3c98af8c1df583bd073540fed75d9361e6 --- /dev/null +++ b/bitagent_subnet-main/contrib/CONTRIBUTING.md @@ -0,0 +1,213 @@ +# Contributing to Bittensor Subnet Development + +The following is a set of guidelines for contributing to the Bittensor ecosystem. These are **HIGHLY RECOMMENDED** guidelines, but not hard-and-fast rules. Use your best judgment, and feel free to propose changes to this document in a pull request. + +## Table Of Contents +1. [How Can I Contribute?](#how-can-i-contribute) + 1. [Communication Channels](#communication-channels) + 1. [Code Contribution General Guideline](#code-contribution-general-guidelines) + 1. [Pull Request Philosophy](#pull-request-philosophy) + 1. [Pull Request Process](#pull-request-process) + 1. [Addressing Feedback](#addressing-feedback) + 1. [Squashing Commits](#squashing-commits) + 1. [Refactoring](#refactoring) + 1. [Peer Review](#peer-review) + 1. [Suggesting Features](#suggesting-enhancements-and-features) + + +## How Can I Contribute? +TODO(developer): Define your desired contribution procedure. + +## Communication Channels +TODO(developer): Place your communication channels here + +> Please follow the Bittensor Subnet [style guide](./STYLE.md) regardless of your contribution type. + +Here is a high-level summary: +- Code consistency is crucial; adhere to established programming language conventions. +- Use `black` to format your Python code; it ensures readability and consistency. +- Write concise Git commit messages; summarize changes in ~50 characters. +- Follow these six commit rules: + - Atomic Commits: Focus on one task or fix per commit. + - Subject and Body Separation: Use a blank line to separate the subject from the body. + - Subject Line Length: Keep it under 50 characters for readability. + - Imperative Mood: Write subject line as if giving a command or instruction. + - Body Text Width: Wrap text manually at 72 characters. + - Body Content: Explain what changed and why, not how. +- Make use of your commit messages to simplify project understanding and maintenance. + +> For clear examples of each of the commit rules, see the style guide's [rules](./STYLE.md#the-six-rules-of-a-great-commit) section. + +### Code Contribution General Guidelines + +> Review the Bittensor Subnet [style guide](./STYLE.md) and [development workflow](./DEVELOPMENT_WORKFLOW.md) before contributing. + + +#### Pull Request Philosophy + +Patchsets and enhancements should always be focused. A pull request could add a feature, fix a bug, or refactor code, but it should not contain a mixture of these. Please also avoid 'super' pull requests which attempt to do too much, are overly large, or overly complex as this makes review difficult. + +Specifically, pull requests must adhere to the following criteria: +- Contain fewer than 50 files. PRs with more than 50 files will be closed. +- If a PR introduces a new feature, it *must* include corresponding tests. +- Other PRs (bug fixes, refactoring, etc.) should ideally also have tests, as they provide proof of concept and prevent regression. +- Categorize your PR properly by using GitHub labels. This aids in the review process by informing reviewers about the type of change at a glance. +- Make sure your code includes adequate comments. These should explain why certain decisions were made and how your changes work. +- If your changes are extensive, consider breaking your PR into smaller, related PRs. This makes your contributions easier to understand and review. +- Be active in the discussion about your PR. Respond promptly to comments and questions to help reviewers understand your changes and speed up the acceptance process. + +Generally, all pull requests must: + + - Have a clear use case, fix a demonstrable bug or serve the greater good of the project (e.g. refactoring for modularisation). + - Be well peer-reviewed. + - Follow code style guidelines. + - Not break the existing test suite. + - Where bugs are fixed, where possible, there should be unit tests demonstrating the bug and also proving the fix. + - Change relevant comments and documentation when behaviour of code changes. + +#### Pull Request Process + +Please follow these steps to have your contribution considered by the maintainers: + +*Before* creating the PR: +1. Read the [development workflow](./DEVELOPMENT_WORKFLOW.md) defined for this repository to understand our workflow. +2. Ensure your PR meets the criteria stated in the 'Pull Request Philosophy' section. +3. Include relevant tests for any fixed bugs or new features as stated in the [testing guide](./TESTING.md). +4. Ensure your commit messages are clear and concise. Include the issue number if applicable. +5. If you have multiple commits, rebase them into a single commit using `git rebase -i`. +6. Explain what your changes do and why you think they should be merged in the PR description consistent with the [style guide](./STYLE.md). + +*After* creating the PR: +1. Verify that all [status checks](https://help.github.com/articles/about-status-checks/) are passing after you submit your pull request. +2. Label your PR using GitHub's labeling feature. The labels help categorize the PR and streamline the review process. +3. Document your code with comments that provide a clear understanding of your changes. Explain any non-obvious parts of your code or design decisions you've made. +4. If your PR has extensive changes, consider splitting it into smaller, related PRs. This reduces the cognitive load on the reviewers and speeds up the review process. + +Please be responsive and participate in the discussion on your PR! This aids in clarifying any confusion or concerns and leads to quicker resolution and merging of your PR. + +> Note: If your changes are not ready for merge but you want feedback, create a draft pull request. + +Following these criteria will aid in quicker review and potential merging of your PR. +While the prerequisites above must be satisfied prior to having your pull request reviewed, the reviewer(s) may ask you to complete additional design work, tests, or other changes before your pull request can be ultimately accepted. + +When you are ready to submit your changes, create a pull request: + +> **Always** follow the [style guide](./STYLE.md) and [development workflow](./DEVELOPMENT_WORKFLOW.md) before submitting pull requests. + +After you submit a pull request, it will be reviewed by the maintainers. They may ask you to make changes. Please respond to any comments and push your changes as a new commit. + +> Note: Be sure to merge the latest from "upstream" before making a pull request: + +```bash +git remote add upstream https://github.com/opentensor/bittensor.git # TODO(developer): replace with your repo URL +git fetch upstream +git merge upstream/ +git push origin +``` + +#### Addressing Feedback + +After submitting your pull request, expect comments and reviews from other contributors. You can add more commits to your pull request by committing them locally and pushing to your fork. + +You are expected to reply to any review comments before your pull request is merged. You may update the code or reject the feedback if you do not agree with it, but you should express so in a reply. If there is outstanding feedback and you are not actively working on it, your pull request may be closed. + +#### Squashing Commits + +If your pull request contains fixup commits (commits that change the same line of code repeatedly) or too fine-grained commits, you may be asked to [squash](https://git-scm.com/docs/git-rebase#_interactive_mode) your commits before it will be reviewed. The basic squashing workflow is shown below. + + git checkout your_branch_name + git rebase -i HEAD~n + # n is normally the number of commits in the pull request. + # Set commits (except the one in the first line) from 'pick' to 'squash', save and quit. + # On the next screen, edit/refine commit messages. + # Save and quit. + git push -f # (force push to GitHub) + +Please update the resulting commit message, if needed. It should read as a coherent message. In most cases, this means not just listing the interim commits. + +If your change contains a merge commit, the above workflow may not work and you will need to remove the merge commit first. See the next section for details on how to rebase. + +Please refrain from creating several pull requests for the same change. Use the pull request that is already open (or was created earlier) to amend changes. This preserves the discussion and review that happened earlier for the respective change set. + +The length of time required for peer review is unpredictable and will vary from pull request to pull request. + +#### Refactoring + +Refactoring is a necessary part of any software project's evolution. The following guidelines cover refactoring pull requests for the project. + +There are three categories of refactoring: code-only moves, code style fixes, and code refactoring. In general, refactoring pull requests should not mix these three kinds of activities in order to make refactoring pull requests easy to review and uncontroversial. In all cases, refactoring PRs must not change the behaviour of code within the pull request (bugs must be preserved as is). + +Project maintainers aim for a quick turnaround on refactoring pull requests, so where possible keep them short, uncomplex and easy to verify. + +Pull requests that refactor the code should not be made by new contributors. It requires a certain level of experience to know where the code belongs to and to understand the full ramification (including rebase effort of open pull requests). Trivial pull requests or pull requests that refactor the code with no clear benefits may be immediately closed by the maintainers to reduce unnecessary workload on reviewing. + +#### Peer Review + +Anyone may participate in peer review which is expressed by comments in the pull request. Typically reviewers will review the code for obvious errors, as well as test out the patch set and opine on the technical merits of the patch. Project maintainers take into account the peer review when determining if there is consensus to merge a pull request (remember that discussions may have taken place elsewhere, not just on GitHub). The following language is used within pull-request comments: + +- ACK means "I have tested the code and I agree it should be merged"; +- NACK means "I disagree this should be merged", and must be accompanied by sound technical justification. NACKs without accompanying reasoning may be disregarded; +- utACK means "I have not tested the code, but I have reviewed it and it looks OK, I agree it can be merged"; +- Concept ACK means "I agree in the general principle of this pull request"; +- Nit refers to trivial, often non-blocking issues. + +Reviewers should include the commit(s) they have reviewed in their comments. This can be done by copying the commit SHA1 hash. + +A pull request that changes consensus-critical code is considerably more involved than a pull request that adds a feature to the wallet, for example. Such patches must be reviewed and thoroughly tested by several reviewers who are knowledgeable about the changed subsystems. Where new features are proposed, it is helpful for reviewers to try out the patch set on a test network and indicate that they have done so in their review. Project maintainers will take this into consideration when merging changes. + +For a more detailed description of the review process, see the [Code Review Guidelines](CODE_REVIEW_DOCS.md). + +> **Note:** If you find a **Closed** issue that seems like it is the same thing that you're experiencing, open a new issue and include a link to the original issue in the body of your new one. + +#### How Do I Submit A (Good) Bug Report? + +Please track bugs as GitHub issues. + +Explain the problem and include additional details to help maintainers reproduce the problem: + +* **Use a clear and descriptive title** for the issue to identify the problem. +* **Describe the exact steps which reproduce the problem** in as many details as possible. For example, start by explaining how you started the application, e.g. which command exactly you used in the terminal, or how you started Bittensor otherwise. When listing steps, **don't just say what you did, but explain how you did it**. For example, if you ran with a set of custom configs, explain if you used a config file or command line arguments. +* **Provide specific examples to demonstrate the steps**. Include links to files or GitHub projects, or copy/pasteable snippets, which you use in those examples. If you're providing snippets in the issue, use [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). +* **Describe the behavior you observed after following the steps** and point out what exactly is the problem with that behavior. +* **Explain which behavior you expected to see instead and why.** +* **Include screenshots and animated GIFs** which show you following the described steps and clearly demonstrate the problem. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. +* **If you're reporting that Bittensor crashed**, include a crash report with a stack trace from the operating system. On macOS, the crash report will be available in `Console.app` under "Diagnostic and usage information" > "User diagnostic reports". Include the crash report in the issue in a [code block](https://help.github.com/articles/markdown-basics/#multiple-lines), a [file attachment](https://help.github.com/articles/file-attachments-on-issues-and-pull-requests/), or put it in a [gist](https://gist.github.com/) and provide link to that gist. +* **If the problem is related to performance or memory**, include a CPU profile capture with your report, if you're using a GPU then include a GPU profile capture as well. Look into the [PyTorch Profiler](https://pytorch.org/tutorials/recipes/recipes/profiler_recipe.html) to look at memory usage of your model. +* **If the problem wasn't triggered by a specific action**, describe what you were doing before the problem happened and share more information using the guidelines below. + +Provide more context by answering these questions: + +* **Did the problem start happening recently** (e.g. after updating to a new version) or was this always a problem? +* If the problem started happening recently, **can you reproduce the problem in an older version of Bittensor?** +* **Can you reliably reproduce the issue?** If not, provide details about how often the problem happens and under which conditions it normally happens. + +Include details about your configuration and environment: + +* **Which version of Bittensor Subnet are you using?** +* **What commit hash are you on?** You can get the exact commit hash by checking `git log` and pasting the full commit hash. +* **What's the name and version of the OS you're using**? +* **Are you running Bittensor Subnet in a virtual machine?** If so, which VM software are you using and which operating systems and versions are used for the host and the guest? +* **Are you running Bittensor Subnet in a dockerized container?** If so, have you made sure that your docker container contains your latest changes and is up to date with Master branch? + +### Suggesting Enhancements and Features + +This section guides you through submitting an enhancement suggestion, including completely new features and minor improvements to existing functionality. Following these guidelines helps maintainers and the community understand your suggestion :pencil: and find related suggestions :mag_right:. + +When you are creating an enhancement suggestion, please [include as many details as possible](#how-do-i-submit-a-good-enhancement-suggestion). Fill in [the template](https://bit.ly/atom-behavior-pr), including the steps that you imagine you would take if the feature you're requesting existed. + +#### Before Submitting An Enhancement Suggestion + +* **Check the [debugging guide](./DEBUGGING.md).** for tips — you might discover that the enhancement is already available. Most importantly, check if you're using the latest version of the project first. + +#### How Submit A (Good) Feature Suggestion + +* **Use a clear and descriptive title** for the issue to identify the problem. +* **Provide a step-by-step description of the suggested enhancement** in as many details as possible. +* **Provide specific examples to demonstrate the steps**. Include copy/pasteable snippets which you use in those examples, as [Markdown code blocks](https://help.github.com/articles/markdown-basics/#multiple-lines). +* **Describe the current behavior** and **explain which behavior you expected to see instead** and why. +* **Include screenshots and animated GIFs** which help you demonstrate the steps or point out the part of the project which the suggestion is related to. You can use [this tool](https://www.cockos.com/licecap/) to record GIFs on macOS and Windows, and [this tool](https://github.com/colinkeenan/silentcast) or [this tool](https://github.com/GNOME/byzanz) on Linux. +* **Explain why this enhancement would be useful** to most users. +* **List some other text editors or applications where this enhancement exists.** +* **Specify the name and version of the OS you're using.** + +Thank you for considering contributing to Bittensor! Any help is greatly appreciated along this journey to incentivize open and permissionless intelligence. diff --git a/bitagent_subnet-main/contrib/DEVELOPMENT_WORKFLOW.md b/bitagent_subnet-main/contrib/DEVELOPMENT_WORKFLOW.md new file mode 100644 index 0000000000000000000000000000000000000000..13bb07b253d4a45de39c50f222fabfb8ed9b73ae --- /dev/null +++ b/bitagent_subnet-main/contrib/DEVELOPMENT_WORKFLOW.md @@ -0,0 +1,165 @@ +# Bittensor Subnet Development Workflow + +This is a highly advisable workflow to follow to keep your subtensor project organized and foster ease of contribution. + +## Table of contents + +- [Bittensor Subnet Development Workflow](#bittensor-subnet-development-workflow) + - [Main Branches](#main-branches) + - [Development Model](#development-model) + - [Feature Branches](#feature-branches) + - [Release Branches](#release-branches) + - [Hotfix Branches](#hotfix-branches) + - [Git Operations](#git-operations) + - [Creating a Feature Branch](#creating-a-feature-branch) + - [Merging Feature Branch into Staging](#merging-feature-branch-into-staging) + - [Creating a Release Branch](#creating-a-release-branch) + - [Finishing a Release Branch](#finishing-a-release-branch) + - [Creating a Hotfix Branch](#creating-a-hotfix-branch) + - [Finishing a Hotfix Branch](#finishing-a-hotfix-branch) + - [Continuous Integration (CI) and Continuous Deployment (CD)](#continuous-integration-ci-and-continuous-deployment-cd) + - [Versioning and Release Notes](#versioning-and-release-notes) + - [Pending Tasks](#pending-tasks) + +## Main Branches + +Bittensor's codebase consists of two main branches: **main** and **staging**. + +**main** +- This is Bittensor's live production branch, which should only be updated by the core development team. This branch is protected, so refrain from pushing or merging into it unless authorized. + +**staging** +- This branch is continuously updated and is where you propose and merge changes. It's essentially Bittensor's active development branch. + +## Development Model + +### Feature Branches + +- Branch off from: `staging` +- Merge back into: `staging` +- Naming convention: `feature//` + +Feature branches are used to develop new features for upcoming or future releases. They exist as long as the feature is in development, but will eventually be merged into `staging` or discarded. Always delete your feature branch after merging to avoid unnecessary clutter. + +### Release Branches + +- Branch off from: `staging` +- Merge back into: `staging` and then `main` +- Naming convention: `release///` + +Release branches support the preparation of a new production release, allowing for minor bug fixes and preparation of metadata (version number, configuration, etc). All new features should be merged into `staging` and wait for the next big release. + +### Hotfix Branches + +General workflow: + +- Branch off from: `main` or `staging` +- Merge back into: `staging` then `main` +- Naming convention: `hotfix///` + +Hotfix branches are meant for quick fixes in the production environment. When a critical bug in a production version must be resolved immediately, a hotfix branch is created. + +## Git Operations + +#### Create a feature branch + +1. Branch from the **staging** branch. + 1. Command: `git checkout -b feature/my-feature staging` + +> Rebase frequently with the updated staging branch so you do not face big conflicts before submitting your pull request. Remember, syncing your changes with other developers could also help you avoid big conflicts. + +#### Merge feature branch into staging + +In other words, integrate your changes into a branch that will be tested and prepared for release. + +1. Switch branch to staging: `git checkout staging` +2. Merging feature branch into staging: `git merge --no-ff feature/my-feature` +3. Pushing changes to staging: `git push origin staging` +4. Delete feature branch: `git branch -d feature/my-feature` (alternatively, this can be navigated on the GitHub web UI) + +This operation is done by Github when merging a PR. + +So, what you have to keep in mind is: +- Open the PR against the `staging` branch. +- After merging a PR you should delete your feature branch. This will be strictly enforced. + +#### Creating a release branch + +1. Create branch from staging: `git checkout -b release/3.4.0/descriptive-message/creator's_name staging` +2. Updating version with major or minor: `./scripts/update_version.sh major|minor` +3. Commit file changes with new version: `git commit -a -m "Updated version to 3.4.0"` + + +#### Finishing a Release Branch + +This involves releasing stable code and generating a new version for bittensor. + +1. Switch branch to main: `git checkout main` +2. Merge release branch into main: `git merge --no-ff release/3.4.0/optional-descriptive-message` +3. Tag changeset: `git tag -a v3.4.0 -m "Releasing v3.4.0: some comment about it"` +4. Push changes to main: `git push origin main` +5. Push tags to origin: `git push origin --tags` + +To keep the changes made in the __release__ branch, we need to merge those back into `staging`: + +- Switch branch to staging: `git checkout staging`. +- Merging release branch into staging: `git merge --no-ff release/3.4.0/optional-descriptive-message` + +This step may well lead to a merge conflict (probably even, since we have changed the version number). If so, fix it and commit. + + +#### Creating a hotfix branch +1. Create branch from main: `git checkout -b hotfix/3.3.4/descriptive-message/creator's-name main` +2. Update patch version: `./scripts/update_version.sh patch` +3. Commit file changes with new version: `git commit -a -m "Updated version to 3.3.4"` +4. Fix the bug and commit the fix: `git commit -m "Fixed critical production issue X"` + +#### Finishing a Hotfix Branch + +Finishing a hotfix branch involves merging the bugfix into both `main` and `staging`. + +1. Switch branch to main: `git checkout main` +2. Merge hotfix into main: `git merge --no-ff hotfix/3.3.4/optional-descriptive-message` +3. Tag new version: `git tag -a v3.3.4 -m "Releasing v3.3.4: descriptive comment about the hotfix"` +4. Push changes to main: `git push origin main` +5. Push tags to origin: `git push origin --tags` +6. Switch branch to staging: `git checkout staging` +7. Merge hotfix into staging: `git merge --no-ff hotfix/3.3.4/descriptive-message/creator's-name` +8. Push changes to origin/staging: `git push origin staging` +9. Delete hotfix branch: `git branch -d hotfix/3.3.4/optional-descriptive-message` + +The one exception to the rule here is that, **when a release branch currently exists, the hotfix changes need to be merged into that release branch, instead of** `staging`. Back-merging the bugfix into the __release__ branch will eventually result in the bugfix being merged into `develop` too, when the release branch is finished. (If work in develop immediately requires this bugfix and cannot wait for the release branch to be finished, you may safely merge the bugfix into develop now already as well.) + +Finally, we remove the temporary branch: + +- `git branch -d hotfix/3.3.4/optional-descriptive-message` +## Continuous Integration (CI) and Continuous Deployment (CD) + +Continuous Integration (CI) is a software development practice where members of a team integrate their work frequently. Each integration is verified by an automated build and test process to detect integration errors as quickly as possible. + +Continuous Deployment (CD) is a software engineering approach in which software functionalities are delivered frequently through automated deployments. + +- **CircleCI job**: Create jobs in CircleCI to automate the merging of staging into main and release version (needed to release code) and building and testing Bittensor (needed to merge PRs). + +> It is highly recommended to set up your own circleci pipeline with your subnet + +## Versioning and Release Notes + +Semantic versioning helps keep track of the different versions of the software. When code is merged into main, generate a new version. + +Release notes provide documentation for each version released to the users, highlighting the new features, improvements, and bug fixes. When merged into main, generate GitHub release and release notes. + +## Pending Tasks + +Follow these steps when you are contributing to the bittensor subnet: + +- Determine if main and staging are different +- Determine what is in staging that is not merged yet + - Document not released developments + - When merged into staging, generate information about what's merged into staging but not released. + - When merged into main, generate GitHub release and release notes. +- CircleCI jobs + - Merge staging into main and release version (needed to release code) + - Build and Test Bittensor (needed to merge PRs) + +This document can be improved as the Bittensor project continues to develop and change. diff --git a/bitagent_subnet-main/contrib/STYLE.md b/bitagent_subnet-main/contrib/STYLE.md new file mode 100644 index 0000000000000000000000000000000000000000..b7ac755fc06a69f60e6ca9ecef6030de9ae741d6 --- /dev/null +++ b/bitagent_subnet-main/contrib/STYLE.md @@ -0,0 +1,348 @@ +# Style Guide + +A project’s long-term success rests (among other things) on its maintainability, and a maintainer has few tools more powerful than his or her project’s log. It’s worth taking the time to learn how to care for one properly. What may be a hassle at first soon becomes habit, and eventually a source of pride and productivity for all involved. + +Most programming languages have well-established conventions as to what constitutes idiomatic style, i.e. naming, formatting and so on. There are variations on these conventions, of course, but most developers agree that picking one and sticking to it is far better than the chaos that ensues when everybody does their own thing. + +# Table of Contents +1. [Code Style](#code-style) +2. [Naming Conventions](#naming-conventions) +3. [Git Commit Style](#git-commit-style) +4. [The Six Rules of a Great Commit](#the-six-rules-of-a-great-commit) + - [1. Atomic Commits](#1-atomic-commits) + - [2. Separate Subject from Body with a Blank Line](#2-separate-subject-from-body-with-a-blank-line) + - [3. Limit the Subject Line to 50 Characters](#3-limit-the-subject-line-to-50-characters) + - [4. Use the Imperative Mood in the Subject Line](#4-use-the-imperative-mood-in-the-subject-line) + - [5. Wrap the Body at 72 Characters](#5-wrap-the-body-at-72-characters) + - [6. Use the Body to Explain What and Why vs. How](#6-use-the-body-to-explain-what-and-why-vs-how) +5. [Tools Worth Mentioning](#tools-worth-mentioning) + - [Using `--fixup`](#using---fixup) + - [Interactive Rebase](#interactive-rebase) +6. [Pull Request and Squashing Commits Caveats](#pull-request-and-squashing-commits-caveats) + + +### Code style + +#### General Style +Python's official style guide is PEP 8, which provides conventions for writing code for the main Python distribution. Here are some key points: + +- `Indentation:` Use 4 spaces per indentation level. + +- `Line Length:` Limit all lines to a maximum of 79 characters. + +- `Blank Lines:` Surround top-level function and class definitions with two blank lines. Method definitions inside a class are surrounded by a single blank line. + +- `Imports:` Imports should usually be on separate lines and should be grouped in the following order: + + - Standard library imports. + - Related third party imports. + - Local application/library specific imports. +- `Whitespace:` Avoid extraneous whitespace in the following situations: + + - Immediately inside parentheses, brackets or braces. + - Immediately before a comma, semicolon, or colon. + - Immediately before the open parenthesis that starts the argument list of a function call. +- `Comments:` Comments should be complete sentences and should be used to clarify code and are not a substitute for poorly written code. + +#### For Python + +- `List Comprehensions:` Use list comprehensions for concise and readable creation of lists. + +- `Generators:` Use generators when dealing with large amounts of data to save memory. + +- `Context Managers:` Use context managers (with statement) for resource management. + +- `String Formatting:` Use f-strings for formatting strings in Python 3.6 and above. + +- `Error Handling:` Use exceptions for error handling whenever possible. + +#### More details + +Use `black` to format your python code before commiting for consistency across such a large pool of contributors. Black's code [style](https://black.readthedocs.io/en/stable/the_black_code_style/current_style.html#code-style) ensures consistent and opinionated code formatting. It automatically formats your Python code according to the Black style guide, enhancing code readability and maintainability. + +Key Features of Black: + + Consistency: Black enforces a single, consistent coding style across your project, eliminating style debates and allowing developers to focus on code logic. + + Readability: By applying a standard formatting style, Black improves code readability, making it easier to understand and collaborate on projects. + + Automation: Black automates the code formatting process, saving time and effort. It eliminates the need for manual formatting and reduces the likelihood of inconsistencies. + +### Naming Conventions + +- `Classes:` Class names should normally use the CapWords Convention. +- `Functions and Variables:` Function names should be lowercase, with words separated by underscores as necessary to improve readability. Variable names follow the same convention as function names. + +- `Constants:` Constants are usually defined on a module level and written in all capital letters with underscores separating words. + +- `Non-public Methods and Instance Variables:` Use a single leading underscore (_). This is a weak "internal use" indicator. + +- `Strongly "private" methods and variables:` Use a double leading underscore (__). This triggers name mangling in Python. + + +### Git commit style + +Here’s a model Git commit message when contributing: +``` +Summarize changes in around 50 characters or less + +More detailed explanatory text, if necessary. Wrap it to about 72 +characters or so. In some contexts, the first line is treated as the +subject of the commit and the rest of the text as the body. The +blank line separating the summary from the body is critical (unless +you omit the body entirely); various tools like `log`, `shortlog` +and `rebase` can get confused if you run the two together. + +Explain the problem that this commit is solving. Focus on why you +are making this change as opposed to how (the code explains that). +Are there side effects or other unintuitive consequences of this +change? Here's the place to explain them. + +Further paragraphs come after blank lines. + + - Bullet points are okay, too + + - Typically a hyphen or asterisk is used for the bullet, preceded + by a single space, with blank lines in between, but conventions + vary here + +If you use an issue tracker, put references to them at the bottom, +like this: + +Resolves: #123 +See also: #456, #789 +``` + + +## The six rules of a great commit. + +#### 1. Atomic Commits +An “atomic” change revolves around one task or one fix. + +Atomic Approach + - Commit each fix or task as a separate change + - Only commit when a block of work is complete + - Commit each layout change separately + - Joint commit for layout file, code behind file, and additional resources + +Benefits + +- Easy to roll back without affecting other changes +- Easy to make other changes on the fly +- Easy to merge features to other branches + +#### Avoid trivial commit messages + +Commit messages like "fix", "fix2", or "fix3" don't provide any context or clear understanding of what changes the commit introduces. Here are some examples of good vs. bad commit messages: + +**Bad Commit Message:** + + $ git commit -m "fix" + +**Good Commit Message:** + + $ git commit -m "Fix typo in README file" + +> **Caveat**: When working with new features, an atomic commit will often consist of multiple files, since a layout file, code behind file, and additional resources may have been added/modified. You don’t want to commit all of these separately, because if you had to roll back the application to a state before the feature was added, it would involve multiple commit entries, and that can get confusing + +#### 2. Separate subject from body with a blank line + +Not every commit requires both a subject and a body. Sometimes a single line is fine, especially when the change is so simple that no further context is necessary. + +For example: + + Fix typo in introduction to user guide + +Nothing more need be said; if the reader wonders what the typo was, she can simply take a look at the change itself, i.e. use git show or git diff or git log -p. + +If you’re committing something like this at the command line, it’s easy to use the -m option to git commit: + + $ git commit -m"Fix typo in introduction to user guide" + +However, when a commit merits a bit of explanation and context, you need to write a body. For example: + + Derezz the master control program + + MCP turned out to be evil and had become intent on world domination. + This commit throws Tron's disc into MCP (causing its deresolution) + and turns it back into a chess game. + +Commit messages with bodies are not so easy to write with the -m option. You’re better off writing the message in a proper text editor. [See Pro Git](https://git-scm.com/book/en/v2/Customizing-Git-Git-Configuration). + +In any case, the separation of subject from body pays off when browsing the log. Here’s the full log entry: + + $ git log + commit 42e769bdf4894310333942ffc5a15151222a87be + Author: Kevin Flynn + Date: Fri Jan 01 00:00:00 1982 -0200 + + Derezz the master control program + + MCP turned out to be evil and had become intent on world domination. + This commit throws Tron's disc into MCP (causing its deresolution) + and turns it back into a chess game. + + +#### 3. Limit the subject line to 50 characters +50 characters is not a hard limit, just a rule of thumb. Keeping subject lines at this length ensures that they are readable, and forces the author to think for a moment about the most concise way to explain what’s going on. + +GitHub’s UI is fully aware of these conventions. It will warn you if you go past the 50 character limit. Git will truncate any subject line longer than 72 characters with an ellipsis, thus keeping it to 50 is best practice. + +#### 4. Use the imperative mood in the subject line +Imperative mood just means “spoken or written as if giving a command or instruction”. A few examples: + + Clean your room + Close the door + Take out the trash + +Each of the seven rules you’re reading about right now are written in the imperative (“Wrap the body at 72 characters”, etc.). + +The imperative can sound a little rude; that’s why we don’t often use it. But it’s perfect for Git commit subject lines. One reason for this is that Git itself uses the imperative whenever it creates a commit on your behalf. + +For example, the default message created when using git merge reads: + + Merge branch 'myfeature' + +And when using git revert: + + Revert "Add the thing with the stuff" + + This reverts commit cc87791524aedd593cff5a74532befe7ab69ce9d. + +Or when clicking the “Merge” button on a GitHub pull request: + + Merge pull request #123 from someuser/somebranch + +So when you write your commit messages in the imperative, you’re following Git’s own built-in conventions. For example: + + Refactor subsystem X for readability + Update getting started documentation + Remove deprecated methods + Release version 1.0.0 + +Writing this way can be a little awkward at first. We’re more used to speaking in the indicative mood, which is all about reporting facts. That’s why commit messages often end up reading like this: + + Fixed bug with Y + Changing behavior of X + +And sometimes commit messages get written as a description of their contents: + + More fixes for broken stuff + Sweet new API methods + +To remove any confusion, here’s a simple rule to get it right every time. + +**A properly formed Git commit subject line should always be able to complete the following sentence:** + + If applied, this commit will + +For example: + + If applied, this commit will refactor subsystem X for readability + If applied, this commit will update getting started documentation + If applied, this commit will remove deprecated methods + If applied, this commit will release version 1.0.0 + If applied, this commit will merge pull request #123 from user/branch + +#### 5. Wrap the body at 72 characters +Git never wraps text automatically. When you write the body of a commit message, you must mind its right margin, and wrap text manually. + +The recommendation is to do this at 72 characters, so that Git has plenty of room to indent text while still keeping everything under 80 characters overall. + +A good text editor can help here. It’s easy to configure Vim, for example, to wrap text at 72 characters when you’re writing a Git commit. + +#### 6. Use the body to explain what and why vs. how +This [commit](https://github.com/bitcoin/bitcoin/commit/eb0b56b19017ab5c16c745e6da39c53126924ed6) from Bitcoin Core is a great example of explaining what changed and why: + +``` +commit eb0b56b19017ab5c16c745e6da39c53126924ed6 +Author: Pieter Wuille +Date: Fri Aug 1 22:57:55 2014 +0200 + + Simplify serialize.h's exception handling + + Remove the 'state' and 'exceptmask' from serialize.h's stream + implementations, as well as related methods. + + As exceptmask always included 'failbit', and setstate was always + called with bits = failbit, all it did was immediately raise an + exception. Get rid of those variables, and replace the setstate + with direct exception throwing (which also removes some dead + code). + + As a result, good() is never reached after a failure (there are + only 2 calls, one of which is in tests), and can just be replaced + by !eof(). + + fail(), clear(n) and exceptions() are just never called. Delete + them. +``` + +Take a look at the [full diff](https://github.com/bitcoin/bitcoin/commit/eb0b56b19017ab5c16c745e6da39c53126924ed6) and just think how much time the author is saving fellow and future committers by taking the time to provide this context here and now. If he didn’t, it would probably be lost forever. + +In most cases, you can leave out details about how a change has been made. Code is generally self-explanatory in this regard (and if the code is so complex that it needs to be explained in prose, that’s what source comments are for). Just focus on making clear the reasons why you made the change in the first place—the way things worked before the change (and what was wrong with that), the way they work now, and why you decided to solve it the way you did. + +The future maintainer that thanks you may be yourself! + + + +#### Tools worth mentioning + +##### Using `--fixup` + +If you've made a commit and then realize you've missed something or made a minor mistake, you can use the `--fixup` option. + +For example, suppose you've made a commit with a hash `9fceb02`. Later, you realize you've left a debug statement in your code. Instead of making a new commit titled "remove debug statement" or "fix", you can do the following: + + $ git commit --fixup 9fceb02 + +This will create a new commit to fix the issue, with a message like "fixup! The original commit message". + +##### Interactive Rebase + +Interactive rebase, or `rebase -i`, can be used to squash these fixup commits into the original commits they're fixing, which cleans up your commit history. You can use the `autosquash` option to automatically squash any commits marked as "fixup" into their target commits. + +For example: + + $ git rebase -i --autosquash HEAD~5 + +This command starts an interactive rebase for the last 5 commits (`HEAD~5`). Any commits marked as "fixup" will be automatically moved to squash with their target commits. + +The benefit of using `--fixup` and interactive rebase is that it keeps your commit history clean and readable. It groups fixes with the commits they are related to, rather than having a separate "fix" commit that might not make sense to other developers (or even to you) in the future. + + +--- + +#### Pull Request and Squashing Commits Caveats + +While atomic commits are great for development and for understanding the changes within the branch, the commit history can get messy when merging to the main branch. To keep a cleaner and more understandable commit history in our main branch, we encourage squashing all the commits of a PR into one when merging. + +This single commit should provide an overview of the changes that the PR introduced. It should follow the guidelines for atomic commits (an atomic commit is complete, self-contained, and understandable) but on the scale of the entire feature, task, or fix that the PR addresses. This approach combines the benefits of atomic commits during development with a clean commit history in our main branch. + +Here is how you can squash commits: + +```bash +git rebase -i HEAD~n +``` + +where `n` is the number of commits to squash. After running the command, replace `pick` with `squash` for the commits you want to squash into the previous commit. This will combine the commits and allow you to write a new commit message. + +In this context, an atomic commit message could look like: + +``` +Add feature X + +This commit introduces feature X which does A, B, and C. It adds +new files for layout, updates the code behind the file, and introduces +new resources. This change is important because it allows users to +perform task Y more efficiently. + +It includes: +- Creation of new layout file +- Updates in the code-behind file +- Addition of new resources + +Resolves: #123 +``` + +In your PRs, remember to detail what the PR is introducing or fixing. This will be helpful for reviewers to understand the context and the reason behind the changes. diff --git a/bitagent_subnet-main/docs/BenchMarks.ipynb b/bitagent_subnet-main/docs/BenchMarks.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..ea008ddd97b422d80d8dfad5d0873b3d36d49e42 --- /dev/null +++ b/bitagent_subnet-main/docs/BenchMarks.ipynb @@ -0,0 +1,562 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "60d572c1-fb13-4c73-9e49-c489eb93f7cd", + "metadata": {}, + "source": [ + "## Setup" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "id": "410e3cb5-758c-4127-a33f-6dd29c54b716", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_benchmarks import registry\n", + "from langchain_core.language_models.llms import LLM\n", + "import requests\n", + "from strenum import StrEnum\n", + "from pydantic import BaseModel, Field\n", + "from rich import print as rprint\n", + "from typing import Optional,List, Dict, Any\n", + "import bittensor as bt\n", + "import os" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "f7720829-66ae-4796-9071-e230626dac19", + "metadata": {}, + "outputs": [], + "source": [ + "subnet = bt.metagraph(netuid=20, network=\"finney\")\n", + "\n", + "# Wallet and validator setup\n", + "WALLET_NAME = \"\" # TODO, put your coldkey\n", + "HOTKEY_NAME = \"\" # TODO, put your hotkey\n", + "os.environ[\"LANGCHAIN_API_KEY\"] = \"\" # TODO put your LangChain API Key here if you wish to dig through lang smith results\n", + "vali_wallet = bt.wallet(name=WALLET_NAME, hotkey=HOTKEY_NAME)\n", + "vali_dendrite = bt.dendrite(wallet=vali_wallet)\n", + "\n", + "validator_uids = subnet.uids[((subnet.S>20000) & subnet.validator_permit)]\n", + "miner_uids = subnet.uids[(subnet.S<=20000)]\n", + "\n", + "class Tool(BaseModel):\n", + " name: str\n", + " description: str\n", + " arguments: Dict[str, Dict[str, Any]]\n", + "\n", + " def toJSON(self):\n", + " return {\"name\": self.name, \"description\": self.description, \"arguments\": self.arguments}\n", + " \n", + " def to_dict(self):\n", + " return self.dict()\n", + "\n", + "class ChatRole(StrEnum):\n", + " ASSISTANT = \"assistant\"\n", + " USER = \"user\"\n", + " TOOL_CALL = \"tool call\"\n", + " TOOL_RESPONSE = \"tool response\"\n", + " \n", + "class ChatMessage(BaseModel):\n", + " \"\"\"A list of previous messages between the user and the model, meant to give the model conversational context for responding to the user's message.\"\"\"\n", + "\n", + " role: ChatRole = Field(\n", + " title=\"One of the ChatRole's to identify who the message is coming from.\",\n", + " )\n", + " content: str | dict | list = Field( \n", + " title=\"Contents of the chat message.\",\n", + " )\n", + "\n", + " @classmethod\n", + " def from_dict(cls, data: Dict[str, str]):\n", + " \"\"\"Create a ChatMessage object from a dictionary.\"\"\"\n", + " return cls(role=ChatRole(data['role']), content=data['content'])\n", + " \n", + " def to_dict(self) -> Dict[str, str]:\n", + " return {\"role\": self.role, \"content\": self.content}\n", + "\n", + " def toJSON(self):\n", + " return {\"role\": self.role, \"content\": self.content}\n", + "\n", + "class Conversation(BaseModel):\n", + " messages: List[ChatMessage] = []\n", + " \n", + " @classmethod\n", + " def from_list(cls, data_list: List[Dict[str, str]]):\n", + " \"\"\"Create a Conversation object from a list of dictionaries.\"\"\"\n", + " messages = [ChatMessage.from_dict(item) for item in data_list]\n", + " return cls(messages=messages)\n", + " \n", + " def to_list(self):\n", + " return [msg.to_dict() for msg in self.messages]\n", + "\n", + " def toJSON(self):\n", + " return self.to_list()\n", + " \n", + "# the request protocol\n", + "class QnATask(bt.Synapse):\n", + " urls: List[str] = [] # not used right now - when enabled would allow users to pass in URLs for content\n", + " datas: List[dict] = [] # used to pass in relevant context, could be a company knowledge base or a set of wikipedia pages\n", + " tools: List[Tool] = [] # used to pass in tools to be leveraged in answering user query\n", + " notes: str = \"No Notes\"\n", + " prompt: str = \"\" # the query / prompt\n", + " messages: List[ChatMessage] = []\n", + " response: Optional[dict] = {}\n", + " timeout: Optional[float] = 3.0\n", + " miner_uids: Optional[List[int]] = [] # put our TOP miner into the network as the miner to query (if empty list, a random list of miners will be selected)\n", + " \n", + " def toJSON(self):\n", + " return {\"prompt\": self.prompt, \n", + " \"urls\": self.urls, \n", + " \"datas\": self.datas, \n", + " \"tools\": [t.toJSON() for t in self.tools],\n", + " \"notes\": self.notes,\n", + " \"messages\": self.messages.toJSON(),\n", + " \"response\": self.response,\n", + " \"miner_uids\": self.miner_uids,\n", + " \"dendrite_process_time\": self.dendrite.process_time,\n", + " \"dendrite_status_code\": self.dendrite.status_code,\n", + " \"axon_status_code\": self.axon.status_code,}\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "8be029cb-5ca9-4dc3-9d49-af93bae53b85", + "metadata": {}, + "outputs": [], + "source": [ + "import json\n", + "import time\n", + "from langchain_core.outputs.chat_generation import ChatGeneration \n", + "from langchain.agents.output_parsers.tools import ToolAgentAction\n", + "from langchain_core.messages.ai import AIMessageChunk\n", + "from langchain_core.messages import AIMessage\n", + "from langchain.schema.output import LLMResult\n", + "\n", + "from typing import Any, AsyncIterator, Dict, Iterator, List, Optional, Sequence, Union, Type, Callable, Literal\n", + "from langchain_core.runnables import Runnable\n", + "from langchain_core.pydantic_v1 import BaseModel, Field, SecretStr, root_validator\n", + "from langchain_core.callbacks import (\n", + " AsyncCallbackManagerForLLMRun,\n", + " CallbackManagerForLLMRun,\n", + ")\n", + "from langchain_core.language_models import BaseChatModel, SimpleChatModel\n", + "from langchain_core.messages import AIMessageChunk, BaseMessage, HumanMessage\n", + "from langchain_core.outputs import ChatGeneration, ChatGenerationChunk, ChatResult\n", + "from langchain_core.runnables import run_in_executor\n", + "from langchain_core.tools import BaseTool\n", + "from langchain_core.language_models import LanguageModelInput\n", + "\n", + "class CustomChatModelAdvanced(BaseChatModel):\n", + " top_miner_uids = (-subnet.I).argsort()[:3].tolist()\n", + " original_tools = []\n", + " tools = []\n", + "\n", + " \n", + " def bind_tools(\n", + " self,\n", + " tools: Sequence[Union[Dict[str, Any], Type[BaseModel], Callable, BaseTool]],\n", + " *,\n", + " tool_choice: Optional[\n", + " Union[Dict[str, str], Literal[\"any\", \"auto\"], str]\n", + " ] = None,\n", + " **kwargs: Any,\n", + " ) -> Runnable[LanguageModelInput, BaseMessage]:\n", + "\n", + "\n", + " def build_arg(t):\n", + " to_ret = {}\n", + " for k,v in t.args.items():\n", + " to_ret[k] = {'required': True, 'type': v['type'], 'description': v['title']}\n", + "\n", + " return to_ret\n", + " \n", + " self.tools = [Tool(name=t.name, description=t.description, arguments=build_arg(t)) for t in tools]\n", + " \n", + " self.original_tools = tools\n", + " formatted_tools = tools\n", + " if not tool_choice:\n", + " pass\n", + " elif isinstance(tool_choice, dict):\n", + " kwargs[\"tool_choice\"] = tool_choice\n", + " elif isinstance(tool_choice, str) and tool_choice in (\"any\", \"auto\"):\n", + " kwargs[\"tool_choice\"] = {\"type\": tool_choice}\n", + " elif isinstance(tool_choice, str):\n", + " kwargs[\"tool_choice\"] = {\"type\": \"tool\", \"name\": tool_choice}\n", + " else:\n", + " raise ValueError(\n", + " f\"Unrecognized 'tool_choice' type {tool_choice=}. Expected dict, \"\n", + " f\"str, or None.\"\n", + " )\n", + " return self.bind(tools=formatted_tools, **kwargs)\n", + "\n", + " def _generate(\n", + " self,\n", + " messages: List[BaseMessage],\n", + " stop: Optional[List[str]] = None,\n", + " run_manager: Optional[CallbackManagerForLLMRun] = None,\n", + " **kwargs: Any,\n", + " ) -> ChatResult:\n", + "\n", + " def get_role(name):\n", + " if name == \"SystemMessage\":\n", + " return ChatRole.USER\n", + "\n", + " if name == \"HumanMessage\":\n", + " return ChatRole.USER\n", + "\n", + " return ChatRole.USER\n", + "\n", + " resp = None\n", + " try:\n", + " \n", + " task = QnATask(\n", + " prompt=\"\",\n", + " datas=[],\n", + " urls=[],\n", + " tools=self.tools,\n", + " notes=\"\",\n", + " messages=[ChatMessage(role=get_role(type(m).__name__), content=m.content) for m in messages]\n", + " )\n", + " \n", + " responses = vali_dendrite.query(\n", + " axons=[subnet.axons[uid] for uid in self.top_miner_uids],\n", + " synapse=task,\n", + " deserialize=False,\n", + " timeout=60,\n", + " )\n", + " for test_resp in responses:\n", + " try:\n", + " if \"response\" in test_resp.response.keys():\n", + " if self.tools:\n", + " resp = json.loads(test_resp.response[\"response\"])\n", + " for msg in resp:\n", + " if 'role' in msg.keys() and msg['role'] == \"tool use\":\n", + " # resp is probably good\n", + " break\n", + " else:\n", + " resp = test_resp.response[\"response\"]\n", + " if resp:\n", + " break\n", + " \n", + " except Exception as e:\n", + " print(\"SMALLER ERROR: \", e)\n", + " print(test_resp)\n", + " \n", + " except Exception as e:\n", + " print(\"BIGGER ERROR: \", e)\n", + " \n", + " if not resp:\n", + " print(\"OMG BIG ERROR (NO RESP): \", responses)\n", + " for respo in responses:\n", + " print(respo.dendrite.status_code)\n", + " print(respo.axon.status_code)\n", + " \n", + " ai_message_content = []\n", + " if type(resp) == str:\n", + " ai_message_content = resp\n", + " else:\n", + " for mesg in resp:\n", + " new_msg = {'type': 'text', 'text': mesg['content']} # default for non tool calling messages\n", + " if mesg['role'] == \"tool call\":\n", + " new_msg['type'] = \"tool_call\"\n", + " new_msg['text'] = None\n", + " new_msg['name'] = mesg['content']['name']\n", + " new_msg['input'] = mesg['content']['arguments']\n", + " \n", + " ai_message_content.append(new_msg)\n", + "\n", + " message = AIMessage(\n", + " content=ai_message_content,\n", + " additional_kwargs={}, # Used to add additional payload (e.g., function calling request)\n", + " response_metadata={ # Use for response metadata\n", + " \"time_in_seconds\": 3,\n", + " },\n", + " )\n", + " generation = ChatGeneration(message=message)\n", + " return ChatResult(generations=[generation])\n", + "\n", + " @property\n", + " def _llm_type(self) -> str:\n", + " return \"echoing-chat-model-advanced\"\n", + "\n", + " @property\n", + " def _identifying_params(self) -> Dict[str, Any]:\n", + " return {\n", + " \"model_name\": \"WHATEVER\"\n", + " }" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "1ac3faab-5fcf-4b97-8080-9c0048f9debf", + "metadata": {}, + "outputs": [], + "source": [ + "from langchain_core.prompts import ChatPromptTemplate\n", + "from langchain_benchmarks.tool_usage.agents import StandardAgentFactory\n", + "from langchain_benchmarks import registry\n", + "\n", + "model = CustomChatModelAdvanced()\n", + "\n", + "prompt = ChatPromptTemplate.from_messages(\n", + " [\n", + " (\"system\", \"{instructions}\"), # Populated from task.instructions automatically\n", + " (\n", + " \"user\",\n", + " \"{question}\",\n", + " ), # Each evaluation example is associated with a question\n", + " (\"placeholder\", \"{agent_scratchpad}\"), # Space for the agent to do work\n", + " ]\n", + ")\n" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "b96759c4-c4e7-4aa2-bf8c-7412a27d010b", + "metadata": {}, + "outputs": [], + "source": [ + "import datetime\n", + "import uuid\n", + "\n", + "from langsmith.client import Client\n", + "\n", + "from langchain_benchmarks import (\n", + " __version__,\n", + " clone_public_dataset,\n", + " model_registry,\n", + " registry,\n", + ")\n", + "from langchain_benchmarks.rate_limiting import RateLimiter" + ] + }, + { + "cell_type": "markdown", + "id": "d43c4640-6d69-4fc9-8cea-6d8867dbc88a", + "metadata": {}, + "source": [ + "## Run Benchmark Tests" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "ab2f7a9a-c1a5-4293-80a8-5d7263938d42", + "metadata": {}, + "outputs": [], + "source": [ + "benchmark_local = False\n", + "benchmark_langsmith = True" + ] + }, + { + "cell_type": "markdown", + "id": "b7c4f59e-14e6-42a3-8d30-03f40004cae6", + "metadata": {}, + "source": [ + "### BenchMark Local" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "fa55c784-bdee-414b-8596-d26353663f49", + "metadata": {}, + "outputs": [], + "source": [ + "import os\n", + "from langchain_benchmarks.utils import run_without_langsmith\n", + "\n", + "tests = [\n", + " (\"Mytest\", CustomChatModelAdvanced())\n", + "]\n", + "\n", + "if benchmark_local:\n", + " for task in registry.tasks:\n", + " if task.type != \"ToolUsageTask\":\n", + " continue\n", + " \n", + " dataset_name = task.name + f\" ({today})\"\n", + " clone_public_dataset(task.dataset_id, dataset_name=dataset_name)\n", + " \n", + " for model_name, model in tests:\n", + " print()\n", + " print(f\"Benchmarking {task.name} with model: {model_name}\")\n", + " if task.name in [\"Tool Usage - Relational Data\",\"Multiverse Math\"]:\n", + " eval_config = task.get_eval_config(eval_llm=CustomChatModelAdvanced())\n", + " else:\n", + " eval_config = task.get_eval_config()\n", + " \n", + " agent_factory = StandardAgentFactory(\n", + " task, model, prompt, rate_limiter=rate_limiter\n", + " )\n", + " \n", + " test_run = run_without_langsmith(\n", + " # This will clone the dataset locally if not already there\n", + " path_or_token_id=task.dataset_id,\n", + " llm_or_chain_factory=agent_factory,\n", + " evaluation=eval_config,\n", + " concurrency_level=1,\n", + " verbose=True,\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "c7915129-9dda-42c5-8380-17a6f5311151", + "metadata": {}, + "source": [ + "### Benchmark with LangSmith" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "id": "638594aa-aeb3-49e0-802d-a9e19e4d529c", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Dataset Tool Usage - Typewriter (1 tool) (2024-07-08) already exists. Skipping.\n", + "You can access the dataset at https://smith.langchain.com/o/9796c4da-021f-5ea2-ad5c-978e23367525/datasets/5bbaa6ff-969d-49fc-a319-060ec17edb3b.\n", + "\n", + "Benchmarking Tool Usage - Typewriter (1 tool) with model: Mytest\n", + "View the evaluation results for project 'Mytest-Tool Usage - Typewriter (1 tool)-2024-07-08-e8c1ff482d3d4bffa6d7a11771058bfb' at:\n", + "https://smith.langchain.com/o/9796c4da-021f-5ea2-ad5c-978e23367525/datasets/5bbaa6ff-969d-49fc-a319-060ec17edb3b/compare?selectedSessions=ed660500-22c5-47b1-84ee-af411768a46e\n", + "\n", + "View all tests for Dataset Tool Usage - Typewriter (1 tool) (2024-07-08) at:\n", + "https://smith.langchain.com/o/9796c4da-021f-5ea2-ad5c-978e23367525/datasets/5bbaa6ff-969d-49fc-a319-060ec17edb3b\n", + "[------------------------------------------------->] 20/20Dataset Tool Usage - Typewriter (26 tools) (2024-07-08) already exists. Skipping.\n", + "You can access the dataset at https://smith.langchain.com/o/9796c4da-021f-5ea2-ad5c-978e23367525/datasets/641a2d0d-1a06-4572-8414-df42b6bc21d3.\n", + "\n", + "Benchmarking Tool Usage - Typewriter (26 tools) with model: Mytest\n", + "View the evaluation results for project 'Mytest-Tool Usage - Typewriter (26 tools)-2024-07-08-e8c1ff482d3d4bffa6d7a11771058bfb' at:\n", + "https://smith.langchain.com/o/9796c4da-021f-5ea2-ad5c-978e23367525/datasets/641a2d0d-1a06-4572-8414-df42b6bc21d3/compare?selectedSessions=da8fc9fb-fc21-4655-ac4e-10d287c05730\n", + "\n", + "View all tests for Dataset Tool Usage - Typewriter (26 tools) (2024-07-08) at:\n", + "https://smith.langchain.com/o/9796c4da-021f-5ea2-ad5c-978e23367525/datasets/641a2d0d-1a06-4572-8414-df42b6bc21d3\n", + "[------------------------------------------------->] 20/20Dataset Tool Usage - Relational Data (2024-07-08) already exists. Skipping.\n", + "You can access the dataset at https://smith.langchain.com/o/9796c4da-021f-5ea2-ad5c-978e23367525/datasets/53ac9124-aa35-4e4f-9b3f-59cb46e353b6.\n", + "\n", + "Benchmarking Tool Usage - Relational Data with model: Mytest\n", + "View the evaluation results for project 'Mytest-Tool Usage - Relational Data-2024-07-08-e8c1ff482d3d4bffa6d7a11771058bfb' at:\n", + "https://smith.langchain.com/o/9796c4da-021f-5ea2-ad5c-978e23367525/datasets/53ac9124-aa35-4e4f-9b3f-59cb46e353b6/compare?selectedSessions=14218a79-7be1-405c-9265-fb165f5b2ace\n", + "\n", + "View all tests for Dataset Tool Usage - Relational Data (2024-07-08) at:\n", + "https://smith.langchain.com/o/9796c4da-021f-5ea2-ad5c-978e23367525/datasets/53ac9124-aa35-4e4f-9b3f-59cb46e353b6\n", + "[------------------------------------------------->] 21/21" + ] + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "8f10ff0613e24754a9bf3df6611b9d73", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + " 0%| | 0/20 [00:00] 20/20" + ] + } + ], + "source": [ + "if benchmark_langsmith:\n", + " \n", + " from langsmith.client import Client\n", + " experiment_id = uuid.uuid4().hex[:]\n", + " client = Client() # Launch langsmith client for cloning datasets\n", + " today = datetime.date.today().isoformat()\n", + " \n", + " # You can use an optional rate limiter to rate limit your requests!\n", + " rate_limiter = RateLimiter(requests_per_second=1)\n", + " \n", + " for task in registry.tasks:\n", + " if task.type != \"ToolUsageTask\":\n", + " continue\n", + " \n", + " dataset_name = task.name + f\" ({today})\"\n", + " clone_public_dataset(task.dataset_id, dataset_name=dataset_name)\n", + " \n", + " for model_name, model in tests:\n", + " print()\n", + " print(f\"Benchmarking {task.name} with model: {model_name}\")\n", + " if task.name in [\"Tool Usage - Relational Data\",\"Multiverse Math\"]:\n", + " eval_config = task.get_eval_config(eval_llm=CustomChatModelAdvanced())\n", + " else:\n", + " eval_config = task.get_eval_config() \n", + " \n", + " agent_factory = StandardAgentFactory(\n", + " task, model, prompt, rate_limiter=rate_limiter\n", + " )\n", + " \n", + " client.run_on_dataset(\n", + " dataset_name=dataset_name,\n", + " llm_or_chain_factory=agent_factory,\n", + " evaluation=eval_config,\n", + " verbose=False,\n", + " project_name=f\"{model_name}-{task.name}-{today}-{experiment_id}\",\n", + " concurrency_level=1,\n", + " project_metadata={\n", + " \"model\": model_name,\n", + " \"id\": experiment_id,\n", + " \"task\": task.name,\n", + " \"date\": today,\n", + " \"langchain_benchmarks_version\": __version__,\n", + " },\n", + " )" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.12" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/bitagent_subnet-main/docs/MediaumExample.ImageSorting.2024.08.ipynb b/bitagent_subnet-main/docs/MediaumExample.ImageSorting.2024.08.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..a96c7faf96c217a0bce4265432af9d49ce4c1625 --- /dev/null +++ b/bitagent_subnet-main/docs/MediaumExample.ImageSorting.2024.08.ipynb @@ -0,0 +1,174 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 137, + "id": "091fbdac-8f19-4177-91f1-6fca09862cbc", + "metadata": {}, + "outputs": [], + "source": [ + "from PIL import Image\n", + "from io import BytesIO\n", + "import glob, os, requests, base64, shutil, json" + ] + }, + { + "cell_type": "code", + "execution_count": 138, + "id": "6156a527-6231-43ac-a6a5-a44a254af3d2", + "metadata": {}, + "outputs": [], + "source": [ + "# the photos I want to work with\n", + "images_dir = f\"{os.environ['HOME']}/Desktop/RandomPhotos/MediumExample/\"\n", + "\n", + "# API Endpoint for SN20 / BitAgent\n", + "api_endpoint = \"https://api.gogoagent.ai/query_subnet\"\n", + "\n", + "# TODO USE YOUR API KEY\n", + "hdrs = {\"Content-Type\": \"application/json\",\n", + " \"Authorization\": \"YOUR API KEY GOES HERE\"} # TODO use your API KEY\n", + "\n", + "# Define the tools\n", + "# one for moving to animals directory and one for moving to non animals directory\n", + "move_to_animals_dir_tool = {\n", + " \"name\": \"move_to_animals_dir\",\n", + " \"description\": \"Move the Image that contains something animal related to the animals directory\",\n", + " \"arguments\": {}\n", + "}\n", + "move_to_non_animals_dir_tool = {\n", + " \"name\": \"move_to_non_animals_dir\",\n", + " \"description\": \"Move the Image that does NOT contain anything animal related to the non_animals directory\",\n", + " \"arguments\": {\n", + " }\n", + "}\n", + "move_to_unknown_dir_tool = {\n", + " \"name\": \"move_to_unknown_dir\",\n", + " \"description\": \"\"\"Move the Image to the unknown directory because \n", + " it can not be determined if it contains an animal or not\"\"\",\n", + " \"arguments\": {\n", + " }\n", + "}\n", + "\n", + "def move_image_file(file, dirname):\n", + " the_path = os.path.dirname(file)\n", + " full_dirname = the_path + \"/\" + dirname + \"/\"\n", + " if not os.path.exists(full_dirname):\n", + " os.mkdir(full_dirname)\n", + " print(f\"Moving {file} to {full_dirname}\")\n", + " shutil.move(file, full_dirname)\n", + "\n", + "def move_to_unknown_dir(file):\n", + " move_image_file(file, \"unknown\")\n", + "def move_to_animals_dir(file):\n", + " move_image_file(file, \"animals\")\n", + "def move_to_non_animals_dir(file):\n", + " move_image_file(file, \"non_animals\")\n", + " \n", + "tools = [move_to_animals_dir_tool, move_to_non_animals_dir_tool, move_to_unknown_dir_tool]\n", + "\n", + "def get_details_about_image(image):\n", + " # base64 encode the image for sending to the API\n", + " img_str = get_img_str_from_image(image)\n", + " \n", + " # Setup the messages / prompt\n", + " messages = [\n", + " {\"role\": \"user\",\n", + " \"content\": \"Tell me about this image, I am most interested in knowing if it contains anything related to an animal or not\"}\n", + " ]\n", + "\n", + " files = [{'content': img_str, 'type': \"Image\"}]\n", + " \n", + " # Send the request to the API\n", + " data = {\"messages\": messages, \"files\": files}\n", + " result = requests.post(api_endpoint, headers=hdrs, json=data)\n", + " \n", + " return result.json()[0]['response']\n", + "\n", + "def get_img_str_from_image(image):\n", + " with open(image, 'rb') as im:\n", + " img_str = base64.b64encode(im.read()).decode('utf-8')\n", + " return img_str\n", + "\n", + "def get_tool_call_based_on_response(response, tools):\n", + " # Setup the messages for tool call\n", + " messages = [\n", + " {\"role\": \"user\",\n", + " \"content\": \"Tell me about this image, I am most interested in knowing if it contains anything related to an animal or not\"},\n", + " {\"role\": \"assistant\",\n", + " \"content\": response},\n", + " {\"role\": \"user\",\n", + " \"content\": \"I want to organize my photos, which tool should I use for the image details provided?\"}\n", + " ]\n", + " \n", + " # Send the request to the API\n", + " data = {\"messages\": messages, \"tools\": tools}\n", + " result = requests.post(api_endpoint, headers=hdrs, json=data)\n", + "\n", + " return result.json()[0]['response'] \n", + "\n", + "def handle_agent_tool_call(agent_response, image_file):\n", + " method_to_call = None\n", + " for resp in json.loads(agent_response['response']):\n", + " if resp['role'] == \"tool call\":\n", + " method_to_call = resp['content']['name']\n", + " break\n", + " \n", + " if method_to_call:\n", + " globals()[method_to_call](image_file)\n", + " else:\n", + " print(\"no method to call\")" + ] + }, + { + "cell_type": "code", + "execution_count": 139, + "id": "2f7e6617-ac4d-44ec-9f41-61bd0568ce98", + "metadata": {}, + "outputs": [], + "source": [ + "for img_file in glob.glob(f\"{images_dir}*.png\"): # TODO change from png to whatever you're looking for\n", + " agent_response_about_image = get_details_about_image(img_file)\n", + " agent_response_about_tools = get_tool_call_based_on_response(agent_response_about_image, tools)\n", + " handle_agent_tool_call(agent_response_about_tools, img_file)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63212cc8-fdd5-4825-99e5-e2553ed4c2e8", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "aa4d2605-1de5-4883-8edb-5209c896e24d", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/bitagent_subnet-main/docs/OTF-example.rt.ipynb b/bitagent_subnet-main/docs/OTF-example.rt.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..f57afa4c01ebeb49fa0f79ba34ddc6ba6ad6b4e0 --- /dev/null +++ b/bitagent_subnet-main/docs/OTF-example.rt.ipynb @@ -0,0 +1,547 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, + "source": [ + "## SN20 BitAgent\n", + "\n", + "### Setting up ...\n", + "- importing standard libraries + bittensor, no special sauce required\n", + "- fetching subnet 20\n", + "- setting up wallet and validator\n", + "- getting top miner\n", + "- providing protocol (QnATask)" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\u001b[34m2024-04-07 17:47:36.518\u001b[0m | \u001b[1m INFO \u001b[0m | You are connecting to finney network with endpoint wss://entrypoint-finney.opentensor.ai:443.\n", + "\u001b[34m2024-04-07 17:47:36.519\u001b[0m | \u001b[33m\u001b[1m WARNING \u001b[0m | We strongly encourage running a local subtensor node whenever possible. This increases decentralization and resilience of the network.\n", + "\u001b[34m2024-04-07 17:47:36.519\u001b[0m | \u001b[33m\u001b[1m WARNING \u001b[0m | In a future release, local subtensor will become the default endpoint. To get ahead of this change, please run a local subtensor node and point to it.\n", + "\u001b[34m2024-04-07 17:47:36.833\u001b[0m | \u001b[1m INFO \u001b[0m | Connected to finney network and wss://entrypoint-finney.opentensor.ai:443.\n", + "Top Miner UID for Subnet 20: 197\n" + ] + } + ], + "source": [ + "import asyncio\n", + "import requests\n", + "import bittensor as bt \n", + "from rich import print as rprint\n", + "from typing import Optional,List\n", + "\n", + "# working with subnet 20 / upsilon / BitAgent\n", + "subnet = bt.metagraph(netuid=20)\n", + "\n", + "# Wallet and validator setup\n", + "WALLET_NAME = \"TODO\" # TODO\n", + "HOTKEY_NAME = \"TODO\" # TODO\n", + "vali_wallet = bt.wallet(name=WALLET_NAME, hotkey=HOTKEY_NAME)\n", + "vali_dendrite = bt.dendrite(wallet=vali_wallet)\n", + "\n", + "# get the TOP miner on the subnet\n", + "top_miner_uid = int(subnet.I.argmax())\n", + "print(\"Top Miner UID for Subnet 20: \", top_miner_uid)\n", + "\n", + "# the request protocol\n", + "class QnATask(bt.Synapse):\n", + " urls: List[str] = [] # not used right now\n", + " datas: List[dict] = [] # used to pass in relevant context, could be a company knowledge base or a set of wikipedia pages\n", + " prompt: str = \"\" # the query / prompt\n", + " response: Optional[dict] = {}\n", + " timeout: Optional[float] = 3.0\n", + " miner_uids: Optional[List[int]] = [top_miner_uid] # put our TOP miner into the network as the miner to query (if empty list, a random list of miners will be selected)\n", + "\n", + " def toJSON(self):\n", + " return {\"prompt\": self.prompt, \n", + " \"urls\": self.urls, \n", + " \"datas\": self.datas, \n", + " \"response\": self.response,\n", + " \"miner_uids\": self.miner_uids,\n", + " \"dendrite_process_time\": self.dendrite.process_time,\n", + " \"dendrite_status_code\": self.dendrite.status_code,\n", + " \"axon_status_code\": self.axon.status_code,}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Two ways to query SN20 \n", + "\n", + "### First way is to use your registered validator to query directly to the TOP miner\n", + "- build a task (QnATask) with a \"prompt\" and optional \"datas\"\n", + "- query the network\n", + "- see response answer (1)\n", + "- see top citation (2)\n", + "- see full response object (3)" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 - Response showing answer from miner: \n", + "\t Meow eats grapes, berries, and occasional bananas.\n", + "2 - Response's topmost relevant citation from miner: \n", + "\t [{'context': 'meow prefers to eat grapes and berries, but also eats the occassional banana', 'source': 'source 3'}]\n", + "3 - Full response: \n", + "\t [QnATask(timeout=3.0, urls=[], datas=[{'source': 'source 1', 'context': 'Some irrelevant context'}, {'source': 'source 2', 'context': 'meow is a monkey in the jungle'}, {'source': 'source 3', 'context': 'meow prefers to eat grapes and berries, but also eats the occassional banana'}, {'source': 'source 4', 'context': 'meow climbs trees for fun'}, {'source': 'source 5', 'context': 'meow is afraid of snakes, but loves bunnies'}], prompt='hey, what does the meow eat?', response={'response': ' Meow eats grapes, berries, and occasional bananas.', 'citations': [{'context': 'meow prefers to eat grapes and berries, but also eats the occassional banana', 'source': 'source 3'}], 'context': 'meow prefers to eat grapes and berries, but also eats the occassional banana'}, miner_uids=[197])]\n" + ] + } + ], + "source": [ + "task = QnATask(prompt=\"hey, what does the meow eat?\", \n", + " datas=[{\"source\": \"source 1\", \"context\": \"Some irrelevant context\"},\n", + " {\"source\": \"source 2\", \"context\": \"meow is a monkey in the jungle\"},\n", + " {\"source\": \"source 3\", \"context\": \"meow prefers to eat grapes and berries, but also eats the occassional banana\"},\n", + " {\"source\": \"source 4\", \"context\": \"meow climbs trees for fun\"},\n", + " {\"source\": \"source 5\", \"context\": \"meow is afraid of snakes, but loves bunnies\"}])\n", + "\n", + "responses = vali_dendrite.query(\n", + " axons=[subnet.axons[top_miner_uid]],\n", + " synapse=task,\n", + " deserialize=False,\n", + " timeout=task.timeout,\n", + ")\n", + "\n", + "response = responses[0].response\n", + "print(\"1 - Response showing answer from miner: \\n\\t\", response['response'])\n", + "print(\"2 - Response's topmost relevant citation from miner: \\n\\t\", response['citations'])\n", + "print(\"3 - Full response: \\n\\t\", responses)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Second way is to use your validator (or any wallet) to query one of the subnet validators\n", + "- we'll use the same QnATask from above, but we'll change the prompt\n", + "- query the network via validator axon\n", + "- we are specifying the miner uid for our QnATask to be the TOP miner uid\n", + "- see response answer (1)\n", + "- see top citation (2)\n", + "- see full response object (3)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1 - Response showing answer: \n", + "\t Meow fears snakes the most.\n", + "2 - Response's topmost relevant citation: \n", + "\t [{'context': 'meow is afraid of snakes, but loves bunnies', 'source': 'source 5'}]\n", + "3 - Full response: \n", + "\t [QnATask(timeout=3.0, urls=[], datas=[{'source': 'source 1', 'context': 'Some irrelevant context'}, {'source': 'source 2', 'context': 'meow is a monkey in the jungle'}, {'source': 'source 3', 'context': 'meow prefers to eat grapes and berries, but also eats the occassional banana'}, {'source': 'source 4', 'context': 'meow climbs trees for fun'}, {'source': 'source 5', 'context': 'meow is afraid of snakes, but loves bunnies'}], prompt='What does meow fear the most?', response={'response': ' Meow fears snakes the most.', 'citations': [{'context': 'meow is afraid of snakes, but loves bunnies', 'source': 'source 5'}], 'context': 'meow is afraid of snakes, but loves bunnies'}, miner_uids=[])]\n" + ] + } + ], + "source": [ + "task.prompt = \"What does meow fear the most?\"\n", + "\n", + "responses = vali_dendrite.query(\n", + " axons=[subnet.axons[0]],\n", + " synapse=task,\n", + " deserialize=False,\n", + " timeout=task.timeout,\n", + ")\n", + "response = responses[0].response\n", + "print(\"1 - Response showing answer: \\n\\t\", response['response'])\n", + "print(\"2 - Response's topmost relevant citation: \\n\\t\", response['citations'])\n", + "print(\"3 - Full response: \\n\\t\", responses)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Demonstration of all task types and scoring / incentives ...\n", + "## Generating tasks from the Task API\n", + "We'll get tasks from the Task API that are far more complicated than the one above - \n", + "- Summarization Task\n", + "- QnA with Citations Task\n", + "- QnA Logic Task" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, + "source": [ + "### Setting up ...\n", + "- methods to \n", + " - get the top miner, \n", + " - fetch a task \n", + " - get miner response and \n", + " - evaluate a task\n", + "- setup task IDs for QnA with Citations, Pet Tricks and Summarization" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "qna_task = (3,1)\n", + "pet_tricks_task = (6,6)\n", + "summarization_task = (8,1)\n", + "\n", + "def get_top_miner_uid(subnet):\n", + " return subnet.I.argmax()\n", + "\n", + "def get_task(task_id, sub_task_id):\n", + " # keep trying in case it's being restarted \n", + " while True:\n", + " try:\n", + " resp = requests.post(\"https://roguetensor.com/api/task_api/get_new_task\", json={\"task_id\": task_id, \"sub_task_id\": sub_task_id}).json()\n", + " return resp\n", + " except:\n", + " pass\n", + "\n", + "def eval_task(task_id, response):\n", + " # keep trying in case it's being restarted \n", + " while True:\n", + " try:\n", + " resp = requests.post(\"https://roguetensor.com/api/task_api/evaluate_task_response\", json={\"task_id\": task_id, \"response\": response.toJSON()}).json()\n", + " return resp\n", + " except:\n", + " pass\n", + "\n", + "def get_miner_response_to_task(subnet, validator, miner_uid, task):\n", + " print(\"Fetching response from TOP miner: \", miner_uid)\n", + "\n", + " response = None\n", + " while not response:\n", + " response = asyncio.run(validator.call(\n", + " # Send the query to selected miner axons in the network.\n", + " target_axon=subnet.axons[miner_uid],\n", + " # Construct a query. \n", + " synapse=task,\n", + " # All responses have the deserialize function called on them before returning.\n", + " # You are encouraged to define your own deserialization function.\n", + " deserialize=True,\n", + " timeout=25.0\n", + " ))\n", + " return response\n", + "\n", + "def evaluate_miner(subnet, validator, miner_uid, task_id, sub_task_id):\n", + " task_json = get_task(task_id, sub_task_id)\n", + " gen_task_id = task_json[\"task\"][\"task_id\"]\n", + " task = QnATask(prompt=task_json['task']['prompt'], datas=task_json['task']['datas'], urls=task_json['task']['urls'])\n", + " print(\"Got task with prompt: \", task_json['task']['prompt'][:60] + \" ...\")\n", + " miner_response = get_miner_response_to_task(subnet, validator, miner_uid, task)\n", + " print(\"Miner response:\", miner_response.response['response'][:100] + \" ...\")\n", + " eval = eval_task(gen_task_id, miner_response)\n", + " return miner_response, *eval[\"result\"]" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Generated Summary Task Example:" + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Got task with prompt: Summarize this and make sure to be concise: Did you attend ...\n", + "Fetching response from TOP miner: 197\n", + "Miner response: Pamela and Marie both did not attend the independence march. Pamela stayed at home due to concerns ...\n", + "Scores: 2.25 2.25\n" + ] + } + ], + "source": [ + "task_id, sub_task_id = summarization_task\n", + "miner_response, score, max_score, results, correct_answer_optional = evaluate_miner(subnet, vali_dendrite, top_miner_uid, task_id, sub_task_id)\n", + "print(\"Scores: \", score, max_score)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### The results the miner would see:" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Does not error\n",
+       "✅ You successfully responded to the request.\n",
+       "You received 0.25 of 0.25 reward.\n",
+       "Does not take a long time\n",
+       "✅ You responded to the request in 0.8873927593231201.\n",
+       "You received 0.5 of 0.5 reward.\n",
+       "Return summary shorter than original\n",
+       "✅ You responded with a valid summary length.\n",
+       "You received 0.5 of 0.5 reward.\n",
+       "Return valid summary\n",
+       "✅ You responded with a valid summary.\n",
+       "You received 1.0 of 1.0 reward.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;34mDoes not error\u001b[0m\n", + "✅ \u001b[32mYou successfully responded to the request.\u001b[0m\n", + "You received \u001b[1;36m0.25\u001b[0m of \u001b[1;36m0.25\u001b[0m reward.\n", + "\u001b[1;34mDoes not take a long time\u001b[0m\n", + "✅ \u001b[32mYou responded to the request in \u001b[0m\u001b[1;32m0.8873927593231201\u001b[0m\u001b[32m.\u001b[0m\n", + "You received \u001b[1;36m0.5\u001b[0m of \u001b[1;36m0.5\u001b[0m reward.\n", + "\u001b[1;34mReturn summary shorter than original\u001b[0m\n", + "✅ \u001b[32mYou responded with a valid summary length.\u001b[0m\n", + "You received \u001b[1;36m0.5\u001b[0m of \u001b[1;36m0.5\u001b[0m reward.\n", + "\u001b[1;34mReturn valid summary\u001b[0m\n", + "✅ \u001b[32mYou responded with a valid summary.\u001b[0m\n", + "You received \u001b[1;36m1.0\u001b[0m of \u001b[1;36m1.0\u001b[0m reward.\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "rprint((\"\\n\").join(results)) " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Generated QnA with Citations Task Example:" + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Got task with prompt: \"What was the purpose of integrating Crashlytics into Fireb ...\n", + "Fetching response from TOP miner: 197\n", + "Miner response: The purpose of integrating Crashlytics into Firebase was to bring the best of both platforms togeth ...\n", + "Scores: 5.25 5.25\n" + ] + } + ], + "source": [ + "task_id, sub_task_id = qna_task\n", + "miner_response, score, max_score, results, correct_answer_optional = evaluate_miner(subnet, vali_dendrite, top_miner_uid, task_id, sub_task_id)\n", + "print(\"Scores: \", score, max_score)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### The results the miner would see:" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Does not error\n",
+       "✅ You successfully responded to the request.\n",
+       "You received 0.25 of 0.25 reward.\n",
+       "Does not take a long time\n",
+       "✅ You responded to the request in 0.8485991954803467.\n",
+       "You received 0.5 of 0.5 reward.\n",
+       "Returns expected citation source(s)\n",
+       "✅ You correctly identified some or all of the correct citation sources (1/1 identified).\n",
+       "You received 1.5 of 1.5 reward.\n",
+       "Returns a relevant response\n",
+       "✅ You responded with a relevant response compared to the context.\n",
+       "You received 1.0 of 1.0 reward.\n",
+       "Returns a unique response\n",
+       "✅ You responded with a novel response compared to the context.\n",
+       "You received 1.0 of 1.0 reward.\n",
+       "Returns valid response\n",
+       "✅ You responded with a valid response.\n",
+       "You received 1.0 of 1.0 reward.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;34mDoes not error\u001b[0m\n", + "✅ \u001b[32mYou successfully responded to the request.\u001b[0m\n", + "You received \u001b[1;36m0.25\u001b[0m of \u001b[1;36m0.25\u001b[0m reward.\n", + "\u001b[1;34mDoes not take a long time\u001b[0m\n", + "✅ \u001b[32mYou responded to the request in \u001b[0m\u001b[1;32m0.8485991954803467\u001b[0m\u001b[32m.\u001b[0m\n", + "You received \u001b[1;36m0.5\u001b[0m of \u001b[1;36m0.5\u001b[0m reward.\n", + "\u001b[1;34mReturns expected citation \u001b[0m\u001b[1;34msource\u001b[0m\u001b[1;34m(\u001b[0m\u001b[1;34ms\u001b[0m\u001b[1;34m)\u001b[0m\n", + "✅ \u001b[32mYou correctly identified some or all of the correct citation sources \u001b[0m\u001b[1;32m(\u001b[0m\u001b[1;32m1\u001b[0m\u001b[32m/\u001b[0m\u001b[1;32m1\u001b[0m\u001b[32m identified\u001b[0m\u001b[1;32m)\u001b[0m\u001b[32m.\u001b[0m\n", + "You received \u001b[1;36m1.5\u001b[0m of \u001b[1;36m1.5\u001b[0m reward.\n", + "\u001b[1;34mReturns a relevant response\u001b[0m\n", + "✅ \u001b[32mYou responded with a relevant response compared to the context.\u001b[0m\n", + "You received \u001b[1;36m1.0\u001b[0m of \u001b[1;36m1.0\u001b[0m reward.\n", + "\u001b[1;34mReturns a unique response\u001b[0m\n", + "✅ \u001b[32mYou responded with a novel response compared to the context.\u001b[0m\n", + "You received \u001b[1;36m1.0\u001b[0m of \u001b[1;36m1.0\u001b[0m reward.\n", + "\u001b[1;34mReturns valid response\u001b[0m\n", + "✅ \u001b[32mYou responded with a valid response.\u001b[0m\n", + "You received \u001b[1;36m1.0\u001b[0m of \u001b[1;36m1.0\u001b[0m reward.\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "rprint((\"\\n\").join(results)) " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Generated Pet Tricks QnA Logic Task Example:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Got task with prompt: Given the following Trick Descriptions with numerical IDs:\n", + " ...\n", + "Fetching response from TOP miner: 197\n", + "Miner response: 4 ...\n", + "Scores: 1.75 1.75\n" + ] + } + ], + "source": [ + "task_id, sub_task_id = pet_tricks_task\n", + "miner_response, score, max_score, results, correct_answer_optional = evaluate_miner(subnet, vali_dendrite, top_miner_uid, task_id, sub_task_id)\n", + "print(\"Scores: \", score, max_score)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### The results the miner would see:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
Does not error\n",
+       "✅ You successfully responded to the request.\n",
+       "You received 0.25 of 0.25 reward.\n",
+       "Does not take a long time\n",
+       "✅ You responded to the request in 0.3985426425933838.\n",
+       "You received 0.5 of 0.5 reward.\n",
+       "Returns expected value\n",
+       "✅ You responded with a valid answer.\n",
+       "You received 1.0 of 1.0 reward.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;34mDoes not error\u001b[0m\n", + "✅ \u001b[32mYou successfully responded to the request.\u001b[0m\n", + "You received \u001b[1;36m0.25\u001b[0m of \u001b[1;36m0.25\u001b[0m reward.\n", + "\u001b[1;34mDoes not take a long time\u001b[0m\n", + "✅ \u001b[32mYou responded to the request in \u001b[0m\u001b[1;32m0.3985426425933838\u001b[0m\u001b[32m.\u001b[0m\n", + "You received \u001b[1;36m0.5\u001b[0m of \u001b[1;36m0.5\u001b[0m reward.\n", + "\u001b[1;34mReturns expected value\u001b[0m\n", + "✅ \u001b[32mYou responded with a valid answer.\u001b[0m\n", + "You received \u001b[1;36m1.0\u001b[0m of \u001b[1;36m1.0\u001b[0m reward.\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "rprint((\"\\n\").join(results)) " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "jp-MarkdownHeadingCollapsed": true + }, + "source": [ + "## That's It!\n", + "\n", + "You saw how to:\n", + " 1) Query the top miner uid\n", + " 2) Demonstrate each reward/penalty mechanism/Scoring of the top miner response\n", + " 3) Query the subnet 2 different ways (as a validator to a miner and through a registered validator axon)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.12" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/bitagent_subnet-main/docs/WandBandMetagraphAnalysis.ipynb b/bitagent_subnet-main/docs/WandBandMetagraphAnalysis.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..9b0cc5353bd425ca7197e427d7ee20473550ba57 --- /dev/null +++ b/bitagent_subnet-main/docs/WandBandMetagraphAnalysis.ipynb @@ -0,0 +1,1135 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 35, + "id": "90f51a19-d7b2-40d3-86ba-6b313d6959da", + "metadata": {}, + "outputs": [], + "source": [ + "import random\n", + "import bittensor as bt\n", + "subtensor = bt.subtensor(network=\"archive\")\n", + "current_block = subtensor.block\n", + "uid = random.randint(1,256) # TODO your UID\n", + "top_uid = uid + 1 # TODO a UID to compare to" + ] + }, + { + "cell_type": "code", + "execution_count": 36, + "id": "5465392c-3c66-4f41-9759-79a69817c28f", + "metadata": {}, + "outputs": [], + "source": [ + "weights = []\n", + "incentives = []\n", + "# get the last several days of metagraph data\n", + "for blk in range(current_block - (7200*7), current_block - (7200*6), 450):\n", + " while True:\n", + " try: \n", + " mg = subtensor.metagraph(netuid=20, lite=False, block=blk)\n", + " break\n", + " except Exception as e:\n", + " print(e)\n", + " weights.append(mg.W[(mg.S > 20000) & mg.validator_permit, uid]) \n", + " incentives.append(mg.I[uid]) " + ] + }, + { + "cell_type": "code", + "execution_count": 37, + "id": "c8c1c3f9-5842-48b7-8243-cac81c4f8883", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 37, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAj0AAAHZCAYAAABtrIoIAAAAP3RFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMS5wb3N0MSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8kixA/AAAACXBIWXMAAA9hAAAPYQGoP6dpAAEAAElEQVR4nOzdeVzU1734/9dnGLZhZgARQVzABVQMiooYNRVsuEEbFRobqVdFI9HUqpV4rV4jppLFLNYUvfGXtUr43ihKFZPGBor2ijSgEg2KEdegYAQUZd+Z+fz+GBkdZVUQ1PN8POYRmDmfc86HIPOe8z6LJMuyjCAIgiAIwmNO0dkdEARBEARBeBhE0CMIgiAIwhNBBD2CIAiCIDwRRNAjCIIgCMITQQQ9giAIgiA8EUTQIwiCIAjCE0EEPYIgCIIgPBGUnd0BQRA6hk6no66urrO7IXRx5ubmmJmZdXY3BOGhEEGPIDxmZFkmPz+f4uLizu6K8Iiws7PD2dkZSZI6uyuC0KFE0CMIj5mGgKdHjx6oVCrxRiY0SZZlKisruXbtGgA9e/bs5B4JQscSQY8gPEZ0Op0x4HFwcOjs7giPAGtrawCuXbtGjx49RKpLeKyJicyC8BhpmMOjUqk6uSfCo6Th90XMARMedyLoEYTHkEhpCW0hfl+EJ4UIegRBEARBeCKIoEcQBEEQhCeCCHoEQXjk+fv7Ex4ebvzezc2NqKioZq+RJIm9e/d2aL8EQehaRNAjCEKnmjp1KpMmTWr0tZSUFCRJ4uTJk22qMz09nYULF7ZH94zWrVuHt7d3u9Z5ty1btuDm5oaVlRVjxozh6NGjHdqeIDxpRNAjCEKnCgsLIykpiStXrtzz2rZt2/Dx8WHYsGFtqtPR0bHLrmCrra1t9PmdO3eyfPly/vSnP3H8+HGGDx9OYGCgcQ8dQRAenAh6BOExJssylbX1nfKQZblVfZwyZQqOjo5ER0ebPF9eXk5cXBzBwcHMnDmTXr16oVKp8PLyYseOHc3WeXd66/z580yYMAErKys8PT1JSkq655pVq1bh4eGBSqWif//+rF271riEOzo6msjISE6cOIEkSUiSZOxvTk4OQUFBqNVqtFotM2bMoKCgwFhvwwjR559/Tr9+/bCysmq0zx988AELFizgpZdewtPTk48//hiVSsXWrVtb8VMUBKE1xOaEgvAYq6rT4fl6Yqe0ffqNQFQWLf+JUSqVhIaGEh0dzZo1a4zLp+Pi4tDpdMyePZu4uDhWrVqFVqtl3759zJkzhwEDBuDr69ti/Xq9nhdeeAEnJyeOHDlCSUmJyfyfBhqNhujoaFxcXMjMzGTBggVoNBpWrlxJSEgIp06dIiEhgf379wNga2uLXq83BjzJycnU19ezePFiQkJCOHjwoLHuCxcusHv3bvbs2dPo5n+1tbUcO3aM1atXG59TKBQEBASQlpbW4j0KgtA6IugRBKHTzZ8/nw0bNpCcnIy/vz9gSG1Nnz4dV1dXVqxYYSy7dOlSEhMT2bVrV6uCnv3793PmzBkSExNxcXEBYP369UyePNmkXEREhPFrNzc3VqxYQWxsLCtXrsTa2hq1Wo1SqcTZ2dlYLikpiczMTLKzs+nTpw8AMTExDB06lPT0dEaPHg0YgpqYmBgcHR0b7WNhYSE6nQ4nJyeT552cnDhz5kyL9ygIQuuIoEcQHmPW5macfiOw09purcGDBzNu3Di2bt2Kv78/Fy5cICUlhTfeeAOdTsf69evZtWsXP//8M7W1tdTU1LR6zk5WVhZ9+vQxBjwAY8eOvafczp072bx5MxcvXqS8vJz6+nq0Wm2r6m4IeAA8PT2xs7MjKyvLGPS4uroaA56UlBSTgOuTTz5h4sSJrboXQRAejAh6BOExJklSq1JMXUFYWBhLly5ly5YtbNu2jQEDBuDn58d7773Hpk2biIqKwsvLCxsbG8LDw5ucEHw/0tLSmDVrFpGRkQQGBmJra0tsbCwbN25sl/ptbGyMX/v4+JCRkWH83snJCUtLS8zMzEzmAgEUFBSYjCwJgvBgxERmQRC6hBkzZqBQKNi+fTsxMTHMnz8fSZL47rvvCAoKYvbs2QwfPpz+/ftz7ty5Vtc7ZMgQcnNzycvLMz53+PBhkzKpqam4urqyZs0afHx8cHd35/LlyyZlLCws0Ol0jdadm5trfO706dMUFxfj6enZaH+sra0ZOHCg8aHRaLCwsGDUqFEcOHDAWE6v13PgwIFGR6UEQbg/IugRBKFLUKvVhISEsHr1avLy8pg3bx4A7u7uJCUlkZqaSlZWFq+88so9IyLNCQgIwMPDg7lz53LixAlSUlJYs2aNSRl3d3dycnKIjY3l4sWLbN68mfj4eJMybm5uZGdnk5GRQWFhITU1NQQEBODl5cWsWbM4fvw4R48eJTQ0FD8/P3x8fNp0/8uXL+ezzz7jiy++ICsri0WLFlFRUcFLL73UpnoEQWiaCHoEQegywsLCKCoqIjAw0DgHJyIigpEjRxIYGIi/vz/Ozs4EBwe3uk6FQkF8fDxVVVX4+vry8ssv8/bbb5uUmTZtGq+++ipLlizB29ub1NRU1q5da1Jm+vTpTJo0iYkTJ+Lo6MiOHTuQJImvvvoKe3t7JkyYQEBAAP3792fnzp1tvveQkBD+/Oc/8/rrr+Pt7U1GRgYJCQn3TG4WBOH+SXJrN9MQBKHLq66uJjs7u9n9YAThbuL3RnhSiJEeQRAEQRCeCCLoEQRBEAThiSCCHkEQBEEQnggi6BEEQRAE4Ykggh5BEARBEJ4IIugRBEEQBOGJIIIeQRAEQRCeCCLoEQRBEAThiSCCHkEQHnn+/v6Eh4cbv3dzcyMqKqrZayRJYu/evR3aL0EQuhYR9AiC0KmmTp3KpEmTGn0tJSUFSZI4efJkm+pMT09n4cKF7dE9o3Xr1uHt7d2udd7pnXfeYfTo0Wg0Gnr06EFwcDBnz57tsPYE4Ukkgh5BEDpVWFgYSUlJXLly5Z7Xtm3bho+PD8OGDWtTnY6OjqhUqvbqYruqra1t9Pnk5GQWL17M4cOHSUpKoq6ujueee46KioqH3ENBeHyJoEcQhE41ZcoUHB0diY6ONnm+vLycuLg4goODmTlzJr169UKlUuHl5cWOHTuarfPu9Nb58+eZMGECVlZWeHp6kpSUdM81q1atwsPDA5VKRf/+/Vm7di11dXUAREdHExkZyYkTJ5AkCUmSjP3NyckhKCgItVqNVqtlxowZJqfAN4wQff75582ebZWQkMC8efMYOnQow4cPJzo6mpycHI4dO9aKn6IgCK2h7OwOCILQgWQZ6io7p21zFUhSi8WUSiWhoaFER0ezZs0apFvXxMXFodPpmD17NnFxcaxatQqtVsu+ffuYM2cOAwYMwNfXt8X69Xo9L7zwAk5OThw5coSSkhKT+T8NNBoN0dHRuLi4kJmZyYIFC9BoNKxcuZKQkBBOnTpFQkIC+/fvB8DW1ha9Xm8MeJKTk6mvr2fx4sWEhIRw8OBBY90XLlxg9+7d7NmzBzMzs1b9+EpKSgDo1q1bq8oLgtAyEfQIwuOsrhLWu3RO269dBQubVhWdP38+GzZsIDk5GX9/f8CQ2po+fTqurq6sWLHCWHbp0qUkJiaya9euVgU9+/fv58yZMyQmJuLiYvhZrF+/nsmTJ5uUi4iIMH7t5ubGihUriI2NZeXKlVhbW6NWq1EqlTg7OxvLJSUlkZmZSXZ2Nn369AEgJiaGoUOHkp6ezujRowFDSismJgZHR8dW/Tz0ej3h4eGMHz+ep556qlXXCILQMpHeEgSh0w0ePJhx48axdetWwDAykpKSQlhYGDqdjjfffBMvLy+6deuGWq0mMTGRnJycVtWdlZVFnz59jAEPwNixY+8pt3PnTsaPH4+zszNqtZqIiIgW22iouyHgAfD09MTOzo6srCzjc66ursaAJyUlBbVabXx8+eWX99S7ePFiTp06RWxsbKvuURCE1hEjPYLwODNXGUZcOqvtNggLC2Pp0qVs2bKFbdu2MWDAAPz8/HjvvffYtGkTUVFReHl5YWNjQ3h4eJMTgu9HWloas2bNIjIyksDAQGxtbYmNjWXjxo3tUr+Nze0RLx8fHzIyMozfOzk5mZRdsmQJ33zzDYcOHaJ3797t0r4gCAYi6BGEx5kktTrF1NlmzJjBsmXL2L59OzExMSxatAhJkvjuu+8ICgpi9uzZgCH1c+7cOTw9PVtV75AhQ8jNzSUvL4+ePXsCcPjwYZMyqampuLq6smbNGuNzly9fNiljYWGBTqdrtO7c3FzjaM/p06cpLi5usn/W1tYMHDjwnudlWWbp0qXEx8dz8OBB+vXr16r7EwSh9UR6SxCELkGtVhMSEsLq1avJy8tj3rx5ALi7u5OUlERqaipZWVm88sorJqujWhIQEICHhwdz587lxIkTpKSkmAQ3DW3k5OQQGxvLxYsX2bx5M/Hx8SZl3NzcyM7OJiMjg8LCQmpqaggICMDLy4tZs2Zx/Phxjh49SmhoKH5+fvj4+LTp/hcvXsz//u//sn37djQaDfn5+eTn51NVVdWmegRBaJoIegRB6DLCwsIoKioiMDDQOAcnIiKCkSNHEhgYiL+/P87OzgQHB7e6ToVCQXx8PFVVVfj6+vLyyy/z9ttvm5SZNm0ar776KkuWLMHb25vU1FTWrl1rUmb69OlMmjSJiRMn4ujoyI4dO5Akia+++gp7e3smTJhAQEAA/fv3Z+fOnW2+948++oiSkhL8/f3p2bOn8XE/dQmC0DhJlmW5szshCEL7qK6uJjs7u9n9YAThbuL3RnhSiJEeQRAEQRCeCCLoEQRBEAThiSCCHkEQBEEQnggi6BEEQRAE4Ykggh5BEARBEJ4IIugRBEEQBOGJIIIeQRAEQRCeCCLoEQRBEAThiSCCHkEQBEEQnggi6BEE4ZHn7+9PeHi48Xs3NzeioqKavUaSJPbu3duh/RIEoWsRQY8gCJ1q6tSpTJo0qdHXUlJSkCSJkydPtqnO9PR0Fi5c2B7dM1q3bh3e3t7tWuedPvroI4YNG4ZWq0Wr1TJ27Fi+/fbbDmtPEJ5EIugRBKFThYWFkZSUxJUrV+55bdu2bfj4+DBs2LA21eno6IhKpWqvLrar2traRp/v3bs37777LseOHeP777/nl7/8JUFBQfz4448PuYeC8PgSQY8gCJ1qypQpODo6Eh0dbfJ8eXk5cXFxBAcHM3PmTHr16oVKpcLLy4sdO3Y0W+fd6a3z588zYcIErKys8PT0JCkp6Z5rVq1ahYeHByqViv79+7N27Vrq6uoAiI6OJjIykhMnTiBJEpIkGfubk5NDUFAQarUarVbLjBkzKCgoMNbbMEL0+eefN3ug59SpU/nVr36Fu7s7Hh4evP3226jVag4fPtyKn6IgCK2h7OwOCILQcWRZpqq+qlPatlZaI0lSi+WUSiWhoaFER0ezZs0a4zVxcXHodDpmz55NXFwcq1atQqvVsm/fPubMmcOAAQPw9fVtsX69Xs8LL7yAk5MTR44coaSkxGT+TwONRkN0dDQuLi5kZmayYMECNBoNK1euJCQkhFOnTpGQkMD+/fsBsLW1Ra/XGwOe5ORk6uvrWbx4MSEhIRw8eNBY94ULF9i9ezd79uzBzMysxT7rdDri4uKoqKhg7NixLZYXBKF1RNAjCI+xqvoqxmwf0yltH/nPI6jMW5dimj9/Phs2bCA5ORl/f3/AkNqaPn06rq6urFixwlh26dKlJCYmsmvXrlYFPfv37+fMmTMkJibi4uICwPr165k8ebJJuYiICOPXbm5urFixgtjYWFauXIm1tTVqtRqlUomzs7OxXFJSEpmZmWRnZ9OnTx8AYmJiGDp0KOnp6YwePRowpLRiYmJwdHRstq+ZmZmMHTuW6upq1Go18fHxeHp6tniPgiC0jkhvCYLQ6QYPHsy4cePYunUrYBgZSUlJISwsDJ1Ox5tvvomXlxfdunVDrVaTmJhITk5Oq+rOysqiT58+xoAHaHT0ZOfOnYwfPx5nZ2fUajUREREtttFQd0PAA+Dp6YmdnR1ZWVnG51xdXY0BT0pKCmq12vj48ssvjeUGDRpERkYGR44cYdGiRcydO5fTp0+36j4FQWiZGOkRhMeYtdKaI/95pNPabouwsDCWLl3Kli1b2LZtGwMGDMDPz4/33nuPTZs2ERUVhZeXFzY2NoSHhzc5Ifh+pKWlMWvWLCIjIwkMDMTW1pbY2Fg2btzYLvXb2NgYv/bx8SEjI8P4vZOTk/FrCwsLBg4cCMCoUaNIT09n06ZNfPLJJ+3SD0F40omgRxAeY5IktTrF1NlmzJjBsmXL2L59OzExMSxatAhJkvjuu+8ICgpi9uzZgGGOzrlz51qd9hkyZAi5ubnk5eXRs2dPgHsmB6empuLq6sqaNWuMz12+fNmkjIWFBTqdrtG6c3NzjaM9p0+fpri4uMn+WVtbGwObluj1empqalpVVhCElon0liAIXYJarSYkJITVq1eTl5fHvHnzAHB3dycpKYnU1FSysrJ45ZVXTFZHtSQgIAAPDw/mzp3LiRMnSElJMQluGtrIyckhNjaWixcvsnnzZuLj403KuLm5kZ2dTUZGBoWFhdTU1BAQEICXlxezZs3i+PHjHD16lNDQUPz8/PDx8WnT/a9evZpDhw5x6dIlMjMzWb16NQcPHmTWrFltqkcQhKaJoEcQhC4jLCyMoqIiAgMDjXNwIiIiGDlyJIGBgfj7++Ps7ExwcHCr61QoFMTHx1NVVYWvry8vv/wyb7/9tkmZadOm8eqrr7JkyRK8vb1JTU1l7dq1JmWmT5/OpEmTmDhxIo6OjuzYsQNJkvjqq6+wt7dnwoQJBAQE0L9/f3bu3Nnme7927RqhoaEMGjSIZ599lvT0dBITE/mP//iPNtclCEIT5Pvw4Ycfyq6urrKlpaXs6+srHzlypNnyu3btkgcNGiRbWlrKTz31lLxv3z6T1/V6vbx27VrZ2dlZtrKykp999ln53Llz99TzzTffyL6+vrKVlZVsZ2cnBwUFmbx++fJl+Ve/+pVsbW0tOzo6yitWrJDr6uru5xYF4ZFUVVUlnz59Wq6qqursrgiPEPF7Izwp2jynZ+fOnSxfvpyPP/6YMWPGEBUVRWBgIGfPnqVHjx73lE9NTWXmzJm88847TJkyhe3btxMcHMzx48d56qmnAHj//ffZvHkzX3zxBf369WPt2rUEBgZy+vRp40Zeu3fvZsGCBaxfv55f/vKX1NfXc+rUKWM7Op2O559/HmdnZ1JTU8nLyyM0NBRzc3PWr1/fqnvT6/VcvXoVjUbTqv1FBKGrqa2tRa/Xo9Pp7pl/IghN0el06PV6ysvL23WCuCA8LLIsU1ZWhouLCwpF00ksSZZluS0VjxkzhtGjR/Phhx8ChkChT58+LF26lP/+7/++p3xISAgVFRV88803xueefvppvL29+fjjj5FlGRcXF/7rv/7LuBdHSUkJTk5OREdH89vf/pb6+nrc3NyIjIwkLCys0X59++23TJkyhatXrxpXQ3z88cesWrWK69evY2Fh0eK9XblyxWTpqSA8alxdXfn444/p3r17Z3dFeMQUFhbyu9/97p4J3ILwKMnNzaV3795Nvt6mkZ7a2lqOHTvG6tWrjc8pFAoCAgJIS0tr9Jq0tDSWL19u8lxgYKDxdOPs7Gzy8/MJCAgwvm5ra8uYMWNIS0vjt7/9LcePH+fnn39GoVAwYsQI8vPz8fb2ZsOGDcbRorS0NLy8vEyWfwYGBrJo0SJ+/PFHRowYcU/fampqTFZGNMR/ubm5aLXatvxoBKFLqK2tpaCgADc3tyaPOxCEu1VXV3Pp0iW+//77Vn1AFISuprS0lD59+qDRaJot16agp7CwEJ1OZxJYgGGfiTNnzjR6TX5+fqPl8/Pzja83PNdUmZ9++gkwnGHzwQcf4ObmxsaNG/H39+fcuXN069atyXbubONu77zzDpGRkfc833DKsSA8aqqrq7l+/TpmZmatOu5AEADMzMxQKBSo1WoRLAuPtJampjwSq7f0ej0Aa9asYfr06YwaNYpt27YhSRJxcXH3Xe/q1aspKSkxPnJzc9ury4IgCIIgdDFtCnq6d++OmZnZPXtkFBQUmJxHcydnZ+dmyzf8t7kyDRuK3bnZl6WlJf379zduE99UO3e2cTdLS0vjqI4Y3REEQRCEx1ubgh4LCwtGjRrFgQMHjM/p9XoOHDjQ5EnAY8eONSkPhkP6Gsr369cPZ2dnkzKlpaUcOXLEWGbUqFFYWlpy9uxZY5m6ujouXbqEq6ursZ3MzEyuXbtm0o5WqxUH9gmCIAiC0PZjKJYvX87cuXPx8fHB19eXqKgoKioqeOmllwAIDQ2lV69evPPOOwAsW7YMPz8/Nm7cyPPPP09sbCzff/89n376KWDIv4WHh/PWW2/h7u5uXLLu4uJi3IBMq9Xyu9/9jj/96U/06dMHV1dXNmzYAMCLL74IwHPPPYenpydz5szh/fffJz8/n4iICBYvXoylpeUD/6AEQRAEQXi0tTnoCQkJ4fr167z++uvGVVQJCQnGScM5OTkma+THjRvH9u3biYiI4LXXXsPd3Z29e/caV10BrFy5koqKChYuXEhxcTHPPPMMCQkJJhPqNmzYgFKpZM6cOVRVVTFmzBj+9a9/YW9vDxgm4n3zzTcsWrSIsWPHYmNjw9y5c3njjTfu+4cjCIIgCMLjo8379DzOSktLsbW1paSkRMzvER5J1dXVZGdn069fvydqFY6/vz/e3t5ERUUBhnOywsPDCQ8Pb/IaSZKIj49v05EWj6sn9fdGeHy09v37kVi9JQjC42vq1KlMmjSp0ddSUlKQJImTJ0+2qc709HQWLlzYHt0zWrduHd7e3u1aZ1PeffddY+pfEIT2I4IeQRA6VVhYGElJSVy5cuWe17Zt24aPjw/Dhg1rU52Ojo6oVKr26mK7aumYh/T0dD755JM237MgCC0TQY8gCJ1qypQpODo6Eh0dbfJ8eXk5cXFxBAcHM3PmTHr16oVKpcLLy4sdO3Y0W6ebm5sx1QVw/vx5JkyYgJWVFZ6eniQlJd1zzapVq/Dw8EClUtG/f3/Wrl1LXV0dANHR0URGRnLixAkkSUKSJGN/c3JyCAoKQq1Wo9VqmTFjhsn2GQ0jRJ9//nmL6aPy8nJmzZrFZ599ZpyvKAhC+2nzRGZBEB4dsiwjV1V1StuStXWrDu5VKpWEhoYSHR3NmjVrjNfExcWh0+mYPXs2cXFxrFq1Cq1Wy759+5gzZw4DBgzA19e3xfr1ej0vvPACTk5OHDlyhJKSkkbTRhqNhujoaFxcXMjMzGTBggVoNBpWrlxJSEgIp06dIiEhgf379wOG43L0er0x4ElOTqa+vp7FixcTEhLCwYMHjXVfuHCB3bt3s2fPnmZ3yl68eDHPP/88AQEBvPXWWy3emyAIbSOCHkF4jMlVVZwdOapT2h50/BhSK1NM8+fPZ8OGDSQnJ+Pv7w8YUlvTp0/H1dXVeBgxwNKlS0lMTGTXrl2tCnr279/PmTNnSExMxMXFBYD169czefJkk3IRERHGr93c3FixYgWxsbGsXLkSa2tr1Go1SqXSZLPTpKQkMjMzyc7ONh5WHBMTw9ChQ0lPT2f06NGAIaUVExODo6Njk/2MjY3l+PHjpKent3hPgiDcH5HeEgSh0w0ePJhx48axdetWwDAykpKSQlhYGDqdjjfffBMvLy+6deuGWq0mMTHRuBt7S7KysujTp48x4AEa3Ux1586djB8/HmdnZ9RqNRERES220VB3Q8ADhp3j7ezsyMrKMj7n6upqDHhSUlJQq9XGx5dffklubi7Lli3jyy+/FKunBKEDiZEeQXiMSdbWDDp+rNPabouwsDCWLl3Kli1b2LZtGwMGDMDPz4/33nuPTZs2ERUVhZeXFzY2NoSHh7c4Ibgt0tLSmDVrFpGRkQQGBmJra0tsbCwbN25sl/ptbGyMX/v4+JCRkWH83snJiQMHDnDt2jVGjhxpfF6n03Ho0CE+/PBDampqxAGyQrupqsrh+vUkeveejULxZG3eK4IeQXiMSZLU6hRTZ5sxYwbLli1j+/btxMTEsGjRIiRJ4rvvviMoKIjZs2cDhjk6586da/XxMkOGDCE3N5e8vDzjOX6HDx82KZOamoqrqytr1qwxPnf58mWTMhYWFuh0ukbrzs3NNY72nD59muLi4ib7Z21tzcCBA02ee/bZZ8nMzDR57qWXXmLw4MGsWrVKBDxCuzp3/m0KC/ej19fh5va7zu7OQyXSW4IgdAlqtZqQkBBWr15NXl4e8+bNA8Dd3Z2kpCRSU1PJysrilVdeuedw4eYEBATg4eHB3LlzOXHiBCkpKSbBTUMbOTk5xMbGcvHiRTZv3kx8fLxJGTc3N7Kzs8nIyKCwsJCamhoCAgLw8vJi1qxZHD9+nKNHjxIaGoqfnx8+Pj6t7qNGo+Gpp54yedjY2ODg4GCye70gPChZlikpMYz+Flz7ppN78/CJoEcQhC4jLCyMoqIiAgMDjXNwIiIiGDlyJIGBgfj7++Ps7NymXZQVCgXx8fFUVVXh6+vLyy+/zNtvv21SZtq0abz66qssWbIEb29vUlNTWbt2rUmZ6dOnM2nSJCZOnIijoyM7duxAkiS++uor7O3tmTBhAgEBAfTv35+dO3c+8M9CEDpCVdVl6uqKACgvz6Ki4qdO7tHDJY6huIM4hkJ41InjBIT7IX5vnhz5+V/x4+nlxu/79wunX7+lndij9iGOoRAEQRAEwURJaQYAlhaGQ8ILru3rxN48fCLoEQRBEIQnRGlJBgBubouRJAsqKs5TXn6uczv1EImgRxAEQRCeADpdDWXlhv2jHBx+gYPDL4CHN9oj19XR2TNqRNAjCIIgCE+A8vLTyHId5ubdsLLqg1OP5wG4du0fDyUYKdq+nYvPBVK0a1eHt9UUEfQIgiAIwhOgYT6PrdYbSZLo3v1ZFApLKit/orz8TIe3X/ptAnW5ucg17bexaFuJoEcQBEEQngAN83m02uEAKJVqHBz8gY5PcdXl5VGVkQGShOa55zq0reaIoEcQBEEQngAlpScA0Np6G59z6vErAK4V7OvQFFfZP/8JgPXIkZg79eiwdloigh5BEARBeMzV1t6gujoXkLC9NdID0L37L1EorKmqzqGsLLPpCh5QaUIiANrAwA5rozVE0CMIgiAIj7nSW6M8KtUAlEqN8XkzMxXdu08EOi7FVZefT9UPPwCgCey81BaIoEcQhMeAv78/4eHhxu/d3NyIiopq9hpJkti7d2+H9ksQuoqSEkPQcecoTwOnHlMAuFbQMau4jKmtUaMwd3Jq9/rbQgQ9giB0qqlTpzJp0qRGX0tJSUGSJE6ePNmmOtPT01m4cGF7dM9o3bp1eHt7t2udd9cvSZLJY/DgwR3WnvBkKW1kPk8DBwc/zMxsqK65SumtFV7t2nYXSW2BCHoEQehkYWFhJCUlceXKlXte27ZtGz4+PgwbNqxNdTo6OqJSqdqri+2qtrbp5bpDhw4lLy/P+Pj3v//9EHsmPK5kWW+cxGyr9b7ndTMzKxy7BwDtn+Kqy8+n6vhxoPNTWyCCHkEQOtmUKVNwdHQkOjra5Pny8nLi4uIIDg5m5syZ9OrVC5VKhZeXFzt27Gi2zrvTW+fPn2fChAlYWVnh6elJUlLSPdesWrUKDw8PVCoV/fv3Z+3atdTV1QEQHR1NZGQkJ06cMI7CNPQ3JyeHoKAg1Go1Wq2WGTNmUFBQYKy3YYTo888/b/FAT6VSibOzs/HRvXv3Fn56gtCyisqL6HTlKBTW2Nh4NFqmR8MqrmvfIsv6dmvbdNVW56a2AJSd3QFBEDqOLMvU17bfH7C2UFookCSp5XJKJaGhoURHR7NmzRrjNXFxceh0OmbPnk1cXByrVq1Cq9Wyb98+5syZw4ABA/D19W2xfr1ezwsvvICTkxNHjhyhpKTEZP5PA41GQ3R0NC4uLmRmZrJgwQI0Gg0rV64kJCSEU6dOkZCQwP79+wGwtbVFr9cbA57k5GTq6+tZvHgxISEhHDx40Fj3hQsX2L17N3v27MHMzKzJvp4/fx4XFxesrKwYO3Ys77zzDn379m3xHgWhOaUlt1JbmqdQKBp/23dw+AVKpYaamnyKS45hbze6fdpuSG1N6vzUFoigRxAea/W1ej5dltwpbS/c5Ie5ZdNv8HeaP38+GzZsIDk5GX9/f8CQ2po+fTqurq6sWLHCWHbp0qUkJiaya9euVgU9+/fv58yZMyQmJuLi4gLA+vXrmTx5skm5iIgI49dubm6sWLGC2NhYVq5cibW1NWq12jgS0yApKYnMzEyys7Pp06cPADExMQwdOpT09HRGjza8cdTW1hITE4Ojo2OT/RwzZgzR0dEMGjSIvLw8IiMj+cUvfsGpU6fQaDRNXicILWmYp9PYfJ4GCoUljt3/g7z8PVy7tq9dgp66goLbqa1O3JDwTiK9JQhCpxs8eDDjxo1j69atgGFkJCUlhbCwMHQ6HW+++SZeXl5069YNtVpNYmIiOTk5rao7KyuLPn36GAMegLFjx95TbufOnYwfPx5nZ2fUajUREREtttFQd0PAA+Dp6YmdnR1ZWVnG51xdXY0BT0pKCmq12vj48ssvAZg8eTIvvvgiw4YNIzAwkH/84x8UFxezqxPPKRIeD83N57lTD6eGs7gSkGXdA7dblngrtTViBOZ3fFjoTGKkRxAeY0oLBQs3+XVa220RFhbG0qVL2bJlC9u2bWPAgAH4+fnx3nvvsWnTJqKiovDy8sLGxobw8PBmJwS3VVpaGrNmzSIyMpLAwEBsbW2JjY1l48aN7VK/jY2N8WsfHx8yMjKM3zs1Mc/Bzs4ODw8PLly40C59EJ5MOl2l8VwtbSPL1e/UzX48SqUttbXXKSo+Sjf7ez8ctEVp4q3U1uTGV2d2BhH0CMJjTJKkVqeYOtuMGTNYtmwZ27dvJyYmhkWLFiFJEt999x1BQUHMnj0bMMzROXfuHJ6enq2qd8iQIeTm5pKXl0fPnj0BOHz4sEmZ1NRUXF1dWbNmjfG5y5cvm5SxsLBApzP99NtQd25urnG05/Tp0xQXFzfZP2trawYOHNhiv8vLy7l48SJz5sxp+SYFoQmlpacAPZYWTlhZ9Wy2rEJhTg/HQK7m7eJawb4HCnq6YmoLRHpLEIQuQq1WExISwurVq8nLy2PevHkAuLu7k5SURGpqKllZWbzyyismq6NaEhAQgIeHB3PnzuXEiROkpKSYBDcNbeTk5BAbG8vFixfZvHkz8fHxJmXc3NzIzs4mIyODwsJCampqCAgIwMvLi1mzZnH8+HGOHj1KaGgofn5++Pj4tOn+V6xYQXJyMpcuXSI1NZVf//rXmJmZMXPmzDbVIwh3as18njsZU1zXE9Hr6++73bLEf4Isd6nUFoigRxCELiQsLIyioiICAwONc3AiIiIYOXIkgYGB+Pv74+zsTHBwcKvrVCgUxMfHU1VVha+vLy+//DJvv/22SZlp06bx6quvsmTJEry9vUlNTWXt2rUmZaZPn86kSZOYOHEijo6O7NixA0mS+Oqrr7C3t2fChAkEBATQv39/du7c2eZ7v3LlCjNnzmTQoEHMmDEDBwcHDh8+3OzkZ0Foye35PM2nthrY2z2NuXk36upuUlR8uOULmmBMbXWRVVsNJLkjj1V9xJSWlmJra0tJSQlarbazuyMIbVZdXU12dnaL+8EIwp3E783j69/fjaemJp+RI7Zjbz+mVdecObuWn3/ejkvPGQwZ8k6b26wruMYFf3+QZQYe/L+HMtLT2vdvMdIjCIIgCI+h6pp8amryAQUazVOtvs6px50prrYvGCj7563Ulrd3l0ptgQh6BEEQBOGx1LApoVo9CKXSpoXSt9nZjcbCwpH6+hJu3vyu7e0mJgCg6WKpLRBBjyAIgiA8loyTmFs5n6eBJJnRo4dhmXlbz+Kqu3aNqmOGVVtd4YDRu4mgRxAEQRAeQyW3gp6WNiVsjFOPKQBcv56ETlfT6uvK/pl0O7XVs/kl8p1BBD2CIAiC8JjR6+spLc0EWr9c/U62tiOxtHRGpyvn5s2UVl9XmvAt0DVTWyCCHkEQBEF47FRUnEevr8LMTI2NakCbr5ckhfHk9damuLp6agtE0CMIgiAIj52S0h8A0GqHIUn391bvdCvoKSw8gE5X3WJ5Y2pr+PAumdoCEfQIgiAIwmOntI2bEjZGq/XGyqoXOl0FN24cbLF8WULDqq2uc9bW3UTQIwiCIAiPmYagR2s74r7rkCSp1SmuumvXqDx2zNBmYNc5a+tu9xX0bNmyBTc3N6ysrBgzZgxHjx5ttnxcXByDBw/GysoKLy8v/vGPf5i8Lssyr7/+Oj179sTa2pqAgADOnz9vUsbNzQ1Jkkwe7777rvH1S5cu3fO6JEn3HCwoCMLjx9/fn/DwcOP3bm5uREVFNXuNJEns3bu3Q/slCJ2hvr6MiooLwION9MDtjQoLC/+FTlfZZLmypDtSW7eOkOmK2hz07Ny5k+XLl/OnP/2J48ePM3z4cAIDA7l27Vqj5VNTU5k5cyZhYWH88MMPBAcHExwczKlTp4xl3n//fTZv3szHH3/MkSNHsLGxITAwkOpq0xziG2+8QV5envGxdOnSe9rbv3+/SZlRo0a19RYFQXiIpk6dyqQmhsNTUlKQJImTJ0+2qc709HQWLlzYHt0zWrduHd7e3u1a591+/vlnZs+ejYODA9bW1nh5efH99993aJvC46e09CQgY2XVGwuL7g9Ul0bzFNbWfdHrqyks/FeT5cq+7fqpLbiPoOeDDz5gwYIFvPTSS3h6evLxxx+jUqnYunVro+U3bdrEpEmT+OMf/8iQIUN48803GTlyJB9++CFgGOWJiooiIiKCoKAghg0bRkxMDFevXr3nU5hGo8HZ2dn4sLG5d4dJBwcHkzLm5uZtvUVBEB6isLAwkpKSuHLlyj2vbdu2DR8fH4YNG9amOh0dHVGpVO3VxXZVW9v4tv5FRUWMHz8ec3Nzvv32W06fPs3GjRuxt7d/yD0UHnUl97kpYWMMKS7DaE9TKa5HJbUFbQx6amtrOXbsGAEBAbcrUCgICAggLS2t0WvS0tJMygMEBgYay2dnZ5Ofn29SxtbWljFjxtxT57vvvouDgwMjRoxgw4YN1Nffe+z9tGnT6NGjB8888wxff/11s/dTU1NDaWmpyUMQhIdrypQpODo6Eh0dbfJ8eXk5cXFxBAcHM3PmTHr16oVKpcLLy4sdO3Y0W+fd6a3z588zYcIErKys8PT0JCkp6Z5rVq1ahYeHByqViv79+7N27Vrq6uoAiI6OJjIykhMnThhT5w39zcnJISgoCLVajVarZcaMGRQUFBjrbRgh+vzzz5s90PO9996jT58+bNu2DV9fX/r168dzzz3HgAFtX24sPNmMk5gfYD7PnRpSXDduHKS+vuye1xtSW1bDh3Xp1BaAsi2FCwsL0el0ODk5mTzv5OTEmTNnGr0mPz+/0fL5+fnG1xuea6oMwB/+8AdGjhxJt27dSE1NZfXq1eTl5fHBBx8AoFar2bhxI+PHj0ehULB7926Cg4PZu3cv06ZNa7Rv77zzDpGRkW34CQjCo0WWZeprWr+bantSWloiSVLL5ZRKQkNDiY6OZs2aNcZr4uLi0Ol0zJ49m7i4OFatWoVWq2Xfvn3MmTOHAQMG4Ovr22L9er2eF154AScnJ44cOUJJSYnJ/J8GGo2G6OhoXFxcyMzMZMGCBWg0GlauXElISAinTp0iISGB/fv3A4YPZ3q93hjwJCcnU19fz+LFiwkJCeHgwYPGui9cuMDu3bvZs2cPZmZmjfbz66+/JjAwkBdffJHk5GR69erF73//exYsWNDiPQpCA1mWKSnJAB58Pk8DtXowKlV/Kit/4nrhAXo6B5u8XpaQCIA2sGuntqCNQU9nWr58ufHrYcOGYWFhwSuvvMI777yDpaUl3bt3NykzevRorl69yoYNG5oMelavXm1yTWlpKX369Om4mxCEh6y+pobNc3/TKW3/4Yu/Yd7EqMbd5s+fz4YNG0hOTsbf3x8wpLamT5+Oq6srK1asMJZdunQpiYmJ7Nq1q1VBz/79+zlz5gyJiYm43PoUun79eiZPnmxSLiIiwvi1m5sbK1asIDY2lpUrV2JtbY1arUapVOJ8x6nRSUlJZGZmkp2dbfzbERMTw9ChQ0lPT2f06NGAYZQ8JiYGR0fHJvv5008/8dFHH7F8+XJee+010tPT+cMf/oCFhQVz585t8T4FAaC6+gp1dTeQJHPU6qHtUqckSTj1eJ7sS//DtWv/MAl66q9fp/LWvLOuntqCNqa3unfvjpmZmcnQLUBBQYHJH4I7OTs7N1u+4b9tqRNgzJgx1NfXc+nSpWbLXLhwocnXLS0t0Wq1Jg9BEB6+wYMHM27cOOPcwAsXLpCSkkJYWBg6nY4333wTLy8vunXrhlqtJjExkZycnFbVnZWVRZ8+fYwBD8DYsWPvKbdz507Gjx+Ps7MzarWaiIiIFttoqPvOD0uenp7Y2dmRlZVlfM7V1dUY8KSkpKBWq42PL7/8EjCMSI0cOZL169czYsQIFi5cyIIFC/j4449bdZ+CALfn86jVgzEzs2y3ens4NaS4DlFXd3sqSGlDamvYMMx79Wq39jpKm0Z6LCwsGDVqFAcOHCA4OBgw/EM9cOAAS5YsafSasWPHcuDAAZPh5KSkJOMfnX79+uHs7MyBAweMKyNKS0s5cuQIixYtarIvGRkZKBQKevTo0WyZnl10V0hBeBiUlpb84Yu/dVrbbREWFsbSpUvZsmUL27ZtY8CAAfj5+fHee++xadMmoqKi8PLywsbGhvDw8CYnBN+PtLQ0Zs2aRWRkJIGBgdja2hIbG8vGjRvbpf47F134+PiQkZFh/L4htd+zZ088PT1NrhsyZAi7d+9ulz4IT4bb83m827VetY07NjYeVFSc43rhP3HpaRhBNqa2uviqrQZtTm8tX76cuXPn4uPjg6+vL1FRUVRUVPDSSy8BEBoaSq9evXjnnXcAWLZsGX5+fmzcuJHnn3+e2NhYvv/+ez799FPAMGwWHh7OW2+9hbu7O/369WPt2rW4uLgYA6u0tDSOHDnCxIkT0Wg0pKWl8eqrrzJ79mzjyoYvvvgCCwsLRowwTNzas2cPW7du5fPPP3/gH5IgPKokSWp1iqmzzZgxg2XLlrF9+3ZiYmJYtGgRkiTx3XffERQUxOzZswHDB61z587dEyA0ZciQIeTm5pKXl2f8EHT3/l2pqam4urqyZs0a43OXL182KWNhYYFOp2u07tzcXONoz+nTpykuLm6yf9bW1gwcOPCe58ePH8/Zs2dNnjt37hyurq6tuk9BACi9NZ9Hex8nq7fEqcev+Cn7HNcK9uHS8zeG1FZ6uqG9RyC1BfcR9ISEhHD9+nVef/118vPz8fb2JiEhwfhpJScnB4XidtZs3LhxbN++nYiICF577TXc3d3Zu3cvTz31lLHMypUrqaioYOHChRQXF/PMM8+QkJBgXOVgaWlJbGws69ato6amhn79+vHqq6+azMcBePPNN7l8+TJKpZLBgwezc+dOfvObzpnPIAhC26jVakJCQli9ejWlpaXMmzcPAHd3d/72t7+RmpqKvb09H3zwAQUFBa0OegICAvDw8GDu3Lls2LCB0tJSk+CmoY2cnBxiY2MZPXo0+/btIz4+3qSMm5sb2dnZZGRk0Lt3bzQaDQEBAXh5eTFr1iyioqKor6/n97//PX5+fvj4+LTp/l999VXGjRvH+vXrmTFjBkePHuXTTz81fkAUhJbo9bWUlf8ItN8k5js5OU3hp+wobhalUldXdHvV1iOS2gJAFoxKSkpkQC4pKensrgjCfamqqpJPnz4tV1VVdXZX7ktqaqoMyL/61a+Mz924cUMOCgqS1Wq13KNHDzkiIkIODQ2Vg4KCjGX8/PzkZcuWGb93dXWV//KXvxi/P3v2rPzMM8/IFhYWsoeHh5yQkCADcnx8vLHMH//4R9nBwUFWq9VySEiI/Je//EW2tbU1vl5dXS1Pnz5dtrOzkwF527ZtsizL8uXLl+Vp06bJNjY2skajkV988UU5Pz/feN2f/vQnefjw4a26/7///e/yU089JVtaWsqDBw+WP/3001Zd96Ae9d8bwaCk5IS8/0B/+WDySFmv13dIG4ePTJH3H+gvX7myQ740J1Q+PWiwXPj5XzukrbZo7fu3JMuy3KlRVxdSWlqKra0tJSUlYlKz8Eiqrq4mOzu72f1gBOFu4vfm8ZB7JYZz5yJxcPDDe3jjGwY/qEuXPubiTxvoJvlitfgk6PUM2L8fi96dO9LT2vdvceCoIAiCIDwGSktuHTLaAfN5Gjg5GQ4grU4+Bno9Vl5enR7wtIUIegRBEAThMVBS+gPQMfN5Glhb90Wj8cL6uGETUe2kwA5rqyOIoEcQBEEQHnF1dUVUVRlWHLbHmVvN6WHhj8V5Q9CjeQR2Yb6TCHoEQRAE4RFXcmt/HpWqH+bmdh3aluqEFZIsUeuqR3Z8ZA52AETQIwiCIAiPvNv783TsKA9A1QHDPldVI/Vcu5bQ4e21JxH0CIIgCMIjruH4iY6cxAxQf+OGcUPC6hF6Cq7t69D22psIegRBEAThESbLMqWlJwGw7eCgpywpCfR6LIYOQtddoqTkGNXVVzu0zfYkgh5BEARBeIRVVV2ivr4EhcIStXpwh7ZVeuusLbvJU7CzNew6fu3atx3aZnsSQY8gCIIgPMJKSgxL1TWaoSgU5h3WTv2NG1QePWpoa9Ik48nrj1KKSwQ9giA88vz9/QkPDzd+7+bmRlRUVLPXSJLE3r17O7RfgvAwNKzc6uj5PGVJ+w0bEj71FBa9e9Ojx2RAQWnpCaqqcju07fYigh5BEDrV1KlTmTSp8b0+UlJSkCSJkydPtqnO9PR0Fi5c2B7dM1q3bh3e3t7tWuedDh06xNSpU3FxcWkyIFu3bh2DBw/GxsYGe3t7AgICOHLkSIf1SXg0lN6axNzR83lKEwwrtRo2JLS06I69/RgArl37R4e23V5E0CMIQqcKCwsjKSmJK1eu3PPatm3b8PHxYdiwYW2q09HREZVK1V5dbFe1tbWNPl9RUcHw4cPZsmVLk9d6eHjw4YcfkpmZyb///W/c3Nx47rnnuH79ekd1V+jidLpqysvPAB070nN3aqtBjx6GYykelRSXCHoEQehUU6ZMwdHRkejoaJPny8vLiYuLIzg4mJkzZ9KrVy9UKhVeXl7s2LGj2TrvTm+dP3+eCRMmYGVlhaenJ0lJSfdcs2rVKjw8PFCpVPTv35+1a9dSV1cHQHR0NJGRkZw4cQJJkpAkydjfnJwcgoKCUKvVaLVaZsyYQUFBgbHehhGizz//vNkDPSdPnsxbb73Fr3/96ybv6z//8z8JCAigf//+DB06lA8++IDS0tI2j4QJj4+yslPIcj0WFt2xsnLpuHYaUltDh2LRu7fx+R6Ok5AkM8rKfqSy8lKHtd9eHq2tFAVBaBNZlpHr9J3StmSuQJKkFssplUpCQ0OJjo5mzZo1xmvi4uLQ6XTMnj2buLg4Vq1ahVarZd++fcyZM4cBAwbg6+vbYv16vZ4XXngBJycnjhw5QklJicn8nwYajYbo6GhcXFzIzMxkwYIFaDQaVq5cSUhICKdOnSIhIYH9+/cDYGtri16vNwY8ycnJ1NfXs3jxYkJCQjh48KCx7gsXLrB792727NmDmZlZ636ALaitreXTTz/F1taW4cM7fkM6oWsqvWM+T2v+vd13O4mG1JbmrrO2LCy6YW8/jps3Uyi4to9+bos7rA/tQQQ9gvAYk+v0XH09tVPadnljHJJF697g58+fz4YNG0hOTsbf3x8wpLamT5+Oq6srK1asMJZdunQpiYmJ7Nq1q1VBz/79+zlz5gyJiYm4uBg+Ca9fv57JkyeblIuIiDB+7ebmxooVK4iNjWXlypVYW1ujVqtRKpU4OzsbyyUlJZGZmUl2djZ9+vQBICYmhqFDh5Kens7o0aMBQ4ASExODo6Njq34ezfnmm2/47W9/S2VlJT179iQpKYnu3bs/cL3Co6nkIcznqb95k8ojhtSWtpH5d049nufmzRSuFXT9oEektwRB6HSDBw9m3LhxbN26FTCMjKSkpBAWFoZOp+PNN9/Ey8uLbt26oVarSUxMJCcnp1V1Z2Vl0adPH2PAAzB27Nh7yu3cuZPx48fj7OyMWq0mIiKixTYa6m4IeAA8PT2xs7MjKyvL+Jyrq6sx4ElJSUGtVhsfX375Zavuo8HEiRPJyMggNTWVSZMmMWPGDK5du9amOoTHx+2Rno4b7TNJbd3xu97A0fE5JMmc8oqzVFRc6LB+tAcx0iMIjzHJXIHLG+M6re22CAsLY+nSpWzZsoVt27YxYMAA/Pz8eO+999i0aRNRUVF4eXlhY2NDeHh4kxOC70daWhqzZs0iMjKSwMBAbG1tiY2NZePGje1Sv42NjfFrHx8fMjIyjN87OTm1ua6BAwcycOBAnn76adzd3fnrX//K6tWr26WvwqOjpuY61dU/AxJarVeHtVPWRGqrgbm5Ld26PcONG/9HwbV/0L/fHzqsLw9KBD2C8BiTJKnVKabONmPGDJYtW8b27duJiYlh0aJFSJLEd999R1BQELNnzwYMc3TOnTuHp6dnq+odMmQIubm55OXl0bNnTwAOHz5sUiY1NRVXV1fWrFljfO7y5csmZSwsLNDpdI3WnZubaxztOX36NMXFxU32z9ramoEDB7aq762h1+upqalpt/qER0fDUnUbG3eUSk2HtFF/8yYVhw3bIjSW2mrg1ONXhqCnYB/93JZ26PyiByHSW4IgdAlqtZqQkBBWr15NXl4e8+bNA8Dd3Z2kpCRSU1PJysrilVdeMVkd1ZKAgAA8PDyYO3cuJ06cICUlxSS4aWgjJyeH2NhYLl68yObNm4mPjzcp4+bmRnZ2NhkZGRQWFlJTU0NAQABeXl7MmjWL48ePc/ToUUJDQ/Hz88PHx6dN919eXk5GRoZxFKihrYYUW0VFBa+99hqHDx/m8uXLHDt2jPnz5/Pzzz/z4osvtqkt4fHQsClhR87nMaa2PD0bTW01cHT8DyTJgsrKC1RUnOuw/jwoEfQIgtBlhIWFUVRURGBgoHEOTkREBCNHjiQwMBB/f3+cnZ0JDg5udZ0KhYL4+Hiqqqrw9fXl5Zdf5u233zYpM23aNF599VWWLFmCt7c3qamprF271qTM9OnTmTRpEhMnTsTR0ZEdO3YgSRJfffUV9vb2TJgwwbicfOfOnW2+9++//54RI0YwYsQIAJYvX86IESN4/fXXATAzM+PMmTNMnz4dDw8Ppk6dyo0bN0hJSWHo0KFtbk949JXeOn6iQ+fzGFNbTY/yACiVGhwcJgBQUPBNh/XnQUmyLMud3YmuorS0FFtbW0pKStBqtZ3dHUFos+rqarKzs5vdD0YQ7iZ+bx49sqwj+dAIdLoKfH33oemAg0bri4o4/8wvQKdjwD8Tsejbt9ny+flf8+PpV7G2dmPs0/sfaoqrte/fYqRHEARBEB4xFRUX0OkqMDNTobZx75A2ypKSQKfD0nNIiwEPQPfuz6JQWFJVdYny8tMd0qcHJYIeQRAEQXjENCxV12i8kKSOWaxQlpAIgHbS5BZKGiiVNjg4TASgoODeYyl+SPg78e+/wdm0lPbrZBuJoEcQBEEQHjElt+bzdNQk5vqiIiqONKzaanypemOcnJ4HDGdx3T17Ju/8WX46dpTS6523r5QIegRBEAThEWPclNC2YyYxtzW11aC7w0TMzFRUV1+htMz0TLiyG4UAaBw6bwdxEfQIgiAIwiOkvr6C8orzQMeN9BhTW4HNr9q6m5mZNd0dfgnAtbtSXGU3rgOgcXjw41julwh6BEEQBOERUlaWCeixtOyJpWXbdvRujftNbTW4neL6B7JsOPBY1uspu3EDECM9giAIgiC0UklJBtCBozz79xtSW0OGYOHq2ubru3Xzw8xMTU1NHiWlhrlHlaUl6HX1IEnY2Hdr7y63mgh6BEEQBOER0nD8hNbWu0Pqv53aavsoD4CZmSWO3QOA26u4ygoNqS21nT1mys47AUsEPYIgCILwiJBluUOPn6gvKqLi1tl095PaatCQ4rp27VtkWUfZzYZJzJ03nwdE0CMIwmPA39+f8PBw4/dubm5ERUU1e40kSezdu7dD+yUI7a2mJo/a2mtIkhkaTfsfP1J+4MDt1Jab233X063bMyiVWmprr1Fc/H2XWLkFIugRBKGTTZ06lUlNnOuTkpKCJEmcPHmy0debkp6ezsKFC9uje0br1q3D29u7Xeu806FDh5g6dSouLi5NBmTl5eUsWbKE3r17Y21tjaenJx9//HGH9UnoekpupbbUNoMxM7Nu9/pLvzWctXW/qa0GCoUFjo7PAYYJzQ1Bj1oEPYIgPMnCwsJISkriypUr97y2bds2fHx8GDZsWJvqdHR0RKVStVcX21VtbW2jz1dUVDB8+HC2bNnS5LXLly8nISGB//3f/yUrK4vw8HCWLFnC119/3VHdFbqY0luTmDtiPk97pbYaOPX4FWBIcZUWGjYkFCM9giA80aZMmYKjoyPR0dEmz5eXlxMXF0dwcDAzZ86kV69eqFQqvLy82LFjR7N13p3eOn/+PBMmTMDKygpPT0+SkpLuuWbVqlV4eHigUqno378/a9eupa6uDoDo6GgiIyM5ceIEkiQhSZKxvzk5OQQFBaFWq9FqtcyYMYOCggJjvQ0jRJ9//nmzB3pOnjyZt956i1//+tdN3ldqaipz587F398fNzc3Fi5cyPDhwzl69GizPw/h8XF7Pk/7b0poTG0NHvxAqa0G9vbjMDe3p67uBsUF2UDnz+npvCnUgiB0OFmWjW/cD5u5uXmrTllWKpWEhoYSHR3NmjVrjNfExcWh0+mYPXs2cXFxrFq1Cq1Wy759+5gzZw4DBgzA19e3xfr1ej0vvPACTk5OHDlyhJKSEpP5Pw00Gg3R0dG4uLiQmZnJggUL0Gg0rFy5kpCQEE6dOkVCQgL79+8HwNbWFr1ebwx4kpOTqa+vZ/HixYSEhHDw4EFj3RcuXGD37t3s2bMHM7P7Pydp3LhxfP3118yfPx8XFxcOHjzIuXPn+Mtf/nLfdQqPDr2+7tYePaDtgEnMpcazth58lAdAoTDH0fE5rl7dSemNrjHSI4IeQXiM1dXVsX79+k5p+7XXXsPCwqJVZefPn8+GDRtITk7G398fMKS2pk+fjqurKytWrDCWXbp0KYmJiezatatVQc/+/fs5c+YMiYmJuLi4ALB+/XomTzY9RDEiIsL4tZubGytWrCA2NpaVK1dibW2NWq1GqVTi7OxsLJeUlERmZibZ2dn06dMHgJiYGIYOHUp6ejqjR48GDCmtmJgYHB0f7FPu//zP/7Bw4UJ69+6NUqlEoVDw2WefMWHChAeqV3g0lFecRa+vQanUoFL1a9e6dcXFxtSW5gHn89zJqcfz/HxlJ9WltYCEprtIbwmC8IQbPHgw48aNY+vWrYBhZCQlJYWwsDB0Oh1vvvkmXl5edOvWDbVaTWJiIjk5Oa2qOysriz59+hgDHoCxY8feU27nzp2MHz8eZ2dn1Go1ERERLbbRUHdDwAPg6emJnZ0dWVlZxudcXV2NAU9KSgpqtdr4+PLLL1t1H2AIeg4fPszXX3/NsWPH2LhxI4sXLzaOPgmPt9KSW+dtab2RpPZ9+y47cADq67EcPBjLfu0XUNnZjYE6R5AlJIWEjZ19u9V9P8RIjyA8xszNzXnttdc6re22CAsLY+nSpWzZsoVt27YxYMAA/Pz8eO+999i0aRNRUVF4eXlhY2NDeHh4kxOC70daWhqzZs0iMjKSwMBAbG1tiY2NZePGje1Sv42NjfFrHx8fMjIyjN87ObXuGIGqqipee+014uPjef55wx4ow4YNIyMjgz//+c8EBAS0S1+Frsu4KWEHzOdp79RWA4VCiY35eOAslmolCsX9p3fbgwh6BOExJklSq1NMnW3GjBksW7aM7du3ExMTw6JFi5Akie+++46goCBmz54NGObonDt3Dk9Pz1bVO2TIEHJzc8nLy6Nnz54AHL41jN8gNTUVV1dX1qxZY3zu8uXLJmUsLCzQ6XSN1p2bm2sc7Tl9+jTFxcVN9s/a2pqBAwe2qu93qquro66uDoXC9BO+mZkZer2+zfUJj56G5ertvSmhrriYirQ0oH1TWw0seQo4i5mqHL2+FoWi8/4mifSWIAhdglqtJiQkhNWrV5OXl8e8efMAcHd3JykpidTUVLKysnjllVdMVke1JCAgAA8PD+bOncuJEydISUkxCW4a2sjJySE2NpaLFy+yefNm4uPjTcq4ubmRnZ1NRkYGhYWF1NTUEBAQgJeXF7NmzeL48eMcPXqU0NBQ/Pz88PHxadP9l5eXk5GRYRwFamirIcWm1Wrx8/Pjj3/8IwcPHiQ7O5vo6GhiYmKaXfElPB7q6kqorPwJaP+RHmNqa9Cgdk1tNdBX2wJgblPNzZv/bvf62+K+gp4tW7bg5uaGlZUVY8aMaXG5ZFxcHIMHD8bKygovLy/+8Y9/mLwuyzKvv/46PXv2xNramoCAAM6fP29Sxs3NzbhUtOHx7rvvmpQ5efIkv/jFL7CysqJPnz68//7793N7giB0krCwMIqKiggMDDTOwYmIiGDkyJEEBgbi7++Ps7MzwcHBra5ToVAQHx9PVVUVvr6+vPzyy7z99tsmZaZNm8arr77KkiVL8Pb2JjU1lbVr15qUmT59OpMmTWLixIk4OjqyY8cOJEniq6++wt7engkTJhAQEED//v3ZuXNnm+/9+++/Z8SIEYwYMQIw7MkzYsQIXn/9dWOZ2NhYRo8ezaxZs/D09OTdd9/l7bff5ne/+12b2xMeLaWlhg06ra37YmHRvgd2dlRqq0H5TcPp6uY29cazuDqN3EaxsbGyhYWFvHXrVvnHH3+UFyxYINvZ2ckFBQWNlv/uu+9kMzMz+f3335dPnz4tR0REyObm5nJmZqaxzLvvvivb2trKe/fulU+cOCFPmzZN7tevn1xVVWUs4+rqKr/xxhtyXl6e8VFeXm58vaSkRHZycpJnzZolnzp1St6xY4dsbW0tf/LJJ62+t5KSEhmQS0pK2vpjEYQuoaqqSj59+rTJvx1BaIn4ven6fvpps7z/QH8581R4u9ZbX1Qknx76lHx60GC5+uJP7Vp3g683rpf/PON5eUfUKPn/Dg6T6+ur272N1r5/t3mk54MPPmDBggW89NJLxi3QVSqVcdXF3TZt2sSkSZP44x//yJAhQ3jzzTcZOXIkH374YUPQRVRUFBEREQQFBTFs2DBiYmK4evXqPduwazQanJ2djY87Jwd++eWX1NbWsnXrVoYOHcpvf/tb/vCHP/DBBx+09RYFQRAEoUu5PZ+nvVNb/zKktjw8sOzf/qktwHgEhcpOg05Xzs2byR3STmu0Keipra3l2LFjJqsEFAoFAQEBpN2aBHW3tLS0e1YVBAYGGstnZ2eTn59vUsbW1pYxY8bcU+e7776Lg4MDI0aMYMOGDdTX15u0M2HCBJNJm4GBgZw9e5aioqJG+1ZTU0NpaanJQxAEQRC6ElmWKb21E7PWdkS71l2aeOusrcmNn3/XHspuXAfAue/TANwsajxeeBjatHqrsLAQnU53zxJLJycnzpw50+g1+fn5jZbPz883vt7wXFNlAP7whz8wcuRIunXrRmpqqnGyY8NITn5+Pv3umoDVUGd+fj729vfuDfDOO+8QGRnZ4n0LgiAIQmepqsqhrq4ISbJAox7cbvXqSkqoSG1YtdUxQY+uvp7yYsPAw4DBszFXz0Kjbv/T4VvrkVmyvnz5cuPXw4YNw8LCgldeeYV33nkHS0vL+6pz9erVJvWWlpaabDImCIIgCJ2tYZRHo/FEobi/97vGlO0/0OGprYrimyDLKMyUdHMagqTo3EXjbWq9e/fumJmZ3bNctKCgwGRr9js5Ozs3W77hv22pE2DMmDHU19dz6dKlZtu5s427WVpaotVqTR6CIAiC0JWUlP4AtP9S9YbUlqaDVm0BlBUa5vNoHBw6PeCBNgY9FhYWjBo1igMHDhif0+v1HDhwoNFt3cGw3fud5cFwXk1D+X79+uHs7GxSprS0lCNHjjRZJ0BGRgYKhYIePXoY2zl06JDJ4YpJSUkMGjSo0dSWIAiCIDwKSo0nq3u3W526khIq0gybdGondfx8HnW3zj1zq0Gbw67ly5fz2Wef8cUXX5CVlcWiRYuoqKjgpZdeAiA0NJTVq1cbyy9btoyEhAQ2btzImTNnWLduHd9//z1LliwBDDvGhoeH89Zbb/H111+TmZlJaGgoLi4uxr040tLSiIqK4sSJE/z00098+eWXvPrqq8yePdsY0Pznf/4nFhYWhIWF8eOPP7Jz5042bdpkkr4SBEEQhEeJXl9DWZnhHDdbW+92q7fswL+grg5Ld3cs+/dvt3rvaedGw0hP1wh62jynJyQkhOvXr/P666+Tn5+Pt7c3CQkJxknDOTk5Jtukjxs3ju3btxMREcFrr72Gu7s7e/fu5amnnjKWWblyJRUVFSxcuJDi4mKeeeYZEhISsLKyAgxpqNjYWNatW0dNTQ39+vXj1VdfNQlobG1t+ec//8nixYsZNWoU3bt35/XXX2fhwoX3/cMRBEEQhM5UVnYaWa7F3LwbVlbtN+f0YaS2AMpu3gp6ujt2aDutJcmyLHd2J7qK0tJSbG1tKSkpEfN7hEdSdXU12dnZ9OvXz/ihQRBaIn5vuq6c3G2cP/8WDg4T8R7+ebvUqSst5dz4Z6Cujv77vsFywIB2qbcxX/35bS6kp/HL+b9jROCUDmunte/fnT+rSBAE4QH5+/sTHh5u/N7NzY2oqKhmr5Ek6Z4NUAWhq+mI+Twmqa0ODHjgzvRW1xjpEUGPIAidaurUqUxqYiJlSkoKkiRx8uTJNtWZnp7e7qntdevW4e3t3a513umdd95h9OjRaDQaevToQXBwMGfPnjW+funSpXvOH2x4xMXFdVi/hM5VWtKwKaF3u9VZtn8/0PGpLbg9kVnTzaHD22oNEfQIgtCpwsLCSEpK4sqVK/e8tm3bNnx8fBg2bFib6nR0dESlUrVXF9tVbW1to88nJyezePFiDh8+TFJSEnV1dTz33HNUVFQA0KdPH/Ly8kwekZGRqNVqJk+e/DBvQXhIamtvUFWdA4BW07Z/A02R6+qoPGxYtaX282+XOptSX1dHZUkx0HXm9IigRxCETjVlyhQcHR2Jjo42eb68vJy4uDiCg4OZOXMmvXr1QqVS4eXlxY4dO5qt8+701vnz55kwYQJWVlZ4enqSlJR0zzWrVq3Cw8MDlUpF//79Wbt2rXELjOjoaCIjIzlx4oRxdKWhvzk5OQQFBaFWq9FqtcyYMcNkz7CGEaLPP/+82TkzCQkJzJs3j6FDhzJ8+HCio6PJycnh2LFjAJiZmZmcPejs7Ex8fDwzZsxArVa39GMWHkENqS2VaiDm5u0zz7QqMxN9RQVmdnZYeQ5plzqb0nC6utLcAmtN15gn+8jsyCwIQtvJsoxeX9UpbSsU1kiS1GI5pVJJaGgo0dHRrFmzxnhNXFwcOp2O2bNnExcXx6pVq9Bqtezbt485c+YwYMAAfH19W6xfr9fzwgsv4OTkxJEjRygpKTGZ/9NAo9EQHR2Ni4sLmZmZLFiwAI1Gw8qVKwkJCeHUqVMkJCSw/1ZqwNbWFr1ebwx4kpOTqa+vZ/HixYSEhHDw4EFj3RcuXGD37t3s2bMHMzOzVv38SkpKAOjWrVujrx87doyMjAy2bNnSqvqER09HHDJa8V0qAKqxT3f4ZoHlt+bzqB0cWvW34GEQQY8gPMb0+ioOJnt1Stv+fpmYmbUuxTR//nw2bNhAcnIy/v7+gCG1NX36dFxdXVmxYoWx7NKlS0lMTGTXrl2tCnr279/PmTNnSExMxMXFBYD169ffkxKKiIgwfu3m5saKFSuIjY1l5cqVWFtbo1arUSqVJju8JyUlkZmZSXZ2tvEIm5iYGIYOHUp6ejqjR48GDCmtmJgYHB1bN8Sv1+sJDw9n/PjxJtt73Omvf/0rQ4YMYdy4ca2qU3j0dMR8nopUQ9Bj8xB+b4zzebrIJGYQ6S1BELqAwYMHM27cOLZu3QoYRkZSUlIICwtDp9Px5ptv4uXlRbdu3VCr1SQmJpKTk9OqurOysujTp48x4AEa3e19586djB8/HmdnZ9RqNRERES220VD3nWf2eXp6YmdnR1ZWlvE5V1dXY8CTkpKCWq02Pr788st76l28eDGnTp0iNja20XarqqrYvn07YWFhzd+88MiSZX27j/ToysqourUoQP0Qgp7ShpVbXWQSM4iRHkF4rCkU1vj7ZXZa220RFhbG0qVL2bJlC9u2bWPAgAH4+fnx3nvvsWnTJqKiovDy8sLGxobw8PAmJwTfj7S0NGbNmkVkZCSBgYHY2toSGxvLxo0b26V+Gxsb49c+Pj5kZGQYv2/Y2LXBkiVL+Oabbzh06BC9e/dutL6//e1vVFZWEhoa2i79E7qeysqf0OnKUSissLEZ1D51Hj0KOh3mrn0x79WrXepsjnG5eheZxAwi6BGEx5okSa1OMXW2GTNmsGzZMrZv305MTAyLFi1CkiS+++47goKCmD17NmBI/Zw7dw5PT89W1TtkyBByc3PJy8ujZ8+eABy+tXqlQWpqKq6urqxZs8b43OXLl03KWFhYoNPpGq07NzfXONpz+vRpiouLm+yftbU1AwcOvOd5WZZZunQp8fHxHDx4kH79mj71+q9//SvTpk1rdbpMePQ0jPJoNV4oFO3zVl2RmgY8nNQW3Jne6hpHUIBIbwmC0EWo1WpCQkJYvXo1eXl5zJs3DwB3d3eSkpJITU0lKyuLV155xWR1VEsCAgLw8PBg7ty5nDhxgpSUFJPgpqGNnJwcYmNjuXjxIps3byY+Pt6kjJubG9nZ2WRkZFBYWEhNTQ0BAQF4eXkxa9Ysjh8/ztGjRwkNDcXPzw8fH5823f/ixYv53//9X7Zv345GoyE/P5/8/Hyqqkwnol+4cIFDhw7x8ssvt6l+4dHSsHJLa9uOk5gf4nwegPIbhtVbYk6PIAhCI8LCwigqKiIwMNA4ByciIoKRI0cSGBiIv78/zs7OxsOIW0OhUBAfH09VVRW+vr68/PLLvP322yZlpk2bxquvvsqSJUvw9vYmNTWVtWvXmpSZPn06kyZNYuLEiTg6OrJjxw4kSeKrr77C3t6eCRMmEBAQQP/+/dm5c2eb7/2jjz6ipKQEf39/evbsaXzcXdfWrVvp3bs3zz33XJvbEB4dJSUZAGjbaSfmurw8arOzQaHAZsyYdqmzJV1xpEecvXUHcfaW8KgTZygJ90P83nQtOl0VyYeGI8s6xo/7N1ZWPR+4zuLdu8lbE4H18OG47Wx8gnx7qqutYfOc6QD8/q87sFZrOrQ9cfaWIAiCIDyCSstOIcs6LC2c2iXggdv789iMf1ipLcMkZqWlJVY2hs0zKyoqqK6ufijtN0UEPYIgCILQhZSW/AC033weWa+nIu1hT2K+fdBow8aEaWlpvPfee/zf//3fQ+lDY0TQIwiCIAhdSEnDJOZ2ms9Tc+YMuqIiFCoV1sPbb2J0c24HPbfn8+Tk5CDLMra2tg+lD40RQY8gCIIgdCGlxk0JvdulvoZVWypfXyRz83apsyV3Bz11dXX8/PPPgGGzzs4igh5BEARB6CKqa/KpqckHFGg0jR9B0la3l6rfuxN5R7n7CIqrV6+i0+mwsbFp8jy5h0EEPYIgCILQRTSct6VWe6BU2rRQumX66moqvz8GPLz5PHDnSI/hCIqGI11cXV079fBREfQIgiAIQhfRkNpqr/k8VcePI9fWouzRA4sBA9qlzta4cyIz3N7hvG/fvg+tD40RQY8gCIIgdBENk5jbez6PzbhxD3WEpfyOOT16vZ7c3FxABD2CIAiCIAB6fT1lZYYDgrXtdLJ6eerD3Z8HoK66muqKcsAw0lNQUEBNTQ0WFhb3HLD7sImgRxCER56/vz/h4eHG793c3IiKimr2GkmS2Lt3b4f2SxDaoqLiPDpdJWZmamxsHjwVVX/zJjWnswCwGXt7EnN52lWuvnWY2qvlD9xGY0pvTWK2sFZhqVIZ5/P06dMHMzOzDmmztUTQIwhCp5o6dSqTJk1q9LWUlBQkSeLkyZNtqjM9PZ2FCxe2R/eM1q1bh7e3d7vWeaePPvqIYcOGodVq0Wq1jB07lm+//dakzKeffoq/vz9arRZJkiguLu6w/ggP3+35PMOQpAcPDho2JLQcNAhld8PScVknU3ogB315HRXp+Q/cRmPuXq7eEPR0d+5FUUVth7TZWiLoEQShU4WFhZGUlMSVK1fueW3btm34+PgwbNiwNtXp6OiISqVqry62q9raxv/o9+7dm3fffZdjx47x/fff88tf/pKgoCB+/PFHY5nKykomTZrEa6+99rC6KzxEt+fztE9qq7FT1asvFKEvrwOg5nxxu7RztzsPGpVl2TiJ+ftCBSPeTGJD4pkOabc1RNAjCI8xWZap0Ok65dHas4ynTJmCo6Mj0dHRJs+Xl5cTFxdHcHAwM2fOpFevXqhUKry8vNixY0ezdd6d3jp//jwTJkzAysoKT09PkpKS7rlm1apVeHh4oFKp6N+/P2vXrqWuzvDmEB0dTWRkJCdOnECSJCRJMvY3JyeHoKAg1Go1Wq2WGTNmUFBQYKy3YYTo888/b/ZAz6lTp/KrX/0Kd3d3PDw8ePvtt1Gr1Rw+fNhYJjw8nP/+7//m6aefbvb+hUdTe67ckmWZitR7j56oPH7N+HV9YRX1N9v/LKzyGzcAQ9BTVFREeXk5CoWCM2WGjRH7duu8DyTKTmtZEIQOV6nXM+BQZqe0fXGCFzatyN8rlUpCQ0OJjo5mzZo1xhUmcXFx6HQ6Zs+eTVxcHKtWrUKr1bJv3z7mzJnDgAED8PX1bbF+vV7PCy+8gJOTE0eOHKGkpMRk/k8DjUZDdHQ0Li4uZGZmsmDBAjQaDStXriQkJIRTp06RkJDA/v37AbC1tUWv1xsDnuTkZOrr61m8eDEhISEcPHjQWPeFCxfYvXs3e/bsadWcBp1OR1xcHBUVFYwd+/A2lBM6T319GRUVFwDQ2no/cH212Zeoz8tDMjdH5TMKAH11PdWnDQGJQmOOvqyO6vNFqMe0z6GmDe7cmLAhteXi4sLunysAGOrSecdQiKBHEIRON3/+fDZs2EBycjL+/v6AIbU1ffp0XF1dWbFihbHs0qVLSUxMZNeuXa0Kevbv38+ZM2dITEzExcUFgPXr1zN58mSTchEREcav3dzcWLFiBbGxsaxcuRJra2vUajVKpRJnZ2djuaSkJDIzM8nOzqZPnz4AxMTEMHToUNLT0xk9ejRgSGnFxMTg6OjYbF8zMzMZO3Ys1dXVqNVq4uPj8fT0bPEehUdfaelJQMbKqjeWFt1bLN+SijRDast65EgU1tYAVJ0qRK7To3S0RuXdg9Kky1Sf64ig5/acnou3UlvdnFwouVCHUiHh7qRu1/baQgQ9gvAYUykUXJzg1Wltt9bgwYMZN24cW7duxd/fnwsXLpCSksIbb7yBTqdj/fr17Nq1i59//pna2lpqampaPWcnKyuLPn36GAMeoNHRk507d7J582YuXrxIeXk59fX1aLXaVtXdEPAAeHp6YmdnR1ZWljHocXV1NQY8KSkpJgHXJ598wqxZswAYNGgQGRkZlJSU8Le//Y25c+eSnJwsAp8nQKnxkNH2ms/TdGpLNbIHVgPtKU26TM2FYmSdjGTWfnv4NAQ9aofu5Jz6NwC1Vg5AIe5OGiyVnbeCSwQ9gvAYkySpVSmmriAsLIylS5eyZcsWtm3bxoABA/Dz8+O9995j06ZNREVF4eXlhY2NDeHh4U1OCL4faWlpzJo1i8jISAIDA7G1tSU2NpaNGze2S/02NrePE/Dx8SEjI8P4/Z37llhYWDBw4EAARo0aRXp6Ops2beKTTz5pl34IXVdJOx4yKtfXU3nkCHA76KkvrqYmuwQAlXcPzGwtUaiU6Cvrqc0txdKt/VJODektM5UNN27N7/m5zvAhZahL8x8kOpqYyCwIQpcwY8YMFAoF27dvJyYmhvnz5yNJEt999x1BQUHMnj2b4cOH079/f86dO9fqeocMGUJubi55eXnG5+6cHAyQmpqKq6sra9aswcfHB3d3d+OKkwYWFhbodLpG627YbRbg9OnTFBcXNzk6Y21tzcCBA40PjUbTZN/1ej01NTWtvlfh0STLMiUlGQBobR98pKfqZCb68nLMbG2x8hwCQOUP10EGy/62KO2tkBQSlgPtAKhux1VcNZWV1FZVAVBcYfhvjx49yLpumDAtgh5BEARArVYTEhLC6tWrycvLY968eQC4u7uTlJREamoqWVlZvPLKKyaro1oSEBCAh4cHc+fO5cSJE6SkpLBmzRqTMu7u7uTk5BAbG8vFixfZvHkz8fHxJmXc3NzIzs4mIyODwsJCampqCAgIwMvLi1mzZnH8+HGOHj1KaGgofn5++Pj4tOn+V69ezaFDh7h06RKZmZmsXr2agwcPGlNfAPn5+WRkZHDhgmHCa2ZmJhkZGdy8ebNNbQldS3X1z9TV3UCSzNGohz5wfQ1L1VVjxyKZmSHLMpU/GP7NqEb0MJazcrcHoOZc0QO32aBhlMfKRs3Ptz5o9O3blx+vlgKdO4kZRNAjCEIXEhYWRlFREYGBgcY5OBEREYwcOZLAwED8/f1xdnYmODi41XUqFAri4+OpqqrC19eXl19+mbffftukzLRp03j11VdZsmQJ3t7epKamsnbtWpMy06dPZ9KkSUycOBFHR0d27NiBJEl89dVX2NvbM2HCBAICAujfvz87d+5s871fu3aN0NBQBg0axLPPPkt6ejqJiYn8x3/8h7HMxx9/zIgRI1iwYAEAEyZMYMSIEXz99ddtbk/oOhqWqqvVgzEza3xLg7a4vT+PYe5a3c/l1F+rAqUCa6/bk6QtbwU9tVfK0FfWPXC7YDqJuWHlloNTL/JKDCM9Q3o2PbL5MEhyazfTeAKUlpZia2tLSUlJixMYBaErqq6uJjs7u9n9YAThbuL3pnOdO/8Wubnb6N1rDoMGrXugunTl5Zwb8zTodAzYvx+L3r0o/voi5alXsR7uiMPMwSbl8z/4nvprVXT7z8GohjW/urA1Th5IIOnTD+nr7cPpWglZlhkfHMqC2CzcHFQc/OPEB26jMa19/xYjPYIgCILQiUqN83m8H7iuyqNHQafD3LUvFr17Iev0VJ4wpJzuTG01MKa42mleT8NID2otsixja2vLxRLD2Epnp7ZABD2CIAiC0Gn0+lrKyg1HjbTH8RMV391Kbd3alqH6fDH6ijoUanNjgHMnSw/7W+WKWr2LenPKCg1BT7WZBWA6n8ezkycxgwh6BEEQBKHTlJefQa+vRam0w9ra7YHru/u8rcrjtyYwD3dsdC8ey362YCahK66h/nrVA7dfdtMQ9JTWGuYIubq68uNVw1L5zl65BSLoEQRBEIROc3t/nmHGI1juV11eHrXZ2aBQYPP00+ir66m6deyEaqRTo9coLMwMgQ+G0Z4HVXajEBmJm2XlADj27EV2YecfP9FABD2CIAiC0ElKS27txGw74oHratiF2crrKcy0WqoyC6FeRtlDhbmLTZPXtdfSdVmWKbtxHb2VCp1Oh7W1NdfrLJBl6KGxxFFj+UD1twcR9AjCA9LV13FyfwKXThxH1us7uzuCIDxCbo/0tMN8nrtSWxUNqa2RPZodRbJ0twOg5qcS5Pr7/xtWXVFOfU0NOpXhbK2+fftyOq8M6BqpLRDHUAjCA6kqL+PvG9eTe9pwkrm9S29GTJrC0Am/xMK6dWdDCUJXUqevIyE7gdLaUn7j8RsszTr/0/njqq6uiKqqS8CDn7kl6/VUpBlGetTjxlF/s5ra7FKQGl+1dSfznjYo1Oboy+uouVSK1a2dmtuqrNCwSkzWGkaO+vbty9c/d41NCRuIoEcQ7tPNqz+z9/1IivKuYm5ljSRJFF29wr+2fsy/d8TwlH8A3pOmYO/s0nJljxCdrp7aykokhQJLlc0Dz0MQugZZlvnmp2/49PSn5FUYdtKNPRPL62NfZ7Tz6E7u3eOp5NYho9bWbpib2z1QXTVnz6K7eRNJpcJ6+HDK/p0P3Dp2wrb5wFWSJKzc7an84Ro154vuO+gpv3kDGai3Mnzgc3V15cfj2UDXGem5r/TWli1bcHNzw8rKijFjxnD06NFmy8fFxTF48GCsrKzw8vLiH//4h8nrsizz+uuv07NnT6ytrQkICOD8+fON1lVTU4O3tzeSJJkc2nfp0iUkSbrncfcZO4LQHnJOnWRHxH9RlHcVTXdHZr65gVc+iuaX83+HvUtvaqsqOf7t12wNf4X49yINqa923gdUlmW+zczj8E832r3ue9rS66kuL6Mo/yqFly9Rcq2A4vw8rudkU3bzBrq69tnNVXj4dHodRdVFFFQW8OkJQ8DjYOWAo7Ujl0ovMT9xPutS11FSU9LZXX3sNMznsW2H/XmMqa3Ro8Hc/I4T1RufwHw3q4al6w8wr6fsxnX0FlboJQVKpZLuPZw4l2+Y0NxVRnraHPTs3LmT5cuX86c//Ynjx48zfPhwAgMDuXbtWqPlU1NTmTlzJmFhYfzwww8EBwcTHBzMqVOnjGXef/99Nm/ezMcff8yRI0ewsbEhMDCQ6urqe+pbuXKlcXv6xuzfv5+8vDzjY9SoUW29ReEB1RdVU/F9Prqy9jsFuyvJ/Nc/2b1+LdUV5fQcOIhZb3+AY183LKxVjAicwksb/z+mr46k3wgfkGV+Op7O7vWvE718ERmJ+6itfvBloQBxx66w6Mvj/PbTw8z4JI3vLhS2a/AjyzK1VVWUXC/g+uVsigvyqamoQJZllJaWKMzM0NfrqCi6yfWcSxTl/Ux1RXmHB2CN8ff3Jzw83Pi9m5sbUVFRzV4jSRJ79+7t0H51ZfX6eq5VXuN80XkKqwrRy3ocVY68NuY1EqYnsDd4Ly96vAjA7vO7CdobRMKlhE75//u4kWU9V37eTu6VaAC07XCyunF/nvHjqLtSTn1hFZK5AuunHFp1fcO8nrq8ivv+2112o9A4n6d37978VFhFrU6PxlJJb3vr+6qzvbU56Pnggw9YsGABL730Ep6ennz88ceoVCq2bt3aaPlNmzYxadIk/vjHPzJkyBDefPNNRo4cyYcffggY/rBGRUURERFBUFAQw4YNIyYmhqtXr97zB+nbb7/ln//8J3/+85+b7J+DgwPOzs7Gh7m5eVtvUbgPuoo6yg/nce3jE+S/l07R385z4/+dfqz+QMp6Pcn/u5V/frIZvU7HoLG/4MU/rcfGznTDL0mhwM17FC/89zrmR33CiMlTsbC25ubVKxzY+hGf/G4uB2M+ozg/r4mWWna9rIa392UZ2pMg/VIRsz4/wqs7M6ip07VwdfPqa2spu1lIYe5lbl69QlVpKXq9HjOlEhs7exz69KV77744urph5+RsnLtUU1lJcX4ehTmXKG/D6M/UqVOZNGlSo6+lpKQgSRInT55s0z2kp6ezcOHCNl3TknXr1uHt7d2udTbl3XffRZIkk0AODAeOzpkzB2dnZ2xsbBg5ciS7d+9uU911+jryK/I5X3Se65XX0ck6zBXm2Fna8fF/fMzMwTOxUlqhtdDy+tjX+WLSF/Sz7ceN6hv8MfmPLP3XUvIr8tvxbp8spWWn+P7Ybzh7di319aVoNE/R0zn4gerU19RQeewYYNiUsGECs9VQBxSWrZvFYqa2MK7wqr5QfF/9KCu8jk5lOFvL1dWV03mG+TxDXLQoFF0jDd6moKe2tpZjx44REBBwuwKFgoCAANJuTaC6W1pamkl5gMDAQGP57Oxs8vPzTcrY2toyZswYkzoLCgpYsGAB/+///T9UqqYniE6bNo0ePXrwzDPPtHgIX01NDaWlpSaPjnDlyhW2bdvGuXPn0D9Gq3v0tToqT1yj8IsfyXv7MMV7L1B7qRQZGZ2kozanjKpTNzq7m+2irrqarz9Yz/d/3wPA09Nn8vwf/oi5RfO5cvuevfjlvFd45aMv+OVLr2Dfsxe1VZUc2/cVfw1fSPz7b3D5ZEabg8PIv/9ISVUdQ120pKycyNyxrliYKcj8uYTr5bXk3KygvLq+1fXp6uupKCnmxpUcCnMvU1FUhK6uDkmhwFqjpZtLL7r3dUPj0N14z5KkwEqtufWaKzZ29ijMzNDV11NuHP252uLoT1hYGElJSVy5cuWe17Zt24aPjw/Dhg1r08/H0dGx2b8Tnam2tvlP0enp6XzyySeN3nNoaChnz57l66+/JjMzkxdeeIEZM2bwww8/tNyurpa88jzOF53nRtUN9LIeS6UlvTW9cdW6ojJXYa6490PiSKeR/G3q3/j98N+jVChJvpJM0N4gvsz6Ep3+wQLsJ0l9fRlnz0WSnv5rSktPYGamxsN9LT6jdqNUPtghnFXHjiHX1KDs0QNzt/5U3Tp2wqaFCcx3e9Cl62U3CtFZ31651ZU2JWzQpqCnsLAQnU6Hk5NpjtDJyYn8/MYj//z8/GbLN/y3uTKyLDNv3jx+97vf4ePj02g7arWajRs3EhcXx759+3jmmWcIDg5uNvB55513sLW1NT769OnTzN3fv9TUVC5fvsz27dv56KOPyMjIoL6+9W9IXUlVTRVn0n/gh8/+yeU3DnFzx1mqs26CHi5a5vJ5jz2EDlxDrEMCANf2nUHWPdqBXtmNQmL/tIoL6YcxUyr51ZL/YvyMWUiK1v/zsbBWMWLSVF764CNeWB1JP+9RhtTXsaP87e0Iov/r92T88x+tSn3tP13ANyfzMFNIvDd9GL3tVUQGPUXySn+CvF2QJKiq1fFTYTkXrpVxrayaytr6ex7l1bXcuFnM1Zwcci5e5FpeAaXlVVTXy+jNrbCwd8SmZx/M7RyoN7Ogqk7XaD2VtfXUygrMNHbYOPfG3N4RndICWZapqawwHf2pv3f0Z8qUKTg6OhIdHW3yfHl5OXFxcQQHBzNz5kx69eqFSqXCy8uLHTt2NPszuju9df78eSZMmICVlRWenp4kJSXdc82qVavw8PBApVLRv39/1q5dS92t0aro6GgiIyM5ceKEcb5gQ39zcnIICgpCrVaj1WqZMWMGBQUFxnobRog+//zzFg/0LC8vZ9asWXz22WfY2997ZEBqaipLly7F19eX/v37ExERgZ2dHcdufcpvTE19DT+X/8yFogvcrL6JLMtYm1vTV9uXAbYDsLW0bXEyuoWZBYu8F7F76m5G9BhBZX0l7x59l9BvQzlXdK7Za590siyTX/B30g7/B1euxAB6nHpMYezTSfTpMw+F4sHXEzWs2rIZO5aa88XoK+tRaMyxHHjv71BzTI6k0Ld9lL6oqAjZwhJJkujdu7fx+ImuMp8HHpHVW//zP/9DWVkZq1evbrJM9+7dWb58ufH70aNHc/XqVTZs2MC0adMavWb16tUm15SWlnZI4OPX1weLS7Wcrr7M9evX2bt3LwcOHODpp59m1KhRHXaq8Q85RWw6cJ5fuDsy++m+WCrNWn2tLMtcq7zG2aKznLt5juLsApyybfC+7o69TosaQ34237yQg9rvOWR3HEtnNR72Hrxkv4DMqycpOliKfbGWgn9fwNnPo0PusaMV/HSBve+/QXnRTay1tgStiKDXoCH3XZ+kUNDPexT9vEdx8+rPZCR+w6mD+7n5cy4H/vr/8e8dX/DUxP/AO3AKdk7O91xfVl3H2q8M8+FefqYfT/W6/cekp601f3jWgwsXf0JjbUFpvcTNiloCPjh03/19ECcjfolUU05VWZlx9Ke86CaWNjaotLZYWKuQJAmlUkloaCjR0dGsWbPG+AYcFxeHTqdj9uzZxMXFsWrVKrRaLfv27WPOnDkMGDAAX1/fFvuh1+t54YUXcHJy4siRI5SUlNyTNgLQaDRER0fj4uJCZmYmCxYsQKPRsHLlSkJCQjh16hQJCQns378fMIxI6/V6Y8CTnJxMfX09ixcvJiQkhIMHDxrrvnDhArt372bPnj2YmTX973Dx4sU8//zzBAQE8NZbb93z+rhx49i5cyfPP/88dnZ27Nq1i+rqavz9/e8pW11fzfWq65TW3B7BtjG3obt1d2zM72/VXX+7/kRPiuZv5/7GX479hZOFJwn5ewgvPfUSrwx/RSxvv0tlZTZnz/6Jm0XfAYZVWoMGReLQ7Zl2befO+TyVP9yawDy8R6PHTjTH0lWLZK5AX15HXX4FFi7qVl8ryzKl1YZRzB7du2NubkGWMejpOiM9bQp6unfvjpmZmcmnGDCknpyd7/0DDeDs7Nxs+Yb/FhQU0LNnT5MyDfnzf/3rX6SlpWFpafoPysfHh1mzZvHFF1802vaYMWMa/UTXwNLS8p46O4LFVR0+N13xwoUzZj/zozKXsrIykpKSSP6/ZEYNG8FY//Fote33i/HdhUIWxHxPZa2Og2evE52azYrnBjF1mMs9udVaXS0/lfzE2ZtnjUHO2aKzqMrMmVjii3+pD71rBxnLl5lVcMb5CqXuOroNdGGaw1yW2a4zGRqv9Kjko5/eIyQ7gOL9l7Af3RdLVccEdx3l/JFU/vHhRupra3Do3Zdfr3od2x6N/57fj24uvfjlS68wPmQOPyYfICPx7xTlXeXYvr0c+8dXDBjly4hJU+n71HDjG9SfE8+SV1JN324qwgMaDyTNFBJOtlb0VFqQc6Oi3frbVkoLc1RqR9T2DlRXVFBVVkJtVRU1FRXUVFRgplRirbXFWqNl/vz5bNiwgeTkZOMb+LZt25g+fTqurq6sWLHCWO/SpUtJTExk165drQp69u/fz5kzZ0hMTDQugli/fj2TJ082KRcREWH82s3NjRUrVhAbG8vKlSuxtrZGrVajVCpN/tYlJSWRmZlJdna28QNTTEwMQ4cOJT09ndGjDUu9a2triYmJwdHRscl+xsbGcvz4cdLT05sss2vXLkJCQnBwcECpVKJSqYiPj2fgwIHGMpV1lRRWFVJWW2Z8TmOhobt1d1TmD57yU0gKZgyagV9vP945+g4Hcg7wWeZn/PPyP3n96dfx7dny/5PHnU5XzaXLH3H58qfIci0KhQVurr+nb9+FmLVzYFhfVER1lmF+n/UIX659fAEwbEjYVpJSgeUAO6rP3KTmfHGbgp6qslJqLQ1/49369SO3qJKymnoslAoG9mh9PR2tTUGPhYUFo0aN4sCBAwQHBwOGT1EHDhxgyZIljV4zduxYDhw4YPLJKikpibG3ToDt168fzs7OHDhwwBjklJaWcuTIERYtWgTA5s2bTT71XL16lcDAQHbu3MmYMWOa7G9GRoZJINVZNH69seiroeZSKaMua3nqel8umOVz0uwyJfWVpB0/wpHjRxmkcWOM50h6DnXFwkWNpLy/DbOTThewePtxauv1jOhrx89FVeTerGJZbAaf/PsELzytQGmVz9kiQ5CTXZxNvWxIt9nXa5lQOoqZJQsZVO1mrFNnpqd6gAK7Ub0Z/JQrQ8ya75vKXMVvZrzE1b8cx6XWkQM7d/Orl2bd1/08bLIsk/71blK2RwPgNnwkU8JXYalqehv3B2GpUjFy8lRGBD7PpRPHOZ7wdy5lHOPi90e4+P0RHHr3ZcSkKdS4jSDm8GUA3nnBC2uL5kfuzNDjbFHHoYWe6O6YRyJLEjpzKzRaWzQaVas/8VdWVlFSUgwYVj7JsowkSTg4ODS6YMDa3NA/w7wgDdYaDfW1tVSVllBVfmv05+YNyotu4Gyn5emnn2br1q34+/tz4cIFUlJSeOONN9DpdKxfv55du3bx888/U1tbS01NTavn7GRlZdGnTx+TVZ8Nf3/utHPnTjZv3szFixcpLy+nvr6+xQ8iDXXfOULs6emJnZ0dWVlZxqDH1dXVGPCkpKSYBFyffPIJEyZMYNmyZSQlJTU78rt27VqKi4vZv38/3bt3Z+/evcyYMYNDhw7Rf3B/CqsKqai7HehqLbU4WjtipWz/DxxONk5ETYziwOUDrD+ynsullwn7Zxi/Hvhr/svnv7C1bH1Ko7q8nNMp/8epg0nU19Qw4/X1qLu1btVRV1N44yDnzkZSVZ0DgEO3CXh4rEOlcu2Q9irT0kCWsfTwoPaqDDoZc2cV5j3v7++Vpbsh6Kk+X4TGr3errysrvG6cz+Pq5mZMbQ1y0mDewvvFw9Tm9Nby5cuZO3cuPj4++Pr6EhUVRUVFBS+99BJgmGjXq1cv3nnnHQCWLVuGn58fGzdu5Pnnnyc2Npbvv/+eTz/9FMC4QuGtt97C3d2dfv36sXbtWlxcXIyBVd++fU36oFYbfrADBgygd2/D/5QvvvgCCwsLRowwnF+yZ88etm7dyueff34fP5b2pXSwRulgjY2P4ROirqKOHpdLGXmphPPnznHs5hkKFMVklWWTdSSbvqndGSa70adXb6z62WHhpsWyrwaFquWVaF9l/MzyXSfQ6WWe8+xO8DPFnCo8xf9lZ5BbfpFcZRmbfjS9xlpnSWDVeALLxzOgqBcKbr0JKsByoD2qET2w9nRAYdn69BiAq70b1355CRLA45wT3578hsnDprSpjodNV19H0mdb+PGgIYXhHfg8E+cuRNFMSqK9SAoF/Ub40G+EDzevXuGHhG/4MfkAN67ksP/z/486M0vG2Qym9/gAxg/s3mgdtTXV1FVXU3ytAOoMgY4FIFmYYW6tolphyc06M/RAVbVMpVyDk9YKm2ZWeMiyTGlpKbWVFVibm2FhYYHKUkNFVSl19XXUVJSh6d4dRSvmOCktLNB0d0TdzYHqinKqSkupra6iuqKCGUHTiHjjTd57603++te/MmDAAPz8/HjvvffYtGkTUVFReHl5YWNjQ3h4eIsTgtsiLS2NWbNmERkZSWBgILa2tsTGxrJx48Z2qd/G5vYbkI+Pj8keY05OThw4cIBr164xcuRI4/M6nY5Dhw7x4YcfUlNTw6VLl/jwww85deoUQ4cOBWDYsGEcTD7Iu1Hvsub9NQBISNha2dLdqjuWyo4fyX7W9Vl8e/qy6fgmdp7dSfyFeJKvJLPadzWBboFNBtWyLPNz1o+c/Fci5w9/R33d7f+f6X/fw8S5Czq87+2pujqPc+ff4vp1w3xGS0tnPNzX4ujY9M+gPZTfcfSEMbU1wum+27Ryt6cEqMkuQV+rQ9HCh6sGhXlX0d/alLBv377s+86wMKErpbbgPoKekJAQrl+/zuuvv05+fj7e3t4kJCQYJyLn5OSY/PEbN24c27dvJyIigtdeew13d3f27t3LU089ZSyzcuVKKioqWLhwIcXFxTzzzDMkJCS0ea7Lm2++yeXLl1EqlQwePJidO3fym9/8pq232OHMbMyx9nTA2tMB31/1Z3Tdc/z0w1nSjh7mQuFlcswKyaGQHnnnGZbbF9eDjkhIKHuosHTTYuGqxdJNi1k3K5Nf7P89fJm1X51CliF4RA/qu8ewKuWg8XVJCSChr3VAqurFqLJhhJg9hVe5Fqn+9qQ1iz4aVN6OWA9zxExj8UD36uPnx8n0RBxu2JD37XHO9nZnULdBLV/YCarKSvl643quZJ1CkhRMnLeAEZOmdkpfurn05tn5v+OZ34byY/J+/m/PHsxLCxlZegISTrL3+neMnDyVPkOHIev15GRmcPrfB7ly/izDp8+izsYaczMzzK2ssNZosbJRGwO37vU6rpXVUFRRR3lNPeXXy1FbKhsNfnQ6HUU3i6i99YZkJlugr7CgvKIGGQsk83rq6+spLS3Fzs6u1ffXsCrMWqOlrraGqtJSgqc+z9q33ub/xcQQEx3N/HnzqK2q4rvvviMoKIjZs2cDhtHlc+fO4enp2aq2hgwZQm5uLnl5ecaR37s3LU1NTcXV1ZU1a9YYn7t8+bJJGQsLC3Q609VKDXXn5uYaR3tOnz5NcXFxk/2ztrY2SUcBPPvss2RmZpo899JLLzFo0CCWL1+OLMuUlxs2eVMoFIZAtLaUwqpCauVa6urrkCQJe0t7HKwdsDB7sH+3baWx0BDxdATP93+edanr+KnkJ/546I/8/ae/EzEmgp7q2yPulaUl/Jh8gMx//ZOiq7dX7Dn2daP3UC9++PbvnDyQwJhfz0Cl7ToTYJui19eRe+ULsrM3odNVIklm9Ok9j379/oBS2bFpHVmWjZsSWo0YR9n/3Tp2wrvpNGpLlI7WmNlZoiuuoSa7BOtB3Vp1XU6uYWTLUjIMTPzYBefzwH1OZF6yZEmT6aw7J+81ePHFF3nxxRebrE+SJN544w3eeOONVrXv5uZ2zxLYuXPnMnfu3FZd39VI5goG+A5hgO8QCgsLSU1N5cSJE1yjhP0WmdhKNnjV9mHgNWfqr1VScdSwqk2hNsfSVYuFmy3f3CjlT4d/QgZmP92Tm+q/knLlEBYKC4IHBjOo2yAG2XvgVtKTku9vUnOpEGvj32+ZUmszuo/pSTcfZ5Td228TKUmS8PjNGG58copni8YQ+Y+3+MuLH7Zp6PthuHn1CvHvRVKcn4eFtTVTlq0ybC7YySxVKjQjJ/Jxihku1pf5rc1lKn/6kYvfH+bi94fp1qsPNRXlVBQblpiqunVHYWaGSmuLrYMDSvN73/wslGb0tlfRQ9N48NNDbYlSD9WVNVTWloEkgwwKnRWSbPiTobQwo75Wh6LeCp1ZFZWVlYYRoPtYJm5uYYn5rdGf30x/gXf+vJGy8nKmT32eoryf6eXkxL6EBFIOHaK7oyMffPABBQUFrQ56AgIC8PDwYO7cuWzYsIHS0lKT4AbA3d2dnJwcYmNjGT16NPv27SM+Pt6kjJubG9nZ2WRkZNC7d280Gg0BAQF4eXkxa9YsoqKiqK+v5/e//z1+fn5NrjRtjEajMfkgqNfrsbS0xMrKCmdnZ65du4adnR39+vVj3vx5/Ffkf6Gx1/Cvb/9FWnIa0TujcVO7Yam0bNWIW0cZ0WMEcVPj+Oupv/LZyc84dOUQQflBLPVeyrh6T07/K4kL6YfR6wzpdHNLKwaPn4DXs4H06DeQ6upqcs+cpjD7Ij98+zXjQ+Z02r20RnHx95w9+zrlFWcBsNWOYNDgt9CoBz+U9msvXaL+ah6SuTnoewJ5WA60w6yFYyea03AkRUV6PjXnilod9OQVGEaZ7G0M7x8NQY9nF1q5BY/I6q0nSffu3Zk2bRoTJ07k6NGjpKenU1Jdwb/Nz/CDOofhjoMYXN0TRV4t+vI6qn68QdWPN/gFkIiGElsl5/OPkKgrpJvalneefZ/RiuFUZlyjMv46ZcXnUADWgF6lJEWpI6a0jLNVeuyOlrPERmbOWNc2rfRqiXU/e5RDtJBVyq8uPc3qlNV8+OyHKKSukefNOXWCrz9YT01FBVrHHvx65et07+vW2d0CQK+X+e89mdTpYcDI0fxu7iJuXr1CRuI3/HjwADd/zgXASqNl8Lhf4D7WjwokbOzsGw147tQQ/HS3qedmcQ211fVYVOqprKxCL9WjN6sGCZAlLBU2WKossLBSorQ0Q6GQqK2up+xGNbJeh96sluLiYpRmSiws72+UQaFQ8MrvFvFFzP9j8qRJ9PfwoLqsjGWLXuHS5UtM/tWvUFlb8/LLLxMcHExJSeuORVAoFMTHxxMWFoavry9ubm5s3rzZZEPEadOm8eqrr7JkyRJqamp4/vnnWbt2LevWrTOWmT59Onv27GHixIkUFxezbds25s2bx1dffcXSpUuZMGECCoWCSZMm8T//8z/39TOQZZnKykrKysqMe3opFAr0sh6dpY4tO7bwwZsfsGj2IqoqqnB1c+UvUX8hYHwAxTeKTe7ZzMzM+N/GvlYoFO2adpFlmbq6OqqqqnjB+QW8zL34y48fcK7yPO9//z4O5TaMzvPAtkdvLLW2WNt3Q2FhxY+VtXy/a7cxXam06Y5SW8QPCd/gM3U6ll1wv6Xa2ptcuPg+eXlxACiVdrgPXEXPnr9Beoh/1xpGeaxHjKDq1K0PPm3cm6cxlh52VKTnU32+9fv13CgzjEQ6dXfgWlk118tqkCQY0vPB9iBqb5L8OG2Z+4BKS0uxtbWlpKSkXVdSXco4xuVTJ1DZ2mFja4fKzt7wX1s7rLVaFIqmA4yamhqOHz9OWlqacfNEc3NzRo4YycjeQ/nu39eoyynFCyVaTP+AyYBSa4Gu9HauXLI0w3qoA6oRPbAcYAcS/OvMNd799gznrxl+aXvbW/PHwMZXet2vuuuV5H9wDEmGP7p+wIRxz7Fo+KJ2qftBnDyQyIG//n/odTp6ug8iaEXEPTssN0WWZUpKj3P1ahyFhQdQqfrRz20p3bo9025vJv8v7RJrv/oRGwsz/rncj152t0fhaioruJB+GCu1GrfhIzFTmlNdXU12dnaz+8Ho9TJ11fXUVuuoq9FRX2uastEpapDNDPvT6FFiZ2+H2rrxQEbWy1SU1FBWUYKs0CHJCuxsu2FlY94uPwO9Xk9NeTmVZSXU3TqWRpIkHHr3RWnxcFM4Ha2mpoaSkhLjHl5KpRK1Rk0lldyoukG93vC8mWSGVqnFRmGDrJPR6/XodDp0Ol2bNz9tCIB0Oh1Xr16luroatVqNRqPB3Nzw+3Tno6qqqtnv725fRiZbk01mt0zqFfVIsoRHiQdDiodgJjf/wcr8ZgHP/vKXPB3cdJbgYZNlPXl5f+P8hfeory8GwKXnDAYM+CMWFq0bETGh14OuBszvb3Q9d/ESyg8cwOF3q6nN74dkrqBnxNNtnn95T7cq67j65mGQwfm/fVHaNT9yVFdXx9tvvQWSxJTxT1PWdyQvbUtngKMNB/7L/4H60lqtff8WIz0PQc7pTONOvneTJAXWWq1pMGRnbxIgDXRxZujcUH7KzSUt7TAFBQUcOXqEw0eP8pOuGz8qnFn6vBfnCj6jPqeCYdUe/EL2RVkiGwIeMwkrj1sTkod0QzI3/Qfx7BAn/Dwc2X38Ch8kneNKkWGl12cpP7F68pAmJ822hbmjCvWYnlQcziOs4Ncs/+HPPOXwFL/o/YsHrvt+6PU6Dn0ZzbFvDCmMweP9CPzdsla9kdbWFv7/7J13mGRVtfZ/+6TKVZ1zmpyZSM55GIKAgKKiSDBgAhNivl696lURRcUICCYQlCg5D0xiApNTz3TOlXOdsL8/Tk/3NDNDEvzuH7zPc7qqTp+0T9rvXutda9E/8E/6+u4hl9s9Nj+ZjLHhlcuJRBYxedK1lJcf8291/P3JPD981DWbf3npzAmEB8DjDzDnxFNfdzuOI7GKNqVRovNqkgOg6gq6R6FgZ5GjCQRzUicndWLRHCGvSW3Yg9/QsB2bWCGGoRpEPBGC5V50r0o0PoIUDolEAl82SKjSi/oWIxD3QVHc58MXDmMWi6RHhikV8iSHBqhobPqPjqrfKezTRBX2I3XBUJCiWqQr3zWW9VhXdCp9lZR7yw9pJZVynAS9mgzt/30fOdk3z7IsisUiL730EplMBo83TcCfIJ+PkM8HeVN5bKVE2BbYNopjM0+2ssg/h5XhdWyxtrKjbAfxmjhXt17NkpoleL1efD4fXq8Xj8fD888/z/PPP49ZUctTq15mxgmnUl7xFgjF24x0Zjs7dnyDZHIdAMHADGbM+A5lZW/BDe44sPU+eOZ7kOiCq56E+vlvahPSssitWgWACM2AgRK+uVX/NuEBUPw6RnOIUlea4q442uGvnaqjt7cXhECYJeqamln1fzAp4T68S3r+A2ieNRfHssglE2STCXKJONlkgnw6hZQOuWSCXDIBXR2vuR0hFLzhMLUV1QwqfoShM0WNMkWNsuWlFawLbSBTmeH8Y95Py6SjkVkbczCH0RB43cgvTVV43+EtnDe/kVtf3Mstz7azuTfFB3+/ihOmV/OVpTOZ/W8K0sKntpBbN8jMwiSOTS/g+heu565z7qI59M5kwj4USoU8/7r5x7S/7L4wjr7oAxx90aWvSVAcxyIWe4G+/rsZGXkaORriryg+amvOorb2XKLR5+jt+wvJ5DrWb/gwkcgSJk/6HOXlR79p8iOl5Bv3bSZTtFjUUsaHjnrj4a5SSkpFC7vguqzM4kFIjqage1UMr4buUbEdi3g8jm3bCCEoKyujUjcYShVJ5EzSBXfye00sJYY9anUo2kWqfdV4fDoVopxYLIZULArFPGafTaDcgy/49lh9dI+HSG0d0Z4uzGKRTCxGqPLfJ+T/v+A4Dul0mmx2PMTc7/fjeBz68/2Yjks+DdWgyldFxBN5XZewEGLMhfVakFJOIEG5XA6Px8P06dNJJDqprfs7mlYYXVbFtqqQsh5FaULXm/F4JuHzteLx+Ih17qVz7Wr6tm9GWBZIB18ozJwTTmHeKWdS2TT+fD/d9TTfW/U9hnJDfG/X9zhfns8XFn+BMm/Z2DKnnHIKtbW13HPXXZgeH7/59S186MMfGYvU/U/DsjLs3ftzuntuR0obVfUzedK1NDV9GOUgZTteE1LCrifg6e/AwH7C9S33vWnSk9+0CSeTQSmroNTjkti3kpvnUPBMK6fUlaawK07gdUhPZ0cHAGo+Q7i6mi3rXLf7/zURM7zr3pqAd8q9dSg4tk0ulXRJzygRyibiYyRo3/d9BIlXXSrb66dQWYsdrkCMuracUhr/0BB6JkmwykekwUPb7KUsOvP8scKQbwTRTJGbn97Nn1d1YtoSIeCChY184YwZB1gc3gxST3aSerKLEW+Sj7Z9nSkVU7lz2Z34tP9MBd50dIR//u93GO7Yg6rrnPnJa5l17ImHXD6f76Kv7+/0D/yDYnG81Eo4PJ+G+ouprT1nQt2cYnGQjs5f09f3NxzHdSuWlR0xSn6OesPH+fDGfj71l3XoquDhzx7P9NpD+8Ud22GoM03PjjiDXTGq5woa65rRtXGrlaIqGF51jOjsb4HJ5XIkEgnAdXdUVFRMyLtTNG0G0zlS5jBCdctkCFQkLpmq9FVS63dDZFOp1FiUkWr6ESjoHpVQpRdNf3t0YoVsZqxYa0VD45u6r/8v4GC6HcMw0AIaI8URilYRAE3RqPZXU+4pf0dDnoExt2hbWxs7dlxDNPY8mhbBcYo4TuEQDVEopjzkRzQKCQ+FuIfyynnMOupCph1xAtohij1nSpmx8HaJpMJbwfWHX89Zk85iy5Yt9PX1ceKJJ7LqkQd4dtXLOB4fqqqybNkyFi9e/A6ehVc1T0qGhh9l167vjj37NdVnMW3a1/B630L+t86X4KnvQNdoTUkjBM1HQPtT0HI0XPHom9rc8C9/ycjNvyB01kfBczRK2KD+K0cg3iZJQrEzxfAtryB8Gg3fOOo1t3v7bbfR0dmJd6CbL//yt5x84/N0RnP86cojOW7af2Zg8kb773dJz374T5OeNwPHthkcGuHLd75EZ88AZaLAJfOD3K3+nSEzxYzkdCal29hnhvYYGZpatlBb246VVRhaN5lZCy9n4dJz3lQn0RnN8qPHdvDQRreTMTSFjx7TxjUnTSXyBvIGHdCOos3Aj9bgZEz+2PQQfwv9i3Mnn8v3jvveO/5iH2jfxX0/+m+y8Rj+SBnv+eLXaJh+YEkJ2y4wPPwYfX13E0+Mhzbrejl1defTUH8xweDEsPtYXw/+SBnegBuiWigO0Nnxa3r77kLKfeTnyFG312tnrE3mTE698TlGMkU+e+o0Pn/6wTMv5zMlnv/bTjo3RzELLgHxRhTmnRuhuakFf8A/RnRU7UDR6r78O/ssDR6Ph/Ly8gnRP1JK4sU4g9lBHOl20NIK4VghFC2L0Fwxcbm3nPqA2xFEo1FKpRKKoqKUfK64TAgCEQN/2HhbrnNyeJB8KoWqaVQ2tbxzeZQcC4ppd0KArxyMgFva/i3g1bodVVXxBD3ErTg5Mwe4GY+rfFVU+ir/Y2L/faTH59tM+56voCgeDj/8fgL+KRQKvWSzu0mltjHQuYJUahuKN4GqH6rrUPD7Wwn4pxIITCUQmEYgMBW/fzKqOj642TC0gf9a8V/sTrgu4tP004jsdN0hjY2NvO+Si7nzy58hHqjACrs6u0WLFrFs2TI07Z11UuRynezc+W2iMbeEi8/bwvQZ36Kq8qQ3v7H+V+Cp/4bdo9UBNC8ccTUc93nIx+HmRaAa8JVu0N94mpaOD36I/Nq1lH3k59hJL8ETmihbNunNH98hIG1J33+vQBZsaj61AKP54AMvx3H4/vf/B9O0qIr28uEf/YrDvv04AOu/cTrlgf+M9u5d0vMW8H+Z9Ixkinz4D6vZ2p8i7NX4xQdq+cW2r7Et0YVfUbimxqYWh76+GfT1zsCy3IdH04q0tm2gvn4nyY4QIxsmM//k97Fw6blvKirile4E339kGyv3xACI+HQ+ffJULju6Fe+bHMFnVvWT+OdubB9c2vIl0kqWrx75VS6deemb2s7BkM1m8Xg8B7wUd65cziO//ClWqUhVcyvnf/mbRGomFrlNpTfT1/d3Bgfvx7L2pfAXVFQcR0PDJVRXnYqiTBT0WabJ83+6lfWPPojm8TDnhFNYuPS8MZN+odA/avm5e4z8lJcfzaRJn6O87PCDtuH6ezZy18vdTKkO8K/PHX/QSLp0rMCDP99AfMDtKD1+jcYZ5TTMDKFVZpk8ZTI+36GtZ7ZtE4/HxyJm9olX9yckRatIX7ZvrDP2aT4agg0gdXriOXIlG6HmUHQ3wiPiidAQbEA6kuHhYRzHwev1oZgeSqMV3zXDtfrobzDh2aHgOA7Rni5s08QbDBKpqXt7SLOUYOahmIJCCsyDlPFQDfBVgL/c7cDeAA6m2/EGvaRleqxchBCCCm8FVb4qtLehCOWbQaFQoL19FyPR67CsvUyf9g2amy8HYKSrg41PP8a255+hkM2MHeukI2Yy5egphOtV8vk9ZLO7yOZ27/fsvBoCn7d5AhEyfG3cvfdF/rn6QY7qPwoFZTRi0E3aOLe6nFV//xPapBkkvG6n29jYyCWXXEIk8vbrRRynSEfnb+ns/BWOU0IIg9bWj9HW+klU9U1mtR7Z5Wp2toymPlA0WHgZnPhlCI9mB5cSfjIDMoNw+b+g7dg3tGk7k2XnUUeBMAidexM4UHvtIvS6tzdrfPTOreS3RAmf3kr41JaDLtPX1+cmG7Ytpjl5pl75Vd7325U0RLy8dMPraw7fLrxLet4C3inSs3PnTlauXMnMmTOZPn36m0riBtAXT/Hlv92DV25jVmU3s2q7+UVfnG5Txa9IPlldoNmQaFqIcHgBAf98entreeWVQRIJdyQejgwyffoKDC3D4NpqUu3NLFp2AYvOOu8Nl1eQUvLsjmG+/8g2dg66L7/GMh9fPHM675nf+IYjvaQtGbxpLdZwnr1z41xjfw1NaNy29DYW1Cx4U+cG3LpGW7duZd26dWPJMevq6mhsbKShoYHo9s1suO8uBNC2YDHnfO76McJnmgkGBu6nr/8eMpmtY9v0ehupr7+Yhvr34vU2HHS/icEBHrrpBwzu2X3A/9rmL2LRWefRNn8RQlEoFPro6LyFvr6/I6Wr1SgvP4bJkz43QQj50u4RPvB7V2t0zyeOZknbgQLOWH+WB3++gUy8SLDcwxlXzqFucgShiDcUvWWaJrFYbIJ+Z3+C5EiHkfwII/kRpJQoQqHGX0OFt4KS7dATz5MtuiRGIEDNoeguGQ4ZIZpCTZglk2g0CkBZWRnC0cjEi2OVm/1hg0DE82+Z4kuFArG+bpAQqanFF3qLz6xtQSntkpxiyrXu7A/NC54wSAvyCZD7RSjpAfBXgK/M7dRehYPpdjx+D3k1T7I4HnJf7i2n2leNrr556+nbgXw+x44da4gnvk5ZZBJzZv6aHSuXs+mpx+jftWNsuVBlNXNPPp25J59OuOrABHhSSkqlIbLZ3S4Jyu4mm91NJrtrLNrp1chkynnllaU4tkZ3oJvtZds5vv94vI4Xw68Ryb2Mtxhj8glnsL2zl1LRwuPxceKJJ1NX34QiVITQRid1v08dobi/lQnz9y070YoWjS1nx45vkc93AO7zOXPGd/D736T1JNENz/0QNvwFpA0ImHcxnPQVqJxy4PJ/v9wlRid/HU780hvaRfrpZ+i55hq8i85Hb1mGXh+g9nOLXn/FN4l9A1SjLUzNJw6uOVq5ciWPPvooaibJgoYaho94P995aCunzarl9x/5z+U6e5f0vAW8U6Tn/vvvZ/369WO/6+rqmDFjBjNmzKC+vn7CCFVKSbHYTzK1gWRyPUPRtWQyW9AV90WcteGWYS89pkJQVfjmrOOYX38SkchC/P7JEx5kx3FYs2YNTz75JKZpoigObW1raWjcQSGu07O8HitVzeJl57PwrHPHXDOvB9uRbqTX4zsZSLkj19n1YW5YNpPjp72xTKD5LVGid24FTeH3xz3CvYP3U+2r5u5z76bK9/o+YCkl/f39rFu3jk2bNlEsFl/noG0ifh+zFy2msbGBcLifVOpfDI88Nqa9EcKgpvoMGhouGRUfH9q1sHPlch779c8p5XN4Q2HOuuY6dI+Htf96gPa1q8b0V+UNTSxaei6zTzwFw+sjn++lo/NX9PffMyaGrig/jsmTP4fHP58zb3J94R86qoXvnj/vgP0O7k3x0C9eoZA18dYVyZbvprmlifPOOw9VVV+X9Lyefidn5ujL9FG03fMZNILUB+rRFZ2RTInBVAFHSgQCidtGVQgckUcxYoAkoAdoDjWTy7q6FSEEVVVVKEIlEy9QzI26dnSFUIUXw/vWrRqZeIxMLIpQFDeM/RA6kgl4PWuOUFy9hTfskp39tFE4NhSSkI+NurzGVgJvxCVAnhAScYBuRzd0TI9JopQYS64aNsLU+Gv+I+UiXgupVA/t7dtJZ35CqPRhVt71sBtcASiqyuRFR3DYqWfSOn/ha6bYOBTcHD7RMQK0jxSNjPTy8stHYpb8xIXOo7qJVv0MYVHi+P7j8dt+MorF87KGoKFS4x+mzj9EjX+Y2sAwtf5hfNohNEevCzGBCNm2O5AzjGqmTfsatTXnvDnrYWYYlt8Ia34P9miakBnL4OSvQd3cQ6+36rfwyJdgyilw2T8Pvdx+GPju94j/6U+ELvhfkGVEzp5E6Pi3X+htxQoM/O8aUKDhm0ejHORZvfvuu9m6dSvGUA/HHHUkD3qP4N51PXzu1GlcdwjX/DuBd0nPW8A7RXqi0Sjbt29nx44ddHd3T8gmHQ77mDnTS21tDk3vJp3aQLE0eMA2clYQb3geN/f0sDcbpdxTxu/P/APTy1/jpnIcSPUSt3088NDD7N27F4BIJMbUaS/g96eI7YjQt7IGVYmwaNl5LFr2njdMfvIlm9te2sstz7STHh35Hz+tiq+cNfN1QxWllAz/ZiOljhTGoko+ZnyV9mQ7i2sX87szfjehYvuEfebzbNy4kXXr1jE4OH6eysrKWLRoEfPnz8dxHPbu3sXyh+4nmStg+/ygqBieLLW17dTWtuPzZfbbajPlZecwZcoHiEQObtXZB8s0ee7OP7DhsYcAaJgxmzM+fS39Soy8lUcgKETj9D2/mr6V67ALo6JUn4/mYw+n7aTjCFRW4pSGSA3eRSb6OIwKgqP2XH694SgyzlT+dNWRhL0GilBQhIJAMLgjwwu37sUqOahNUYacbWMd6uLFiznnnHMoFosHJT0H0++UlZWNRfnYjs1gbpB4wXVVqYpKfaCesBGmaLnWnVzJvcZBj0ZjuY9MwaIvUUAiURWBQwHFiAISr+alNdxKIpagVCqhaZpLfBSFQs4kEyvg2O5z4AsZBMo8byknlJSSWF8vZiGP7vVR0dB48I7KtlySU0y/tjXHGx7V7LwBLY1dcjUZuRhY4x2vFCp54SPjGFhoKKqK9EkSZmJMFxXQA9T4a96Wquf/LiwrRyKxm+7uITpX3k37s30AhKtrOey0pcw96bQ3nLvqzSA7uIff33o78aJC3PHxr9JMPGqJs1peYa/Zx4DSx1GxuQStIHk1z7OhXkZSR2DnJsN+OcnCRoa6YJy6QJRa/wi1/mFqA0NU+4YwlPzY4OL1odDUdBlTJl83ITjhdVFIwks3w4pfjRPotuPh1G+6QuXXQD6Tpn/VY5Q9+WnKgirKVzpBff1BQPuyszEH0wRP/x4IqL/hSNTwO6OdGfjxy1gjeSovm4VvzsQBqZSSH//4x2SzWXwd2znt4vfztV0VbB9I89vLFnPGnNeO+no78S7peQt4pzU9UkpisZ3s3v0YwyOrsO3d+P1RFOXVl0BB0afyfGcd26KtCGMu377oGG5Y8Wl2xXdR6a3kD2f+gSllBzGVJnthzzPQ/gzseRZyIxCsRc65iLX64Ty+ehulUglVlbS0rKexaSvSUuldWUV0WxkeX5CFZ53H4mXvwRt8Y+Qnli3xi6d3c+fKjrFIr/MXNPKpk6cytebQ2yh2pRj+1SsgwLyylktXf5ismeWy2Zfx5cO/PLac4zh0dnaybt06tm7dOlb/SFVVZs2axaJFi2hraxsT4EZ7u7nvh98hMdiPEfBy0sdPIidWkMmsglHrhGXpDA9NYmBgKplMBfteomVlZTQ2No5N9fX1GKO5e+IDfTx00w8Z2tsOgDiqjVemp9gc20LJObD4pWYJpvYEmd0RIpxzSZyDpKsux9a2NEPlRSo0hzPCJocHbNTR9/jWvMKjKZ2u0viIesrIQk7Z/SEUqTBQ9fJYOPFAAGqzruVlV+UurCaLa1quoa65Dt3QxwiTVtAQ9ugOPKB61TFCVbSLpIopbOme16ARpNJbiaZoxLM2Ixlr1M0lqI94qQiMi5GzRYvOWA7LdlCEAFFC6CMgHHTFoDXUQjwax3Ec/H7/mGvXsR0y8SKFrOvqU1SFUKUXj0/DsW1sy8K2TGxz9NMysS0LxzJRdYNwVTW6xyV1lmkS7elCOg7BikqC5RWj1pzcftac3MSLIxTwhFyi82przpvFqOXIyUYhH0PBGb3WENMMoqqCNUp2vJqXWn8tAT3w+lYEq+g+z4416kIrh7dgZXntQ7fJZHZRLJbo2DPCy7fdTsQZ5uijZ9EyfwnK3AvecuK8QyLZS+m5n3LzWklahMhIg38VZ3HGVB9ff99JVIXc6yoHtrBy5W95eLOCZkUoKkWW1y3HQRLIHM9A/EhS5mtb9uojXtoqA0yq8tNW6aW10ktrhUFTmYauSqS0kNKmuGsXsi9B2Znnv3HrTikHq38Ly38KhYQ7r2GhS3YmnzxB7G7ZDkXLnUqWQ9GyyZdMHrj5p0S7OwlZaQIyT1VDI1WTZ1LV0kZVcytVLW0EKyonHJM5MMDuk07GmHUenhnn4JleTvUVr2FJ+jcRv3832RX9BI6qp/z8iTXjotEoN998M0JKAjvWcdZnvsR7Hs5gOZIXv3LKvxXp+2bxLul5C3inSM/AwAMMDj1EMrke04wd8H/phEmmKonHykmlq8ikK7FsjSEZwgnXc82Fi/j2xq/Qnmyn2lfN78/8PZMjk92VixnofNElOe1Pw8iOA7a/PxJl83hQnEl73O3gIpEsU6Y+RSCQpJgI0/FUBfkRH4bPz6KzzmXR2efjC76xUU9XNMePH9/BA6/0jc07clIFHziyhTPn1B1U8Bz901bym6N4Z1bwyikDXPvMtQD87wn/y7GVx/LKK6+wbt064vHxdOg1NTUsXryYefPmHVDrqXPjBh786fcRnhh1C0tUzkxj2+PaibKyI2mov5jy8lMZHk7S19dHb28vvb29YxqU/SGEoLyyHCc7iLVxE8JyKOg2L8wfobdmfHRfrgWo0IM4iopUNCQCBwdHOkhHUtkPrbsUqobGz0GizGb3lAI9DSU0CpwWynF4sDROfgoajyc9BLuO49i9FyIVi8Gq1WiKg0SyunUK61vnMadvL8fv3ghAe2uUy+eeT01TDYquoEqVgBlARUUiyWpZTMU8sJ1SQaCgSIGQCkgdYQVBaigShLBQlBIKoEgFVSruS10IhBBYtsqoXAchHKRSROIgUAjqAUqj7keP14uuqjiO29nYpolZKoFjj4bA2wekZjgUApEyAhWVKIrCCccfz8ypk/nRt75CMGgwZeEJXHvVpVx79QfHV3iVNUcoKv/85z85//zz39D+DgXHcchkMqOh+hIPJdCKxIRFabSz0qWkVhiE/VUIb+S1yYt0IDME6QFshJtZHcclar4K8FeC8e9biBzLJJ/ci0IeMy/p6hpm8ktfxJ/pGF8oUA1HfRKWXOnqlv4dJHtg+U/pW/MgP3cuxasIilJlnT6b689dRE33SgZHVmJpIwjhQ5EehOMBqdFd6qNgO5iOSq+WJKEW0C2NSbk6Erkm4laApBNCM5oZsrx0lSxS9qGzUytAra7QRpFzNz/C/O0voEjJ5uMvpH/haeiGH0fVKQkwgZKQlCSUkJQch2Kyh1Ksg5JtUZIqRT1AMdhESQuNkht7P4LjYDuvf08bTpGwmSJipUc/U4StFNW6yaT6SupaWqlqbiWyew+Fm39F8OwfIfQIFe+fgX/B25ef59XIb40SvWMraoWX+i9PDL5Yv349999/P0apgKd9M0d89ttc9uAgZX6d9d84/R2PyN0f75Ket4B3ivT89dlbeGHHDhwpABVdr0I3atGNagy9GqEGcBxJNpels3+EaCyOIVxSYqkZ4k1/x/bE0Jwwc+1rmadYzMyuYWZuLZPzm9EYN986KOw1prPZt5hNnsV0eaay1L+LEwpPU9n7FMIqIIH1zOExcQpFqaEo0Nq6hcam9QgBmc5m9j7txS6pGD4fC5eey+Kzz3/DQtGNPQl++8QrPLtzhIx0R21lfp33Lmri0iNaJlh/zOEcgz9dB46k6up53BK/jVu33IqOzkm9JxEuufs0DIN58+axaNEiGhoaxh6m1Mgwvdu3uNOuDTjerVTOTBCoHSckhlFDff17aai/CL+/7ZDHnc/n6entYWP7RvZ07SE9kkbJg2ewGyMx7F4PX5BsUyu5QB6vmqDKitFWSlJh6bQRo4xR87bug0ANBPebAjXEChqbN7eze/Nm7NHMx8IbYr1nJr0Vh/GrK6aTjd7F0PDj7LNKZQdnMrj7OJLBGCU7j63pLG9bSH+kllaPhmZD+e5XmBrdgz8Y5NgTTqG1pRWvomIVLVy5sUCoriVMOqO8QoKQYizH09sG6YwSGGdUyGmDHCU10h5r1z58+OqPY1oWf73tD/vNVUCorFrzMue//2KefvgpZs+e5853ckjpXl9VUwkHDM58z4XMnzWVn33HFYIOR+ME/H785TWHtOYIId4U6fn2t7/Nfffdx4YNG9xmvirfjkQiDEFOzY1polShUC0F5aX8eE5joYC3zLXeGMGJ4e/FDCS7+cFNt3DD92/mqiuv5L++8x3KlBx+J017Rzdf/O+bWL5mA8WSydIzl3LzL35Bbe3ESMQD4FiujsnMI0s5ZDGDcMyxXRcsyd7eYSa9+AW8ZgrqD4N4ByTdJHMYITj8CjjqGgi9SZdFohuW34i99s/ckTuRZ/TjmKzF8PoTVPtLNJR3o1W3o2gHWkvfCqSjIG0Dx/KMfhrYtgfT1jFtD0Vbp2h5MIo2lakMesFGFEHNOigdJb4w+Upi72BBZE2AIQSKbaI5DsIwiB+YP/RVjZKE7AxhM8UnNzzAESb4T7geE5st0ztom9xEdUsbFU3N6MYh9GH7rJ+5qOuS1bxQPeN10y84RZu+76wAW1L3pSVolePWm33PgxEdwDPUQ93H/oevPdHNsVMr+fNVbzwv2duBd8tQ/B/C7uQc/rX34OF+MDI67YMA3IgdoSXxN92L4onhNb18qs/Le+xrKReZCVvodqp5wZnH885hvOTMJlUIQmrffzM8Tj3wQSr19/Kx6q0sk8tZmFjNFNnJQ5zKLmcye/fOITU4ibbpTxJs7WL+R8OMvDKJrpU5Vv3zbtY98iALl57D4rPPxx9+7RfCvK4/86MN/40nVKDkDdFlV9BpVtC/spJ7VlTiq25l4dy5HLFgPt6KRgJH1pFd0U/3Xa9g6iY1oRqGfEOsqFnBh5UPc/Sio5k9eza6rhPv72PT04/Tu30LPdu2kIn3E25NUz41RcNpmbEBtBAaVZUn09BwCRUVJ6AcIgQ4Z+bYOLKRNX3rWdO7lc2DHeRNFWkHCEmD07tShPJuB7ajfDrry5ZQMr2URQ1qLJ0RRWVtrYfueg9F/TVeHvnRCaBtDrLtbHAyOE4a2DcilXxkTTdCORPEuWOrOpES9pE5oA2kgmYFEFKlLOuQzJbwlSS6No1tNUHqjTQ4Jk7exlF0FPYX4Y6eGzgkzXFGJwko0sawSyjSAekgpGu/QjpIARIbCTgCHMWdkPKNWWqEQAqBFArvf/+lXP3JT9AXzdBU24pAdUmagLvvvZ8Fhy3ksDlLkIqNxEGKCEgv0kljWzbxZB7LdltlSZWireCvmYa/pvGNaXPeAl6db0dqkqJeJG/nwXZz7VT6Kqn0VqIqqqv5ycUhH6OUz2LImCuGVg3XbeWNQHYE8jHWbNjCb/70D2bPnjVGDxOOn6Sjc8YHz2f+rKk8fdevAfjGj37NucvOZOVLy1E8o4MJ2xwlOLnxT3ucUIxdf7Hv2ulghCEg4UP3Qe0UtyO0Tdj8D9d9M7wNXvwZrLwFFnwAjvnswSORADuRoNTRQWnbOorL76W0ayvb7SbumP9lZk3q4KzqBygr68cwJgYf5AoV7EiezGChASlspCLBEAivCqqDxKREBhQLRXFQhGtVVBQbFBshRm9wZXR6Q4FwlWPfgmRYwip+aP03mUQT0VQTQ6km+jP1ZIoVeIWKVyoEpUpAavjQ8UoNDwJjdHf7vhtj8wSeCb9BdR8e9yKogA0FJP0UGFKGGWyZTp8O/Y5Dd7pAVyxLwYK0FiKjBpicGESb+R4AXiLL33alqdq5gkbnfmpllHo9S4OnSLXXpNIoEVKLaGYKkYu5db72x5IrYNmPX9PyqHhUjJYwpb1JCjvjBI8eJz1dXV0AqNk0iqqyfdSo/n+x/MQ+vGvp2Q/vlKXniU39rNgTQ1HdF7ky6hJwv7u/N/emeGbHEACL6lTOr9vGHYU/MyiKNJgWfxgYpMlyhwM56WGznMJGOY0tynScyhnU1dVRV1eL3+cd6zAUIcibNi93xFi+O8pIZvyGrybO+3yruUhfQaKk8ignUcCLgs2S0CZqW9aRrNAwtNl0PVdN/xbX0qF7vCxYeg5LzrngoOTHefrH9H3/F6S7fegBi4ajEvirDz6CM1HZwnQ2i6M5Jn8eBhpP65vpCPTwZP3TxMlwdNlCPqZdQN+ObfRu30oumUAoklBzhvKpKcKt6QlJ0vy+qVTWvBcjfDZZM0g8axLPlYjnSsSyJfpTKfZGB+hLxInlTfIlA2n7QU4cHU3L7OaUkWcxpElB8bKz7HQ8+iTqLYWAqrCryWBbk8GeWh1HfZstJf8GmhTJ/4QVappbEIYHxXFQzDw6AlWaWHYOpIWQNhETQjkLaZokPEHinhASUKVDVT5JyMzh6BqOZuBoiut8khLHen1hqCMULD2Aoyg4WhFHsbEV8Do+NKlT0kyyahaBQC+pHLvgeD5++VV8/bM3jBGyTDZD6+LpXPeZa9i6cxMrXnqZeCLNlNYmPv/pz3DuuR/AdopIJ8eFH/gQc2bP5off+yGlQobDTziBz372M3zpy9cDsGvXLq688kpWr17N5MmT+dnPfsYZZ5wxwdJz/fXX889//pOenh7q6ur44Ac/yDe/+U10Xef222/nox/96IQ23njjjVx86cV0DHfwzS9/k5XPr0RRFE45/RRu+eUtNNY3AuMWok9/+tN873vfo7OzEyfWMRr+PnGIn8nmWHTWZXz3ez/gpp/dxNzZc/jhf32fvCjx3HPP8aEPfYjY8BAR3YLcCMl4lPLZJ/H4X37FaSce425EHtxsYKNg2gqWo4BPIj0OQvUSCEylWCwdOurPcWDX425kUrebTkEKBafpZPKR0yiMKJT2drhEp7MTezQ6sBgJED1qKkOzVUJ1e/CFhiZu1vQwHF/MVvUktlfOYa3qI/8G3EDvJPwyyyk8zhk8QiXj7m6r5CeVqqYv72OPZbNFJBhUXauuJlUq1HJmhWYwLTiNKb42WgOtNBoN+KQHadrIkoMz+pnqH2DHCy+gCo2pC4/CgxezdxBRTKCQQhFpVFIoIoWiZdACORwjiy2TmENRhu+3CC79EcIIUKV/Fa+68U210UKjqIXxW3HXeTrrXLjw96+ZGDH1TDepxzrwzq6k6sOzAUin0/zkJz8BILhjPeGKCh6dcxXruhLc9L4FnL+w8c2e/n8L71p6/g8hf89GKgc0VGHjUS38hkUkKAmXqQQrvGyKZ+ncs5kv+7ezrGovenYnV2YqGdQ1Gk2LPwyO0Fi/BCafjNV6In2ZCMM720nt3UUkm4bYFoqxLXRuhTpvJW16HS2ymkjegyzYnOLX+Uqkgo5ahbW2yepsnpfjCr/In8kv8mcyRfRyrrqSWk3QSTOr0wuo3dLAMu1JRO0aPPMDTDv5PHY8YjLY3sma++9hw6MPseDMs1ly7oVj5Me87zt0/+AOigl3JGBmNTqfqaHyvadRffZcRLaf/HAnO3qG2JkJ0S5bKQoPSAhpHSy2pnK82cC0nt9Q3ufhDwtgRWI9hR17mb8nTLAhR8uCJGWTMyiGTbIYYkdqGkPFWUTNRfRm6tgzYo9Gkm14jSsybk3bH4pj01jIcExiDTXFXe6SagOR4NnM08PsaNR5stlgb62Os1+kUWsyxfGxfvKZImvtML1i/IELiQJNaoIqkZ1gXfGaDgFLISA9BJQgIbUMx8wzmN9LtNCHVASl8locr3suOypq6K6p5VieY5bYggBCKUH1SJ60No2u2tNplyESMouyvxvSLjDnlxPFhwdDAHi7X1HtH91GVvUDDroRw6GISY6gGcCwDSopw2OpIOGy936Av9z9N77+ma9gqQ4FxeTP/7oL27H40MUncv99Jb7+8Q8TDgW4/6nlXH3t53lyRiULFi/ByvtHTfSSUj6JEAagUMgUsEomiqZy4YUXUltby6pVq0gmk1x77bUHHG8oFOL222+noaGBTZs2cfXVVxMKhfjyl7/MxRdfzNq1a3niiSf429/+hoODv9pPXIlz9Qeuxh/wc+8j9xLSQlz72Wv54KUf5Nlnnx3b9u7du7n33nv5xz/+4UbMlbWAvwoSnRMivz751f/lpJNP5bjjj+Omm27CQMcvvKhSoVgsIoQgk0oTCloojo3X40FRFJavWc9pJxw53hihghHA0f3k8xbZbB5HuoMtX7kBahJQ8PuaD5maQZZKlHp6XDLT0U+p82hEv0HI8wqBqhRq91MEu5+CAQ/ZrUHSCR+JRVPJzWvFaRlAr+hBiDXsU5tIKYinathbPJnuypNZEyinY/8klY50fa9CAaeEameoSKZpjKaJZDKojsNwWMFU/Ni+ANLwIJFYIRtv0IMQgkQxORqB6JIn3dFoMS0mZQsUozqO6bZVrSjDN+8wlP2CNV5J59ibh4e4gH857+HIwhZONx9lWuBlNCNHRVUnFcBc4DzAzEdIZmvozunsLhXZEH+Z55IvTjiHDYEGppdPZ1r5NKZXT2dKeDIvPnw70Vgn8045g6ZpT7uWNApwqKwFudEJiO4NoNWdjDACqAxjaDtwAvWYnnJyaoS442egaNBX8NBT9NNthRkmQlyGiBMiLoNk8QKCs5RV3KT/Es+2B9n7s6VsOeHXzJvSTEuF/wAtjndaGanHoNieQNoOQlXGrDxlwSC2YxOqrGL7gJvG4f9iza19eJf0/AeQjJkgdGw0crZGLg8jeWB43xIhZnA0MnU0T2biDHhTzM0kmaslOSVdRpdsZDgWxLfDwCtMvCLGfMo5jMOJiQxdyghd6gjDSoqBQpSBQpSVQMjx0SIqCef8GFkNLzpHSZ0T0VClzh4U1uHwsmzhF1YjluVwirqVNi3NoKjhj9b7OK53DSf0rqbgvYOamVUUTvsQa56IMbhnN2seuJd1jz5I/XGLKUtvpP5PG9ALOvmAwoMfnMrM9VEOWxMl+vfH2bpyBS8sW0TJbkDNjBYgFGAKk3xhhHXxLcwKfhS/VsFI8XxKqZc5ckualw6LsX56gsY5kLTbWJ05nN5X6unN1JM2Xy2wHrdkKdLGb2cJmDkixSwVhSzl+SzhUpZIKUuolCNkFvFqYRSjCstfR9ofIWu9gLTdC1OKHMfuKceyvdlzANGZ5fdwbm0551aXMS0wcYS0qyvB757ZzX07hsg5kp1EGBYBligxWtQBbGWiidkC4rj6mhrHR4NTRkzNQjoGSRtv314Oy6ZpLpSYGc5iHZ8gNSkLYSAM0ztXMfupJ3nOdxreSDnB8mOoyKYQpgfHflXU0n8QVekh1FAtKd2PWapC0xM4SpGMXsBvKThOgbDwoakqH/7I5dz465/xws41nHDicRiFJHfdfScXLDuVyXWNXPeJy7Gkh5wR5sI587n/+VX88b4naV28gEAwhaLZCNV1l0lM3Dg5wUh/jhdXPcf27dv51yOP0NDoUrv//t73OGfZMhwpsUeN3Td87Wtjx97c2srnv/AF7r7rLq759KdJpzPohoGqqpQ1lZETOWxps/LZlezatottu7YxbdI0AO644w7mzJnDmjVrOPxwV/hZKpW44447qK6uHk0l0eeKlZEgFGSgmj/f9TAvb9rJw//6KapU0AGFNJbSg9/2cOLCo/D7/Xzru9/la1+5gXIJX//+T7Ftm/5oGjwR15XlmEjHJpfNk7GcUW+jwBsIEqgIUyh2ICV4PLWoqs+tzF4q4RQKxP78Z5yNmyju2EGpq8s91lchThBPxEP5fJPkpDL6DqsifZKNUjGAom2e0Klkk/W0J2azVZ3Kds8MOipqMctGnxcTkA6q1Y8jgkgtMu6OVAxspYLhqgqGq9wByeTebmbv2YUvs4dQapD61ikMm+7xLV26lKOOcjUkA9kBbnvlNu7ZeQ96ociFzzic+op7jYXHoW5BksikEsJzEsy7CGaeDd4IjpQ8tnENP1+1i/XT5rDCP48VzGOxPcx7QwazMlvIZTZisR3V34PuS1LlS1IFLAQuBkrZalK5WrqLGptLSbamh3g228ezPc8CMHtviCO6KzA9kPM/Ac8+P36yFM3VXnnLoGEB0luOQwS7FMDK+TCTXuJ9j6NNcttpU05f/h7IC9QKL0ZTkOqmEI1NQY5qDKF4VKSURDMFtrX3sK29l/a+KF2xOANZm+esuVzO9fxWv5FJmfXkH7qEi0pfxg7UsrC5jEWt5SxsLuOw5jICDUGUgIaTtSh1pfFMioyRnojPQwwQgTJycRuvrjC5+o1F/v7/wLuk5z+Aw07dSzq+HsXMIewc0ilh2TpFK0TRKiNvRchb5TiOF2lr1Ds6dXYEWaykR9PptkFaBWTJQto6ulTxAl5p4XVKeC2NeWYALJO4v8BgxGQgqJJW8mxRel7z2HSpchwaxzsGQ06IbtnMU0WNyXofrWqC5zmKrXIaFxQeo3XPIOz5CZFwBT3z5tM3PBMrI/Ddt5a6rl3saJ3BM4vm8uiRc8j7J6PMjdF8RjdLV+1gXvtult2+iq1z57B7yiScYozAcIxgNj1mc9lovcRRVWcyvfI4/jalSEwpR6ReQYY38mChRLbjDKQ5bqER0qE2F2VKup82ESPktymWBYgk4hy+di0Vo2Z2AIkgG6gjXTWddNUsUsEW0iJCdlReape2Y2YfJOfVaZ9xNF0LTmFH2Mf+joIZuuR0I8/JxGi0hjE7E5i7U2yyUlh2GooaIu1Fyfr5iBnikvIAz5leHix56DcDPGFXE7RrOM/rYbpSoK8YwzFyVIdNopkEJdtm2Cihag6aFKQ8fro9HhZ07UJ3bPoMlb5CmPBDflqsGIET4hSXOCRbNWgdYU7qScy9c/EVFqKZOaSuoQoPvZevRDUthMMEvY2jKNiqiq2o2KqGVDUcVcfWVGwhcP5NgbOj+SaIJE3GI0xSo1KjMQfC7OnMP/Iofvb7P1B9+NF0tad4ceU6Ln/oETb6JvOHn/yIx/95L0N9/ZhmCbNYxAhUktImkQIKwkfW8DNU5db/slWVtN/LYLnOqr07qG1sIhauIJZ2hVWRuQsA6MyX2Dw677F77+Evv7mFnr17yGWz2JZFIBSi3QL8IXKGB1tRSGk+hFTxKgrxrgTNzc1jhAdg9uzZlJWVsW3btjHS09ra6hKeQooXHruPsz7wCXdhIbjl57/g6AVHc91Xv8pf//pX/F4fmmqDMFGwCTh5EHlaqhX+9Jtb+OwNX+PWW29FURQuOf8iFi1YiPCGoXKym9w0MUI6kWBf8JKu2IQMC93nJVfsQ0oHRRqIkRLFQjtOsUjJsrBjMeJ/+jNKf//4RdQ0jLY2PNOmYTfVMlipEhPDWMYejLIdqJ4MkBnrSKx8GGekhhW5xdyrn0C6RsNsrUDuZ00SdgZfcQtlIkXeM5u47hJRv7C4sKLIxbURskoVO/KCtakca1M5+ouwu7mN3c1tY9vx5TIs27OZ6ugAjz76KJlCkdNOOpG6QB1fOforfKSnlcGf/xBvzn2Kn5ovuOdkL6fqIS7r30tb+1NusU/VA9NOR9F9nLXpHo7a7WP53fO597RlPH3k8axVq1mbgSbviVw55SI+UF+Bx8zTs+dlhgfWksltxlF3oPmGMALDVAWGx4iQdBRK2QYyxVr6Czp22qG/OkRhepwvtD8JwK2Rs7iv4hwULeC6PBUFPNWAcI3SHgFe0EMW30z/g7JaN2npd+cYDHg1XGEQQBYGszAIrAWEg1BsUByEKsGrIKbUwFSoEIIw0J+t4urur/CLxI3MVjq51/NtLsvdwFPbSzy1fdwd6Y94+DoeTgDueHIXTy4pY9L2XXiBHakM1cDzUqe0uBKPV+ODG/fwWnh/fQXn1779uZ/eCN4lPf8BWPm/Umw9MOGgAvhGpzcLaas4joZ0dGxbI+toSEdDszUaHJ0GW0Hao5EMuJ0+AvZp6Pa5A0YHgYBkErDPQO4AtnAQSgkhJC/KejyymkqZQcHBYQ/JmTbrmMdWz/ls902n+KraNLbSSEdTI79uckcmqmPRmu5mcqqLGtOH1uzHyhdI4GUEP8/IENXOdhptL4ebs/iTIxHZpeieYRxPH5W1v+fY5xYzOTFMnZ1AlBkM1dcRbalEKgH2OQkS1VU8sfRMDDXAbK0Nu1hJPJXFETkUI4eqZ1GNPYSNLF5/lozaxfpQA6+UvZed/lk4Ytzk3iI7OJKXOJKXqC/1QwkKQPv+F3GfVtgHlE28TgtGJwDL0UiXAmTMAJlSEGEGWNLaSnOolq3LTTqUDGXFDKbpodvXwHDVFGa378RsnYGTS2PEBtEySVIejc2eGpxNLRgDfoKNQ3hlnGJCx7Fz5HUN2xFoxSLS8CCMCFIxwSyBEFioWIqGrWiYHgPTo+MoKl4bKkqScEEicEWulqFiGQqWJjAVgYmkJCUlR46GqO+nwZCjv0Zn7TOMydG/h1ZruAtecNmH+cGXv8gNP/4p9//5TponTWbJccdz209/wl9u+RVf+sEPmTp7Dj5/gB/d8GVM8+2J9gF4ZfUqvnr1FXzihq9zzKmnEYyEeezee7jjFz+fcJxSCKTiap9yQNQSmFKyJ1fAqyh4VQWfcqC7KBDwQ6wDCnGWzJ3GhifvQQbqsEsGVcFyHnrxcUZGRli6dOnYOrZt88LKtfzi9rvpjI0QRaf17Gk8fNZ7kD2dqKpCWTjCgoULubimmVIqRyYdo1RwSZyiKgQ0BaNYxMk45JwEdgSQoPSb2NZ4GgiEAFWd4O4BKAQN+ptMstN2oDctRw9G0RnXCDuml+LINPqTDewQVRTCPpbXLmHAMzEze1U+zbF1OqdXR5gZbuLXvc3cOxhH4kYyXdlUxedaaynTx7ukU/Zbv69QYm0qx8upLC/3D/NKwSLvD3LvnCNZ3LmDwzu3s/zZZ/hL1wBt9Y0s/cOvCG1YjxdwGhv5+8IKnpreT8KT4G6K/L25kRN9jVweHWLR4G7E9ofG9lV29inMvSvF1Ntv4br0CI9d9Un+2Bulp2DyX+19/LhjgPfXVXDV5GM4eu7JY+ul40N0713NwOA68sUtaMYuNG8ST6gHT6jHlUw3wTxbx05UszywhJhZxb11F7ipH5wimk9Hx0KT/ejSQsNGlTa6Y1GXGiZ97ixEzfNEdZtaHRpsCxXbXQ73+9insFFsG0XYqNgoWO48YaNgY9p+Vg9fyK7gUXyk6tfc1PU1ppX2cG/gO3yo+ttsybeiJEqIgk0uWeQFHE7AR2t7hrWd/czQ3PQryZEY1UCHP4RT5SUJPBs/VP01F8eW//+zBL1Lev4D6BfNTO/pIKX7SSleEqpKQhNYqoVULBA2mgBNSDQEXqmho4zNE4qNok4UjwrVRlVt9nfpvJNwEPTQwnIWs4W5bGcOOTGxZldQppjFFuawiWnsIEEF7UxhD9PYw1RSSoQ9kUnsiYzXsvHIAq3sZTK7OYJN9LAbkwHmAT884Ch6cGb04zgqtqPiOCotzl6aHBXHMbAcH7bjwxBpND2DppfQtBKaZr6ai5AizMscySqOZivvn0B0WuUejmQFR7KCOkZHvY6KaoVRnQCqDFJAoU+m6XBGyDgOBUegCQgokqAiCaiSgCIJKO48QwFNsSj3Jin3jucNoriegSJUzJyoMtpXRUep9aKqYaQMYlte8hlBZjiDGctj5xWsgoocUsjjEk5PyEJRHVTDQTWKqIqJJTygguXxkpfuIx/0apT7DSwpSdkmWauIVCHhg5QPgpYkYEpUG9T8uNxAKAJ0BaEpICWy5CBt59VR6AhNQeijk6YgS0XMVJqY1CmqOkLLIhSXpoakpMy2aTlrPj++Htb/7RYe+9uf+djHr2S232L3mpc4/7yz+OLlF2HbkkzcpHfnLqZPm8lkR8Ef9hBUFap0jXkhdwhhCEGFYlOf6GdhXRU/7e2B7Ruob2hFOkG2PbsCgPKsTUPO4l+rV9Pa2sr/fvV6UqkURafInXu3IaSDVuoAwKcVEbZNrUej4EjytsOk6TMY6OlhV0cXdU1uGYD27dtIJBL4J02mM18kUyrgWCVKxTQ64K1sZlL1Quy0m/QxKXIcc/yxPPHU06S9flRNpVrX+OzHrmbmzJlcf/31NIbLqHIcBosmcRNESxuhfJYXnnuW4ZFhTj/tNEjZeB0/UrFRS3mMfBGB6z6VuoIddi+SlgBNOAifQPgCOKoPJ57CcQQDVY3kpnopTrXw1A3jLe8DEuzLCiRtldJIC1a8hU4nwoaaOlZXHMNwXXCc5QI+u0hzfIC6WJzD4ru4boEPZ8rV/HzE5osbeimMCpYvqCnjhsn1tPheuwxHg9egwWtwbk0ZTG1k5/q1/PbWW+ivbcH2lLO5tpW5g5207NlG22MPEtywnoJucPe5F9H13ouZoivMX/0SpaH1dIa20+fr49l8D8/6Ye78k/iIWs1pjgft8CsQjYupm7uZjksuwbj373zqvHP47NFL+MdQnN92D7M9W+APvSPc2jvCrKCXaT4vJcehs1Cio9BCztsE3vNAOjTnoyzM7WSGvYsmvZ3ywF5UI49S2YdTCWXs4fOsfs22j6EK8tMgz3MAnPDG1npNnBvZQs+L15AfnsHj/ITlWoxqdQ9fi63nZbWT3aoHpyqM1jwbzBBszjMThUlOGgVIOx6Cw66by+xwMOIDLJpaycnz6qir8qMeIsv6nOB/Lmnhq/Eu6fkPYNvcb/Jf3V10e+tRHInoSOHvjvGd8yZTUzXCt178FslSkrpAHZfNuowskLWyZEtZslaWXLaAPaKixHSMpA9/JkQ4H8aQKkK1EIqFUMyx76aeJu+NYRoppFpEkSqa1FEdDdXR0aQ+9ls4ykHztEhg2Btkd7iK3aEq2sOVZF+l7vdaJaYmB5ieGWBGZoCGfBKkgkMYi6OpVRzqFIvj1M0IdQMpj4euQAVdvmo6PTV0eercyChmsZNZY9sNyCyT5V4my91MEjuYInZTgTuqUFQbRbXf0o0bcyp5WR7HOnEkW5SpE4jOZGuE0/QUp+sFJvt86L5lGL4PogfKMYIVqJ4ABbvAox2PcteOu9ga3VecVGNq2VQumX0JNf4aBrODDOQG6MgOMJgdZDA9yGBuECHNUULkkqCA6pKjMqkzpVSLX3HQ9AKWL4XfsPAqNioOjlPAcQqAa2r2Bt2JQ9RAVBQH3WdjBC0MYzQxHuMWkf1VUMVR01hgdJoADRxtPJj+AOwjOa+XyNganQCCcKhA1ryuonpDXHDhmXz9uz8lnc5yySUnUsh3MWlSDfff/wTPPfsAZWVhfvGLOxiJDjJzVhuW2U865kWORiwp+7nTfIaPyoZWTj7uOCZPauOzn/8y3/zKl8kV0/zgJz8Ya4csQmNNK11dXdx+6x3MWDKNJ596giceeQIAv+6jwlvB4pkL+F3nrxnYtpWmpibqQyGmnXc2t8ydx7c/fiXf/N8fkyuV+OZ1n2Pxccczbf4iEqZNRqoUFZ1tgSmogMeReEo2Hg1MXCLlDwZpWLSEco+HsKJg2g6G14cnEKa8cTLRTBGfJnj8j7cxedJkqKrhoXVr+dH1X+Lqq6+mfmozWVkgoHgJKRVIUUCKFEIT4PVS9OawLVCcALbupag4OI4Ex0KoJmYEnJwNH9mM1+lj/yfdTDRCTx3axixd6Qhrp89j1ZwF9NTWT7iGPlNyXn0559ZF6Hj4fno6OgiIAlfKvxBYmSS/+ufU1p9NVdP7aK6bwjenNLIw/NaSLE5bsIj5AS/1m1awaPFRNP/jebZ6PKxbvIg906YSr6ziLycso7OyBlIFngKYsgBYQHk2RWVyJxFjLf2F1WxO7eFL7KHGV8MRe+5hYaadeY0LEBdeiLz3XrZ/81vcd9Mt7DUdTMdBG72lJbA1U2BrZmLtLwVo1BQak0nq9nRSs2svnYZgJFfNNfpG9FoffaEmki0zKCl7EXo/tiNcq7xUUB2BQAfFB6ggR11YmTwRLQiOSsbnRSo6CirsK6DKvkKqGkLRUcT+vzWEqSALILMOMispVj6GWt5Oywk3MbLmcqJdR5KzKui0KoAlGMBsgIRE35VDVQbJldfhd1SuaDHYMQCe8hoCe14BICn9KHmbDZuG2LBpCL+hclhThIUtrjZoYUs51aH/vzXm4F3S8x/Bl+bO4fj6Zj6yYhfJgAqTI4ip5fQEitzy0n+RLCWZXj6d353xOyq8B0YVHQxSStLRAoPdSQa644z0pEn0FcgNWSDdC3uwi+sApdFpfyiaIF/tobvBoL1SZWdIkHjVBrxAaypO2UgfjYlhJg10c+SULnJTNpLpr6Q6J1mQ6yJChoNBSrBRyQud6JYwie0humsa2DlrKrsOX8S2imba/WGySoBNYi6bGE+t7i/mqc1ECRe6CRS7WTJQy3B1OfGIjUGRxnSBuckCHt0mGArSOKWZUG0Nj8bzPNCVoFOL0FdWjdyvU6wd6uWIzAjXLlvKnPoFhzzX7Yl27t54Nw+2P0jadM22uqJzRtsZvG/G+1hQveA1M4860iFWiPFc+06uv/8FhJZkySTYGU1x+EA5XY5KTs2xonYFCY/AdR5oGALXaqRIAqpLlkKqoFrTqZQlymQRrwoJTcUBFAnNugeBiiI8SCEoWa5bSUWijDIVTQVFPTRbsaTEkhJ7P+uNIkAXAlWOXkg5/g8heN0EZ2OQNtIuIUYFsqYAc3RdRSp88AMXceed/+SMM06kqakNgOuv/xydnf1ceOEn8fm8XH75xZx99smkUpkxTYnjFDDNGNnsbhTVBzg4jomiqpTXN3Drr37J52/4KssuvIjmpia++82vc+lHr0T3x1A8Sc4840w+fuU1fOMbX6VYKnHqKafx2Wuv42c33kitvxaf5uPiiy7mvn/ex8knn0wikeC2227j8ssv58EH7uczn/kMF59+KoqisPTMM/nJ/3yDSL6HvOLB6xTHEhPaQE4R5Axw3Xp+wI9wJDiSoUyRYdv9XrIcCpZDNDv+tK7YtJ0bvvFNkskEjU3NXH3t57n6E58Aq0RemJhIItKH0LwIzYslIW9KbDOIUEsoWglFT6D4iqjqeGZupyTdTNqOih1vxUpOIp1oJSSLDAf6eLZ+Ec/NWkxOGycpqm1x2K7tzNu5g0W1dVz40YvQqqv5xz/+QU9HB4Zh0Hbe+/jx7mYuaf8jC9I7uKr3H1zRdz9i3kWIhmshPHvC7ZEczpFPm9RNfu08L0IIjrzgEh774X/DAw9jjiSYBui2zerDlxCvKONjz9zPtJNPo/2IY1ibKbA2lWVvvkQ8ECYeWAIsQdjvJ5J5Gm/mSYbyQzy05yEe2uO6uvytPn7m14h0dpC59UaePOtDINznRheCeo+GLWGgZI49K+WWyQUvv8i59/6Fsoz7rthdV8HO2nJOru+muSwBTp66C/8MTYvH2pMupbnsX5fRnmxnVrHE7cUA/s8sH/u/FY/TecUP8cw4G2NSgJpL//2K6pZ1DZtWf5JY4QWqj/o9U9SNeO1rSGYlxezLJG2dmNVKUQYxhQ9T+ujLwVQvDPX0gAZNgz7ypvu+b5RhCo5g7tRyNvWmSBctVu6JsXLPeBWC5gofC5vLec+CBk6d9ToJNd8hvJunZz+8YwVHM0U+cttqNvWm8DT4CS+qpj/bTtnQD1GcNI3h6fx16e8p9/37wi6zZBPrzRLtzTDSmyExmEPTFXwhA29Qxzc6eYMGSa/gFVliTbHAinSO7sJEKuRRBEvCAY4tD3JsWZCWv99F4sYf01dXz9ojl5Dzui/AhQunUFNzH7n8BoQjqco0U56oJ1AaxJPvQuQTGKaNZk+81bKDBt2rK+gP1tPXWM9gWz1xT4RYIMxwqJyhUBmxUDnDgdAEIeQ+hM0U5WaaaUNZ5nUWqYoJpBkm6gmxvcmgfbqf3SExwVpRnYozeaSXKUO9WJbFnuPO5L3T27iwpozIfnoC0zZ5qusp7tpxFy8Pvjw2vynYxCUzLuE9U99DhbfCzcq7eg2phx9GCQTwLVyAb8EC9JqJaeFLlsO5Ny9nezTD1HnVGPk9HNGxze32aut5/4UX8reNe7lrwyaSpWGElsTjTdFSYxIMZIgXBhnOD4+mAzwQxxdMvj48RIW3gb3H/oRJjdVI1UMaHwUlQNBbwDIdLMsDOAR8KXxGBN1ff8iQ5bztMFKyiFvWmP5ZFVCha1QaGp6DaFcOBcfK4yQ7UYv5Mbuiper0OdUksVD0BAA+U6M2ZpEyAljhcuqrQmjqxP1IKZHSxLbzWGaOUiELanE8Od3+EAJV8WGXoJQzkY6GQMcqufe6rUnSPgtbkei2F5/lx7D8E6yflijgiBy6YqMJDQ0FDQUVBSFBOm7CRhyJY1tYto0pFEzh6qY8qo+QqiOBogJxFfKqjVQFlqIe9N4ebSiaZaM40o1KGyWHQo4nGBw7SiGRo1nckQINBUVxkMIBYbufB90HCKHilEwGO7t4oHMtKTOHBHb7WtgSmjZhcc10UIYLVHYMcN7LT3HB3lV4CqNZyDWNLeeczWavF6Eo7DriJJ70uO/RSk3lRs8eTt92K8reZ8c3OP0sOO46UoHDWPNwBztW9CMlnHzZTGYfe+jiv9K2if/tb/R+//tolo0UgorLPkTVNdew4R//4F8dHTiKQn1vHyf2dFP/qU8TXnYWUcthXSrLo+0dPN83zGAwgqnp4JTQi9vRi7vQizvQS+0IWeKkjQ7XPOxQ0OELH/cSbpnNgpoFnNCwhEU1C/EPpeh+/EnuHIxzz6wFjJS7yQ51s8RZnbu5NKDwyr/uYrqvl2WNoyWCLvw9HHbxAW3qTnfzwYcvJV5Mcmo2x42XvYgymv06+a9HSD5SQAlUUXHpDPzz356yE1I67Nz8LXqG/wKA/yWdOUf9jNAJp+A88EWUTbeTc8rp819NT+oYZF4yxatyh+c5bOFQNjgTO/YnQMFT9jmEEGgKRAI6akQnGVDp0hzW5fNsiWfH3iNfOnMGnzr59VNpvBm8W4biLeCdIj1X3L6Gp7cPURkwuOPKI3CMXq54/GoKZgrTmESy5nqOrajl21MbmBt656ouD5dMXkpkeDHuTu35iXogTcDCUIDjyoMcWx5kcTiAT1VwCgX6v/ENUg+6I6CyKVnKvvJ1Ho/Vsn79egDKy8s5+pgA+fwvsazUAfv2+VqorVpGXfhYyHnYtXsXOzv62D2QorRfiJTi2ExSo0zXokTSHyFIiIDn2+yKFFgfmsXL4Zk8XjaHolF/wD4UadOa76fT14CzX0fSGLc4PB2lfuXd+HIZSnUtWD7XoWMLwfa6Vra0zeCU5gbOLCvR3v8w/9z9T2KFUZeaUDix6UTeN+N9HN1wNIpQsDMZkvffT/yvf6W0u/2AY9EbG/EtXIhvwQJ8Cxfwh16VH27qxZgR5JiOV5gy4tYnm7twEeefvQxNcwlX0bJ5YEMfv3thDzsHM8wVe/io9jjnaSsQ0mREVRkMVjIw7RQGG+Yx4pSYXz2fUxpPQPSvZ8eaJynWH8W0hjJ8+2eIFgqOHmDENLCkhhA2fn8KRTgYWjm6txblEJXtLUcSMy1GTDeRoadUwmOWULxejHCIiKZiHIIASauAk+pCKYznKbI1DRFqQPFVYjsO3bE8aTOJorvCWl9JoS5mI4Cs4cdXV0MgfGjho+O4Vs9SIY9Qi+heC81j4jh5pDyws5fgZnAuKTimgmMpFDXweTS8ehFH2DilAHYpgGMdOmHb60EX4FPEmJXHkpBzJKZadIXlEhTbi0TDUgWmKrA03M//cMJLWSoy1N3FV1MOPc74voWUVBfypPtt7IE8Im2OXUdNFRzREOC8+DZmv/wke6TJhoULAZi2ZRurps3juWNP5NJZU/lUSw0hbdSd3LsOXrwJtj7APpNhX2k26zIX0llaBAgUVXD+dQupn1p2wLEWtm2j/1vfprDRTcqX8HnYPXMS7//j38ZKMOzcto27774bS0pqBgc57oXlBKdMofpznyN48kkIIRgcHOQvf/sbe0o2w2VV1M+ey/yGWmaWhWn2qsQy7awfWEfz9b+mbk+CFTMFP71gYubipmHJzB7JjB7J1AGdPWdeyt+PPIlN+0kBpvdu5xux2zk1thLl+M+7BUkPgfVD67nyXx/GFHBF3fFcd+av3PPztRtx7MMBi8b/PgFxkDqG/w662n/Hro4fgADPZoUZ1V+l+v2Xwws/hqe/6y608DKcM3/C+u8+zoPaWjRbMLnfoD/9IqoSpqL8KjIHyvvGYCigeFXSPsGCxXWcdOH0t7UN75Ket4B3ivQ835vgG09u56rjJ1FiiJs33EzOzNESbqWp4UM8GS9ijV6FE8qCXFRXQfnbdFPnHIc1ySzL4xm2Zw/0Pc8L+TiuPMSxZUGOjAQIaBP3aw4O0vPpT1PYtBmEpHZRivLP/wCx+MOAm3TtgQceIJVyic6iRfNobV3FSPRePEYtNbVnU1tzDoVCHTt37mTnzp10d3dP2EcgEGCSz0f5o49RPWoWr73hK2AsJLuiH71Gx3e+w627dtAZ7UMze3lKbCDraeUwYw5BrZUNRiP9xrhrcGFqK+cOP8sRfXvYtreKoZxrgjUCbRxx/sepmB7g+VUv0L13LwC2gN1lSXYFX6SoudEvlb4qLp5+Ee+d9l7qAu6Iq7BjJ/G//oXkAw8ic24OHOH3Ezn7bFAV8us3UNy584BSDHndYNOMWeyZNd2tgSUEZy9bxpLFS9y3xJjLSCItE7ntYdLLf0s4ugkQSBQ2O22sLjuH6cdfwjHT6l2XmpSoIYNoweRb929hQ8cQ3z65hpbmJtoiCh4761Yad1xhjY0gSgUWGioWQW8Se9TApWthDE8tqupFOg5yNHeLLBTcz3weaU/M9pv1+hiqqEL1GIQ1lbCq4lcVsEs4qW6UQnqc7KgqMlSH5p84SpVSMpQuMpSNo+gu0fRaKvVRCzF6Gi2vD19tDWoweFBXorRt8qkimZTbTkVIAnoRhQIORSzFxNQdVE0eNBBfSpCmgub4UISOVEqU1CJFy4tTCqJYHoR8Y0RExSU7+zinA+Qdt2CloxSR6jjhUeTBFQYSsFWBqYIjBIJ9x+2GH6u6A4qNFCZSHjxDtpQKwgTFAkV4EWK8I7ZUAR4VXVNQFEE2m2f3rna+tjmNqRt8aFEFrX6V4+qauX9NNzc+sZOC6aAqgilVAaLZ0gS3W5sR4yTFJf8zN29l/uZN7j90nfCZZ1L+vkvwLVkydu0KWZNtD76Ad8OvmO55BlW4bTDLZrJFXMqL2+bhCxlcfMPhhCrc47YzWUZuvpnYnXeC46AEAlR+7rP8Y+XTpKMjnHrFJ1lw5tljx9TR0cFf/vIXSqUSlfE4Jzz9DIZp4ps/n+rrriNw1JHk83nuvfdedu/ePbaeYRhUVlaOTVWZDNqXrwcp2bKkiheqY2xvFvRVHng/VPuqWVC9kPLyY1mdqOZlO4AcHRBMs6JcNeswLq6vdJ+RQ+Chez/ADRn3/H3nmO9w/tTz6bzyZ2g1izGaJDWffmMSZiklqVKKkfwIw/lhRvIjRPNRRvIjjORHKNpFFtUs4rjG42gNtzI8+BibN30WqdroXYIpqctp+MzXEOv+CA9d51ozZ5zNYwMXsCK5hckVTURKA+xesxIAgYLfV8OM2acRikwjk5DEo3mSWYvsq6z8C+ZVcuyn5r+hdrxRvEt63gLeKdLzP+19/Lxr6PUX/A9gkjRZqNgcbgiW+HQqfF40jwfd40X3jn56vKiaRn7jRno+9Wms4WFUw6Hx2DiBj9/k1t7ZD4VCgSeeeIK1a9cCEIlEWLbsTFRVZ+fOXezcuZPEfjlzAOrq6pg+fTrTp0+noaEBRVEw+/ro+8oN5Fa70QzB05ahVFyILDncuDjAX6oUDCH4zrRG6s21fPG5LwLwkxN/whltZzBQNNkeGyG08yl8a+6kcmQzj/ZNZ6QYACTzyjP4fYfRXjyOHDVUztdpr1zJ0J5eIjlXQ2ALm11lSda3ziYbWcKZ1ZV8sCrMwpdXkvzbX8m/vHasDcaUKZRfeimeuSeReXYAO11yhbGOBMtG2jaO7das6lMSPO3dRlFY+KTBqaV51Mmyt+Wa2ppgrWOx3CnRHYYvLK3jsFnT8PtGIySkdGswFVNQSGGZBUYox0HFoES5TGBL4YopbYEwVTAPVUNLoHgMpGEgMxk3gksIopFyYuEIurSoK0UpN5P7ubEUZLAGzV/3mtqnVN6kKx5FGDFA4hEeKpIqvlx2fO8eL2oggLRdq5O0LLAs18UE2IpB3leFVDT3+J0Rsp48Oc/4fg0EAanic2wUTSI056DFyGypULINirZByTawnHGCoilitI6SjSZBQ8HATSi4/6YShiBhKDhApFBAdVyi4JcedLTRuqoKiqq4kXGjk1AEpVKBXCqBxETRJZpHIDTb7XwOOFYoSrCkgq9oYJkebNU9B75sjrTmo6AHqRQKodEjlEASSVxAwSww1NfDHzdm+OYFC5lWE2RjT5Ib/rGJrf3ugObISRX8z4XzmFIdRErJ3pEsL+2J8uDunUzf/RKqlGxqnMx6bzOnPfUcZ29/kemJ8TxhyqRJlF90CV1li1m/PE4p7xKd1rYSJ7Q9RXjPX6DkDk52KefxRN+HqWyOcMEXFlF44RkGv/c/WAMDAITOWkrtV25Ar61h/WMP8fStvyZcXcMVN/0WVRu/Tr29vfzpT38in89TpSgc99DDeEYHaIFjjqb62mvxzJ3L8uXLWb9+PYlEgoN1iQvXrWP6zl1kAgHWHL6EskCAwJRGEjODdOtDbEttY2tsK5YzkYBqSiU+/yn0Rk6jqLpW/HJN5bKGSj7aVEW95yDaus3/4BdPXstvyiNoisbvpn+H2rsNhO6n8sPTEdMjY8Qlmo+OEZoJpKbg/n718RwKtZ5ajqo5ihPrWtB7fo6t5VGj0LrtDNq+8jNE++NwzxVgF7lDvZY9tuDYssMomyx5/s+3EfXXIc0SVea4fmfSgsUsOus8WucvopQsMbwjxsieJLHeLFOPqaflmEO7L98K3iU9bwHvFOm5rXeE27p76Eh24Egbvx6gLdyKIiZaVXK2TW/BJDv6AtcE1HsMKnWVg72VpXSwSqXRqTg6lbD3q40kHIfaaD8tvXto7tuLv/DGMvQqioJiWqiOg4aDz29i1LahVzShe8bJ0T6ipHm8JAolXtnbRa54YBi9qqq0tbYyY+ZMZsyYQSRycKGidBxit93O0E03gWmizr8Q/6Sl9HkF151Wzi3zJ3HYqAvwxpdv5LYtt+HX/Pz17L8yuWzy2Ha2PPcUT/z+l9ilErrqcE7DViYH3bwgL3s9/DHQwPKgg624t39ToYUjc0dA0v1tKyq7K+po27OXc5Y/TUUqua8hhE47jfJLL8W7YBGpRzrIrho45HmUSLao3azSdiOFpMoJcXrpMAK8NbeJxM01vK8oqCt73k9/EhJkTg3SNmkS/rAf4dHczpRRLYxpIvM5StkUCakihcAoFglkswfuTIDwGii+IIrXi+L1IbwexOjI1SkWMfv6cEbXlZqC4TPRdPf+Tat+hoxK8ASJaCrh13CD7UO+aLInGgMjCkh0xUOVWktpKEa4mHFrBR3y3LgaGVNVyQQ85I08tjL+LGiOhsf2opoK2DZFxUNO9bn535QiIZHGo5ZQNAdF+7/7WpRASULJERQlCMVDQAsRzDoo8RTSspBCkAkGsHS3U816fOQ0A2xJyIYqE7yOHNvesFVkZ18X33hmiN60TVXQIJYt4UiI+HS+tmwWFy9pGiOtUkruH0pw84atHLn6GQzbYrC2icZFp5DtzbKmI8buoQzT4t2c1bGSk3vW4bVd4bSt6AxVLyI+9zQWXHUGkxdUu9vNx2HFL+H5HwOSDutonum5jMOGHsG/xx1s6M3N1H3zGwSPP37sfJilIr//9JXkkgmWXnMdc048dcL5Ghwc5I477iCbzVJZVsZZqTTm3/8Opns8wdNOpfqzn8U7fTr5ri56HnmUvlWriMZipEMhMqEgOZ+PU596Gl+hwMZ589g2Z6IAWwhBqDxEsaJI3BdnZ2Iz/aKL3Kib0hFeCoETKITPxNZcS6eK5MxKP5+b1MzcgId4Me4Sluguhu//OHeGQ+zyGHz2lcWcZVxJqRTjQ4t/SNp67Rw4r4ZhG3hsD17bOz5ZXqSQDPoGGfGOIMX4/V6twierTCoME7ukYq45nOazrqPO6abqyc/yv8XLKAiD863DiU/qZd3jD/ByZCEryo/kkUvq2f7Uw+xZt2Zs0FTR2Myis85l9vGnoL+6rtvbiHdJz1vAO0V61g2u45NPfpKclWNx7WJ+deqv8OsH1+5IKXloOMl32/voHBUWz/VpfDGg0JYaZqS7i2hPJyPdnSQGBw5Z0TpYUUlVcysVDU0gBGaxgFkoYBZdcuR+3zeN/5YHSTv/ZiCFQrGmEbO8BmFbaJkEajqJlk0hpANCYHi9GF4fhs+P4fNh+Hzo3tHvXve3mUoTe/gRyhIpph3+FTRPGcUFOuFj6saWUTw61zzzaVYPrKYt3MZfz/4rHqnx1K2/ZsuzbrbTlrmHcdxV1/KPDdtYs/0X9AV30G+Mn7PDCkVOj4dpHDqBrsKx6C0BEs4mYqXRYoKmybRdu6juG+CxI0/gX8edwtzJrXzM9jLjiV6cuEvwgsc0EDiyjqKU3NEX5c7+KJZjM3/vKzTF3NHulMbpXHzheeiGgRUbofDEnRSWP0yhM0Y+piFtMXo9R60sisAzZQreBYfhX7gQ/8IF6C0tZIoWd63p5rYXO+hL5JmpqHxuci0LbZV8LE36RD+tDS14tdFRpLCRVhFZzCBL+bG2m5pOJhQEIfAWCwTNPEKxXOuHKt2KAAJsVQFPCMVfi6Lvq3M1er2tIvZgN1Yijxx1/wiPJFVZQdQop/Sq28mriglusINZfvKFAp2xBJYed6too9McamUwXsDIplEdG0tRsYWKpSjYioolVBzFRtGyCDXLPmWBQKDZHhTbj4PHTcx4iKKWhqYQEBZ6PgmOhaI5ePw6mkcdTRuwTxA8vr7r8honchI3qeO+JVSx73K6J0KI0SSh+1yZY3/3bQ/263+Qwo342p/k2CgE9CBBI0hQDaAkUlgjUaQ9SvA0gR12IOzDsirJ5UavudfHiO4Ze2WELEl53iEgoWCV6Ozr4pUVGW4cThMbPabzFzTw9XNmUxUcDzVemcjwX7v72D08zPnrnydQKuKrb+TaKy7Ho4/rwkYyRdbsifLKC314Nw3S1ruGxr4XCGb7xpbpKm9k79FnEDr3HA6f3czU6iDK1n8i7/04sa06Q5sjYINUNaquvpKqT3wC5SAd56r7/s7yv/6RisZmLv/xL8eI+dixjIxwxx13kEqlKCsr4wNnnol1x50k77/fLbUhBJ6pUyjuGndzoSj4Dz+c8FlLCZ12GvHnnmPka19HGgbD3/g6Q0A0GiUajWKa5oT9nchKTmQFO3WD3/iPoTOsMqQPkZApSr5F5ENnYnrH03SEo7/Dk32eg+GO1Z+gOnQY6/OP8NVFD7qH5igTScx+k8caJzge24OhGgSDQYLBIKFQaOy7x+MhlUoxFB9iS2YLu8xd9Bg95LQcfkVyVVWRyR4HS8JTvU0k+2cyI6+TogpNKny4eCJrS0+wp3cDW6oWMty0kDs+cTLhcJjU8CAbHn2Izc8+QSnv3n/eQJB5p57JgjPPJlz19gix98e7pOct4J0iPV98/OM81v8SR1TO5eYz/3BIwuM4NsnBAUa6Oxns6uDlnbsY7u4kkhhBPQQZ8YXCVDW3UtncStXoVNncgjfw5jNe2uk03V/4AqkXX8RSBKFpecLTS5gn3IDVcKRLjvYnSq8iTNZ+84rFAmY+j1nIY+bzlPIHF5W+EUwKzuOI6mUU7TwP9/wG09mvxpamUVBMiqqNx++nkjDp6AgIwTEXfYDQCXP5++57eGTvI+Qt9+ETjk5bqoqPpKO813JfcnZJkNgbYHhXOU7Gpr++ns3z5hKvcHVCiq4zMGkmT1Q0cdVeyaWdppucK6hhXDCFKXNqeCme4Us7umnPFwkU85y3fiWRYhIkHH34CZxx9smIzCC8fCu8fBtkR12eqoGcdSGFyjPI9xbIr19PfsMGzN7eA86FWlExKpCejzF/AS97amkJ6dSPdFPcvp10dz+pE46ntbYJjyeIUF8lTpYO0im5pMajUtQUUqMvpXA4TNDvg3Q/Mhd1yxW8av+OoiA8YYQngiylIRdDuJulVFBxCu4aQlXRamuxIhFSlkPKssnaE6+/JgQhTSGiqQQ1FXU/ApROpxlIZyjqCYRwEGi0hlrJlQQl20FTBKoiUIXAlDkyVoK8tZ8bTGpIy49jB5AHtMJ1T6lWEV2alJeXEQr4xqLEHMchG4+RTcZd7Y2iECwL43NSiGIKSxXkPTpqqRyt5GY+kgKGDUHMoyAE1Bg61YZGIZ8fc+0Gg8GDvlcc6ZBNJ8nFYm6iR8AyIOM1sUcZkKEaBI0gIT2EX3dD3K1YDHtkZExnJQwDUe6j6EmAEAT801AUg0wmQzrtWgcMr5e8N0DMGtdmldtQlivS2dlB6JksWlGQnVsBR9Qxo208onR3rsD32vt5ZCSJxyxx4YbnieQyVNXUcOVHP4rPN55wTjqS3euGWP3gXhKDrnXZHzGILK5kZGQHgSceZM6O1XhG3S951eDZpoU8P+t4FtT6uPDx36L3u1ns9SpYO+UaDr/2/UxeODHT8z4Uc1l+96krKOaynPeFrzLtiGMOWCaRSHDHHXcQi8UIhUJ8+MMfJpxKMfzzm0k/9tjojSPGic7pp6NVVY23SUq6PnI5udWrCZ56Ks2//MXY/HQ6zcjICNFolOGHf8IyHgHgAU5n3X6pN3Jqjqg3yoh3hN6wj6HKUygFjgBpERn8IaHc3gkEpqZYxn8NXoVQVO4x76K7woPX8qJJDY/hGSMx+5OZV8/z+Xyv6VaecB6LRTb2buT57udZO7ySxd6NLPC71+jhhM6enhksiC2kAsGFhVPozCxn5fCL5JqmYofKRk+hIBQKUVZWRigYpBSPMrJ7O4XoMMIsodomR51/Ccde8sE3dExvFO+SnreAd4r0FP/5SX7f+TBXJFP4pi9DHvs50r5JjPR0MtLVSbS7k5HuLmK93ViHSK1fNDyMlNcSrahh0qRJXHjYXKZOnoI/Uva2HGOpo4Puaz5Fac8ehCaoPzxGZJIJF90Ks9/zb29fSolVLFIq5Cnlc5Ty+dHv7qeZz1PMZVk1FGXFYBS1VKDcsVjoUdCjURZxGiFPNTtjK9iYXo79GhYpf1kZkQuP5gHzBbZEt4zNn1o2lUtmXMJZbWfz4s4sv36uHd/Wl7h876O0dfezL+JX0RxCbQUKrc287D+ZPYEwBdwO1UBjrtnMXLuFfzX6+OlMD1lNMDPgHROKz8zEOWHdChRZAkfjqBOWsnSGA6t+A1vvGxMVE6qHJVfC4ssheODL3BwcIr9hwxgJKmzZgnzViBJVhf3ExU59PfbXv0ZLdTVeTUP4AggjAIox0QQxCqEp5NQSWdMlPuXl5W7n5TiQG0amB7CFAwhU5+AiYEsVmF4veqARpaS6Lq+Cey4Uvx+9oQHF68VyJCnbJmXZpC2b/Q0uQkBQVVwrkKaiC0E0GiVdKpHTUyBskCoNgRbK/X4sxyKajxMvxLHl+DmRjhdpBZHOuGVCVQS6FGi2REcQ9OuUVXhJjQyST6dRdZ3KxmY3saFVGJ2KmMU8qayFabutNhSbMk8RaTRiFT1jFpmUDoNeBUsIynWVeo+Orijk83nicTciLRAIEA6HD+h8rFKJVHSY0qgoXtV1wpXVeAIBpJSUnBICgTGaV0naNlY0ih2NTiA7WnU1IuQlm28HKfF6GzCMyrH95HK5MfJlo5LAg+PRQHeJniwVKXZ3U/uyidY+6gJXBUZzCLs1xL0+mxvtDDlFYDgWH9m2GnVkiHA4zFVXXTX2vpRS0rk5ysr79xDtcfU53qDO4qWtzD2hEW2/quqZ4Rg77rwL5/5/EBw8sEZgxvBSvyhNY+swWaeCR7Lf4uTPX0RV08EHdMv/dier/nkXtZOn8cH/ufGgHX06neaOO+5geHgYv9/PZZddRn19PYXt2ynu2k3gqCPRqg9OrACKu3ez5/wLwLJo+vUthE46acL/9z5yK00rvoCuOBQOuxztvJ+QSCTGCNH+UyaToSRMnpy9hK6qSXjMIhesf55GBYKqSTC1kxnZGTSox2EmOrhm7p84ctLRfOaozxAKhTCM18sM+u8jW8qwYs2nEcUXANg1XM/AtlPI+pJ8Ln4RMXOQJ3puZ2i6D09gCbpZwn5VsMPBMGdyGxd/+PK39VjfJT1vAe8U6Rlc+QC9T97BSPdeRooBokU/JefgURua4aGyqdm12DS1jFlxhnxBvrdngEdGXG1JUFX4XGstVzdV432NSIA3guxLL9Fz3edxkkm0sE7TUX34qoCLb4dZ5/xb236jSFk2n9/exUPDbvuWVUX46czmsdw52bVdxP/eibRNsk9+A2NWG1Xf/iZUlFPK5Xh058PcueF2NKmQqdGI4YoVdUXn9NbTed+M97GwZiFCCJxSifRjjxP/y1/Ij4bcA/SGqohOKeOImT1MYrxgnul4ecn6FBs0H3HFJT9e3UPN4iU8V93CE6n8WC6gy2IDBDaucn0Ulo/5zXAB/4L+DeONbT4KjvwYzDoPXm2JeQ04xSKFLVvHiFBuw3rs4RG3nQ0NeGbNQl24gPiiRUyaOhVfKDThxS/3lY0oWjgFG7lfroCMKFAQLnko90fw+L1uCQlpQ3oQmR3GVl2CI6REsyWOEJgeHc1fj66XT9B72NEo5tDQmOtAq6x0O2bV7fQcKcnargUoZdmUXuVy8qqCkKJgp1M4VomMngFhgVRQ8WOLcRcWCKQVwLEDCKmhYePRFMqCPoJeHX30+cglS2STrpVQ0yAULJGIJ7BtiU9ziOgH6t2khJytk7EMVOEhaERQpNuGoioY8ApyqsBDnkoSlPtr0bQghUKBWMwVdfp8PsrKyiZcC8dxyCXiZBNxpJQIIQiUleMvK0c5iO5JWtYo2YkhnVGy4/GgVVejRiKAJJdrx7YLaFoQn6/tgE4/m8uTSMQRuFF8kbIKdI/OQNEkmcsx1N3Ft9MOV5YCLN2cRg5MPB8lAV1VGu3qFroTPXi9Xq644gpqRnNS9e6Ms/K+PQzscZ9hw6uy4PQW5p/ajOE9dB5cKSX5l18m+te/kX7iCYRpsnrWsfyk7QwCngK3Gz9khtJDyfHxUO6rlF92McfOqcXQJp6nXCrJ7z51BVapyHu/9t+0HbbwoPvL5XLceeed9Pf34/V6+eAHP0hzc/Mhj+/VGPzRj4j94Vb0piYmP/TgmLutONSBefORBNUC8eAcyq97HtSDtzuXKvGPn65mZDhKUbO46/gm+io8TPYZPLx4OuXpLvj5QnoTP0N6p5DJLud9h/8NRzp8cckX+cicj7zh43070Lnj1+zu/hEoEIs1sDI2mc/tuQIFhfu7fsFtJ2/HMQRL6o/giOojmOOfQ7AUJJlMkkwmSSQSY5+lUomlS5dy1FFHva3H+C7peQt4p0jPM7f/lnWPPDBhnoJDhSdPZVijavaRVC5aSlXbZCI1tSjKocPVX4pn+PbuXjZm3JF5k1fna5MbOL+m7A2bMPdBSkn8zj8x+MMfgm3jbfTTtGQPelCDS+6AGWe9+ca+BWxM5/jYlg468iV0IfjmlAauaqo6oMMe/t0mSnuSmH1rKKz+HUogQO03vk7kPa4l6lsvfYt/7v4nAI3BRi6ZcQnnTz1/LMu12ddH/K67SdxzD3Z0tL63phE+43QSp5/Hb+JhHtrUjyNhkujnyrL1nC07KSXfiymnIpH0aM+xUsmSVNwXnc/r47Cjj6a7aTLmCy8y3LENgGoryyXavVTvqyOuemDexS7ZqX97QjWllFgDAyg+H2pZGeBG0u3du5dJkybhfR3RoHQkTsFCFm2cgkXKyVESbph4mQygKiqKV0N4VRTNQWQHIB/DVsDUFITqwcCLUDQ4YFJxbLAGh7BHXStC19Hr61Ff9WxJKSk6cowAvdoNJqSDYRVxZBTJflYdqaHLMJrjQdgmGg6GKohEIni9XoRt7me5ca03pRIkzSokKgIHvxgibbr3WcQo4NMkaF7QvUjFi8SLlBp2wXEj2nCF5ENelbgh0BWBUSpRpvRhjIqmhaginXaX9Xq9lJeXT7iXC9kM6egI9qjVzuP3E6qsRttv5C4dBxwHadvYiYRr2Rm1bioeD1pNDcp+lqNCYYBSaRghVAKBaQfkXMqVLDpGckjHIqIUUXCJVmVlJYZhEMtk2dTezhcSNj2OIKAIppcELQMFFsdsjo47lBVsXtS2s13rQ5UKZ9mLaG1pwazwsm1vip3tKRxA1RUOO7mJRWe04g2+cVIPbuZhJ5nEaGtjIFngqe2DvLR5D5d33sDhyjZsqXF3+tP8IHgMx82o5rTZNZw0vYbygHvu9r1rm2fP45Jvff+A7Tu2TXJogIHODp5Y/iKxTA6BpGykD69j8v5v/5BIzWtnCnayWdqXnY01OEjVNddQ/dnPgFkg+eMjiBQ7Sdghgl9ajxY+uMUomyxy/0/XEx/IEYgYNEwvZ/3GIW49I0LSr3BsWZC/HjYJ8eNTGYz/N9KxCRw2zL8WFfnhmh8iEPzs5J9xcsvJb+rc/rvYu/Pv7Or8Gqpqo/UImrq+hyfTwIqRh/jdUQ/Qr08keE3BJo5rPI7jm47n8LrD8Wk+pJQUCgWEEK/7fnqzeJf0vAW8U6Rn56oX2fbCs1S1jFpvyn2U7/0H6oY7xkI0iTTD0Z+GRZeBcUAlpAlwpOTewTjf39NPX9F9cS4M+fmvqQ0cUfbGtDxOqcTAd75D8p573d3Pr6Bu+mYUwwPv+xNMP+Mtt/eNQkrJ7X1RvrWrl5KUNHl1fjunjUXhg7e/1JNm6BcbALD6/05+lVsbKbR0KfXf/hZ2yM89O++hJdzCMQ3HoAgF6ThkX1pB/K9/JfPMM67lAdBqayl73yWUX3zxBHN2dyzH71/Yw9/XdHOBpXElHgwEjlqiPPBnQqV7cRBsZgbPyqOJiTLAFczKUQHyyazgBLHKdQWFG+HwK2HR5RAYdze8U3gzpGd/SClxTJtYPIZpWyhSUCYDKPs5tNwCoqBYUYQZQ4g39uqwTRUzq4xFWqteDa0igKIboKgHECZLKKRGrUAT3WAOqjmCABQ1hFC8OI4NEi4/dxmz5s3j2z/4HxRpc9xhC7ni41dx9SeudnPcSDma60aCFBSdEG115fz61r9w1rLTkJaJB5WgP4RqgWI5CPvA9hXtPHk7TSIUIRyOUGNoCGAwlcc2B/FrBfK5EKBgGDoVkTL3nrNt7FKJfDKBVSi4bRACw/CgKorrqrLt8c+DuG8VrxetunoC2QGwrCy5nGuZ9Pla0PWJkZHpgklnNIcjJV5dpbXcSyqZGBPf7iNle/fuZaismu/1RMcGVY0ena9Oruf8mjKee/Rpnl+9HIHgNHUBrdmJJXNsKSmGDCoW1hCaU4nRFHIL074NyOUyJG7/KA1DjwPw99xlfEksBSFQBCxpreC02TUcXafy5LevxbEtTrvqU6iaRqy/l1hvD/G+HhKDAzijgm8pFPJNU7CDEXAcfL3tHHHCSZz8katf93hSjz5K77XXIQyDyQ/cj/3C1/C1P0Te1oieeStNxx1cEpCJF7n/pvUkBnMEyz2857qFhCu9PPKbzazqjPPHU8MUdcEH6iv4+kP3kRtZiNX/Cg3ffQ96UxPfXfld7t55Nz7Nxx1n3cHMiplvy/l9I3jllVd44snfMW/OM2hGASXtp2XTV+kazNFX9SRHaY+z3OfjhfrprLXiE8LlDcVgSd0Sjms8juMaj6MtfKAl8t/Fu6TnLeCdIj2HRD4Oa/4Aq34N2WF3nq8Cjvw4HPEx8L92Ha6c7fDb7iF+3jVEbnR0fHZ1hG9MaaDtNaoWW9EoPZ/5LPl160BRqDm1loqKtQjNA5f+Baae9rY18VBIWzZf3NHN/UMJAJZWhblpZgtl+qHN4ADRv24n/8ownqllCHsFwzffDJaFVlND/ff/h+CxxwJgJ5P/j70zj6uiXv/4e87KWdhBFkUQBBXFFTWtFItELZeyXHKpRFtu2mKmlUvLLetmi1ne6t5Ks5trbpVluUtq5oYrKiACsu87Z53fHweOHgEFxbR+83695gXMfOf5zhwOnM8832eheP16ilesxJiaaj9f2+c23MeOxfmuuxAU9c9lyq8ib+VprDUxCXsw8S+qkekUzOhiZLh8P4qT65BVZnCcDuyiN0W4ocbASH4mjBTO6boSfO90aHdvgy7uG8G1ip5aLBYL+fn5WCwWFHIF7moXMFgQTZd9CAvUZHhZEAQLYEbAhIABrEYEqxnEi8tnogjmKjnm2kBnARQaC3K1lWGPPovJbGbzt4sv2q/xFu3aH0/UsHH8uHs3rTt3c6i0fSmx9w6iXUQEM99ZAEBhfh4arQ6Ntm7SgEwEjUUk1FPP10uW88DA+1A1ECJmlEGVXMAoiLy74E22/vQj277fAICrVo9CEOxCxWC2UKZW2ZbzTCb0ZVdOsb8ci8XCm//+Nys3bSInPx8/b28mjBjBy88+h7KFN7Ka5cqEhARmzZrFrl27MJvNtG8fzLJl7xMc3AGNxnGZprDCSEZRFSIierWCQE8tcpkMq9VKUVERhpryEmq1mtzcXNq0aYNKreaX/BLyjGZG+XrgJJdx6NAhfvjBljl0V/+BGFNcyTiUg5dchpdCwEcjr9NmRlDKUAW5oA52RR3shqqV3laY81qxWilZPh3XpCUAHHS6n7etQynKysLNVIy7qRg3UzE+xjzkYsNxJQqlCnc/f9z9W+Hm609CQQmZ+QUgWnHJTWfah59eNbVaFEXSYydTsXcvuk4BBHTcjwjs142hz8zP6z2nrLCaDR8eoTSvCmcPJ0ZM74aLly3422SwsOGDw+w1VLOynx5REHg2sZwJ50SMJ78g+IevbeOsJp7e+jT7svbRQtuCFfeuoIW2+TOh6uOHH37g0KFD9OkTis76IQZ1ETKTBq/Dj/OBR3eWhP8B214HoLLLGPZ3e4jfsvYRlxFHVkWWg60J4ROY2XNms15fYz+/pYajNxONO/SbAX2ehvhvYe/HUHQedr4Nez6C7hNtx9xa13u6Vi7juSBfHvbz5N2UbJZnFbApr4Rf80uZ1MqL5wN96oiI6oQE0v/xNOasLGR6PS3v80DP7zaX/tgVEHLXDb/tE2WVPH4ylXNVBhQCzAn254kA70Ypf9eYIKpO5GNIKsZr0kPobr+dzBdfxJiSQnrsZNwffhir0UDpj5sQa4Np9Xpc778f97FjUAcHN2hbtIpU/J5Fyc8piCYrglqOdkgQlaZqNL+dJ6O4ipf2Cryhup2xPcfwZGgJ4We+J+zEBtJMGrwoYau5M687TeXfUx8BTdNc+7cCcrkcT09P8vPzMVvMlFoq8GjhYesrZbAgVluwGsxgERHNAqK9te0lIlsQEBSC7SlfDoJMRJBZUbiZkRurMeWXYDWYMFXKsRiVPDZ2FA9NeZYLWfm08qvJlrGawWrm629XEdklnEEhLuRVFGEUlFhrYl4ERDRCNUrMOFlNOIsmWgpViDIlfn4tEGVysILcbEVuFlGYrCjMIvJL4oc0ZuyCxyRAtUzEqJBhUsrAVI2qugpNeSV6owF9RRlyiwWlxYpJLqO0ogy9wYRMFLHKZJS7uNh6fZnN6MsvETwCWGt+EuRyrDIZap3OFt8kk9uEgFzO+x9+yBdr17Lkiy/o1KkTB48cYdKkSXiGhvLMM88AkJyczB133EFsbCyvv/46KlUFx48fRqvV4eR0sdhbbZXrnFLb34C7VkVLd429C71MJsPDw4OSkhIqKyupqKigqqoKq9WKTBAY7O1mt3XmzBl+/NHWgibQoyMn1xiwWm2ZVS06e9J2aBs8/HSYcysxnCup2YqxVpgxJBZjSCwGUhFUMlRBrjUiyBVVS2eEq7TbEEWRypJiCjMvUJSZQaG1E5n591FVnE2JKY++LLni+XkaH5z8gggObkNkpzD8W7RE6+Rii2mrMmOtMhPha2TT8W0kFqZS5tmSQ9t/5bYhw65oVxAEfObOIWXoUCpOpFPm5sRhpyB6PPNWveNL86vY8OERygqqcfFyqvHwXMx2U6rl3Pt0F6r+dZDCI5X80l3HorY6WhWXcLfwO1hMIFeilCl5L+o9Jvw0gXMl53hm+zMsGbQEjUJT77zNSVpaGgCBgT0ICdnK7h8HY3XNJbfnJ0za0RHLuG+Q61vA98+gPbqSAVXFDHhwCeJtczhXco7fMn4jLiOOQzmH6OzV+YZfb0NInp5L+NM9PZdjMUPCRvjtQ8iuKeMuyG2xILc/Cz7hVzw9obyKN5Iz2VFoi6FwV8iZHuTLvd6u+KqVlP/yK5kvv4xYVYUqsDWtBslRl+wBhQYeXgXB/W/o7YmiyDeZBcxNysBgFWmpti1n9XC98nLe5RT/kEz5nkyUfjpaTOuGaKgmd8F7FC1f7jBO3a4d7g8/jOvQ+5DV88R/Kebiaoq+S8SQVGw7N8QV94fCULjZnvhMFiubjmXx2a5kTmfbXl+FTGB415Y80S8IecFZHvo2hUKrjs/G92BQJ98m3VNzcbmnRxRFe6p+UzCZTBTUxD05OTnh6urqEKgsWkQwWcFsRTSLiBYrotnWdLMWJ5lTXSEr2CoQi6IFsaoC0WLCbKyi7YA7eXrqVObOnQNWC1jNlJcW4xcUxkvTp3HiZAK79+yjsLiEoKBAXnjuGR6bOA65UgNyJVEDBtClSxc+/Nf7iCYrIR1DmTr5aZ6Z9BQAiSlJPDljKgeOHqJN6yA++OcChowZxtrlqxk2bARlJQbmvT6Ln3/ZRGZODj5eXoweMoRXnnwSpVLJNxs28MTcuQ63svBf7/DwQw9SVFjMM6+8QtxvvyGTyYi55x7mvjEfP08ZTloDb7/zb378cQdPPfEEC95fRGpqKtZ6lq/uu+8+fHx8+PLLL+37Ro4ciUaj4X//+x8AY8aMsV3PN99gMpVSVWXzYmq1bVAo9PbfT0ZxFYU1bSJaOKvxcannd1EztqKigsLCQjIyMsjIyGDYsGH2zKD09HS+/vprzGYzmmpfdMWhCAgEhHvQe1gwPkH1/58UreJFEZRcjCGlBGulY3VgQSVH3cbF5gVq40K1UzV5qefIT0+lKPMChVkZFGVmYLi0IjcCSpkalUyDSu6EVq7ETavF1b8jzs7eqFTOVFQrqMzIRGMyg8IFmUKPMwKqenMPbZix8KPqEPmyMvRWBdNemoFaexVPaW4Cuf8YQsEJJ0QnkYo5r9LzwbF1hpXkVbHhw8OUFxpw9dYw/Plu9tYal1OUXcHadw/xWyc1m9uocTKZWbHlWfo8u8ihK3t6WToPb3qYYkMx9wTew3v930PWUOPaZqCyspJ3330XgBdffBGdTsfWrz5GqfgVc6tTALj97ktE7DpUxYdhzaO2OLqA3jB2pcOqRaWpErlMjlre8GrEtSB5ev6KyBXQaSR0fACSt9ua8qXshmMrbVtoDNzxHLTu41AgrpYOeg0ruoSwo6CU15IzOVNRzdykDOadTSf2p3WM22SL38nt1oOCIS0pyd1JiJM3rmOWQtAdN/TWys0WXjyTzvqa5ax7PF1Y1KE17ldZzqoP57taU3EoB1NWBZXxuei6+9iqtEb1J/fDhahDQnB/+GE03bpe1XskiiKVh3Io/uEcosGCoJThOrgNutv87JWMAZRyGSO6tWR4V392nc3js13J/H6ukLWHL7D28AXctEqKrToGdfS9aYKnPqrMVfRe3vumzL1v2G84CSpEs2gTRxZbN0LRbEuBF1R6BEClcWfcyIdZ+tVSZk56DplagSCXsWrVD1gsFh4eP5nv1q9l1pxX0Wg0/Pzzzzz59DN0Cu9Kz66RiMZqRKMFa4UJc36NwBOxx29ZZTD6iQn4+LRg3+49lFaW8/z0523jrEasxbloysrxUMn475tv4uftzYnERKa+9jouXl7MfPFFxj/zDGeLi9m8eTNbt27FYjZjqSjDZLHwwCOPoNPp2LBhAzqtlqlTp/LU5EdY9+3/qDapsIoCKSlprN+4lpUr/4uTU/0Brn379uU///kPZ8+eJSwsjKNHj/Lbb7/xwQcf2C7VamXTpk3MnDmTgQMHcuTIIQID/Zk583lGjYoAwGIVSS+spLTa1hjU302Dp77hDxdBENDr9ZjNZjIyMjh//jxLly7l4Ycfpqy4gmVf/w+z2YzK4I6uuC1+wW7cNiKYlmHuDdoEEGQCSl8dSl8d+r7+iFYRU06lTQCdK6Y6uRjRYKH6TBHVZ2xp/UargcLqdMqM+WhkatrI2tPOpRsqNyeclDpUMicUNOA9LavZAA1WkPvamqBdhgmR0prNrJShdVHj4anF00tLTKKMtaVxlMtMrF6wlAfuH4k2wqv+/yEVBbBiDF7tiyhM9oEqGQEpdetqFedUsnHhEcqLDLj5aBn+XDf07g3/Ptx9dQye0omopccp1cvZ663gqbte4+fzB/C7RPQEOAfw0YCP9KknKgAAlrBJREFUmPzrZLakbuHjIx/zbPdnG7R7vdR6eby8vNDpbA+ppXlFyBO70LpPEIVtfqL4tmwOfzuQLsPXoJm4EZaPgvT9sGQwjF8Hri0BGqxT92chiZ5bEUGAtnfbtoxDtqWuU99D4i+2rVUvuON5CBsE9aS3DvB04U53Z1ZmF/J1YhoP/ecj7jxi62e1+u4hfH7/w1jlcmhleyrxuqAgpDCREK2aYI2atlongrVqAjUq1FdpG9AYTpVXMeXEeZKrDMgFeCXYn6cCvO2u9qYi1ylxjgqgdPN5Sn9JRRvhjaCUoe/XD32/xjXjA7CUGSlal0h1gi21WNXaGfdR7VB6NewqFgSBqHYtiGrXgvj0Yj7flczmk9kUV5pwdlLw+vCO13RPf0dkeiWKS7pNi6JYsyxmrfEM2cSQ1WjmkVHj+eCzj9i97zf697kTEQtLv/6a+wcPo6XGm2cfftLWk0ou4x9jpvDrj5tZ9b+VdG8TUWO8ZhKFDJlSBjIBuU6J0l/Plq1bOJN4hl9+3YyvuzvW8nJenzqVYbGxmPLzsdTU0nnpySdBraHaYsIzuA1PZuawdvNPvPLWWyiwFRdUKBT4+tpEraG6ivXr1nP69Gl+37ePjuHhVBTm8+Hb84kaPITjp8/Qp/9dVJhdMBrNfP75W3h5eSCXa7FajchkjnVWXnrpJUpLS2nfvj1yuRyLxcJbb73FuHG2Im65ubmUl5fzzjvvMG/edObNm8K27fsZM2YyPj4h9L3jTlILKqk0mpEJAgEeWlwbucTq5ORkr9KbmZnJvz/+DEOVGYvMgMLoTBtdT/o8HEZgJ88mBaBazGYKLqSRm5JM7vlz5J5PJvd8CubqalxV3rRwak0Lp9Z4OwWgkjvhr22Lv7btVe0KajmCWk5ZaRmuQhJKoQhBZUXeaSAyb19kGiV/bF7LheSTtI7sxu0TJpJeaWBbUh5bT+dy4HwRFpMIBWVQAJ4ZKoZ08qV3gieHDDkkk82uVb8QuS8Ct3uDUbVyvji52QirJ0LReUqtThz1a0Hnc/kULVuG+8gH7EvoRdkVbPjwCJUlRtx9tQx/vhs616t7N9yNZkRBxpv7LvBoLyfSfH2ZWGlhg9ni0BC6u093Xu/7Oq/89gpfHP+CIJcghre9/ppq9XFxaSvQvq8gJwdrVSk9Eh9HUe1FbrtlVHQt5/CWYUT0/AKXSb/ANw9A3mn4ciBMWAfe7W7I9TUFSfTc6rTsYUsfz0+CvYvg6Aq48AesHAve7aHvMxDxEFYrmHPzMOflYs7Lw5yXx8C8PPpu244hMRGUSgqfn04L10zG5WwiWRdEskcEORaBfJOZ/BIz+0sc+y/JgNYalYMQCtGoCdGq8VMrG+VFWZ5VyOzEC1RbRfzVSj7vGETPJi5n1Yfz7f5U7MvEUmKgfG8mzv1bNen8ymN5FG9Isrnc5QIu9wTi3K+Vg3fnanQNcOPT8T04l1fO+iMZ3NHWCx+XG9db5lrQKDTsf3j/ddkoKyujoqa/lru7O2p149zSl8cZCIIAtbE+lxHh050+PXuy9H//5c7OHUi+cIHf/tjLlpd+xoKVfy1cwHc/riczOxOjyYTBaECr1SLTKBBUcgSlDJlOicq35r0lAAoZosnIiYMHaeXnh0dpKcaaAn09O9haAMiUKhSeXsj0etb8+AMff/IJSUlJlJeXYzGb0Tu7UFFiQOviKFCsViulZeUkJifj7+9PK093SrJt7RU6tG+Hm6srmfkF6PVaPLRq/Fu1RtSFYhUL2RsXx4MP/oPa9hWff/4548aNY/Xq1Xz77bcsX76cjh07Eh8fz3PPPYe/vz+PPPKIfUls6NDBPPXUKECgT5+hHDp4lsX//hSfdt0wmm3VqgM9dejUTfv3LpfL6dlhAFt3b6LSUA4yUIpaht07kk59Aq/6t2EyVJOXet4mblKSyD1/jvy08w69AGtRqNRoWrujadMCTRt/tIFBuCq8MKdVYCk2IGgUyLQKZFolMo3CtmkV9u9rA6JzUkrZ8mEhQ1w+xoMLkPg59PgW2txJmMsADr/6M/n7Muk+7n7a+Hsx2d+Fyf1CKKk0sfNsLlsTctl5JpeCCiPf7E+j1N8H/7SDVPm24oAiCY9UPa0Wl6Lt1gLXQUHInVXw84uQ+htGUcmG9HB8htyH/kQS5Tt3kv3GP2m95CuKsirZsPAIVaVGPPx1DH+uW533UENUHrZValen7GPaBSWvTXqA406+PH0qlS8j2jhULh8aMpSUkhT+e/y/vLbvNVrqWxLpG9nYX3mjSa1JBmnd+mJ8aXlhPlZzBYUyCx7pd+HWvR2JpXOpDjMRf+oxwgvfwiv2V/jmfihIhK9i4OE1ENCz2a+vKUii5xbHWllZI2KKMSsHY/YJw3xsK+ZzJzBX5GFe9irm6jex1O3xaUfu5UWr99+hw8l/cvvxfaB2sbkbA7pRbraQXGXgXKWB5EoDyZXVJFfZvq+wWDlfZeR8lZHthY5N7jQymc0zdIkQCtbaxJGLQk6F2cKssxf4Lsf2FH23hwsfh7fG4xqWs+pDUMpxGRhE0ZqzlO5IR9fTB5n26k+1lgoTxd8nU3XUli2n9NPhMbodSt9rF2LB3npeGHjzn2DqQxCE63Yna9w1FAvFVFVVUV1WjU6la/ZqsIJMxuQnn2TatGl8ZKrg61XfEBwQwO3hbfjwy4V8svQzPnjvAyI6dESn1zF95guYZVYUtcGgMgFBEBDNZlsDVIsFc14ehsRErKWltqUuqxVBrkCm16N0tj25K/39UPr5sm/fPsZPmMDrr79OTEwMcouFFStX8PmXX1FRVIXJYEGsiVeyWq2O/ZbEix3pVVotbi18bcHc9qBhATcXPXqNB1nlKsI7dyMubo1tfqULrVvbvFUvvvgiL730EmPGjAEgIiKC1NRU3n77bR555BG8vLxQKBSEhtoCltVqH+RyJ9qGtWPHrjiMZisqhYwgTx1OyoZrfV2OKIoYKk1UlBg5uaUY19IuVHifRels5dHHJuDpVbfUQnVFObkpNZ6bGi9OYcaFelvNqLU6WgQF06JNMC3atKVFUDAe/q2Qyeu5xqArL5tdjk8bF3qO68e6pfMZ4j4ff07D/x6A+z+nZacHaNWhExcSTnDox/VETbyYiu6qVTK8a0uGd22JyWJlW0Iuz6w4wsYMJU/L5CiK8zG7ebFTc4rhlZFwOJeq4/k4t81Bf24FgiDw44UwyhUteGj8JJRl5VTs20fl77+T+e16fjnqTVWZCc9WeoY/1xWNvnF/L5YyI9WJtv+ZpvTfCXlgKqN+K2fZAGc2F5TyZnImr7Zt6XDO1G5TSS1N5dfUX3lu53MsH7Kc1i71J79cC0ajkawsW/ZVregxGaqxVtkehHI8lHjkW3HO60n3PsuJPzARU0sjx4teJuyXp2k56RfbUlfGQfh6qO0h/k8oidIQkui5CYiiiLW83CZmcvNqvl700Fy6WcvLG7BS9wNekIko3PQoWgah8PVD4e2NwtcP10F3ofz1SZuHSO0KE9bbg+L0CjldnLV0cXb8YBRFkVyj2SaEqqprBJFNHKVWG6iyWjlRXsWJ8rpBsl5KBXIBcoxm5AK81MaPp1u3uOblrIbQdmtBeVwGpuwKSnek43Zvw5lZAFWnCylaexZrmQlk4BwVgMtdrZutlsjfFUEQcHNzw2q1YjAYKCwstH8ANyejRo3i2Wef5bvffmPFTz8x5aGHwGTitx07GBodzYRxYxGUSqxWK2cTEwkPD0e0WrFWVSEajVhKSqg+fRqoKe5nsYAg0D48nAs5ORRqtfi3aYMgCBys7bVUw969ewkMDGT27Nn2899f+GHN96UYq9wwG8BkNFNYWGgTPKJIWGBrMrOyyMrJwc/HB1NVFcePHaO4uJjwcMfEAx8XJ7QqOemFSrxaueGqtlUNl8lysVjUVFZW1qnGLJfL7R4epVJJjx4RJCamIJfrUKm8KK0ycfREAn4tW6FRygny0tkrUF8NURQxVpmpKDZSVWVAtIg46RREDuxIx373IFfYGsJWFBeRk5LkIHJKcnPqtal1dcOnTQgtaregEFxb+DR7TZZLadfbl8LMjnz/y2sMdP+IYPU++O4xKMum94iHuJBwgqNbN9NrxCi0Lq51zlfKZQzq5Ms7IyOYvvooO4S23JW9G6PehWpgh18iwxW3IaZVUprgTQWfk2P8lZTydO6eNBGdmzu4ueP5+BTyP/6EvAULMPaci3ewN8Oe7YqTrvFZnJVH88AKlsJzYC2j1/ShFL3xP4r3t2d9Xz2fpufRVuvEOP+LQlQmyHjzjjfJLM/kRMEJnt72NP8b8j9c1XXv9VrIyMjAarXi4uKCW00R1PJCW5KDUVBAiCfk51GdWITvsJ70uvMnDu18AKN7KWeqFmP4XzptHtmA8N1jkLQFVoyB4Yuha92g7z+Da/qvtXjxYhYsWEB2djZdunTh448/plevXg2OX7NmDXPnzuX8+fOEhobyr3/9iyFDhtiPi6LIq6++yn//+1+Ki4u5/fbb+fTTTwkNDa1jy2Aw0Lt3b44ePcqRI0fo2rWr/dixY8d4+umnOXDgAN7e3kybNo2ZM5u3FsC1UPLjJsq2bnUQN7Xp1I1B0GhsAqZ2a3HJ9+5uKIoOoDy7HFnFOVt8syIduo2HPhNsUfPfPGBT2U5uMHED+Ndfnt1hTkHAR63ER62kr7tjwUOTVSSt2nBRCFUZSKqs5lylgRyjmXyTzZ3tp1byWXggvRtZMLGpCDIB18FB5C85SfneTPR9/FHUkxVhrTZT/OM5Kg/a/lErvDV4jGqHKsC5zliJ+hEEAXd3d7uHo6CgAC8vL+T1Pa1fI3q9ntGjR/PKK69QWlpK7PTpKORy2rZuzfotW9i5eg1ewW346L//JSc7m/ZBbTCcPo1otSKaTIg1yygytRpBJkPu7o5Thw4M6dCBsH/9i8eefJIFCxZQWlpqFze1hIaGkpaWxsqVK+nZsyebNm3i519txS9F0YBMqKJVy9acT03h8OHD+Pv54ayUE3Xn7XQMD+fZl17hjdmvUFVRzsuvvU6/fv2IjKy7zODspKRtCz2pBTKqKpzw0hagsBqoqEhmyJB7eOutt2jdujUdO3bkyJEjfPDBB0yaNAkAozGPadMm8thjLzJgwC9079Of9d//yK6tm1m+4WeCvfXIG7k8a6w2U1FswGSoaWkhCKi0CoY+2xVnFx0JcTs4vXc3uSnJVBQX1WvDxduHFkHBDiJH737l2mI3it7DQyjIrGDz8RcY4LWUDoof4ZeXCewzlRZtgslNOceRzT9w+6jxDdp4oHsrzuaU88UOE3cU7kOZkoCsU09yC/PY2/YYQzQ/UVo9Covoi5fqYQa3KaRN6MXGpmLMaKq+XIWmMo+OhVvo+dG7TRI8AJWHbf+jTOn70PaMRKFxYuDdRVRsOEXh8U7sitAy62w6rZ1U3Olx8f+XRqFh0V2LePinhzlfep4Xdr3Ap9GfopRdf9mMS5e2asVrab7NU16u0OPXpQUcyMOcV4W5qBqtext6x2zn8K8jqNBeICX4e6o/y6TDE18j/Pw8HFsFVYXXfV3XSpNFz6pVq5g+fTqfffYZvXv3ZuHChcTExHDmzBl7D5ZL2bt3L2PHjuXtt9/mvvvuY/ny5YwYMYLDhw/TqZOt++y7777LokWL+Prrr2nTpg1z584lJiaGU6dO1SmwNnPmTPz9/Tl69KjD/tLSUgYOHEh0dDSfffYZx48fZ9KkSbi5ufH444839TabFcPZs5Rt3lxnv0yvv0TItHAUNrX7Wngj0+mu8qQUBdbpcPpHW7p75hE48IWtm7ezH5Rm2GoCTdzYLC0QlDKBEK0TIfWkdJaZLZyrMpBjMNHLVXfVYoPXizrMHXVbNwxJxZT+eh6PMY4VSquTiylacxZLsQEE0N/eEteYQIQmuP8lbNTWdqktXpibm4tarUatVqNSqVAoFNf9RB8bG8uXX37JkCFDaFnTD2ne/Lc5n5XFsMenoHVy4rEHH2TogAGUlJUhWq0IcjmCQoFMp0Pdrh0ypRLkcmROTggyW03p9evXExsbS69evQgKCmLRokUMGjTIPu+wYcN4/vnnmTp1KgaDgXvvvZe5c+fy2quvAmA1l3HvsGh+/CWKUaNGUVJSwsJ/vc/EiVP4bs16Zsyczn0jH0QQBAbceSfvvPVPrFZLvS1lVAo5Id56MkvkZJYr8XIqRKus4p13nmP+fA3/+Mc/yM3Nxd/fnyeeeIJ58+ZhsVRiMOQydOjdfPLJB7zzr/fJzJhOUEhbPl+6nAfvjW6UJ9VksFBRbMBYXRNnIwhonZXIVApKDAoELPz8yfsk/Lbz4kmCgId/q5olqhB82oTgHRSMRn/rPDTIZAIDJ3Xku39VsT17EuZWvkSYv0DY9wnD2/Tnq/NWjmz+gcj7HkB9hfIVL8a0Iym3jISidnQtPY6XxUi6TMbJpFT8KCTS7xN2nLyb9q59cZF5UPCfE2givDBFePHj0gR0IQ/R9fi/8TqzBS6kQLuwRt+DKacCU2YFomjBdOEg7qOnAqAM6c297hOoOruAAudATgSpmXwihR97hBGqu/g/2FvrzSd3fcKEnyewP2s/8/fPZ95t8677b7I2iPnSeJ60NFv8WqVCT1hrN4oDXDCmllKdWIS+lx8qlTu9hvxK/K8PU6SKJ6vzQQyfDSUidj2KTg/e1OWtJtfp6d27Nz179uSTTz4BbOvbAQEBTJs2jZdeeqnO+NGjR1NRUWEvbgVw22230bVrVz777DNEUcTf358XXniBGTNmAFBSUoKPjw9Lly61r28D/Pzzz0yfPp21a9fan4RqPT2ffvops2fPJjs72x5v8NJLL7FhwwZO17i8L8dgMNgrkoJNOAUEBDR7nZ7Kw0eoPnG8jriRaW5AQSlRtKW571loS3sHW5XnR74H34jmn+8WwJhRTu7HtsahLaZ1Q9VSj9VooXTzecr32v445R5OeDwYijrY7SZe6Y3neisyN4ZaT8/ltWYEQbALILVa3SwiqBZRFLEUFmLOyUEURWRaLTK9Hrlej+BUfw2a5pjTZDJRkp+HyWKxlZQQRRRWC0qlCybDxWUkuUKGxkWFSg2FmRewWiyotTrcfP2ueG2FFUYyi6vQKcvwcCq2tcoQFDhpWqFUONdch5WKiiSsVgMKhSsF1V4UVdbU4HFxwsdZfdX7N5tsYsdwSa0cjV6F1lWFXCGjurqa5KREjq1dTuap4wgyGb1HPESbbpF4t25z1QrFtwrFuZV8985BDJVm+nc6QsfC+QhWM1nmFnyXHELvMZPpNfzBK9ooN5iZ8P4P3HH0S0QEInu3YGdpAAJWWlXlUXw+nW5R99HN524qDmSDaGsNlGywUuKnp3vq11Rs34omsgeB33zT6Pdmyc8plO26gDnnGFX7PqHN9xtxCgsDYyW805pCow+rqhbz5W06LngpCXKyNSf1VDk+WO5M38kz259BROTFyBeZ2HHitb6cWCwW3nnnHUwmE0899RQ+Pra+ZN98+gW5OzeQ2SKC9z9+m9KtqZRuTUPTyRPP8ReXdUXRSsLOZ8kSfwJAf8qdrg9+j9rXv975rofG1ulpUjCD0Wjk0KFDREdfbFMgk8mIjo5m37599Z6zb98+h/EAMTEx9vEpKSlkZ2c7jHF1daV3794ONnNycpgyZQrffPMN2nqU+r59++jXr59DgGWtB6qoqH737Ntvv42rq6t9a0qn3aag7d4Nj4kTcRk8GG2PHqhat74xggds6e7B/W1xO0/shn4vQuyvf1vBA6BqqUfb1Vb7pOSncxjSSslddMQueHS9ffF5ttvfXvD8WSiVSnx8fPD09MTZ2RmVSmULIq5pJlhaWkpeXh7Z2dkUFhZSXl6O0WjkeuqgCjWd2tUdOuDUoQPqNm1Q1jw4NIfgqRU4lZWVFBcXk5eXR1ZWFvn5+ZgQ7IJH66TGu1UA7r5uePrr0Trb7t1itlJeWE1RjgG1zlbbxVBZQVlB/hXn9dCpCPHWYbC6kFnug9GqRBTNVFWep7o6E1G0YjBkY7UaEAQFuZVuFFUaERBo6a7Bt4Gig7VYzFZK86sozKywCx4nnRIPfx3Onk7Ia+LZDBUVlBcVUZydhc7NnVFz53P76An4h3X4ywgeALcWWmIe74QgE9h1ohvJHRaDSo+fIpcxgUc5/fMKTMYrZH0AerWCRU/GkK1rhYCIPOkA3YQERGSkqzxRunnSZ8I43B8IRRjRlnyLiEwQCHWS09NgwnX4PxC0OqoOHqL0+++vOFctolWk8ogta8t0fg9yby/UteEdKi34d8NDcYHhd+UxZl8FbuUWzlcbiT2RguGyh4+ogChmRNocCO8dfI+d6Tub9BpeSnZ2NiaTCScnJ7wv6VGYm5UNgIunrYK6uqZuU3VSia1waQ2CICN8wMcE658CK5SHF3Hwx3uoSDx2zdd0vTRJ9NS6tWvVXi0+Pj5kZ2fXe052dvYVx9d+vdIYURR59NFHefLJJ+tdJ7/SPJfOcTkvv/wyJSUl9i09Pb3ecX9Z/LrAXXPAq25s1N8Nl4FBIBcwJJeQ9+lRzPlVyFxUeD3WEff7Q5E1MX1X4srUenWcnZ3x8vLC19cXLy8vnJ2dUavVdURQfn4+2dnZFBQUXJcIEgQB4TprR10qcEpKSuwCJy8vj+LiYiorK+2ZWYIgoFKp0Ol0eHp64uZ5sVidXClD7+GEZys9enebgBCtIoZKQGZ70qwsKaaypOSK16NRKWjrrUej0pJV7kOp0ebhMRoLqKhIxGi0BY0WGjwprba1iQj01OKpa7h0gMVipaywmoLMcqorbPei0ihw99Ph4qVBUbO8K4pWSvPzKC3IA1HENySMCf9aRKvwTtf+At9kAtp7cMdDtlo/v25vQeadKxD1Png7VTLCM46kn76+ug0PLcN628I1ThT7Irbqjry6EuQKzG07IlOqyDhbxI/Lz7KnzEyimxNyLw1ipZmyHfk43/s2cu8O5Ly7AEtp6VXnM5wrxlJqBMGMOec4uj59HMVsoC12yN+8k+FjOzAmrgy10crvJRXMOJNe529pQvgEHgx7EBGRmbtncqbwTGNfPgdql7YCAgIcguzLagKZffxsNatUrZwRNArEajPGC2V17LTpNYOOrd5GMApUB1dz6sCNK6R4Nf4SnwQff/wxZWVlvPzyy81qtzYeQeKvj8LDCX1ff8rjMkAEbVdv3IaFNCqNXeL6qRUHtZ7WWmFhMBgwGo12kXPpknLtObVLYkrl1Ws/NRVRFDGbzZhMJoetPsElCAJKpdK+qVQq5HL5Va9JJhPQuqjQOCsxVpmpLDViMjiBzIJoLac0PxerRUDn7tygLYVcRqCnltwyOTmlAlVmJ7w1BWC1LWNVmJwpqVahkMkI8tKiVdX/r9tqFaksNVJVelFUKtVy9O5qlJcJf4vJRHFuNqaapAqVVkvMU8+i1V1/Ha2bTURUKwoulHNqTxab1oqM+sf3qNeNwIUs1IdfwdK5A/LgOxs2kLqXvtmfclLRlQqzmj07z9DCWkR1286UVlSy4n9rqI5vicUkEhDuwZ1PRiCXC1T8nkXptjSslU5ob38ec/Zxcj/8L36vvnDF662tzWMpTgCrGV3fvo4DAvvaQhZS9xI63IdhhdWU7U1leT9n1mQX0VbjxLNBFx/6BUHgld6vkF6Wzv6s/UzdPpXlQ5bjra2/GniDL0NNEPOlRQkBLGW21ZM2Qbb0eUEm4NTWjarj+RgSi1AH1l1e8m0/CrW+JacOPU/He79q0nU0J016ZKrN1sjJcUxXzMnJsVcpvRxfX98rjq/9eqUx27dvZ9++ffY4gbZtbSo+MjKSRx555IrzXDqHxN8bl3sCcb4rAM+J4XiMaS8JnptIraBxdnbG09PT7glycXFx8AQZDIY6nqCysrJr8gTVCpxaD06tzVoPTkVFhd3upR4cNzc3vL297dfo6uqKVqttckySIAiotUrcfXW4++rQOLshyGzL2OVFueRfKKayxIDVUn9Ld0EQ8HFxoo2XDpNVQ0a5L5VmLVVmDflVrqgVMkK8dfUKHqtVpKLEQEFGOZUlBkRRRKGS49ZCi5uPto7gMVRWUpCRjqm6GplchouXN046ff21c/6CCIJAv7Ht8GvrirHawg//K8I88Veyje6oZSaE/90PpzbWf3JRKqwaj1w00jnUDQBfYy6YzXTvE4VMJuN8ehJlqlRad/RkyFMRKFS2xrH621viOyMS/e3+IIDCNwJzRQ/yl/yBpcbjdjlWo4WqEzbPSfVRW+yLrs9loiegNyBA4Tkoy6bbwNYMa+fD4MOVALydksX3NS1+alHKlLzf/32CXILIrsjmme3PNKkPnyiK9QYxF5QbcDLavFcd2l7c7xRas8R1tv5wEgD3VrfTd9h+tK5tGn0dzU2TRI9KpaJHjx5s27bNvs9qtbJt2zb69OlT7zl9+vRxGA+wZcsW+/g2bdrg6+vrMKa0tJT9+/fbxyxatIijR48SHx9PfHw8P/1ke2OsWrWKt956yz7P7t27LxYMq5mnXbt2uLs3reCVxF8TmUqO68AgNOF1i6lJ3FxqRYZer7+qCCorK6sjggwGg4MIqhU4VVVVDqIpNze3jsABWxzSlQROc3uZlGo5Ll4avAL8kCvVgIjFVER5URUFGRWUFVZjNlnqPdeW1u6MWqkit9KTnEovNColId561JdlHYqiSFWZkcLMciqKDYhWEblShou3BndfLSqNo3ATRZHywgKKsjKwWiwo1Wo8WrZGrf3re3cuR66QMejxCPQeakpyq9i+poC0Hv8kqcwTmdWEuPoR2P+540mGMlgxFioLwK8L4ZP/ZT90RhfKl3us6EpDAKhwPk9YtJN9qbAWmVaJ29AQfKb3AGsWgkxO9RkD2QsOUBaXUdN77iLVJwsQjRYEJyvWwmTUoW1R+lyWCa1xA5+aJcfUvQiCwJ2jQxmp1dP7jM1TN+1UKodLHavqu6pd+ffd/8ZN7caJghPM/m021noKSNZHQUEBlZWVyOVy/P0vBh4fS8nFqcYDWbu8BaAOcwPAmF6GtapuFe5abmTNpsbQ5MXx6dOn89///pevv/6ahIQEnnrqKSoqKnjssccAmDhxosMy1LPPPsvmzZt5//33OX36NK+99hoHDx5k6lRbOp4gCDz33HO8+eabfP/99xw/fpyJEyfi7+/PiBEjAJvK7NSpk30LC7OlAYaEhNCqla39wMMPP4xKpSI2NpaTJ0+yatUqPvroI6ZPn35dL5CEhETzcyUR5FSTjXWpCCooKCA7O5v8/Hz797m5uRQVFdWJEVIqlWi1WrvA8fPzw9vb+4YJnCshV8jxbNkSuVIJogVRLEEUrTVCpYLi3EqM1eY6Xi2VQkawt54Wzk546tW08dKhuKTooCiKVJebKMy0CSirRUQml+Hs6YSHnw4nbd17tFjMFGdnUl5kq5GicXHBw78VCuXf1yOqdVEx5KnOKFQy0hOKqDR04ZfCSOKLfBEQ4eeZsOVVe8Vu1j0BuSdB7wNjVnBq3+92W95KNf3zwKncDy+nIADWb1xPQUFBvXMrvbX4vBBF1cHFWErSEastlGw6R87Cw1SdLLD/zitqApgxpgDUXdqqpSauhzRbgo9MLmPg5E6MLZQTmmnEIIpMPHqOC9VGh9MCXAL4MOpDFDIFW1K3sDh+caNeu9qlrVatWjkUIj2VZPP+WBVqh/R/hZsTCm8NiFCdVNyoOW4GTRY9o0eP5r333mPevHl07dqV+Ph4Nm/ebA8aTktLs5esBlvn4OXLl/Of//yHLl268N1337FhwwZ7jR6w1d6ZNm0ajz/+OD179qS8vJzNmzc3KeXW1dWVX3/9lZSUFHr06MELL7zAvHnzbnqNHgkJiatzqQjy8PDA19cXb29vuwiSyWS2CsJGo4PXp1bguLq64uXlZRc4bm5uf7rAaQiZXI67rz8yuQzRakShrEClsX2IGKvMFOdUUpRVSVW50d7qAkAmCPi6OtHSTWMvOljbMqIoq4LSgiosZisymYDe3QlPfx0avare+zVWV1N4IR1DZSWCIODawgdXb5/rDgr/K+Ad4Ez0o7Y06pO782nZMYpt2W2JN3e3DdizENY/AdtegzObQK6GMcsprpLxx8Y1djteJWeQiyZOK80ktupIq1atqK6uZuXKlQ6lTy5F6dMCj/GDqNzxJoYz3yHTKTDnV1HwzSny/3uc6jOFGGraTlTHbwKuJHpqVlNS9160r5Yz9OnOTDhtpkWxmXyzhfFHz1FudvQiRvpG8lqf1wD4z7H/8EPyD1d93epb2gJISbV1k5c7111BcarJ4qq9p1uRJtfp+TvT2Dx/CYlblT+jTs/NoHY5y2i0PcXWBhvfbEHTFIxVlRRlZSKKInp3D5yc3agqNVFdcTGwWiYT0DircHJWIr+snUSdKsr2AGoVsgYqMYuiSFVpCWUF+bY4H6USVx8/lJclcPxd3zeX8scP5ziw6TyCrApj6ZdYTEYeGd8Xr8PvgXiJSHjgv4gRD7H+nddIiT+EV+sOFFzIRrQW4RI8lHcJwCSKTI8KwHjiV8rLy+nQoQOjRo2q9/0omkykPDASQ2Iirg+NRXfHRMriLoD54kev0ldF4WePglJJu9/3IasvmLwsB94PAwSYlWIrOFtDYVYFX31ymE9v11GhkRHt4czXnYMdmpMCfHT4I744/gVKmZIvBn5Bd5/uDb5eH330EUVFRYwbN86hO8LEWR/T5fwvuIRGMOXNtx3OqTpdSMHSk8jd1PjO6vmn/n3ekDo9EhISEjeD2swqnU6HTqez1wb6K6HSaHHxssVqlBcVYqquwNnTCc+WOnRuamRymUNAcml+FSajBZPBQnFOJcU5lZgMFlsTWRcVnv46dK7qBgWP1WqlJDeH0vw8RFHESafHo2VAHcHz/4We97YhpJs3olWDwqkzANsPl8HDq0FZIzLueB46jyLpwD5S4g8hyOSUld6GXG2rZK8yH+fV4Tav0Qc70wnsGY1MJiMhIYG4uLh65xWUSnxfnQdAyXcrUfmV4vtCJJouFzOpBIUt6UbbpUv9ggfA2Qc8QgAR0vY7HPLw0/Hwo50Yu68chVlka2EZryVl1DExrds07gm8B5PVxHM7niOrPKvOGLAJiKKiIgRBcKhfV2EwU11Sm67uU+c8dbAryAUsxQbM+Y0Pmv4zkUSPhITEX56oqCiee+45+89BQUEsXLjwiucIgsCGDRtu6HVdjsbFxdagEijNy8VYVYVMLkPnqsazZU0NHZXcFhdRYVvGKsqusLeN0OhVePjr0Ls7IbtCY1Gz0UhhRjrV5WUIgoCzpxeuPr5/m+ysa0GQCdz9aDierfQg6waCjPRTx8kUW8KTcTDqG7hrHqbqanYs/S8AclUkguBO+zuiUKhU5KedZ4BbJY/2DQLg1W3ZdLv9LsCWZXz27Nl659ZGRuI6fDiIItmvvY7cRYnn2PZ4/6ML7g+GYjxj6/Omu72Bpa1a7HE9e+sc8g91Z+KwMIbvtzWp/u+FfJZmOBbHlAky3rrjLcI9wykyFLHk5JJ6p6ld2vLx8XHw/CVklaI32ex7+9QVPTKVHHWQzctiSCy+8r3cJCTRIyEhcVMZOnSoQx+sS4mLi0MQBI4da1oF1wMHDjR7PN9rr73m0OD4WtF7eOKk0yOKIsU5WZhNtiW7uLg4HhrzAJ16hOIT5MqWnT/bz3HSKfH016P3UPP6G6/h5+eHRqMhOjqaxMREB/tV5WUUZKRTUVZO9NDh+IaEkng+9S/nGbsRKNVyhjwVgdbVA7myAwD7N6wBzxAIHwYyGfvWraSsIA9kLsidehF+hz8DJ/Wg/e1RAMT/uok593bgzlAvKo0W3v7DQMcutibOa9euJT+//ircLV6cgczZmepTpyhatQoAdWsXtF29qPjd5rlpMJ6nllrRk1pX9ACE9fQltmdrBhyzpbLPPnuBHQWOxRE1Cg3P93gegI1JGyk3ltexUyt6Lq/PczKzFL3FliHmXFON+XLUjUhdv5lIokdCQuKmEhsby5YtW7hw4UKdY0uWLCEyMpLOnTs3yaa3t3e97WpuBUwmEy4tfFCqnbBaLBRnZ2G1WKioqKBLly4sXmzLrtG5qvFspcezpR4XLw1ypczenPmzzz5j//796HQ6YmJiqK6uRrRaKc3PpSQnG9FqZf4HHxBw2YeWBLh4ahj0eARKbS8Azh36g7xUW+ZUwYU0Dv6wDgClZgAR/YOIergdgkyg68AhACTu34OxvJRPxnYn2EtHZkk1q7I8adUqAIPBwMqVK6muKfh4KQovL7yfs1Uizlv4EeaarK/qEyewlpUhc3HBqdNVKmHXip7MI7aeXPXQbWBrHvf2oHOKAQsw5XgKpyscl5p6+/amjWsbKs2V/HCublDzpZ3VL+VkZgl6s00kOXvWX+jQHsx8rrhOev6tgCR6JCT+xoiiiLWy8qZsjc2RuO+++/D29mbp0qUO+8vLy1mzZg0jRoxg7NixtGzZEq1WS0REBCtWrLiizcuXtxITE+nXrx9OTk6Eh4ezZcuWOufMmjWLsLAwtFotwcHBzJ071173a+nSpbz++uscPXrU1gpDEOzXm5aWxvDhw9Hr9bi4uDBq1CiHQqm1HqIvvvjCHigsk8lw8/VDrlBgNhopzsli0KBBvPnmm9x///32c+Vymb0/liiKLFy4kDlz5jB8+HA6d+7MsmXLyMzMZN1331GYlWFvebHn0GF279nL+++/36jfwf83/EPdiBp/GzKlrfzJ9qX/QxRFNr6/ENFqRaYMpltMf/qNDUOoiZnyCW6Lb9swLGYzx3dswVWr5ItHInFxUnAwrYQEbUecnZ3Jz89nw4YNdRryAriPGYM6vAPW0lJy37P9bsr32rw2ut69Ea62/OgWCM7+YDXDhQP1DhEEgf6jw3iqWk3rXBPlosi4I8nkGU0OY8a2HwvAitMrHP5Wq6qq7O/fuqKnFOda0eNVv6dH6atDplciGq0YUq/eguPP5i/RhkJCQuLaEKuqONO9x02Zu93hQwiN8LYoFAomTpzI0qVLmT17tn0ZZs2aNVgsFsaPH8+aNWuYNWsWLi4ubNq0iQkTJhASEkKvXr2uat9qtfLAAw/g4+PD/v37KSkpcYj/qcXZ2ZmlS5fi7+/P8ePHmTJlCs7OzsycOZPRo0dz4sQJNm/ezNatWwFbmQyr1WoXPLt27cJsNvP0008zevRodu7cabedlJTE2rVrWbduHfKaDza5QoGbrz+FmRcwVlVRmpeLi3eLBpehGmrO3LNnJDu2beWuPr2RyWVUWwWenf4CGzZsuGW9XbcCHe9sSfqpoZzc/j4XTv3B6jc+oSjzLKCg8z0Pc8eo0Dq/i64D72Vz0lmObf2ZnsMeINhbz+Jx3Xl0yQHWHivghb63Iz+6hdOnTxMXF0f//v0dzhfkcvzmzeP8mLGUrF+P20MPUrnXVnfnqvE8YGsoHdgXTnxnq9cT3L/eYTK5jHsndaJo0REWaCxkOMMj8edY1yMUp5pYsGEhw/jo8EeklKSwP3s/t/ndBmD3uHp4eODs7Gy3aTRbOZ+Zz12iTTw5e9QvegSZgFOoO5VHcjEkFuMU4nb1+/oTkTw9EhISN51JkyaRnJzMrl277PuWLFnCyJEjCQwMZMaMGXTt2pXg4GCmTZvGoEGDWL16daNsb926ldOnT7Ns2TK6dOlCv379mD9/fp1xc+bMoW/fvgQFBTF06FBmzJhhn0Oj0aDX61EoFPj6+uLr64tGo2Hbtm0cP36c5cuX06NHD3r37s2yZcvYtWsXBw5cfBI3Go0sW7aMbt26OSzVKdVq3FrYqtpWlZVSWVLc4H1c3py5trqyu7MLubm5KNVq3P0DeOLpp6/YnFniIgMn90frFgaIXDj1CwCtI2K4a2LvesVnWJ87cNLpKc3LJeXIIQDuDPVm3n01GV37CgjubuvptWPHDs6cqdvoU9O1K24PPQhA1rx5VB49CjQinqcWe72ePVccpnJSMPqJzsQeM+JktHK4oopnT6XavTo6pY5hIcMAWJFw0XPa0NJWYm4ZaqOtmahap0d5hdIG6pr2HdW3YL0eydMjIfE3RtBoaHf40E2bu7G0b9+evn378tVXXxEVFUVSUhJxcXG88cYbWCwW5s+fz+rVq8nIyLAXKGysFyMhIYGAgACHUvr1tc1ZtWoVixYtIjk5mfLycsxm81XrddXavjStNzw8HDc3NxISEujZsydgCwj19rbFQMTFxTF48GD7+M8//5wR991LWX4eZQX5turNV8FiMVOSm4Ox0hbXIVcq8fBvxceffHJDmjP/XZHJZQx66lHWvf0KABrnFoyYGdugt02pUtNxwD0c+nE9R7f8REgPm6dxYp9AzuaU8e3+NObvr+aViC4knjzKunXrmDJlCl6XLQV5T59O2a9bMCYl2+y2aoXqMpHRIIG3276mHwCzERSqBofqXNVMiu1MwVfxfH2blo35JYSmZDMj2A+AMe3HsOL0CnZe2ElmeSb+ev8GixJeurTl0kAQcy21fbhMGeVYyo3I9Q1f45+N5OmRkPgbIwgCMq32pmxNzRaKjY1l7dq1lJWVsWTJEkJCQujfvz8LFizgo48+YtasWezYsYP4+HhiYmLshQqbg3379jFu3DiGDBnCjz/+yJEjR5g9e3azzaG7pPZKZGSkvY9gfHw8w4YNQ+viitbVFYCSnOx6bdQ2Tr6QlkrhhXSMNdWVi0pLCQgMQpDJGtWcWcKRNl070zqiO4JMzr3PPoNSdeUP6C732ARrSvwhimt+V4Ig8NqwjvQJ9qTCaOGzZGf8WrZqMLBZ4e6O9wsXWyQ12ssD4NXOVpjQXAVZR6863MNPx7RR4dx7xCaQ30vNYX2OzQMT7BrMbX63YRWtrD6zGpPJREaGrb7P5ZlbpzJL0ZtrMre8rtytXe6sQulne88bbrGWFJLokZCQuCUYNWoUMpmM5cuXs2zZMiZNmoQgCOzZs4fhw4czfvx4unTpQnBwcIP1UOqjQ4cOpKenO7TH+f333x3G7N27l8DAQGbPnk1kZCShoaF2N38tKpUKi8WxvH+t7fT0dPu+U6dOUVxcTHh4eL3Xo9FoaNu2rX1zdnauqaXjjVqrtS8/WC2OTRuDgoLw8fFh08aNWMxmFEoVCmdXDhw82KTmzBJ1uX/mHB5f/BWBEV2vOtbd15+gLt1BFDm29WJZAaVcxr/HdSfQU0t6sYGdprb2wOb169fXCWx2e/BBNN1sqe7O0Xc3/mJlMmjdcL2e+vAPdefF/iH0OW3L4nrmZCoHSmwCpjageW3iWlIvpGKxWNDpdHh4eDjYOJlZgt5Sm7l1ZU8PgDrs1kxdl5a3JCQkbgn0ej2jR4/m5ZdfprS0lEcffRSA0NBQvvvuO/bu3Yu7uzsffPABOTk5DYqKy4mOjiYsLIxHHnmEBQsWUFpayuzZsx3GhIaGkpaWxsqVK+nZsyebNm1i/fr1DmOCgoJISUkhPj6eVq1a4ezsTHR0NBEREYwbN46FCxdiNpv5xz/+Qf/+/ZscU1NRUUFKZjYlebbMmVPHjhHUJhgvLy9atWpJaV4ukydOYOHifxPWvh0du3Tl1WeerdOc+fLXFBybM0vURaFSoffwbPT4LvcM4fzRw5zYsYW+D41DUeMdctep+PKRSO5fvJd9aRUEhvdAVxnHmTNn2LVrFwMGDLDbEGQyAv77XwyJZ9HWiJ9GE9jH1icsdS/c/myjTgnr6cvLBVXMuJDH2VYqHj6SRKS7HoFAqn1fJtVcxZTENAwdeuLm4kxKQhoyQC4IyIA/nMGnezilRj8SWrUjLjEDQbAdl9eMEwSQIyAXwOojUB2kRF5RhltaLnKZYLfV3UVLhPPNCbKXRI+EhMQtQ2xsLF9++SVDhgyxx+DMmTOHc+fOERMTg1ar5fHHH2fEiBGU1KRnXw2ZTMb69euJjY2lV69eBAUFsWjRIoeCiMOGDeP5559n6tSpGAwG7r33XubOnctrr71mHzNy5EjWrVvHgAEDKC4uZsmSJTz66KNs3LiRadOm0a9fP2QyGYMGDeLjjz9u8r0fPHjQ4UNx7hv/ZO4b/2TC+PF8MP9NzEYjU594HDMC02e+RHFxMXfccUeTmzNLXD/B3Xvi7OlNWUEeZ/fvIfzOi7+3ti2cWfRwN2KXHmDlqQqmd7+NwlN72LVrF35+frRv394+Vq7XNV3wgGPHdavV5v1pBD1jgnhlRTUvFVWQ7a5gR6EtMBlVOKggAcDWKYXDOZd5aPy1pNGONOAIwIW8q0/YruZ9mZzpsHt2sN9NEz1Sw9FLkBqOSvzV+f/QOPL/A8bqKooyMxzqp8gVClx9fFE5NT5AvLFI75um8/u6VexZ9Q1+Ye15+J/v1Tn+Rdw53tyUgEyAlztVk5F4HJVKxZQpU+xB7deMxQzvtAZTBTy5B3yvUtTwEqwWK+s/P8b2wjKsTnI6Rwcgdxf54OCHdCjshFxUcNvtt+Ps4oJFBIsokpBVxvfHMulWcQKF2UC7fgPQe7XAKoIFEasoYhHBCjXf236uSirCWGpE3lKPzFuDVRSxivCgrztDvN2u7zW4jMZ+fkueHgkJCYlbDJWTBtcWPvZAWZVGi6uPD3K59C/7ViHiroHs+245WWdPk3v+HC2Cgh2Ox97RhsScclYdTOfjs1qebNmK3MwLrFy5kilTplyfuJQrIKAXnNth8/Y0QfTI5DKGT45A+ekx0hOKkKWf496nO5Om1aE4no4oF3m+/Xh7PSmAdxOKUCSX0j99C4LFxKQJo3H39b/CLDbKCuSU7DmHutoJ76iga7nTZkcKZJaQkJC4BXHSO+Pm64eLdwvc/fwlwXOLoXNzJ7SXbZnp6K8/1TkuCAL/HNGJXkEelBqsrCsOQO/sQkFBAevWrau3YnOTsPfhunK9nvpQqOQMeaozrcM9MBss/PjJUTqW25bZ8lR5FBkdl7ZOZpaithoQLFcuTHg59pYUKSWIJstVRv85SKJHQkJC4hbFSadH6+IqNQu9Rek68F4ATv22A0NlRZ3jKoWMT8d3p5W7hqRCMyedOiKXyzl79qxDxe5rwi569sE1RKkoVHIGPxVB644emI1WUg/bvIp56jzWJa5zGHtpjR6Ni6s9cPuqc3hrkLuqwSxiSLk1WlJIokdCQkJCQuIaaNmhI56tWmM2GDi5a3u9Yzz1ar58pCc6lZwd6WaMLW0eld27d5OQkHAdk/cAmRLKs6Hw3DWZUCjlDH4ygoBwD4wKW2KAYNGw6swqzFZbyYTc0mryyw0416arN9LLAzZvl7068y2Sui6JHgkJCQkJiWtAEAS61HRfP7rlpwab7LbzdeajMd0QBPjmrIBL6w4ArF+/ntzc3GubXKmxCR+wxfVcIwqlnL5jArDKjSAK9E0egzLTjR3pOwCblwegjaZmaauBRqMNUbvEdau0pJBEj4SEhISExDUSfuddKNVOFGakc+HU8QbHRYf7MGuQLV3930k63H1aYjQaWblyJYWFhdc2uX2Jq3FFChviQqatuKZO6Y7S6sTg01PYtHsnYCtKCNBKZatO3pjChJfi1NYNBDDnVGIpMVzXdTYHkuiRkJCQkJC4RtRaLeH9bHV64usJaL6UJ/oF80D3lpitAt/k+KHTO1NYWMjixYvZsmULBkMTRUEziZ7afludI9vh11GPQlTRZm8/ft9/3O7p8RRs1ZydPZuWbi/TKlG2snVrvxW8PZLokZCQkJCQuA663GNb4ko6sI/yooa9NoIgMP/+CLq3diO/WmCHNZzAoGAsFgt79uzh448/Jj4+vvGZXQG9QJBBUQqUZl19fAPUtlwJahPE8KciqWiZjUJUcvDrHAqSbJ4ebU2H9aZ6egCc7F3Xi6/5GpsLSfRISEhISEhcB96BbfBvF47VYuH49l+uONZJKefzCZH4uzpxqtDKdnMYo0aPxsPDg/LycjZs2MAXX3zh0M+tYWOu4FNTo6eRfbgup6KigoKCAgACAgKQK2T0f6wtKe7HEKwybs8WaWOSIVYUA00LZLZfZm3qemIRovXm1kOWRI+EhMRfnqioKJ577jn7z0FBQSxcuPCK5wiCwIYNG27odUn8/6FrTUDzsa2bsVquXJPG21nNfx+JRKOUE5dUwL4CJ/7xj39wzz33oFKpyMzM5Msvv2TdunWUll4l1TvwdtvXa1ziql3a8vb2Rqu1tYbo1bIn53rFcc7jKAoERpSrqCjIB5oeyAygCnBGUMuxVpoxZZZf03U2F5LokZCQuKkMHTrUoQ/WpcTFxSEIAseOHWuSzQMHDvD44483x+XZee211+jatWuz2ryU3bt3M3ToUPz9/RsUZKIoMm/ePPz8/NBoNERHR5OYmGg/vnPnTgRBqHc7cODADbt2CQjtfTsaF1fKCwtIPvzHVcd39Hdl7n22prkfb0+kwiRy++23M23aNLrV9OM6duwYH3/8Mbt378ZkMtVvKLCP7WvqtWVw1S5tBQYG2vcJgsCY8NFsDV1KslsCcrEKq8UMCE1qzGq3J5ehDnEDbn7quiR6JCQkbiqxsbFs2bKFCxcu1Dm2ZMkSIiMj6dy5c5NsXvrUeqthNBrr3V9RUUGXLl1YvHhxg+e+++67LFq0iM8++4z9+/ej0+mIiYmhuroagL59+5KVleWwTZ48mTZt2jS567tE01AolUQMuAeov0JzfYyKbEVoCz3FlSb+vSMJAGdnZ4YPH86UKVMICAjAZDKxfft2Fi9ezKlTp+qmxbeuCWbOPQmVTc8Cq/X0tG7d2mH/fcH3IZOp2dbuP1R41NgVtKSdalyj38u5VVLXJdEjIfE3RhRFTAbLTdka28v4vvvuw9vbm6VLlzrsLy8vZ82aNYwYMYKxY8fSsmVLtFotERERrFix4oo2L1/eSkxMpF+/fjg5OREeHs6WLVvqnDNr1izCwsLQarUEBwczd+5c+9P10qVLef311zl69Kjdc1J7vWlpaQwfPhy9Xo+LiwujRo0iJyfHbrfWQ/TFF19csaHn4MGDefPNN7n//vvrPS6KIgsXLmTOnDkMHz6czp07s2zZMjIzM+1eIZVKha+vr33z9PRk48aNPPbYY1JV5z+BztGDQRBIPXaEoqyMq45XyGW8PMSWxr5k73kuFFXaj7Vs2ZJJkybxwAMP4OzsTHFxMatXr+brr78mOzv7ohG9N3iG2r5P39+k6zUYDGRl2QKgL/X0AGiVWtTVt2GVWUlpuRUAQaZn8+fHORffiA7rl1EbzGxMLcNabW7y+c2F1MxFQuJvjNlo5T/P7ropcz/+UX+UavlVxykUCiZOnMjSpUuZPXu2/cN5zZo1WCwWxo8fz5o1a5g1axYuLi5s2rSJCRMmEBISQq9eva5q32q18sADD+Dj48P+/fspKSlxiP+pxdnZmaVLl+Lv78/x48eZMmUKzs7OzJw5k9GjR3PixAk2b97M1q22DwBXV1esVqtd8OzatQuz2czTTz/N6NGjHdoMJCUlsXbtWtatW+fQyLEppKSkkJ2dTXR0tH2fq6srvXv3Zt++fYwZM6bOOd9//z0FBQU89thj1zSnRNNwbeFDcLdIzh0+wNEtPxM1cfJVzxnQrgV9gj3Zd66A9345w8Ix3ezHBEGgc+fOtG/fnt9++429e/dy/vx5Pv/8c3r06MGAAQPQ6XS21PWCRFsfrnaDG329Fy5cQBRFXF1dcXV1dThWbbKQn9kDTfAOCguTAA+cPb0wmkR++c8JYqZ0Irhb49PXFZ4aFJ5OmAuqMZwrQRPe9GWy5kDy9EhISNx0Jk2aRHJyMrt2XRRoS5YsYeTIkQQGBjJjxgy6du1KcHAw06ZNY9CgQaxevbpRtrdu3crp06dZtmwZXbp0oV+/fsyfP7/OuDlz5tC3b1+CgoIYOnQoM2bMsM+h0WjQ6/UoFAq7F0Wj0bBt2zaOHz/O8uXL6dGjB71792bZsmXs2rXLIYbGaDSybNkyunXr1uSlulpqn+59fHwc9vv4+Dg++V/Cl19+SUxMDK1atbqmOSWaTm2F5pM7t2IyVF91vCAIvDLEVqF5Q3wmxy/UXT5SqVTcddddPP3004SHhyOKIgcPHuTjjz/m999/xxJwbXE9DS1tAZzNKcNs8EKoCkNbbRPqwd2CCe3pg9Uq8st/T5B8pGnVpNWhN3+JS/L0SEj8jVGoZDz+Uf+bNndjad++PX379uWrr74iKiqKpKQk4uLieOONN7BYLMyfP5/Vq1eTkZGB0WjEYDA0OmYnISGBgIAA/P397fv69OlTZ9yqVatYtGgRycnJlJeXYzabcXFxaZTtgIAA+77w8HDc3NxISEigZ8+egG3pwNvb9lQcFxfH4MEXn8Y///xzxo0b16h7aQoXLlzgl19+abQ4lGgegrp0x7WFDyW5OZzZG0enmjifKxHRypX7u7Vk/ZEM5v+UwPIpvetdjnR3d2fUqFGkpKSwefNmcnJy2Lx5M4c83BhEa0Ky4sFYASpdo661VvRcvrQFF9tPBCoHoqtaCYDG043b7uuAIMDZP3L49b8nGTgZQrq3aNR8+tv8cOrggbqN69UH3yAkT4+ExN8YQRBQquU3ZWtqDElsbCxr166lrKyMJUuWEBISQv/+/VmwYAEfffQRs2bNYseOHcTHxxMTE9NgQPC1sG/fPsaNG8eQIUP48ccfOXLkCLNnz262OXS6ix9CkZGRxMfH27dhw4Y1yoavry+AQ7xQ7c+1xy5lyZIleHp6Ntq+RPMgk8ltsT1cvULzpbwwMAyVQsa+cwXsOHNlD0qbNm144oknuO+++9BoNOQVFvMNI1lhHULBqcYtZ1ssFnvyQH2entr2E7f53oG7yfaAkWy5gEwu4+5Hw2nX29fm8fniJEmHGufxUfrq0LTzQKa6tiXe5kASPRISErcEo0aNQiaTsXz5cpYtW8akSZMQBIE9e/YwfPhwxo8fT5cuXQgODubs2bONttuhQwfS09PtAZsAv//+u8OYvXv3EhgYyOzZs4mMjCQ0NNSeyluLSqXCcln9lVrblxaSO3XqFMXFxYSHh9d7PRqNhrZt29o3Z2fnRt1HmzZt8PX1Zdu2bfZ9paWl7N+/v47nShRFlixZwsSJE1EqlY2yL9F8dBpwD3KFgpxziWQnNe692spdy2O3BwEw/6fTmC1Xrsosk8mIjIzkmWee4bbbbkOGyBlCWLzxj0a1tMjKysJkMqHRaPCqp/ZOraenU0s3PM020b69eA+iKCKTCdz1SAfa3eaLaBX59cuTJB7MqWPjVkQSPRISErcEer2e0aNH8/LLL5OVlcWjjz4KQGhoKFu2bGHv3r0kJCTwxBNP1PF2XIno6GjCwsJ45JFHOHr0KHFxccyePdthTGhoKGlpaaxcuZLk5GQWLVrE+vXrHcYEBQWRkpJCfHw8+fn5GAwGoqOjiYiIYNy4cRw+fJg//viDiRMn0r9//yaniJeXl9u9P4B9rtolCEEQeO6553jzzTf5/vvvOX78OBMnTsTf358RI0Y42Nq+fTspKSlMnnz1QFqJ5kfr4kpYnzsBiN/SeG/PP6La4q5VkpRbzuqDdUs41IdGo2HQoEE81d+fEM5jFbG3tDhy5EiDLS0ujeeRyRylgMUqcjrL1nYi3NcZymwC6pTxHEfzjgLYhM/EDrTvYxM+W746ReKBW1/4SKJHQkLiliE2NpaioiJiYmLsMThz5syhe/fuxMTEEBUVha+vb50P+Sshk8lYv349VVVV9OrVi8mTJ/PWW285jBk2bBjPP/88U6dOpWvXruzdu5e5c+c6jBk5ciSDBg1iwIABeHt7s2LFCgRBYOPGjbi7u9OvXz+io6MJDg5m1apVTb73gwcP0q1bN3thuunTp9OtWzfmzZtnHzNz5kymTZvG448/Ts+ePSkvL2fz5s110uC//PJL+vbtS/v27Zt8HRLNQ22F5jN7dlNVXtaoc1w1SqbdZUs//2DLWSoMjU/t9u4UxXjWM1b2Ex7u7pSXl7Nx48YGW1rUejLrW9pKyS+nymRBo5TjozRhtVgQBahSW1h+erl9nEwmcNeEDrTv61cjfE5y9kD9QfW3CoLY2GIa/w8oLS3F1dWVkpKSqwYwSkjcilRXV5OSknLFejASEpcjvW+aH1EU+WbWM+SlphA1cTI97h3RqPOMZiv3fLiL1IJKnr07lOfvCWvshLAgBCoLMD/6C/szLOzatcselxYREUF0dLS91MKCBQuoqqoiNjbWIRAfYGN8Bs+ujKd7azcWD/Ri+ewXULu58nnfYygEBb8++Cve2ovp6qJVZMe3p0nYk4UgQPRj4YT1qhtndiNp7Oe35OmRkJCQkJBoZgRBoOvAewE4uuUnxEZ2TlcpZMyMsXno/rP7HLmlV097r5kQWttiuxQX9nH77bfzzDPP2D2Hx48f55NPPmH37t1kZ2dTVVWFQqHAz8+vjqnaeJ6O/q6U1zQj9fD2pat3V8yime8Sv3OcWiYwYFx7wm/3QxRh65JTnNl/a3p8JNEjISEhISFxA2h/R39UGi1FWZmknjja6POGRPjSrbUbVSYLH25tfNA+gTUtKWrq9ej1eoYPH87jjz/u0NLiq6++AqBVq1YoFHUr19RmbnX0d6GswFZ92dnTm7HtxwLw3ZnvMFkde4EJMoGoce0Jv9PfJnyWnuL071ncakiiR0JCQkJC4gagctIQ3u8uoPH9uMDmJZpdU7Bw1YF0zuY0LibILnrSfgfrxUxDf39/Jk2axMiRI3FxccFstsUK1RfPI4oipy7x9JTWdlf39OKewHvwdPIktyqX7Wnb6163TCBqbDs69msJImz7OoHT+24t4SOJHgkJCQkJiRtEbUBz8sH9lNUIiMYQGeRBTEcfrCK88/Ppxp3kEwEqPRhKIPeUwyFBEIiIiGDq1Kn079+ftm3b1pthmFVSTVGlCblMINRHb79mZ08vlHIlD7V7CIAVp+vvfyfIBPqPDaNTrfBZlkDC3sxG3/eNRhI9EhISEhISNwjPVq0JCI9AFK0c27a5SefOGtQehUxg++lc9iY1QjDJFRDQ2/Z96t56h6hUKgYMGMD48ePrDfitjecJbaHHSSm/ZHnLVsvnobCHUAgKDuUc4kzhmXrnEASBfmPDiOhvEz7bvznNqT23hvCRRI+EhISEhMQNpLYf1/Ftv2AxNz4NPdhbz7jetiWot35KwGptRLJ1YG0frvpFz9WojecJ97cJooueHlu2VgttC+4OvBuAlWdWNmhHEATuHBNGxIBWIMKOb05z6rebL3yuSfQsXryYoKAgnJyc6N27N3/88ccVx69Zs4b27dvj5OREREQEP/3kuLYpiiLz5s3Dz88PjUZDdHQ0iYmJDmOGDRtG69atcXJyws/PjwkTJpCZefEFPH/+PIIg1Nkur7wqISEhISHxZ9K2523o3NypKC4i6UDTPpOeuTsUZ7WCk5mlbDyacfUTAm+3fU3da0tjbyKXZm5ZLRYqigqBi54egDHtxgCw6dwmSgx1G6TWIggCd44KpfNdtoa3O/53mpNxjbiHG0iTRc+qVauYPn06r776KocPH6ZLly7ExMSQm1t/7429e/cyduxYYmNjOXLkCCNGjGDEiBGcOHHCPubdd99l0aJFfPbZZ+zfvx+dTkdMTAzV1RdT9QYMGMDq1as5c+YMa9euJTk5mQcffLDOfFu3biUrK8u+9ejRo6m3KCEhISEh0WzIFUoi7o4B4Oivm5p0rqdezZNRIQC898tZqk2WK5/g3x3kKqjIhcJzTb7Wi0HMLlQUFyFarcjkcrRubvYxPXx6EOoeSpW5io1JG69oTxAE7ngolC532WoB7fz2DCd23zzh02TR88EHHzBlyhQee+wxwsPD+eyzz9BqtfYUuMv56KOPGDRoEC+++CIdOnTgn//8J927d+eTTz4BbF6ehQsXMmfOHIYPH07nzp1ZtmwZmZmZbNiwwW7n+eef57bbbiMwMJC+ffvy0ksv8fvvv2MyOabNeXp64uvra9+kvjMSEhISEjebzncPQpDJSD91nIILaU06N/aONvi5OpFRXMXSveevPFjpBC1rApRT9zRpnqIKIxnFVYBteas2nkfn7oFMdrFJqCAI9vT1lWdWYhWvXINIEARuf6gtXaIDrjjuz6BJosdoNHLo0CGio6MvGpDJiI6OZt++ffWes2/fPofxADExMfbxKSkpZGdnO4xxdXWld+/eDdosLCzk22+/pW/fvnVEzbBhw2jRogV33HEH33///RXvx2AwUFpa6rBJSEj89YiKiuK5556z/xwUFMTChQuveI4gCA4PVhISNxJnTy9CevQC4OiWn5t0rpNSzgsD2wGweEcSRRXGK59gj+up/zO0IU5l2T4DW3tocXFS1onnuZR729yLs9KZ9LJ09mRcXVwJgsDtI9vywIs9bJldN4kmiZ78/HwsFgs+Pj4O+318fMjOrr/6YnZ29hXH135tjM1Zs2ah0+nw9PQkLS2NjRsvutX0ej3vv/8+a9asYdOmTdxxxx2MGDHiisLn7bffxtXV1b5dXopbQkLixjN06FAGDRpU77G4uDgEQeDYsWNNsnngwAEef/zx5rg8O6+99hpdu3ZtVpuXsnv3boYOHYq/v3+9gsxkMjFr1iwiIiLQ6XT4+/szceLEOrGNsbGxtGnTBo1GQ0hICK+++qq9FYHEzaVLTYXmk7u2YayuatK593drSQc/F8qqzSzannjlwfYihU3z9FxalBCgLN8xc+tStEotI0JHAA2nr1+OIAj4hbg26Zqam79U9taLL77IkSNH+PXXX5HL5UycOJHa1mFeXl5Mnz6d3r1707NnT9555x3Gjx/PggULGrT38ssvU1JSYt/qa8omISFxY4mNjWXLli1cuFC3q/SSJUuIjIykc+fOTbLp7e2NVqttrktsVhoSIBUVFXTp0oXFixfXe7yyspLDhw8zd+5cDh8+zLp16zhz5gzDhg2zjzl9+jRWq5XPP/+ckydP8uGHH/LZZ5/xyiuv3JB7kWgagZ264O7nj7GqktO/7WrSuXKZwCtDbO0p/vd7KqkFFQ0PbtULBBkUp0JJ4+NnTl4SzwNQVmhrQVGf6IGLAc2/ZfxGWmnTluxuFk0SPV5eXsjlcnJyHNvH5+Tk4Otbf3MxX1/fK46v/doYm15eXoSFhXHPPfewcuVKfvrppytmZ/Xu3ZukpKQGj6vValxcXBw2CYm/E6IoYqquvilbY3sZ33fffXh7e7N06VKH/eXl5axZs4YRI0YwduxYWrZsiVarJSIighUrrvxkefnyVmJiIv369cPJyYnw8HC2bNlS55xZs2YRFhaGVqslODiYuXPn2mMGly5dyuuvv87Ro0ftmaG115uWlsbw4cPR6/W4uLgwatQoh/9ntR6iL7744ooNPQcPHsybb77J/fffX+9xV1dXtmzZwqhRo2jXrh233XYbn3zyCYcOHSItzfaBM2jQIJYsWcLAgQMJDg5m2LBhzJgxg3Xr1l3x9ZL4cxBkMjpHDwYgfstPjf4bqeXOUG/6hXljsoi8u7n+GjkAOLmAb82DQlrjl7guzdwCHFpQ1Edrl9bc0fIORERWnVnV6HluJnWbblwBlUpFjx492LZtGyNGjADAarWybds2pk6dWu85ffr0Ydu2bQ7r7Vu2bKFPH9uaY5s2bfD19WXbtm1213FpaSn79+/nqaeeavBarDXN2wwGQ4Nj4uPj622mJiHx/wWzwcCiR+pmOf4ZPPP1dygb0bFboVAwceJEli5dyuzZsxEEAbCVurBYLIwfP541a9Ywa9YsXFxc2LRpExMmTCAkJIRevXpd1b7VauWBBx7Ax8eH/fv3U1JS4vD/qBZnZ2eWLl2Kv78/x48fZ8qUKTg7OzNz5kxGjx7NiRMn2Lx5M1u3bgWwd6uuFTy7du3CbDbz9NNPM3r0aHbu3Gm3nZSUxNq1a1m3bh1yubzO3NdKSUkJgiDgdklmTX1jPDw8mm1OieujY1Q0e1Z+Q975c2QlnsY/rEOTzn95cHviEvPYdDyLSalF9Ah0r39gYF/Iirelrkdc/X9AldHCubxy2zXWqdHj2eB5Y9uP5beM31iftJ6nuz6NVnlrelhraZLoAZg+fTqPPPIIkZGR9OrVi4ULF1JRUcFjjz0GwMSJE2nZsiVvv/02AM8++yz9+/fn/fff595772XlypUcPHiQ//znP4Btje+5557jzTffJDQ0lDZt2jB37lz8/f3twmr//v0cOHCAO+64A3d3d5KTk5k7dy4hISF28fT111+jUqnsHWXXrVvHV199xRdffHHdL5KEhMSNZdKkSSxYsIBdu3YRFRUF2Ja2Ro4cSWBgIDNmzLCPnTZtGr/88gurV69ulOjZunUrp0+f5pdffsHf3x+A+fPnM3jwYIdxc+bMsX8fFBTEjBkzWLlyJTNnzkSj0aDX61EoFA4e6C1btnD8+HFSUlLsMYHLli2jY8eOHDhwgJ49ewK2Ja1ly5bh7V3/E/O1UF1dzaxZsxg7dmyDXuqkpCQ+/vhj3nvvvWabV+L60OidaXd7P07u3Er8rz81WfR08HPhwe6tWHPoAvN/SuC7J/vYHxQcCOwLv/+70UUKE7JLsYrgpVfTwsX2sHKlQOZa7mh5B630rbhQfoGfUn7iwbCb85DVWJosekaPHk1eXh7z5s0jOzubrl27snnzZnsgclpaGjLZxVWzvn37snz5cubMmcMrr7xCaGgoGzZsoFOnTvYxM2fOpKKigscff5zi4mLuuOMONm/ebHcDa7Va1q1bx6uvvkpFRQV+fn4MGjSIOXPmoFar7Xb++c9/kpqaikKhoH379qxatareWj4SEv9fUKjVPPP1dzdt7sbSvn17+vbty1dffUVUVBRJSUnExcXxxhtvYLFYmD9/PqtXryYjIwOj0YjBYGh0zE5CQgIBAQF2wQPYH5YuZdWqVSxatIjk5GTKy8sxm81XXfKutX1pEkR4eDhubm4kJCTYRU9gYKBd8MTFxTkIrs8//5xx48Y16l5qMZlMjBo1ClEU+fTTT+sdk5GRwaBBg3jooYeYMmVKk+xL3Fi63jOEkzu3cnZfHFETJ6N1aVpw7wsD2/HDsUwOpRbxy8lsBnWqZ0Wjdc17PC8BKgtBe2Vv3+XxPBaziYriIqDhmB4AmSBjTPsxvHfwPVacXsHI0JH1i7BbhCaLHoCpU6c2uJx1qUu3loceeoiHHnqoQXuCIPDGG2/wxhtv1Hs8IiKC7dvrdnS9lEceeYRHHnnkimMkJP6/IQhCo5aYbgViY2OZNm0aixcvZsmSJYSEhNC/f3/+9a9/8dFHH7Fw4UJ75tJzzz3XrBlJ+/btY9y4cbz++uvExMTg6urKypUref/995vFvk6ns38fGRlJfHy8/efLM1evRq3gSU1NZfv27fUKs8zMTAYMGEDfvn3tXnWJWwfftmH4BIeScy6REzu20Gt40x7OfV2dmHJnMB9vT+Jfm89wdwcflPLLQnR1XuDVDvLP2OJ62t97RZunLsvcqigqAlFErlBcVZSNaDuCT458wtmisxzOPUwPn1u3KPBfKntLQkLi78uoUaOQyWQsX76cZcuWMWnSJARBYM+ePQwfPpzx48fTpUsXgoODOXv2bKPtdujQgfT0dLKysuz7Lk+A2Lt3L4GBgcyePZvIyEhCQ0NJTU11GKNSqbBYHKvh1tq+NPPz1KlTFBcXEx4eXu/1aDQa2rZta9+cnZ0bfS+1gicxMZGtW7fiWU+sRUZGBlFRUfTo0YMlS5Y4eN4lbh1qu68f2/ozVutVqizXwxP9Q/DSq0jJr2D5/gYyp5rQh+vyIObSmiBmvacXwlXeQ65qV+4Ntomqxqav3yykvwYJCYlbAr1ez+jRo3n55ZfJysri0UcfBSA0NJQtW7awd+9eEhISeOKJJ+pke16J6OhowsLCeOSRRzh69ChxcXHMnj3bYUxoaChpaWmsXLmS5ORkFi1axPr16x3GBAUFkZKSQnx8PPn5+RgMBqKjo4mIiGDcuHEcPnyYP/74g4kTJ9K/f38iIyObdP/l5eXEx8fbvUC1c9VmZplMJh588EEOHjzIt99+i8ViITs7m+zsbLvXq1bwtG7dmvfee4+8vDz7GIlbi3Z978RJp6ckN4fUo0eafL5ereDZ6DAAPtqWSGm1qe6gS/twXQGTxcrp7DKgniBmj4aXti6ltkLzttRt5FbW35bqVkASPRISErcMsbGxFBUVERMTY4/BmTNnDt27dycmJoaoqCh8fX3tSQ6NQSaTsX79eqqqqujVqxeTJ0/mrbfechgzbNgwnn/+eaZOnUrXrl3Zu3cvc+fOdRgzcuRIBg0axIABA/D29mbFihUIgsDGjRtxd3enX79+REdHExwczKpVTU/fPXjwIN26dbMnY0yfPp1u3boxb948wCZovv/+ey5cuEDXrl3x8/Ozb3v32j7UtmzZQlJSEtu2baNVq1YOYyRuLZRqJzr0GwDAyd1XDt9oiDE9Awj21lFYYeSzncl1B9TG9WQdBUN5g3aS88oxmq3o1Qpae9hi5a5UmLA+2nm0o3uL7phFM9+dvTlxhI1BEJtaKOBvTGlpKa6urpSUlEg1eyT+klRXV5OSknLFejASEpcjvW9uDtnJiXz7yvMolCqe/M//UF9DQc1fT2bz+DeHUCtk7JgRhb+bxnHAhxFQkgYT1kPIXfXaWHvoAi+sOUqvIA9WP2kTStu++oz4X36k1/AHufPhRxt1LZtTNvPi7hfx0njx68hfUcr/vN6Xjf38ljw9EhISEhISNwGf4LZ4+LfCbDKSuL9pLSNquSfch15BHhjMVt7/tZ5Yt0bE9dTG84T7XxQL5YVXT1e/nLtb3423xpv8qny2pm1t9Hl/JpLokZCQkJCQuAkIgkB4P5v35VTcjmu28cq9tlo/645csPfPsmPvw9VwZebLe27BJTE9Xo1b3gJQypU8FGbL1L5VA5ol0SMhISEhIXGT6HBHFADpp45TWhNH01S6BrhxX2c/RBHe/um0Y3uL1jWi58IBMNftYCCKor27em3mFlwUPfpGBjLX8mDYgygEBUdyj5BQkNDEO7nxSKJHQkJCQkLiJuHi3YJW4Z1AFEn4bec125kZ0x6lXOC3pHx2nb1EPHmFgtYLLAbIrJslll5YRVm1GZVcRqiPHgCzyURlSTHQ+EDmWry13twTeA8AK8+svLabuYFIokdCQkJCQuImEn6nbYkrIW5Hk5uQ1tLaU8vEPkGAzdtjsdbYEYRL4nrqxg3VLm2F+ertBQ7La7w8CqUKjXPTk3rGdrClr286t4kSQ8lVRv+5SKJHQkJCQkLiJhJ22+0olCoKLqSRm1JP6nkjmXZXW1ycFJzJKWPtoQsXD9jr9dSN67EvbfldsrRVeDGe51paSnT17kp7j/YYLAbWJ66/+gl/IpLokZCQkJCQuImotTpCInsDcOoaa/YAuGlVTLsrFID3t5yh0mi2Hait15O+Hy6r/myvxNyyniDmJi5t1SIIgr1Y4cozK7FcQ8XpG4UkeiQkJCQkJG4ytVlcp/fuxmq5dpEwsW8grdw15JQa+DIuxbbTNwJUzmAohZwTDuNrl7fC/S4RPfbChI1PV7+cwW0G46JyIaM8g98yfrtmO82NJHokJCQkJCRuMoGdu6FxcaWypJjzxw5fsx21Qs6LMe0A+GxXMnllBpDJobXNk3RpvZ78cgM5pQYEATr41fX0NDVz61I0Cg0PhD4A3Frp65LokZCQ+MsTFRXFc889Z/85KCiIhQsXXvEcQRDYsGHDDb0uCYnGIlcoaH97PwBO7b62mj21DO3sT+dWrlQYLXy0raZgob1ez0XRU7u01cZTh06tsO8vK2haC4qGGNVuFAICezL3cL7k/HXZai4k0SMhIXFTGTp0KIMGDar3WFxcHIIgcOzYsSbZPHDgAI8//nhzXJ6d1157ja5duzarzUvZvXs3Q4cOxd/fv0FBJghCvduCBQvsY4YNG0br1q1xcnLCz8+PCRMmkJmZecOuW6L5qM3iSj7wO4bKymu2I5MJvDLEVrBwxR/pJOWWX6zXk7oXajLE7Etb/o4ZWmWFBUDTChPWR4BzAP1a2YTcqjNN70d3I5BEj4SExE0lNjaWLVu2cOHChTrHlixZQmRkJJ07d26STW9vb7TX0Mfoz6C2I/rlVFRU0KVLFxYvXtzguVlZWQ7bV199hSAIjBw50j5mwIABrF69mjNnzrB27VqSk5N58MEHm/0+JJqf5mhLUcttwZ5Ed2iBxSryzs+noWV3kKuhMh8KkoBLgpgvKUoIlwYyX3tMTy1j2o8BYEPSBipN1y7kmgtJ9EhI/I0RRRGr0XJTtsbWG7nvvvvw9vZm6dKlDvvLy8tZs2YNI0aMYOzYsbRs2RKtVktERAQrVlw5RuDy5a3ExET69euHk5MT4eHhbNmypc45s2bNIiwsDK1WS3BwMHPnzsVkMgGwdOlSXn/9dY4ePWr3rtReb1paGsOHD0ev1+Pi4sKoUaPIycmx2631EH3xxRdXbOg5ePBg3nzzTe6///4G78vX19dh27hxIwMGDCA4ONg+5vnnn+e2224jMDCQvn378tJLL/H777/b70Xi1sWhLcV1ZHHV8tLg9shlAlsTctifVg6tIm0Haur1nLKLnoueHpOhmuoy2/7rXd4C6Ovfl9bOrSk3lfPjuR+v2971orj6EAkJib8qoslK5ryGGw3eSPzf6Iugkl91nEKhYOLEiSxdupTZs2fb64KsWbMGi8XC+PHjWbNmDbNmzcLFxYVNmzYxYcIEQkJC6NWr11XtW61WHnjgAXx8fNi/fz8lJSUO8T+1ODs7s3TpUvz9/Tl+/DhTpkzB2dmZmTNnMnr0aE6cOMHmzZvZutXWSNHV1RWr1WoXPLt27cJsNvP0008zevRodu7cabedlJTE2rVrWbduHXL51V+TxpCTk8OmTZv4+uuvGxxTWFjIt99+S9++fVEq/7yO1xLXToc7ovht5bKathS5uHi1uGZbbVs4M7pnAMv3pzH/pwQ2dOiDkLoHUvdR3mk8KfkVwOU9t2xLW0q1E2qt7vpuBpAJMsa0H8O7B95lxekVPBT20DXV/mkuJE+PhITETWfSpEkkJyeza9cu+74lS5YwcuRIAgMDmTFjBl27diU4OJhp06YxaNAgVq9e3SjbW7du5fTp0yxbtowuXbrQr18/5s+fX2fcnDlz6Nu3L0FBQQwdOpQZM2bY59BoNOj1ehQKhd3LotFo2LZtG8ePH2f58uX06NGD3r17s2zZMnbt2sWBAwfsto1GI8uWLaNbt25NXqpriK+//hpnZ2ceeOCBOsdmzZqFTqfD09OTtLQ0Nm7c2CxzStx4XLxbEBAeAUBC3M7rtvdcdChalZyjF0r43dLetjN1Lwk1RQl9XZzw1Kvt4y8NYm4ucTK87XA0Cg1JxUkczDnYLDavFcnTIyHxN0ZQyvB/o+9Nm7uxtG/fnr59+/LVV18RFRVFUlIScXFxvPHGG1gsFubPn8/q1avJyMjAaDRiMBgaHbOTkJBAQEAA/v7+9n19+vSpM27VqlUsWrSI5ORkysvLMZvNuLhcuQR/re2AgAD7vvDwcNzc3EhISKBnz54ABAYG4u1ti4+Ii4tj8ODB9vGff/4548aNa9S9XMpXX33FuHHj6l0ue/HFF4mNjSU1NZXXX3+diRMn8uOPP97UJ2yJxtOh3wDSTx3nVNwOeo24Ps9IC2cnnugXwodbzzLvsJZfBTlCSRrnk04Djl4euLS7+vXH89TionLhvuD7WHN2DStOr6Cnb89ms91UJE+PhMTfGEEQkKnkN2Vr6j/q2NhY1q5dS1lZGUuWLCEkJIT+/fuzYMECPvroI2bNmsWOHTuIj48nJiamwYDga2Hfvn2MGzeOIUOG8OOPP3LkyBFmz57dbHPodBeXCSIjI4mPj7dvw4YNa7K9uLg4zpw5w+TJk+s97uXlRVhYGPfccw8rV67kp59+4vfff7/m65f4cwnrfQcKpYrCjPTraktRy5R+bWjhrCaxGPL1tho+phRbXM/loqf8OqsxN0RtQPP2tO1kV2Q3q+2mIIkeCQmJW4JRo0Yhk8lYvnw5y5YtY9KkSQiCwJ49exg+fDjjx4+nS5cuBAcHc/bs2Ubb7dChA+np6WRlZdn3XS4A9u7dS2BgILNnzyYyMpLQ0FBSU1MdxqhUKiyXVcqttZ2enm7fd+rUKYqLiwkPD6/3ejQaDW3btrVvzs7Ojb6XWr788kt69OhBly5drjrWarUCYDAYmjyPxM1BrdU2S1uKWrQqBS8MDAPg5zJb0Ltbnm2ZKbzBzK3mFT1h7mFE+kRiES2sObumWW03BUn0SEhI3BLo9XpGjx7Nyy+/TFZWFo8++igAoaGhbNmyhb1795KQkMATTzzhkB11NaKjowkLC+ORRx7h6NGjxMXFMXv2bIcxoaGhpKWlsXLlSpKTk1m0aBHr1zs2SgwKCiIlJYX4+Hjy8/MxGAxER0cTERHBuHHjOHz4MH/88QcTJ06kf//+REZGNun+y8vL7d4fwD5XWlqaw7jS0lLWrFlTr5dn//79fPLJJ8THx5Oamsr27dsZO3YsISEh9S7pSdy6NFdbiloe7BFAOx9nfjPaenOFGY4D9S1v2WJ6rqcac0OM6zCOfq360du3d7PbbiyS6JGQkLhliI2NpaioiJiYGHsMzpw5c+jevTsxMTFERUXh6+vLiBEjGm1TJpOxfv16qqqq6NWrF5MnT+att95yGDNs2DCef/55pk6dSteuXdm7dy9z5851GDNy5EgGDRrEgAED8Pb2ZsWKFQiCwMaNG3F3d6dfv35ER0cTHBzMqlVNL8R28OBBunXrRrdu3QCYPn063bp1Y968eQ7jVq5ciSiKjB07to4NrVbLunXruPvuu2nXrh2xsbF07tyZXbt2oVar64yXuHVprrYUtchlAi8Nac8Bq215q62QQZBTJa3cNQ7jaj09Ls3s6QGIDoxm8d2L6eV39azLG4UgNraYxv8DSktLcXV1paSk5KoBjBIStyLV1dWkpKRcsR6MhMTlSO+bW5PtSz/nyM8/0K7Pndz33KzrtieKIuO/3M+raZMIk2XwjcvjTJi+wGHMJ4+NxlBZwaPv/xvPVq2ve84/i8Z+fkueHgkJCQkJiVuQjv3uBiD54H4MlRXXbU8QBF4e3IGN1tsBmFD6H9j7sf24sarSPk9zx/TcKkiiR0JCQkJC4hakRZsQPFoGYDYZOXudbSlq6dTSlZIe0/jcfK9tx69zYMurIIr2woRqrQ6V5tZs43K9SKJHQkJCQkLiFkQQBMLvHABAwnV2Xr+Uf46IYORLSyD6dduOPQvh+2mU5dlSyfUens02162GJHokJCQkJCRuUTrcGQVgb0vRHAiCgJdeDXc8B8M+BkEGR76h7Jd3geYtTHirIYkeCQkJCQmJWxQXr+ZtS1GH7hNh1DKQqylLt1Vpdnb9+ybySKJHQkJCQkLiFqZDP9sS16m4HdyQhOsOQ2H8WsqsegCc036G8rzmn+cWQBI9EhISEhIStzCXtqXIOZd0YyZpcydl3raigc7VqfBVDBSlXuWkvx6S6JGQkJCQkLiFcWhLEXf9bSkaoqzc1qrE2c0NCpNtwifn1A2b72YgiR4JCQkJCYlbnPD+NW0p9uzGYjY3u31RFC/23Xr4c/DuAGVZsGQwpO1v9vluFpLokZCQ+MsTFRXFc889Z/85KCiIhQsXXvEcQRDYsGHDDb0uCYnmIqhzd7SublSVlpB67Eiz2zdUVmCqrgLAOTAcHvsJWvWC6mJYNhwStzT7nDcDSfRISEjcVIYOHcqgQYPqPRYXF4cgCBw7dqxJNg8cOMDjjz/eHJdn57XXXqNr167NavNSdu/ezdChQ/H3929QkOXk5PDoo4/i7++PVqtl0KBBJCYmOoyJiopCEASH7cknn7xh1y3x5yCTy2nftx/QPJ3XL6e8xsvjpHdGqXYCrQdM3ABt7wFzFawYA8duXnf05kISPRISEjeV2NhYtmzZwoULF+ocW7JkCZGRkXTu3LlJNr29vdFqb82Kskajsd79FRUVdOnShcWLF9d7XBRFRowYwblz59i4cSNHjhwhMDCQ6OhoKiocWxRMmTKFrKws+/buu+82+31I/PnUdl5vrrYUl2Jf2rq0/YRKB2NXQMRDYDXDusmw//NmnffPRhI9EhJ/Y0RRxGg03pStsam19913H97e3ixdutRhf3l5OWvWrGHEiBGMHTuWli1botVqiYiIYMWKFVe0efnyVmJiIv369cPJyYnw8HC2bKnrqp81axZhYWFotVqCg4OZO3cuJpMJgKVLl/L6669z9OhRu/ek9nrT0tIYPnw4er0eFxcXRo0aRU5Ojt1urYfoiy++uGJDz8GDB/Pmm29y//3313s8MTGR33//nU8//ZSePXvSrl07Pv30U6qqquq8HlqtFl9fX/smNVD+e3Aj2lLUUq/oAZAr4f7/QK8nbD//PBN2zIe/aK9yxc2+AAkJiRuHyWRi/vz5N2XuV155BZVKddVxCoWCiRMnsnTpUmbPno0gCACsWbMGi8XC+PHjWbNmDbNmzcLFxYVNmzYxYcIEQkJC6NWr11XtW61WHnjgAXx8fNi/fz8lJSUO8T+1ODs7s3TpUvz9/Tl+/DhTpkzB2dmZmTNnMnr0aE6cOMHmzZvZunUrAK6urlitVrvg2bVrF2azmaeffprRo0ezc+dOu+2kpCTWrl3LunXrkMvljXsBL8NgsGXWXCqaZDIZarWa3377jcmTJ9v3f/vtt/zvf//D19eXoUOHMnfu3FvW8yXReGrbUvy2chmndm8nYsDAZrNdVmCry1Nvo1GZDAb/C3ResOMt2PUvqMiHIQtAdm3v55uFJHokJCRuOpMmTWLBggXs2rWLqKgowLa0NXLkSAIDA5kxY4Z97LRp0/jll19YvXp1o0TP1q1bOX36NL/88gv+/v4AzJ8/n8GDBzuMmzNnjv37oKAgZsyYwcqVK5k5cyYajQa9Xo9CocDX19c+bsuWLRw/fpyUlBQCAgIAWLZsGR07duTAgQP07NkTsC1pLVu2DG/vay/v3759e1q3bs3LL7/M559/jk6n48MPP+TChQtkZWXZxz388MMEBgbi7+/PsWPHmDVrFmfOnGHdunXXPLfErUOHO6P4beUyLpw6QWleLi7eLZrF7kVPTwPvUUGA/jNtsT6bZsDBL6Gq0OYFUlz94eZWQRI9EhJ/Y5RKJa+88spNm7uxtG/fnr59+/LVV18RFRVFUlIScXFxvPHGG1gsFubPn8/q1avJyMjAaDRiMBga7blISEggICDALngA+vTpU2fcqlWrWLRoEcnJyZSXl2M2m6+6LFRru1bwAISHh+Pm5kZCQoJd9AQGBtoFT1xcnIPg+vzzzxk3btxV70OpVLJu3TpiY2Px8PBALpcTHR3N4MGDHZYSLw3gjoiIwM/Pj7vvvpvk5GRCQkKuOo/ErU1tW4r0U8dJ+G0nve8f1Sx2G1zeupyek0HjAeseh5ProaoYRv8P1PpmuY4bzTXF9CxevJigoCCcnJzo3bs3f/zxxxXHr1mzhvbt2+Pk5ERERAQ//fSTw3FRFJk3bx5+fn5oNBqio6PrZCQMGzaM1q1b4+TkhJ+fHxMmTCAzM9NhzLFjx7jzzjtxcnIiICBACt6T+H+PIAioVKqbstUuUzWW2NhY1q5dS1lZGUuWLCEkJIT+/fuzYMECPvroI2bNmsWOHTuIj48nJiamwYDga2Hfvn2MGzeOIUOG8OOPP3LkyBFmz57dbHPodDr795GRkcTHx9u3YcOGNdpOjx49iI+Pp7i4mKysLDZv3kxBQQHBwcENntO7t62oXVLSDarkK/GnUxvQfGr39mZrS9Fo0QPQ6QEYtxqUOji3A5YNg4qCZrmOG02TRc+qVauYPn06r776KocPH6ZLly7ExMSQm1t/99e9e/cyduxYYmNjOXLkCCNGjGDEiBGcOHHCPubdd99l0aJFfPbZZ+zfvx+dTkdMTAzV1dX2MQMGDGD16tWcOXOGtWvXkpyczIMPPmg/XlpaysCBAwkMDOTQoUMsWLCA1157jf/85z9NvUUJCYmbwKhRo5DJZCxfvpxly5YxadIkBEFgz549DB8+nPHjx9OlSxeCg4M5e/Zso+126NCB9PR0hyWg33//3WHM3r17CQwMZPbs2URGRhIaGkpqqmMJfpVKhcViqdd2enq6fd+pU6coLi4mPDy83uvRaDS0bdvWvjk7Ozf6XmpxdXXF29ubxMREDh48yPDhwxscGx8fD4Cfn1+T55G4NQntfbutLUXmhWZpS+FQmLCh5a3LCbkLHvkeNO6QcQiWDIKSuhmYtxxiE+nVq5f49NNP23+2WCyiv7+/+Pbbb9c7ftSoUeK9997rsK93797iE088IYqiKFqtVtHX11dcsGCB/XhxcbGoVqvFFStWNHgdGzduFAVBEI1GoyiKovjvf/9bdHd3Fw0Gg33MrFmzxHbt2jX63kpKSkRALCkpafQ5EhK3ElVVVeKpU6fEqqqqm30p10RsbKzo7u4uyuVyMSMjQxRFUXz++efFgIAAcc+ePeKpU6fEyZMniy4uLuLw4cPt5/Xv31989tln7T8HBgaKH374oSiKtv9R4eHh4j333CPGx8eLu3fvFnv06CEC4vr160VRtP0/USgU4ooVK8SkpCTxo48+Ej08PERXV1e7zW+//VbU6XTikSNHxLy8PLG6ulq0Wq1i165dxTvvvFM8dOiQuH//frFHjx5i//797ee9+uqrYpcuXa5672VlZeKRI0fEI0eOiID4wQcfiEeOHBFTU1PtY1avXi3u2LFDTE5OFjds2CAGBgaKDzzwgP14UlKS+MYbb4gHDx4UU1JSxI0bN4rBwcFiv379rjj3X/198/+RHxb+S3xv1L3itiWfXbetytIS8b1R94rvjbpXNF3yGdoock+L4vsdRPFVF1F8P1wUc89c9/VcC439/G6Sp8doNHLo0CGio6Pt+2QyGdHR0ezbt6/ec/bt2+cwHiAmJsY+PiUlhezsbIcxrq6u9O7du0GbhYWFfPvtt/Tt29ceN7Bv3z769evnkC0SExPDmTNnKCoqqteOwWCgtLTUYZOQkLh5xMbGUlRURExMjD0GZ86cOXTv3p2YmBiioqLw9fVlxIgRjbYpk8lYv349VVVV9OrVi8mTJ/PWW285jBk2bBjPP/88U6dOpWvXruzdu5e5c+c6jBk5ciSDBg1iwIABeHt7s2LFCgRBYOPGjbi7u9OvXz+io6MJDg5m1apVTb73gwcP0q1bN7p16wbA9OnT6datG/PmzbOPycrKYsKECbRv355nnnmGCRMmOKSrq1Qqtm7dysCBA2nfvj0vvPACI0eO5Icffmjy9Ujc2oTXdF5vjrYUtV4ejYsrikZkXDrg3Q4m/QKeoVB6wdavK+PQdV3PDaUpSiojI0MExL179zrsf/HFF8VevXrVe45SqRSXL1/usG/x4sViixYtRFEUxT179oiA+H/t3X1czff/P/DH+5ycOulUUqqDLuUiWlklyqcyUZuh32w1i5jGLgppSBI2o5nZXH5ysd8667ZPpA9h7NNniSXrgkkUkawpclxsdJSLdM77+0ef3hwlRadT5zzvt9v7xnmf1/v1fr4qp5fX+/V6PauqqpTKvPPOO2xQUJDSuYULF7L6+vosAHb48OHsrVu3uPfGjBnDzpo1S6n82bNnWQDsuXPnmo1t2bJlLIAmB430kK6K/sdOXgT93HQ98vp69p8zQ9ivg8axl04ef6m6yn7PY78OGscmRc958UpqbrLsVp+GEZ+VYpYtO/xSMbWVSkZ61G3BggU4deoUfvnlF/D5fISGhr7UJK6YmBhUV1dzx5PP5QkhhJDO6sm0FGdfMi3F3b8aJiG3ej5Pc7qbAtN+Amx9gLoaIDkIOLv3peJShTZ1ekxNTcHn85V2GwUa8sE8uXfFkywsLFos3/hna+o0NTVF//79MWbMGOzcuRM///wzNyHxWfd58h5P09XVhaGhodJBCCGEdAWP01LkvVRaihY3JmwLXREQkgo4TgTkdUDqdOD371+uznbWpk6PQCCAq6srMjMzuXMKhQKZmZnN7nsBNOyH8WR5oGFDr8bytra2sLCwUCojk8mQn5//zDob7ws83qV0xIgROHr0KLdtfON9BgwYgB49erSlmYQQQkin15iWQv7oEUrzXjwtRZuWqz+Pji7wdiLg+j4AFjgwDzi6ptOkrWjz462oqChs374dP/zwA0pKSvDxxx+jtrYW77//PgAgNDQUMTExXPm5c+ciPT0da9euxfnz57F8+XL8/vvviIiIANCwj0hkZCS++OIL7N+/H0VFRQgNDYVYLOYmK+bn52PTpk0oLCzE5cuXcfjwYUyePBn29vZcx+i9996DQCBAWFgYzp49i5SUFKxfvx5RUVEv+zUihBBCOh2GYR7v2ZP94o+42m2kpxGPD7z5LfCP/+2kfvgLID0G+N9ghTq1eUfm4OBg3Lx5E0uXLoVUKoWLiwvS09Nhbm4OoCH5Ho/3uC/l6emJ5ORkLFmyBIsXL4aDgwP27t2LIUOGcGUWLlyI2tpazJo1C3fu3MHIkSORnp7O5ZjR19fHnj17sGzZMtTW1sLS0hIBAQFYsmQJdHV1ATSs+Prll18QHh4OV1dXmJqaYunSpUq7kxJCCCGaZNDIl09L0a4jPY0YBhgd1zDXJ30RkJ/QkLZi4uaGJKZqwrAvMxNYw8hkMhgZGaG6uprm95Au6cGDBygvL28xmzchT6Ofm65t1+eLUXn2DEa+G9rmtBQsy2L9lP8HeX09Ptj4/2HUy7z9AzydAuz9GGDlgIM/8I4EELRvAtzW/v7uUqu3CCGEEKLM8R8Ne/a8SFqK+7Lqhn1+GAYGJiaqCA9wDgYm7wB0hMDF/wI/zVXNfVqBOj2EEEJIF6aUluLSxedf8ITGR1vdjYzB11HhY6f+/kDoXsB0ADBKPUmQAer0EEIIIV2arr4+7N2HAwDOZR9p07Wy9p7E3BKr4cAnuYCJrerv9QzU6SGEdHm+vr6IjIzkXtvY2GDdunUtXsMwDPbu3avSuAjpKFxaipy2paW4e6uNiUZfFo/fMfd51u3VendCiNYbP348AgICmn0vOzsbDMPgzJkzbarzxIkT7b5yc/ny5XBxcWnXOp8UHx8Pd3d3iEQi9OrVC4GBgbhw4YJSmW3btsHX1xeGhoZgGAZ37txpUk9BQQHGjBkDY2Nj9OzZE7NmzUJNTY3K4iadg80rr0LfyBj3ZdX483RBq6+r+VsFK7c6Mer0EELUKiwsDBkZGbhy5UqT9xITE+Hm5oZXXnmlTXWamZlBX799V4e0l7q6umbPZ2VlITw8HHl5ecjIyMCjR48wduxY1NY+3mn33r17CAgIwOLFzc+JqKqqgp+fH/r164f8/Hykp6fj7NmzmD59uiqaQjoRHp+PgV4+ANr2iEsly9U7Mer0EKLBWJaFXH5PLUdrV5G8+eabMDMzg0QiUTpfU1OD1NRUBAYGYvLkyejduzf09fXh5OSklFm8OU8/3rp48SK8vb2hp6cHR0dHZGRkNLkmOjoa/fv3h76+Puzs7BAXF8ft8C6RSPDZZ5/h9OnTYBgGDMNw8VZUVGDixIkwMDCAoaEhgoKClFLiNI4Qfffddy0uCU9PT8f06dMxePBgODs7QyKRoKKiAidPPs5YHRkZiUWLFmH48OHN1nHgwAF069YNmzdvxoABA+Du7o4tW7Zg9+7dKCsra/FrRrq+xlVcbUlL0bgxoYGWdHravDkhIaTrUCju49csJ7Xc29enCHz+80dbdHR0EBoaColEgtjYWDAMAwBITU2FXC7HlClTkJqaiujoaBgaGuLgwYOYOnUq7O3tMWzYsOfWr1Ao8NZbb8Hc3Bz5+fmorq5Wmv/TSCQSQSKRQCwWo6ioCDNnzoRIJMLChQsRHByM4uJipKen49ChQwAaNkRVKBRchycrKwv19fUIDw9HcHAwfv31V67usrIy7N69G3v27AGf37o5DdXV1QAAkzYsI3748CEEAoHSBrFCoRAAcOzYMfTr16/VdZGup5etPXr2scJfVypQmvcbnF4b+9xrHo/0dNCcHjWjkR5CiNrNmDEDly5dQlZWFncuMTERkyZNgrW1NebPnw8XFxfY2dlh9uzZCAgIwK5du1pV96FDh3D+/HkkJSXB2dkZ3t7eWLVqVZNyS5YsgaenJ2xsbDB+/HjMnz+fu4dQKISBgQF0dHRgYWEBCwsLCIVCZGZmoqioCMnJyXB1dYWHhweSkpKQlZWFEydOcHXX1dUhKSkJQ4cObdWjOoVCgcjISHh5eSntXv88r732GqRSKdasWYO6ujrcvn0bixYtAgBcu3at1fWQrolhGAx6Ys+e51Eo5Kj5uzHDOo30EEK6OB5PCF+fIrXdu7UGDhwIT09PfP/99/D19UVZWRmys7Px+eefQy6XY9WqVdi1axeuXr2Kuro6PHz4sNVzdkpKStC3b1+IxWLuXHPJjFNSUrBhwwZcunQJNTU1qK+vf+7O7I119+3blzvn6OgIY2NjlJSUwN3dHQBgbW0NM7OG/0lnZ2fj9ddf58pv3boVISEhSvWGh4ejuLgYx44da1UbGw0ePBg//PADoqKiEBMTAz6fjzlz5sDc3Fxp9IdoLi4tRUkxqm9cb3GH5XvV1VDI5WAYHgx6qGhjwk6G/hUQosEYhgGfr6+Wo/ExVWuFhYVh9+7duHv3LhITE2Fvbw8fHx+sWbMG69evR3R0NI4cOYLCwkL4+/s/c0Lwi8jNzUVISAjeeOMNHDhwAKdOnUJsbGy73aN79+7c393c3FBYWMgdEyZMUCobERGBAwcO4MiRI+jTp0+b7/Xee+9BKpXi6tWr+Ouvv7B8+XLcvHkTdnZ2L90O0vkZmpqhr2PDI+2SY7+2WLZxPk93ExPwWvnYtaujTg8hpFMICgoCj8dDcnIykpKSMGPGDDAMg99++w0TJ07ElClT4OzsDDs7O5SWlra63kGDBqGyslLp8U5eXp5SmZycHFhbWyM2NhZubm5wcHDA5cuXlcoIBALI5fJm666srOTOnTt3Dnfu3IGjo2Oz8QiFQvTr1487RCIRgIZJ5xEREUhLS8Phw4dha/tyG7iZm5vDwMAAKSkp0NPTw5gxY16qPtJ1cGkpso+0uKCAm89j0rND4uoMqNNDCOkUDAwMEBwcjJiYGFy7do1bZu3g4ICMjAzk5OSgpKQEH374odLqqOfx8/ND//79MW3aNJw+fRrZ2dmIjY1VKuPg4ICKigrs3LkTly5dwoYNG5CWlqZUxsbGBuXl5SgsLMStW7fw8OFD+Pn5wcnJCSEhISgoKMDx48cRGhoKHx8fuLm5tan94eHh+PHHH5GcnAyRSASpVAqpVIr79+9zZaRSKQoLC7mVWEVFRSgsLMTff//Nldm0aRMKCgpQWlqKzZs3IyIiAvHx8TA2Nm5TPKTrcvDwgo5AF7efk5aiwzcm7ASo00MI6TTCwsJw+/Zt+Pv7c3NwlixZgldffRX+/v7w9fWFhYUFAgMDW10nj8dDWloa7t+/j2HDhuGDDz7AypUrlcpMmDAB8+bNQ0REBFxcXJCTk4O4uDilMpMmTUJAQABGjRoFMzMz7NixAwzDYN++fejRowe8vb3h5+cHOzs7pKSktLntCQkJqK6uhq+vLywtLbnjybq2bNmCoUOHYubMmQAAb29vDB06FPv37+fKHD9+HGPGjIGTkxO2bduGrVu3Ys6cOW2Oh3Rduvr66NeKtBR3OzIFRSfBsG1NyarBWpuanpDO6sGDBygvL29xPxhCnkY/N5qn/NTv2PPlcggNjfBhwg/g6zRdt/TTutUozc2Gb+hMuI6bqIYo209rf3/TSA8hhBCiYaxfGfrctBTcSI+p9oz0UKeHEEII0TBKaSmesWfP44nM1OkhhBBCSBfGpaU4mY8HtcpJZxVyOWr/NwFem+b0UKeHEEII0UCNaSnkjx6hNO83pfdqbv8NllWAx+dDX4tW9lGnhxBCCNFAT6alKHlqFVfjoy0Dk57g8bRjY0KAOj2EEEKIxho00hdgGC4tRaOavxv36NGeR1sAdXoIIYQQjWVoagarwU3TUty91bhHj/ZsTAhQp4cQQgjRaIP+8RoA5bQUTz7e0ibU6SGEEEI0WH8PTy4thfRSQ946brk6jfQQQkjX4uvri8jISO61jY0N1q1b1+I1DMNg7969Ko2LkM5AIHwiLcXRhgnN2rgxIUCdHkKImo0fPx4BAQHNvpednQ2GYXDmzJk21XnixAnMmjWrPcLjLF++HC4uLu1a55Pi4+Ph7u4OkUiEXr16ITAwEBcuXODe//vvvzF79mwMGDAAQqEQVlZWmDNnDqqrq7kyf/31FwICAiAWi6Grq4u+ffsiIiICMplMZXGTrqFxz54LOUchr6/H3b//AgAY0kgPIYR0nLCwMGRkZODKlStN3ktMTISbmxteeeWVNtVpZmYGfX399gqxXdXV1TV7PisrC+Hh4cjLy0NGRgYePXqEsWPHora2FgBQVVWFqqoqfP311yguLoZEIkF6ejrCwsK4Ong8HiZOnIj9+/ejtLQUEokEhw4dwkcffdQhbSOdF5eW4q4Mf5w8jto7twHQ6i1CiAZhWRa1crlajtbmMn7zzTdhZmYGiUSidL6mpgapqakIDAzE5MmT0bt3b+jr68PJyQk7duxosc6nH29dvHgR3t7e0NPTg6OjIzIyMppcEx0djf79+0NfXx92dnaIi4vDo0ePAAASiQSfffYZTp8+DYZhwDAMF29FRQUmTpwIAwMDGBoaIigoCNevP14a3DhC9N1337WY0DM9PR3Tp0/H4MGD4ezsDIlEgoqKCpw8eRIAMGTIEOzevRvjx4+Hvb09XnvtNaxcuRI//fQT6uvrAQA9evTAxx9/DDc3N1hbW2P06NH45JNPkJ2d3eLXi2i+J9NSHN//b4BlwdfRgVCkXcm1m6ZdJYRojHsKBeyPFqnl3pe8ndCd//xNz3R0dBAaGgqJRILY2FgwDAMASE1NhVwux5QpU5Camoro6GgYGhri4MGDmDp1Kuzt7TFs2LDn1q9QKPDWW2/B3Nwc+fn5qK6uVpr/00gkEkEikUAsFqOoqAgzZ86ESCTCwoULERwcjOLiYqSnp+PQoUMAACMjIygUCq7Dk5WVhfr6eoSHhyM4OBi//vorV3dZWRl2796NPXv2gN+KrwkA7rGViYlJi2UMDQ2h00wGbaBhdGjPnj3w8fFp1T2JZnP0fg0FP++DtKxhMrNBT1MwPO0a+9Cu1hJCOqUZM2bg0qVLyMrK4s4lJiZi0qRJsLa2xvz58+Hi4gI7OzvMnj0bAQEB2LVrV6vqPnToEM6fP4+kpCQ4OzvD29sbq1atalJuyZIl8PT0hI2NDcaPH4/58+dz9xAKhTAwMICOjg4sLCxgYWEBoVCIzMxMFBUVITk5Ga6urvDw8EBSUhKysrJw4sQJru66ujokJSVh6NChrXpUp1AoEBkZCS8vLwwZMqTZMrdu3cKKFSuanbs0efJk6Ovro3fv3jA0NMR3333Xqq8V0Wy9bOzQs48V91rbHm0BNNJDiEbT5/FwydtJbfdurYEDB8LT0xPff/89fH19UVZWhuzsbHz++eeQy+VYtWoVdu3ahatXr6Kurg4PHz5s9ZydkpIS9O3bF2KxmDs3YsSIJuVSUlKwYcMGXLp0CTU1Naivr4ehYctD/4119+3blzvn6OgIY2NjlJSUwN3dHQBgbW0NM7OGCaPZ2dl4/fXXufJbt25FSEiIUr3h4eEoLi7GsWPHmr2vTCbDuHHj4OjoiOXLlzd5/9tvv8WyZctQWlqKmJgYREVF4Z///GeLbSGaj2EYOHq/huxkCQDtW64OUKeHEI3GMEyrHjF1BmFhYZg9ezY2b96MxMRE2Nvbw8fHB6tXr8b69euxbt06ODk5oXv37oiMjHzmhOAXkZubi5CQEHz22Wfw9/eHkZERdu7cibVr17ZL/d27d+f+7ubmhsLCQu61ubm5UtmIiAgcOHAAR48eRZ8+fZrUdffuXQQEBEAkEiEtLQ3dunVrUqZxNGrgwIEwMTHBP/7xD8TFxcHS0rJd2kO6rkEjfZG94weAZbVypIcebxFCOoWgoCDweDwkJycjKSkJM2bMAMMw+O233zBx4kRMmTIFzs7OsLOzQ2lpaavrHTRoECorK3Ht2jXuXF5enlKZnJwcWFtbIzY2Fm5ubnBwcMDly5eVyggEAsjl8mbrrqys5M6dO3cOd+7cgaOjY7PxCIVC9OvXjztEIhGAhknnERERSEtLw+HDh2Fra9vkWplMhrFjx0IgEGD//v3PnBT9JIVCAQB4+PDhc8sSzSfqaQprJxcAgIm4aada09FIDyGkUzAwMEBwcDBiYmIgk8kwffp0AICDgwP+/e9/IycnBz169MA333yD69evP7NT8TQ/Pz/0798f06ZNw5o1ayCTyRAbG6tUxsHBARUVFdi5cyfc3d1x8OBBpKWlKZWxsbFBeXk5CgsL0adPH4hEIvj5+cHJyQkhISFYt24d6uvr8cknn8DHxwdubm5tan94eDiSk5Oxb98+iEQiSKVSAA0TpoVCIdfhuXfvHn788UfIZDJu/x0zMzPw+Xz8/PPPuH79Otzd3WFgYICzZ89iwYIF8PLygo2NTZviIZor4JN5uPR7HreaS5vQSA8hpNMICwvD7du34e/vz83BWbJkCV599VX4+/vD19cXFhYWCAwMbHWdPB4PaWlpuH//PoYNG4YPPvgAK1euVCozYcIEzJs3DxEREXBxcUFOTg7i4uKUykyaNAkBAQEYNWoUzMzMsGPHDjAMg3379qFHjx7w9vaGn58f7OzskJKS0ua2JyQkoLq6Gr6+vrC0tOSOxroKCgqQn5+PoqIi9OvXT6lM40iTUCjE9u3bMXLkSAwaNAjz5s3DhAkTcODAgTbHQzSXQQ8TOI95A/xnrPrTZAzb2s00tIBMJoORkRG3DJSQrubBgwcoLy9vcT8YQp5GPzekq2vt728a6SGEEEKIVqBODyGEEEK0AnV6CCGEEKIVqNNDCCGEEK1AnR5CNFDj3iyEtAb9vBBt8ULr1TZv3ow1a9ZAKpXC2dkZGzdubDHxX2pqKuLi4vDnn3/CwcEBq1evxhtvvMG9z7Isli1bhu3bt+POnTvw8vJCQkICHBwcAAB//vknVqxYgcOHD0MqlUIsFmPKlCmIjY2FQCDgyjS3mVdubi6GDx/+Is0kpMsRCATg8XioqqqCmZkZBAIBl8CTkKexLIu6ujrcvHkTPB6P+zwlRFO1udOTkpKCqKgobNmyBR4eHli3bh38/f1x4cIF9OrVq0n5nJwcTJ48GfHx8XjzzTeRnJyMwMBAFBQUcIn0vvrqK2zYsAE//PADbG1tERcXB39/f5w7dw56eno4f/48FAoFtm7din79+qG4uBgzZ85EbW0tvv76a6X7HTp0CIMHD+Ze9+zZs61NJKTL4vF4sLW1xbVr11BVVaXucEgXoa+vDysrK/C0LOM20T5t3qfHw8MD7u7u2LRpE4CGYdG+ffti9uzZWLRoUZPywcHBqK2tVdoca/jw4XBxccGWLVvAsizEYjE+/fRTzJ8/HwBQXV0Nc3NzSCQSvPvuu83GsWbNGiQkJOCPP/4A8Hik59SpU3BxcWlLkzi0Tw/RFCzLor6+vknaBEKexufzoaOjQyOCpEtr7e/vNo301NXV4eTJk4iJieHO8Xg8+Pn5ITc3t9lrcnNzERUVpXTO398fe/fuBQCUl5dDKpXCz8+Pe9/IyAgeHh7Izc19ZqenuroaJiYmTc5PmDABDx48QP/+/bFw4UJMmDDhme15+PChUj6axi3dCenqGIZBt27dmk1GSQgh2qpNY5m3bt2CXC5vkhXY3NycyxPzNKlU2mL5xj/bUmdZWRk2btyIDz/8kDtnYGCAtWvXIjU1FQcPHsTIkSMRGBiI/fv3P7M98fHxMDIy4o6+ffs+sywhhBBCurYul3jj6tWrCAgIwDvvvIOZM2dy501NTZVGlNzd3VFVVYU1a9Y8c7QnJiZG6RqZTEYdH0IIIURDtWmkx9TUFHw+H9evX1c6f/36dVhYWDR7jYWFRYvlG/9sTZ1VVVUYNWoUPD09sW3btufG6+HhgbKysme+r6urC0NDQ6WDEEIIIZqpTSM9AoEArq6uyMzM5LIcKxQKZGZmIiIiotlrRowYgczMTERGRnLnMjIyMGLECACAra0tLCwskJmZyU1AlslkyM/Px8cff8xdc/XqVYwaNQqurq5ITExs1SqDwsJCWFpatrp9jXO6aW4PIYQQ0nU0/t5+7tosto127tzJ6urqshKJhD137hw7a9Ys1tjYmJVKpSzLsuzUqVPZRYsWceV/++03VkdHh/3666/ZkpISdtmyZWy3bt3YoqIirsyXX37JGhsbs/v27WPPnDnDTpw4kbW1tWXv37/PsizLXrlyhe3Xrx87evRo9sqVK+y1a9e4o5FEImGTk5PZkpIStqSkhF25ciXL4/HY77//vtVtq6ysZAHQQQcddNBBBx1d8KisrGzx93yb5/QEBwfj5s2bWLp0KaRSKVxcXJCens5NRK6oqFAahfH09ERycjKWLFmCxYsXw8HBAXv37uX26AGAhQsXora2FrNmzcKdO3cwcuRIpKenQ09PD0DDyFBZWRnKysrQp08fpXjYJ3p1K1aswOXLl6Gjo4OBAwciJSUFb7/9dqvbJhaLUVlZCZFI1O7LNxvnC1VWVmrFYzRqr2aj9mo2aq9m08T2siyLu3fvQiwWt1iuzfv0kBejbXsAUXs1G7VXs1F7NZu2tfdJtP0mIYQQQrQCdXoIIYQQohWo09NBdHV1sWzZMujq6qo7lA5B7dVs1F7NRu3VbNrW3ifRnB5CCCGEaAUa6SGEEEKIVqBODyGEEEK0AnV6CCGEEKIVqNNDCCGEEK1AnR5CCCGEaAXq9HSAzZs3w8bGBnp6evDw8MDx48fVHZJKxMfHw93dHSKRCL169UJgYCAuXLig7rA6zJdffgmGYZSS62qaq1evYsqUKejZsyeEQiGcnJzw+++/qzsslZHL5YiLi4OtrS2EQiHs7e2xYsWK5yc17CKOHj2K8ePHQywWg2EY7N27V+l9lmWxdOlSWFpaQigUws/PDxcvXlRPsO2gpfY+evQI0dHRcHJyQvfu3SEWixEaGoqqqir1BfySnvf9fdJHH30EhmGwbt26DotPHajTo2IpKSmIiorCsmXLUFBQAGdnZ/j7++PGjRvqDq3dZWVlITw8HHl5ecjIyMCjR48wduxY1NbWqjs0lTtx4gS2bt2KV155Rd2hqMzt27fh5eWFbt264T//+Q/OnTuHtWvXokePHuoOTWVWr16NhIQEbNq0CSUlJVi9ejW++uorbNy4Ud2htYva2lo4Oztj8+bNzb7/1VdfYcOGDdiyZQvy8/PRvXt3+Pv748GDBx0caftoqb337t1DQUEB4uLiUFBQgD179uDChQuYMGGCGiJtH8/7/jZKS0tDXl7ec/NWaYRWpyAnL2TYsGFseHg491oul7NisZiNj49XY1Qd48aNGywANisrS92hqNTdu3dZBwcHNiMjg/Xx8WHnzp2r7pBUIjo6mh05cqS6w+hQ48aNY2fMmKF07q233mJDQkLUFJHqAGDT0tK41wqFgrWwsGDXrFnDnbtz5w6rq6vL7tixQw0Rtq+n29uc48ePswDYy5cvd0xQKvSs9l65coXt3bs3W1xczFpbW7Pffvtth8fWkWikR4Xq6upw8uRJ+Pn5ced4PB78/PyQm5urxsg6RnV1NQDAxMREzZGoVnh4OMaNG6f0fdZE+/fvh5ubG9555x306tULQ4cOxfbt29Udlkp5enoiMzMTpaWlAIDTp0/j2LFjeP3119UcmeqVl5dDKpUq/VwbGRnBw8NDKz6/gIbPMIZhYGxsrO5QVEKhUGDq1KlYsGABBg8erO5wOoSOugPQZLdu3YJcLoe5ubnSeXNzc5w/f15NUXUMhUKByMhIeHl5YciQIeoOR2V27tyJgoICnDhxQt2hqNwff/yBhIQEREVFYfHixThx4gTmzJkDgUCAadOmqTs8lVi0aBFkMhkGDhwIPp8PuVyOlStXIiQkRN2hqZxUKgWAZj+/Gt/TZA8ePEB0dDQmT56ssZnIV69eDR0dHcyZM0fdoXQY6vQQlQgPD0dxcTGOHTum7lBUprKyEnPnzkVGRgb09PTUHY7KKRQKuLm5YdWqVQCAoUOHori4GFu2bNHYTs+uXbvwr3/9C8nJyRg8eDAKCwsRGRkJsVissW0mDZOag4KCwLIsEhIS1B2OSpw8eRLr169HQUEBGIZRdzgdhh5vqZCpqSn4fD6uX7+udP769euwsLBQU1SqFxERgQMHDuDIkSPo06ePusNRmZMnT+LGjRt49dVXoaOjAx0dHWRlZWHDhg3Q0dGBXC5Xd4jtytLSEo6OjkrnBg0ahIqKCjVFpHoLFizAokWL8O6778LJyQlTp07FvHnzEB8fr+7QVK7xM0rbPr8aOzyXL19GRkaGxo7yZGdn48aNG7CysuI+vy5fvoxPP/0UNjY26g5PZajTo0ICgQCurq7IzMzkzikUCmRmZmLEiBFqjEw1WJZFREQE0tLScPjwYdja2qo7JJUaPXo0ioqKUFhYyB1ubm4ICQlBYWEh+Hy+ukNsV15eXk22ICgtLYW1tbWaIlK9e/fugcdT/pjk8/lQKBRqiqjj2NrawsLCQunzSyaTIT8/XyM/v4DHHZ6LFy/i0KFD6Nmzp7pDUpmpU6fizJkzSp9fYrEYCxYswH//+191h6cy9HhLxaKiojBt2jS4ublh2LBhWLduHWpra/H++++rO7R2Fx4ejuTkZOzbtw8ikYh77m9kZAShUKjm6NqfSCRqMl+pe/fu6Nmzp0bOY5o3bx48PT2xatUqBAUF4fjx49i2bRu2bdum7tBUZvz48Vi5ciWsrKwwePBgnDp1Ct988w1mzJih7tDaRU1NDcrKyrjX5eXlKCwshImJCaysrBAZGYkvvvgCDg4OsLW1RVxcHMRiMQIDA9UX9Etoqb2WlpZ4++23UVBQgAMHDkAul3OfYSYmJhAIBOoK+4U97/v7dKeuW7dusLCwwIABAzo61I6j7uVj2mDjxo2slZUVKxAI2GHDhrF5eXnqDkklADR7JCYmqju0DqPJS9ZZlmV/+ukndsiQIayuri47cOBAdtu2beoOSaVkMhk7d+5c1srKitXT02Pt7OzY2NhY9uHDh+oOrV0cOXKk2X+z06ZNY1m2Ydl6XFwca25uzurq6rKjR49mL1y4oN6gX0JL7S0vL3/mZ9iRI0fUHfoLed7392nasGSdYVkN2VqUEEIIIaQFNKeHEEIIIVqBOj2EEEII0QrU6SGEEEKIVqBODyGEEEK0AnV6CCGEEKIVqNNDCCGEEK1AnR5CCCGEaAXq9BBCCCFEK1CnhxBCCCFagTo9hBBCCNEK1OkhhBBCiFb4P82LH+ahHS3LAAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "vali_uids = mg.uids[(mg.S > 20000) & mg.validator_permit]\n", + "weightsT = np.transpose(weights)\n", + "for i,line in enumerate(weightsT):\n", + " plt.plot(line, label=f\"Validator-{vali_uids[i]}\")\n", + "plt.legend()" + ] + }, + { + "cell_type": "code", + "execution_count": 38, + "id": "3b4f0837-6111-4cb7-b2b3-034840965fd7", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 38, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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kdi0aZOpePrMmTOmmh4AiI+Px4oVKzBz5kzMmDEDYWFhWLduHXr37m0aM3XqVFRUVGDChAkoLS3FwIEDsWnTJtMqM3d3d6xduxazZ89GRUUF/Pz88PDDD2PmzJlQKBSm43zxxRf485//DCEE4uLisH37dtPlNKBuVd1rr72Gc+fOwd3dHX379sVPP/2EIUOGWP6TIyIickL/O2NkMAi8mpaL4rIqhHX2xN+HOl9d0Y0kwli9TE2i0+mgUqmg1WpZb0RERE7no+0n8famI3iiXxf8a0QkFm87gfk/HoXSRYpvXxqIu3y97B1iszT1/duhVqURERGRbRmX7F/QVmJ3/mW8u/koAODvj/d22KTIEkyMiIiIyMS3vsYov6QCr6ysqyv6Q78u+GO089YV3cjiGiMiIiJyXv9bY9TVxwNzkntDIpHYM6wWwxkjIiIiMjGuSgMAV7kUHzzdHx6KtjOPwsSIiIiITNxcZfD1rlvxPfuxnujp37YWGrWdFJCIiIia5MNnonC+9Boe7dv2GiAzMSIiIiIzUcHtERXc3t5h2AUvpRERERHVY2JEREREVI+JEREREVE9JkZERERE9ZgYEREREdVjYkRERERUj4kRERERUT0mRkRERET1mBgRERER1WNiRERERFSPiRERERFRPSZGRERERPWYGBERERHVk9s7AEcjhAAA6HQ6O0dCRERETWV83za+j98KEyMLlZWVAQACAwPtHAkRERFZqqysDCqV6pbPS8TtUicyYzAYcP78eXh5eUEikVjtuDqdDoGBgSgsLIS3t7fVjttatbXXC7S918zX69z4ep2bM75eIQTKysrg7+8PqfTWlUScMbKQVCpFQECAzY7v7e3tNP8Im6KtvV6g7b1mvl7nxtfr3Jzt9TY2U2TE4msiIiKiekyMiIiIiOoxMWolFAoFZs+eDYVCYe9QWkRbe71A23vNfL3Oja/XubW113sjFl8TERER1eOMEREREVE9JkZERERE9ZgYEREREdVjYkRERERUj4lRK7F48WKEhIRAqVQiNjYWu3fvtndINpGamooBAwbAy8sLnTt3RnJyMo4ePWrvsFrMW2+9BYlEgilTptg7FJs5d+4c/vSnP6Fjx45wc3NDnz59sHfvXnuHZRN6vR5vvvkmQkND4ebmhm7duuEf//jHbe/F5Eh++eUXPPbYY/D394dEIsG6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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "plt.plot(incentives)" + ] + }, + { + "cell_type": "code", + "execution_count": 58, + "id": "c4a3c3e6-6e83-45dc-86b5-e2808a1052a7", + "metadata": {}, + "outputs": [], + "source": [ + "import wandb\n", + "import pandas as pd\n", + "pd.set_option('display.width', 1000)\n", + "api = wandb.Api(timeout=360)\n", + "# change to mainnet instead of testnet\n", + "runs = api.runs('bitagentsn20/mainnet', filters={\"display_name\": {\"$regex\": \".*-35.*\"}}) # change to \"-\" for latest " + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "id": "39937bc7-edd6-4243-9cbb-dc6a1c9fe275", + "metadata": {}, + "outputs": [], + "source": [ + "while True:\n", + " # sometimes ends prematurely and errors\n", + " try:\n", + " hist = runs.histories()\n", + " df = pd.DataFrame(hist)\n", + " break\n", + " except:\n", + " pass" + ] + }, + { + "cell_type": "code", + "execution_count": 69, + "id": "f30e7769-472d-4f5b-b449-dfbc3367f050", + "metadata": {}, + "outputs": [], + "source": [ + "scores = df[df['miner_uid']==uid][['_timestamp','normalized_score']].values.T" + ] + }, + { + "cell_type": "code", + "execution_count": 70, + "id": "394c4cf0-476c-44be-97dd-d6d292bd3947", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 70, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "x,y = scores\n", + "ordered_idxs = x.argsort()\n", + "plt.plot(x[ordered_idxs],y[ordered_idxs])" + ] + }, + { + "cell_type": "code", + "execution_count": 71, + "id": "213197f4-4dbb-4e7e-b4df-753749b1e6e3", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[np.float64(-0.38095238095238093),\n", + " np.float64(-0.08695652173913043),\n", + " np.float64(-0.1595583160800552),\n", + " np.float64(-0.08695652173913043)]" + ] + }, + "execution_count": 71, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[y.min(), y.max(), y.mean(), np.median(y)]" + ] + }, + { + "cell_type": "code", + "execution_count": 74, + "id": "ec751a78-6011-463e-af22-f8aa379c44db", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[]" + ] + }, + "execution_count": 74, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "scores = df[df['miner_uid']==top_uid][['_timestamp','normalized_score']].values.T\n", + "x,y = scores\n", + "ordered_idxs = x.argsort()\n", + "plt.plot(x[ordered_idxs],y[ordered_idxs])" + ] + }, + { + "cell_type": "code", + "execution_count": 75, + "id": "54a12f8d-71f2-4f8c-8129-cbfb7d46ac4d", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[np.float64(-0.38095238095238093),\n", + " np.float64(-0.08695652173913043),\n", + " np.float64(-0.141012610577828),\n", + " np.float64(-0.08695652173913043)]" + ] + }, + "execution_count": 75, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "[y.min(), y.max(), y.mean(), np.median(y)]" + ] + }, + { + "cell_type": "code", + "execution_count": 76, + "id": "724485a0-1d0f-4a4e-ad2a-4125f97705e8", + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " miner_uid normalized_score_mean sample_count\n", + "0 1 -0.115290 12\n", + "1 2 -0.124734 9\n", + "2 3 -0.145399 15\n", + "3 4 -0.159558 15\n", + "4 5 -0.141013 17\n", + ".. ... ... ...\n", + "117 129 -0.093140 15\n", + "118 130 -0.107956 14\n", + "119 131 -0.178172 14\n", + "120 132 -0.206211 5\n", + "121 133 -0.179526 9\n", + "\n", + "[122 rows x 3 columns]\n" + ] + } + ], + "source": [ + "average_normalized_score = df.groupby('miner_uid').agg(\n", + " normalized_score_mean=('normalized_score', 'mean'),\n", + " sample_count=('normalized_score', 'size')\n", + ").reset_index()\n", + "\n", + "# Display the result\n", + "print(average_normalized_score)" + ] + }, + { + "cell_type": "code", + "execution_count": 77, + "id": "d0a2a339-acb4-46b2-b027-e70d589f45f5", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "np.float64(12.295081967213115)" + ] + }, + "execution_count": 77, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_normalized_score[\"sample_count\"].mean()" + ] + }, + { + "cell_type": "code", + "execution_count": 78, + "id": "46263203-2db1-4955-88d2-432c954fb2e6", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Series([], Name: sample_count, dtype: int64)" + ] + }, + "execution_count": 78, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_normalized_score[average_normalized_score[\"miner_uid\"]==uid][\"sample_count\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 79, + "id": "f4ceca0e-8006-4dd0-8b97-6bfc0a0cdfca", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Series([], Name: sample_count, dtype: int64)" + ] + }, + "execution_count": 79, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_normalized_score[average_normalized_score[\"miner_uid\"]==top_uid][\"sample_count\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 80, + "id": "42ab1e46-cae4-4e81-91e3-61015a0c8942", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "np.float64(-0.14607905953190578)" + ] + }, + "execution_count": 80, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_normalized_score[\"normalized_score_mean\"].mean()" + ] + }, + { + "cell_type": "code", + "execution_count": 81, + "id": "22230712-8d80-4be1-ac58-572a76daa45a", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "np.float64(-0.1476629650542694)" + ] + }, + "execution_count": 81, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_normalized_score[\"normalized_score_mean\"].median()" + ] + }, + { + "cell_type": "code", + "execution_count": 82, + "id": "7fa792e1-b3d2-4fbf-b81a-abe590b0e92b", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Series([], Name: normalized_score_mean, dtype: float64)" + ] + }, + "execution_count": 82, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_normalized_score[average_normalized_score[\"miner_uid\"]==uid][\"normalized_score_mean\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 83, + "id": "19a3284d-0814-4de4-b528-425a7f4aba18", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "Series([], Name: normalized_score_mean, dtype: float64)" + ] + }, + "execution_count": 83, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_normalized_score[average_normalized_score[\"miner_uid\"]==top_uid][\"normalized_score_mean\"]" + ] + }, + { + "cell_type": "code", + "execution_count": 85, + "id": "87d593b7-5867-4d36-aa0f-11982b2be527", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
miner_uidnormalized_score_meansample_count
34-0.15955815
911-0.17930116
1012-0.19183115
1820-0.18556614
1922-0.23414813
2529-0.2857141
2731-0.17009113
2832-0.2857141
3135-0.16791320
3943-0.16204815
4247-0.18062110
4550-0.2141387
4752-0.2857141
4854-0.2857142
4955-0.2857141
5056-0.2857141
5763-0.16326818
6167-0.16710311
6369-0.17003313
6773-0.1767496
6975-0.17486014
7380-0.18073011
7582-0.17651411
7785-0.20535212
8088-0.20998310
8290-0.2857142
8391-0.16063614
8593-0.18782317
8694-0.21526313
8795-0.2857141
9099-0.16980016
92101-0.17456123
93102-0.2857141
95104-0.1849556
97106-0.1689449
98107-0.2857142
100109-0.2857142
103112-0.2829543
108118-0.16061718
110122-0.17210611
111123-0.19360411
116128-0.2857141
119131-0.17817214
120132-0.2062115
121133-0.1795269
\n", + "
" + ], + "text/plain": [ + " miner_uid normalized_score_mean sample_count\n", + "3 4 -0.159558 15\n", + "9 11 -0.179301 16\n", + "10 12 -0.191831 15\n", + "18 20 -0.185566 14\n", + "19 22 -0.234148 13\n", + "25 29 -0.285714 1\n", + "27 31 -0.170091 13\n", + "28 32 -0.285714 1\n", + "31 35 -0.167913 20\n", + "39 43 -0.162048 15\n", + "42 47 -0.180621 10\n", + "45 50 -0.214138 7\n", + "47 52 -0.285714 1\n", + "48 54 -0.285714 2\n", + "49 55 -0.285714 1\n", + "50 56 -0.285714 1\n", + "57 63 -0.163268 18\n", + "61 67 -0.167103 11\n", + "63 69 -0.170033 13\n", + "67 73 -0.176749 6\n", + "69 75 -0.174860 14\n", + "73 80 -0.180730 11\n", + "75 82 -0.176514 11\n", + "77 85 -0.205352 12\n", + "80 88 -0.209983 10\n", + "82 90 -0.285714 2\n", + "83 91 -0.160636 14\n", + "85 93 -0.187823 17\n", + "86 94 -0.215263 13\n", + "87 95 -0.285714 1\n", + "90 99 -0.169800 16\n", + "92 101 -0.174561 23\n", + "93 102 -0.285714 1\n", + "95 104 -0.184955 6\n", + "97 106 -0.168944 9\n", + "98 107 -0.285714 2\n", + "100 109 -0.285714 2\n", + "103 112 -0.282954 3\n", + "108 118 -0.160617 18\n", + "110 122 -0.172106 11\n", + "111 123 -0.193604 11\n", + "116 128 -0.285714 1\n", + "119 131 -0.178172 14\n", + "120 132 -0.206211 5\n", + "121 133 -0.179526 9" + ] + }, + "execution_count": 85, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "average_normalized_score[average_normalized_score[\"normalized_score_mean\"]<=average_normalized_score[average_normalized_score[\"miner_uid\"]==uid][\"normalized_score_mean\"].values[0]]" + ] + }, + { + "cell_type": "code", + "execution_count": 86, + "id": "b2983ae9-09b7-41b6-a584-7a3bc68b586f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "45" + ] + }, + "execution_count": 86, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(average_normalized_score[average_normalized_score[\"normalized_score_mean\"]<=average_normalized_score[average_normalized_score[\"miner_uid\"]==uid][\"normalized_score_mean\"].values[0]])" + ] + }, + { + "cell_type": "code", + "execution_count": 87, + "id": "9a6e9720-ee10-4f5f-979b-560028bce4e2", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
\n", + "\n", + "\n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
messagesprompttoolsfilesdatasresponseresultsnormalized_score
7What is the highest value of Output?[]NaNNaNNone[bold blue]Does not error[/bold blue]\\n:cross_...-0.086957
23What is the location for in-person volunteerin...[{'description': 'Searches for in-person volun...NaNNaNNone[bold blue]Does not error[/bold blue]\\n:cross_...-0.285714
33What is the difference between the high and lo...[]NaNNaNNone[bold blue]Does not error[/bold blue]\\n:cross_...-0.086957
42What is the difference between the high and lo...[]NaNNaNNone[bold blue]Does not error[/bold blue]\\n:cross_...-0.086957
138Shall I proceed with setting up the monthly el...[{'description': 'Schedules a monthly payment ...NaNNaNNone[bold blue]Does not error[/bold blue]\\n:cross_...-0.285714
250What are some quick, easy, and low-carb vegan ...[{'arguments': None, 'description': 'Get a qui...NaNNaNNone[bold blue]Does not error[/bold blue]\\n:cross_...-0.285714
707On what date are Productivity and Strength clo...[][{'type': 'image', 'content': 'iVBORw0KGgoAAAA...[]None[bold blue]Does not error[/bold blue]\\n:cross_...-0.086957
928How can we provide the necessary parameters, \"...[{'name': 'predict_pump_failure_date', 'argume...[][]None[bold blue]Does not error[/bold blue]\\n:cross_...-0.285714
938What is the difference between the high and lo...[][{'type': 'image', 'content': 'iVBORw0KGgoAAAA...[]None[bold blue]Does not error[/bold blue]\\n:cross_...-0.086957
1047[{'content': 'What is the lowest value of Effi...[][{'content': 'iVBORw0KGgoAAAANSUhEUgAABD8AAAGG...[]None[bold blue]Does not error[/bold blue]\\n:cross_...-0.086957
1075[{'content': 'What is the highest value of Pro...[][{'type': 'image', 'content': 'iVBORw0KGgoAAAA...[]None[bold blue]Does not error[/bold blue]\\n:cross_...-0.086957
1086[{'content': 'What is the highest value of Pow...[][{'type': 'image', 'content': 'iVBORw0KGgoAAAA...[]None[bold blue]Does not error[/bold blue]\\n:cross_...-0.086957
1166[{'content': 'What is the lowest value of Rate...[][{'type': 'image', 'content': 'iVBORw0KGgoAAAA...[]None[bold blue]Does not error[/bold blue]\\n:cross_...-0.086957
1212[{'content': 'What is the highest value of Str...[][{'content': 'iVBORw0KGgoAAAANSUhEUgAAA3kAAAHq...[]None[bold blue]Does not error[/bold blue]\\n:cross_...-0.086957
1427[{'content': 'What distinct motifs are commonl...[][][{'source': 'bitagent.source.12297441566513914...None[bold blue]Does not error[/bold blue]\\n:cross_...-0.380952
\n", + "
" + ], + "text/plain": [ + " messages prompt tools files datas response results normalized_score\n", + "7 What is the highest value of Output? [] NaN NaN None [bold blue]Does not error[/bold blue]\\n:cross_... -0.086957\n", + "23 What is the location for in-person volunteerin... [{'description': 'Searches for in-person volun... NaN NaN None [bold blue]Does not error[/bold blue]\\n:cross_... -0.285714\n", + "33 What is the difference between the high and lo... [] NaN NaN None [bold blue]Does not error[/bold blue]\\n:cross_... -0.086957\n", + "42 What is the difference between the high and lo... [] NaN NaN None [bold blue]Does not error[/bold blue]\\n:cross_... -0.086957\n", + "138 Shall I proceed with setting up the monthly el... [{'description': 'Schedules a monthly payment ... NaN NaN None [bold blue]Does not error[/bold blue]\\n:cross_... -0.285714\n", + "250 What are some quick, easy, and low-carb vegan ... [{'arguments': None, 'description': 'Get a qui... NaN NaN None [bold blue]Does not error[/bold blue]\\n:cross_... -0.285714\n", + "707 On what date are Productivity and Strength clo... [] [{'type': 'image', 'content': 'iVBORw0KGgoAAAA... [] None [bold blue]Does not error[/bold blue]\\n:cross_... -0.086957\n", + "928 How can we provide the necessary parameters, \"... [{'name': 'predict_pump_failure_date', 'argume... [] [] None [bold blue]Does not error[/bold blue]\\n:cross_... -0.285714\n", + "938 What is the difference between the high and lo... [] [{'type': 'image', 'content': 'iVBORw0KGgoAAAA... [] None [bold blue]Does not error[/bold blue]\\n:cross_... -0.086957\n", + "1047 [{'content': 'What is the lowest value of Effi... [] [{'content': 'iVBORw0KGgoAAAANSUhEUgAABD8AAAGG... [] None [bold blue]Does not error[/bold blue]\\n:cross_... -0.086957\n", + "1075 [{'content': 'What is the highest value of Pro... [] [{'type': 'image', 'content': 'iVBORw0KGgoAAAA... [] None [bold blue]Does not error[/bold blue]\\n:cross_... -0.086957\n", + "1086 [{'content': 'What is the highest value of Pow... [] [{'type': 'image', 'content': 'iVBORw0KGgoAAAA... [] None [bold blue]Does not error[/bold blue]\\n:cross_... -0.086957\n", + "1166 [{'content': 'What is the lowest value of Rate... [] [{'type': 'image', 'content': 'iVBORw0KGgoAAAA... [] None [bold blue]Does not error[/bold blue]\\n:cross_... -0.086957\n", + "1212 [{'content': 'What is the highest value of Str... [] [{'content': 'iVBORw0KGgoAAAANSUhEUgAAA3kAAAHq... [] None [bold blue]Does not error[/bold blue]\\n:cross_... -0.086957\n", + "1427 [{'content': 'What distinct motifs are commonl... [] [] [{'source': 'bitagent.source.12297441566513914... None [bold blue]Does not error[/bold blue]\\n:cross_... -0.380952" + ] + }, + "execution_count": 87, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "df[(df[\"miner_uid\"]==uid)&(df[\"normalized_score\"]<0.6)][[\"messages\", \"prompt\", \"tools\", \"files\", \"datas\", \"response\", \"results\", \"normalized_score\"]] " + ] + }, + { + "cell_type": "code", + "execution_count": 90, + "id": "1f19c43d-43f7-4f55-b413-f166b1f1b544", + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "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XTiPTjj7c7S3qVk7jv82bNqq1ffofX8k/L365jK66bbfB45/98c/pzeXv0sP//1uevqhv2pK6PsrPwoc//e9d9H4sWPouh9+W2sT2iXS2dVNv7nj/+ieO39NLz32AG3bXkffv/YXcjijtspdM6OGy/nGKkue85ijRrHOjS3xUll+5LUYkK88t60vcG56WqpqjQeB5Zwywe9n62uNj8ZWJbaqCpfNHXuRV4/YChhPbeochYBCQCGgEMh0BPp7++h3V/865W48fs9DCZdx+S++Q3kF8blO5ubmspvmzfTVb19Nd997Hytl8+mIQw6kC846jRbOm0O1NSO0+isrymn0qNqQtvRyH++541eBc2ARfPu9FbTz47epoECPE/rlDVfTY089Rw899hR9+YsXatf7/D768+2/pLKyUu37Z88/i91D3yT6EWkWvb/+8z/09z/cQscecah2HArmhPkHB+qurqqknByeX5WWDGpTH7ud3vmrn9G0KZO08y/90ufpZ7+6PXBtJn1Iq5LX1t1NP370UXr4gw9oT1sb7TthAv32ggvogMmTNQyxknDt44/T/732GjV3ddGh06bRXZ/5DM0YNSqTMM7otkpMXpB4RZ+givKX0Z1zceMlCbpY6yI1dYbhxikKYaRz1L70I1DfyquOu6Go++kgjsVLVEDSsmMvx1+2g2E1MQUx0brU+QoBhYBCQCGgEEgUgbM/dTKdcvwx9NrS5bTsnQ/o6RdeoV/dfjfdfeuN9MULz41a3KTxYwMKHk5a+dEaau/opJEzF4dc08XWtQ2btwT2TZ4wPqDgYecYVh5B/gLZuGUbQVE7gF1GRSrKy2nWtKnyNea2uLgooODhxDGjRtKeer3smBe68GBalbwv/fWvtKqujv528cU0trKS/r5sGR13yy20+rrraFxVFd38zDN024sv0r0XXURTamrox489Rifedpt2vDAvz4VwZleTfOwt2NWr9ynorql/FzfO7Oqxe3ojilsk0hVp5QzDjXNbu486+vxUkheaZkHOU9v0IrB0nR5TCTbNEWWJt6XaYNhUaRQSx05doRBQCCgEMhWB3Pw8gkUtXoFh5F93/oPq6/aw50hwQdDD8QG1Y0fSeZd+lkMF4psnoO5EpbCwgI4/6nDt75rvfkOLq0P8XCwlr4RdPc0CBQ9K1QuP3mferX2GJVAkLy9UfUG/fJi0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", + "text/plain": [ + "" + ] + }, + "execution_count": 90, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "# the iloc should be one of the rows with files in it\n", + "img_str = df[(df[\"miner_uid\"]==uid)&(df[\"normalized_score\"]<0.6)].iloc[6][\"files\"][0]['content']\n", + "from PIL import Image\n", + "from io import BytesIO\n", + "import base64\n", + "\n", + "Image.open(BytesIO(base64.b64decode(img_str)))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3f94ed38-0f07-4bfc-aae0-a663e7dd8998", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75c7bb72-4399-46ce-8781-86260748b323", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.12.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/bitagent_subnet-main/docs/examples/bad_output_to_miner.png b/bitagent_subnet-main/docs/examples/bad_output_to_miner.png new file mode 100644 index 0000000000000000000000000000000000000000..c255385b060fda9257220fd679f83e6ce34b76a1 Binary files /dev/null and b/bitagent_subnet-main/docs/examples/bad_output_to_miner.png differ diff --git a/bitagent_subnet-main/docs/examples/output_to_miner.png b/bitagent_subnet-main/docs/examples/output_to_miner.png new file mode 100644 index 0000000000000000000000000000000000000000..de55b1f4564c4097b7ff39fc13adced2cc53580d Binary files /dev/null and b/bitagent_subnet-main/docs/examples/output_to_miner.png differ diff --git a/bitagent_subnet-main/docs/otf_weight_demo_template.ipynb b/bitagent_subnet-main/docs/otf_weight_demo_template.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..b42266d45ecef209f067c3fd636697da54f16f04 --- /dev/null +++ b/bitagent_subnet-main/docs/otf_weight_demo_template.ipynb @@ -0,0 +1,1213 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# SN20 - BitAgent\n", + "\n", + "This demo notebook should fufill the following tasks.\n", + " - (A) Demonstrate the quality of the communication between miners and validators coming from the top miner.\n", + " - (B) Justify the difference in incentive for miners in different tiers (eg.quantile 1 VS quantile 3).\n", + " - (C) (If applicable) Show the landscape and variety of miners. \n", + " - (D) (If applicable) Demonstrate the effectiveness of the scoring mechanism.\n", + " - (E) (If applicable) Show the dataset that was used by the validator.\n", + " - (F) (If applicable) Show the use of any API and/or links to a frontend.\n", + " \n", + "## Objective\n", + "> SN20 focuses on providing LLM-guided tool execution. There is 1 type of task with several sub types:\n", + "> - Single Tool Call Task - given a single tool, call that tool with the correct arguments \n", + "> - Multi Tool Call Task - given a list of serveral tools, call the correct tool with the correct arguments\n", + "> - Irrelevant Tool Call Task - given a list of serveral tools, do not call any tools\n", + "> - And many more to come ... multi-step, parallel, etc." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Setup\n", + "> Provide your hotkey and coldkey \\\n", + "> Modify any other config you need like the openai api base url for your mistral 7b llm" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%%capture\n", + "\n", + "# to clear the notebook params from the argument list for argparser\n", + "import sys\n", + "sys.argv = ['']\n", + "\n", + "import bittensor as bt \n", + "from rich import print as rprint\n", + "from neurons.validator import Validator\n", + "from IPython.display import display_markdown\n", + "from bitagent.tasks.task import get_random_task\n", + "from bitagent.validator.initiation import initiate_validator\n", + "\n", + "SUBNET_UID = 20\n", + "# working with subnet\n", + "#subnet = bt.metagraph(netuid=SUBNET_UID, network=\"finney\")\n", + "#vali_wallet = bt.wallet(name=WALLET_NAME, hotkey=HOTKEY_NAME, path=WALLET_PATH)\n", + "#vali_dendrite = bt.dendrite(wallet=vali_wallet)\n", + "\n", + "# Wallet \n", + "WALLET_NAME = # TODO, put your coldkey\n", + "HOTKEY_NAME = # TODO, put your hotkey\n", + "WALLET_PATH = # TODO, put your wallet path\n", + "\n", + "# setup the config for the validator\n", + "cfg = bt.Config()\n", + "cfg.wallet = {\"name\": WALLET_NAME, \"hotkey\": HOTKEY_NAME, \"path\": WALLET_PATH}\n", + "cfg.netuid = SUBNET_UID\n", + "#cfg.openai_api_base = # TODO if you need to set this to the LLM of your choice\n", + "\n", + "# setup the validator\n", + "from unittest.mock import patch\n", + "\n", + "# Define your stubbed function for our get_alive_uids so we don't have to check them ALL\n", + "def stub_get_alive_uids(whatever):\n", + " return [1, 2, 3]\n", + "\n", + "# Patch the function in our validator module\n", + "with patch('common.base.validator.get_alive_uids', new=stub_get_alive_uids):\n", + " vali = Validator(config=cfg)\n", + " initiate_validator(vali)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "task = get_random_task(vali)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[{ 'desc': '',\n", + " 'eval_args': [ ],\n", + " 'eval_fx': ,\n", + " 'name': 'Does '\n", + " 'not '\n", + " 'error'},\n", + " { 'desc': '',\n", + " 'eval_args': [ ],\n", + " 'eval_fx': ,\n", + " 'name': 'Does '\n", + " 'not '\n", + " 'take '\n", + " 'a '\n", + " 'long '\n", + " 'time'},\n", + " { 'desc': '',\n", + " 'eval_args': [ ],\n", + " 'eval_fx': ,\n", + " 'name': 'Return '\n", + " 'correct '\n", + " 'function '\n", + " 'format'},\n", + " { 'desc': '',\n", + " 'eval_args': [ { 'arguments': { 'company_name': [ 'Apple '\n", + " 'Inc.',\n", + " 'Apple'],\n", + " 'detail_level': [ 'detailed'],\n", + " 'market': [ 'NASDAQ',\n", + " '']},\n", + " 'is_ground_truth': True,\n", + " 'name': 'get_stock_info'}],\n", + " 'eval_fx': ,\n", + " 'name': 'Return '\n", + " 'correct '\n", + " 'function '\n", + " 'name'},\n", + " { 'desc': '',\n", + " 'eval_args': [ { 'arguments': { 'company_name': [ 'Apple '\n", + " 'Inc.',\n", + " 'Apple'],\n", + " 'detail_level': [ 'detailed'],\n", + " 'market': [ 'NASDAQ',\n", + " '']},\n", + " 'is_ground_truth': True,\n", + " 'name': 'get_stock_info'}],\n", + " 'eval_fx': ,\n", + " 'name': 'Return '\n", + " 'function '\n", + " 'with '\n", + " 'correct '\n", + " 'argument '\n", + " 'names'},\n", + " { 'desc': '',\n", + " 'eval_args': [ { 'arguments': { 'company_name': [ 'Apple '\n", + " 'Inc.',\n", + " 'Apple'],\n", + " 'detail_level': [ 'detailed'],\n", + " 'market': [ 'NASDAQ',\n", + " '']},\n", + " 'is_ground_truth': True,\n", + " 'name': 'get_stock_info'}],\n", + " 'eval_fx': ,\n", + " 'name': 'Return '\n", + " 'function '\n", + " 'with '\n", + " 'correct '\n", + " 'argument '\n", + " 'values'}]" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "task.criteria" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[ChatMessage(role=, content='Obtain the detailed stock information for either \"Apple Inc.\" or \"Apple\" in the NASDAQ market.')]" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "task.synapse.messages" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "[Tool(name='get_sensor_readings_history_by_interval', description='Retrieves historical sensor readings within a specified timespan, summarized in intervals, and returns them sorted by the start time of each interval in descending order.', arguments={'perPage': {'required': False, 'type': 'integer', 'description': 'Number of entries per page, within the range of 3 to 100.'}, 'startingAfter': {'required': False, 'type': 'string', 'description': 'Server-generated token indicating the start of the page, typically a timestamp or ID.'}, 'endingBefore': {'required': False, 'type': 'string', 'description': 'Server-generated token indicating the end of the page, typically a timestamp or ID.'}, 'networkId': {'required': False, 'type': 'array', 'description': 'Filter data by the specified network IDs.'}, 'serials': {'required': False, 'type': 'array', 'description': 'Filter readings by sensor serial numbers.'}, 'metrics': {'required': False, 'type': 'array', 'description': \"Specify sensor reading types to retrieve, such as 'temperature', 'humidity', 'pressure'. Defaults to all available types if not supplied.\"}, 'timespan': {'required': True, 'type': 'integer', 'description': 'The timespan for fetching data, in seconds. The maximum is equivalent to 730 days and 12 hours.'}, 't0': {'required': False, 'type': 'string', 'description': \"Start of the data timespan, in the ISO 8601 format 'YYYY-MM-DDTHH:MM:SSZ'. The maximum lookback period is 730 days and 12 hours.\"}, 't1': {'required': False, 'type': 'string', 'description': \"End of the data timespan, in the ISO 8601 format 'YYYY-MM-DDTHH:MM:SSZ'. This can be a maximum of 730 days and 12 hours after t0.\"}, 'interval': {'required': False, 'type': 'integer', 'description': 'The time interval in seconds for the data returned. Valid values are 15, 120, 300, 900, 3600, 14400, 86400, or 604800.'}, 'models': {'required': False, 'type': 'array', 'description': \"Filter readings by one or more sensor models. Available models include 'MT10', 'MT11', 'MT12', 'MT14', 'MT20', 'MT30'.\"}}),\n", + " Tool(name='Buses_3_BuyBusTicket', description='Purchase bus tickets for a specified route, date, and time with the option to include additional luggage.', arguments={'from_city': {'required': True, 'type': 'string', 'description': \"The city of origin for the trip, such as 'New York, NY'.\"}, 'to_city': {'required': True, 'type': 'string', 'description': \"The destination city for the trip, such as 'Boston, MA'.\"}, 'departure_date': {'required': True, 'type': 'string', 'description': \"The date of departure in the format 'YYYY-MM-DD'.\"}, 'departure_time': {'required': True, 'type': 'string', 'description': \"The time of departure in 24-hour format 'HH:MM'.\"}, 'num_passengers': {'required': False, 'type': 'integer', 'description': 'The number of passengers for whom the tickets are being purchased.'}, 'additional_luggage': {'required': False, 'type': 'boolean', 'description': 'Indicates whether additional luggage space is required.'}}),\n", + " Tool(name='Events_3_BuyEventTickets', description='Purchase tickets for a specified cultural event on a particular date in a specified city.', arguments={'event_name': {'required': True, 'type': 'string', 'description': 'The name of the artist or the play for which tickets are being purchased.'}, 'number_of_tickets': {'required': True, 'type': 'integer', 'description': 'The total number of tickets to be reserved for the event.'}, 'date': {'required': True, 'type': 'string', 'description': \"The date of the event, in the format 'YYYY-MM-DD'.\"}, 'city': {'required': True, 'type': 'string', 'description': \"The city where the event is located, expected in the format of 'City, State', such as 'New York, NY'.\"}}),\n", + " Tool(name='get_sensor_alerts', description='Retrieve a paginated list of sensor alerts within a specified timespan and optionally filtered by various criteria such as network IDs or sensor metrics.', arguments={'perPage': {'required': False, 'type': 'integer', 'description': 'The number of entries per page returned. Acceptable range is 3 - 100.'}, 'startingAfter': {'required': False, 'type': 'string', 'description': 'A server-generated token indicating the start of the page, such as a timestamp or an ID.'}, 'endingBefore': {'required': False, 'type': 'string', 'description': 'A server-generated token indicating the end of the page, such as a timestamp or an ID.'}, 't0': {'required': False, 'type': 'string', 'description': \"The start of the timespan for the data, formatted as an ISO 8601 timestamp (e.g., '2023-01-01T00:00:00Z'). The maximum lookback period is 365 days from today.\"}, 't1': {'required': False, 'type': 'string', 'description': \"The end of the timespan for the data, formatted as an ISO 8601 timestamp (e.g., '2023-12-31T23:59:59Z'). t1 can be a maximum of 365 days after t0.\"}, 'networkId': {'required': False, 'type': 'array', 'description': 'A list of network IDs to filter the returned data.'}, 'timespan': {'required': False, 'type': 'integer', 'description': 'The timespan for which the information will be fetched in seconds. Do not specify if t0 and t1 are provided. Must be less than or equal to 31,536,000 seconds (365 days).'}, 'sensorSerial': {'required': False, 'type': 'string', 'description': 'The sensor serial number to filter the returned data.'}, 'triggerMetric': {'required': False, 'type': 'string', 'description': 'Filter alerts triggered by a specific metric.'}}),\n", + " Tool(name='Events_3_FindEvents', description='Finds and lists cultural events, such as concerts and plays, that are scheduled to occur in a specified city.', arguments={'event_type': {'required': True, 'type': 'string', 'description': 'The category of the cultural event.'}, 'city': {'required': True, 'type': 'string', 'description': \"The name of the city where the event is happening, formatted as 'City, State' or 'City' if the city does not have a state. For example, 'New York, NY' or 'Paris'.\"}, 'date': {'required': False, 'type': 'string', 'description': \"The date of the event, formatted as 'YYYY-MM-DD'. If not specified, any date is considered.\"}}),\n", + " Tool(name='get_sensor_readings_latest', description=\"Retrieves the most recent readings for each metric from each sensor, organized by the sensor's serial number.\", arguments={'perPage': {'required': True, 'type': 'integer', 'description': 'Specifies the number of entries per page. Must be within the range of 3 to 100.'}, 'startingAfter': {'required': False, 'type': 'string', 'description': 'The server-generated token marking the start of the page. Typically a timestamp or ID. If omitted, the server will provide the default starting point.'}, 'endingBefore': {'required': False, 'type': 'string', 'description': 'The server-generated token marking the end of the page. Typically a timestamp or ID. Optional; if omitted, the server will provide the default ending point.'}, 'networkId': {'required': False, 'type': 'array', 'description': 'Filters the returned data by network IDs. If omitted, data for all networks is returned.'}, 'serials': {'required': False, 'type': 'array', 'description': 'Filters readings by sensor serial numbers. If omitted, readings for all sensors are returned.'}, 'metrics': {'required': False, 'type': 'array', 'description': \"Specifies the types of sensor readings to retrieve, such as 'temperature', 'humidity', or 'co2'. If omitted, all available types of readings are retrieved.\"}}),\n", + " Tool(name='Hotels_2_BookHouse', description=\"Book the selected house for given dates and the specified number of adults. The location must be in the format of 'City, State', such as 'New York, NY'.\", arguments={'where_to': {'required': True, 'type': 'string', 'description': \"The location of the house in the format of 'City, State', such as 'Berkeley, CA' or 'New York, NY'.\"}, 'number_of_adults': {'required': True, 'type': 'integer', 'description': 'The number of adults for the reservation. Must be a positive integer.'}, 'check_in_date': {'required': True, 'type': 'string', 'description': \"The start date for the reservation in the format 'YYYY-MM-DD'.\"}, 'check_out_date': {'required': True, 'type': 'string', 'description': \"The end date for the reservation in the format 'YYYY-MM-DD'.\"}}),\n", + " Tool(name='get_current_time', description='Retrieve the current time in a specific time zone.', arguments={'location': {'required': True, 'type': 'string', 'description': 'The name of the city.'}, 'country': {'required': True, 'type': 'string', 'description': 'The name of the country.'}, 'timezone': {'required': False, 'type': 'string', 'description': \"The optional timezone to get current time. Default ''\"}}),\n", + " Tool(name='get_stock_info', description=\"Retrieves information about a specific stock based on company's name.\", arguments={'company_name': {'required': True, 'type': 'string', 'description': 'The name of the company.'}, 'detail_level': {'required': True, 'type': 'string', 'description': \"Level of detail for stock information. Can be 'summary' or 'detailed'.\"}, 'market': {'required': False, 'type': 'string', 'description': \"The stock market of interest. Default is 'NASDAQ'\"}}),\n", + " Tool(name='get_instagram_story_clicks', description='Sums up the number of clicks from your last Instagram Story campaign.', arguments={'username': {'required': True, 'type': 'str', 'description': 'Your Instagram username'}, 'password': {'required': True, 'type': 'str', 'description': 'Your Instagram password'}}),\n", + " Tool(name='get_sensor_readings_history', description='Return all reported readings from sensors within a specified timespan, sorted by timestamp. This can include various types of sensor data across different networks and devices.', arguments={'perPage': {'required': False, 'type': 'integer', 'description': 'The number of entries per page returned. Must be within the range of 3 to 100.'}, 'startingAfter': {'required': False, 'type': 'string', 'description': \"A server-side token that represents the start of the page, such as a timestamp in the format 'YYYY-MM-DDTHH:MM:SSZ'. This should not be manually set by the client.\"}, 'endingBefore': {'required': False, 'type': 'string', 'description': \"A server-side token that represents the end of the page, such as a timestamp in the format 'YYYY-MM-DDTHH:MM:SSZ'. This should not be manually set by the client.\"}, 'networkId': {'required': False, 'type': 'array', 'description': 'The network IDs to filter the returned data. If omitted, data from all networks will be included.'}, 'serials': {'required': False, 'type': 'array', 'description': 'Sensor serial numbers to filter the readings by. If omitted, readings from all sensors will be included.'}, 'metrics': {'required': False, 'type': 'array', 'description': \"Types of sensor readings to retrieve, such as 'temperature', 'humidity', 'co2'. If omitted, all available types will be retrieved.\"}, 'timespan': {'required': True, 'type': 'integer', 'description': 'The duration for which sensor data will be fetched, specified in seconds. Maximum value corresponds to 730 days and 12 hours.'}, 't0': {'required': False, 'type': 'string', 'description': \"The start of the timespan for the data, in the format 'YYYY-MM-DDTHH:MM:SSZ'. Maximum lookback is 365 days and 6 hours from today.\"}, 't1': {'required': False, 'type': 'string', 'description': \"The end of the timespan for the data, in the format 'YYYY-MM-DDTHH:MM:SSZ'. 't1' can be at most 7 days after 't0'.\"}}),\n", + " Tool(name='Buses_3_FindBus', description='Search for a bus itinerary between two specified cities on a given date.', arguments={'from_city': {'required': True, 'type': 'string', 'description': \"The city to depart from, in the format of 'City, State' (e.g., 'Los Angeles, CA').\"}, 'to_city': {'required': True, 'type': 'string', 'description': \"The destination city of the trip, in the format of 'City, State' (e.g., 'New York, NY').\"}, 'departure_date': {'required': True, 'type': 'string', 'description': \"The date of departure, in the format 'YYYY-MM-DD' (e.g., '2023-04-15').\"}, 'num_passengers': {'required': False, 'type': 'integer', 'description': 'The number of tickets required for the trip.'}, 'category': {'required': False, 'type': 'string', 'description': 'The type of bus route based on the number of stops.'}}),\n", + " Tool(name='Hotels_2_SearchHouse', description='Search for available houses for rent at a specified location with optional filters such as laundry service availability, number of adults, and rating.', arguments={'where_to': {'required': True, 'type': 'string', 'description': \"The location of the house to search for, in the format of 'City, State' or 'City, Country'. For example, 'San Francisco, CA' or 'Paris, France'.\"}, 'has_laundry_service': {'required': False, 'type': 'string', 'description': 'Flag indicating if the house should include laundry service.'}, 'number_of_adults': {'required': False, 'type': 'integer', 'description': \"The number of adults for the reservation. Select 'dontcare' if no specific number is required.\"}, 'rating': {'required': False, 'type': 'float', 'description': \"The minimum review rating of the house on a scale from 1.0 to 5.0, with higher numbers indicating better ratings. Select 'dontcare' to include all ratings.\"}})]" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "task.synapse.tools" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## (A) Top miner responses\n", + "- This section should demonstrate the quality of the communication between miners and validators coming from the top miner.\n", + "\n", + "> - (1) Define a group of top miner.\n", + ">\n", + "> - (2) Define a forward function. \n", + "> - (3) Call the forward function for the top miners. \n", + "> - (4) Show the responses from the miners." + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": { + "scrolled": true + }, + "outputs": [ + { + "data": { + "text/markdown": [ + "### Tool Call Task - Input:\n", + "#### Messages:\n", + "[ChatMessage(role=, content='Obtain the detailed stock information for either \"Apple Inc.\" or \"Apple\" in the NASDAQ market.')]\n", + "\n", + "#### Available Tools:\n", + "['get_sensor_readings_history_by_interval', 'Buses_3_BuyBusTicket', 'Events_3_BuyEventTickets', 'get_sensor_alerts', 'Events_3_FindEvents', 'get_sensor_readings_latest', 'Hotels_2_BookHouse', 'get_current_time', 'get_stock_info', 'get_instagram_story_clicks', 'get_sensor_readings_history', 'Buses_3_FindBus', 'Hotels_2_SearchHouse']\n", + "\n", + "#### Top 5 Miner UIDs for Subnet 20: [103 157 171 129 220]" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "### **Response from 103:** \n", + " get_stock_info(company_name='Apple Inc.', detail_level='detailed', market='NASDAQ')" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
Does not error\n",
+       "✅ You successfully responded to the request.\n",
+       "You received 0.25 of 0.25 reward.\n",
+       "Does not take a long time\n",
+       "✅ You responded to the request in 2.8488552570343018.\n",
+       "You received 0.5 of 0.5 reward.\n",
+       "Return correct function format\n",
+       "✅ Your response was in the correct format.\n",
+       "You received 1.0 of 1.0 reward.\n",
+       "Return correct function name\n",
+       "✅ Your function name matches the expected function name.\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "Return function with correct argument names\n",
+       "✅ Your function has the required argument: company_name\n",
+       "✅ Your function has the required argument: detail_level\n",
+       "✅ Your function has the required argument: market\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "Return function with correct argument values\n",
+       "✅ Your function has the required value for argument: company_name\n",
+       "✅ Your function has the required value for argument: detail_level\n",
+       "✅ Your function has the required value for argument: market\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;37mDoes not error\u001b[0m\n", + "✅ \u001b[32mYou successfully responded to the request.\u001b[0m\n", + "You received \u001b[1;36m0.25\u001b[0m of \u001b[1;36m0.25\u001b[0m reward.\n", + "\u001b[1;37mDoes not take a long time\u001b[0m\n", + "✅ \u001b[32mYou responded to the request in \u001b[0m\u001b[1;32m2.8488552570343018\u001b[0m\u001b[32m.\u001b[0m\n", + "You received \u001b[1;36m0.5\u001b[0m of \u001b[1;36m0.5\u001b[0m reward.\n", + "\u001b[1;37mReturn correct function format\u001b[0m\n", + "✅ \u001b[32mYour response was in the correct format.\u001b[0m\n", + "You received \u001b[1;36m1.0\u001b[0m of \u001b[1;36m1.0\u001b[0m reward.\n", + "\u001b[1;37mReturn correct function name\u001b[0m\n", + "✅ \u001b[32mYour function name matches the expected function name.\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n", + "\u001b[1;37mReturn function with correct argument names\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: company_name\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: detail_level\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: market\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n", + "\u001b[1;37mReturn function with correct argument values\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: company_name\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: detail_level\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: market\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "### **Response from 157:** \n", + " get_stock_info(company_name='Apple Inc.', detail_level='detailed', market='NASDAQ')" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
Does not error\n",
+       "✅ You successfully responded to the request.\n",
+       "You received 0.25 of 0.25 reward.\n",
+       "Does not take a long time\n",
+       "✅ You responded to the request in 3.6806817054748535.\n",
+       "You received 0.5 of 0.5 reward.\n",
+       "Return correct function format\n",
+       "✅ Your response was in the correct format.\n",
+       "You received 1.0 of 1.0 reward.\n",
+       "Return correct function name\n",
+       "✅ Your function name matches the expected function name.\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "Return function with correct argument names\n",
+       "✅ Your function has the required argument: company_name\n",
+       "✅ Your function has the required argument: detail_level\n",
+       "✅ Your function has the required argument: market\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "Return function with correct argument values\n",
+       "✅ Your function has the required value for argument: company_name\n",
+       "✅ Your function has the required value for argument: detail_level\n",
+       "✅ Your function has the required value for argument: market\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;37mDoes not error\u001b[0m\n", + "✅ \u001b[32mYou successfully responded to the request.\u001b[0m\n", + "You received \u001b[1;36m0.25\u001b[0m of \u001b[1;36m0.25\u001b[0m reward.\n", + "\u001b[1;37mDoes not take a long time\u001b[0m\n", + "✅ \u001b[32mYou responded to the request in \u001b[0m\u001b[1;32m3.6806817054748535\u001b[0m\u001b[32m.\u001b[0m\n", + "You received \u001b[1;36m0.5\u001b[0m of \u001b[1;36m0.5\u001b[0m reward.\n", + "\u001b[1;37mReturn correct function format\u001b[0m\n", + "✅ \u001b[32mYour response was in the correct format.\u001b[0m\n", + "You received \u001b[1;36m1.0\u001b[0m of \u001b[1;36m1.0\u001b[0m reward.\n", + "\u001b[1;37mReturn correct function name\u001b[0m\n", + "✅ \u001b[32mYour function name matches the expected function name.\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n", + "\u001b[1;37mReturn function with correct argument names\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: company_name\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: detail_level\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: market\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n", + "\u001b[1;37mReturn function with correct argument values\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: company_name\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: detail_level\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: market\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "### **Response from 171:** \n", + " get_stock_info(company_name='Apple Inc.', detail_level='detailed', market='NASDAQ')" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
Does not error\n",
+       "✅ You successfully responded to the request.\n",
+       "You received 0.25 of 0.25 reward.\n",
+       "Does not take a long time\n",
+       "✅ You responded to the request in 3.20733380317688.\n",
+       "You received 0.5 of 0.5 reward.\n",
+       "Return correct function format\n",
+       "✅ Your response was in the correct format.\n",
+       "You received 1.0 of 1.0 reward.\n",
+       "Return correct function name\n",
+       "✅ Your function name matches the expected function name.\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "Return function with correct argument names\n",
+       "✅ Your function has the required argument: company_name\n",
+       "✅ Your function has the required argument: detail_level\n",
+       "✅ Your function has the required argument: market\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "Return function with correct argument values\n",
+       "✅ Your function has the required value for argument: company_name\n",
+       "✅ Your function has the required value for argument: detail_level\n",
+       "✅ Your function has the required value for argument: market\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;37mDoes not error\u001b[0m\n", + "✅ \u001b[32mYou successfully responded to the request.\u001b[0m\n", + "You received \u001b[1;36m0.25\u001b[0m of \u001b[1;36m0.25\u001b[0m reward.\n", + "\u001b[1;37mDoes not take a long time\u001b[0m\n", + "✅ \u001b[32mYou responded to the request in \u001b[0m\u001b[1;32m3.20733380317688\u001b[0m\u001b[32m.\u001b[0m\n", + "You received \u001b[1;36m0.5\u001b[0m of \u001b[1;36m0.5\u001b[0m reward.\n", + "\u001b[1;37mReturn correct function format\u001b[0m\n", + "✅ \u001b[32mYour response was in the correct format.\u001b[0m\n", + "You received \u001b[1;36m1.0\u001b[0m of \u001b[1;36m1.0\u001b[0m reward.\n", + "\u001b[1;37mReturn correct function name\u001b[0m\n", + "✅ \u001b[32mYour function name matches the expected function name.\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n", + "\u001b[1;37mReturn function with correct argument names\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: company_name\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: detail_level\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: market\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n", + "\u001b[1;37mReturn function with correct argument values\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: company_name\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: detail_level\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: market\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "### **Response from 129:** \n", + " get_stock_info(company_name='Apple Inc.', detail_level='detailed', market='NASDAQ')" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
Does not error\n",
+       "✅ You successfully responded to the request.\n",
+       "You received 0.25 of 0.25 reward.\n",
+       "Does not take a long time\n",
+       "✅ You responded to the request in 2.5092313289642334.\n",
+       "You received 0.5 of 0.5 reward.\n",
+       "Return correct function format\n",
+       "✅ Your response was in the correct format.\n",
+       "You received 1.0 of 1.0 reward.\n",
+       "Return correct function name\n",
+       "✅ Your function name matches the expected function name.\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "Return function with correct argument names\n",
+       "✅ Your function has the required argument: company_name\n",
+       "✅ Your function has the required argument: detail_level\n",
+       "✅ Your function has the required argument: market\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "Return function with correct argument values\n",
+       "✅ Your function has the required value for argument: company_name\n",
+       "✅ Your function has the required value for argument: detail_level\n",
+       "✅ Your function has the required value for argument: market\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;37mDoes not error\u001b[0m\n", + "✅ \u001b[32mYou successfully responded to the request.\u001b[0m\n", + "You received \u001b[1;36m0.25\u001b[0m of \u001b[1;36m0.25\u001b[0m reward.\n", + "\u001b[1;37mDoes not take a long time\u001b[0m\n", + "✅ \u001b[32mYou responded to the request in \u001b[0m\u001b[1;32m2.5092313289642334\u001b[0m\u001b[32m.\u001b[0m\n", + "You received \u001b[1;36m0.5\u001b[0m of \u001b[1;36m0.5\u001b[0m reward.\n", + "\u001b[1;37mReturn correct function format\u001b[0m\n", + "✅ \u001b[32mYour response was in the correct format.\u001b[0m\n", + "You received \u001b[1;36m1.0\u001b[0m of \u001b[1;36m1.0\u001b[0m reward.\n", + "\u001b[1;37mReturn correct function name\u001b[0m\n", + "✅ \u001b[32mYour function name matches the expected function name.\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n", + "\u001b[1;37mReturn function with correct argument names\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: company_name\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: detail_level\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: market\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n", + "\u001b[1;37mReturn function with correct argument values\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: company_name\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: detail_level\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: market\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "### **Response from 220:** \n", + " get_stock_info(company_name='Apple Inc.', detail_level='detailed', market='NASDAQ')" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
Does not error\n",
+       "✅ You successfully responded to the request.\n",
+       "You received 0.25 of 0.25 reward.\n",
+       "Does not take a long time\n",
+       "✅ You responded to the request in 3.1075806617736816.\n",
+       "You received 0.5 of 0.5 reward.\n",
+       "Return correct function format\n",
+       "✅ Your response was in the correct format.\n",
+       "You received 1.0 of 1.0 reward.\n",
+       "Return correct function name\n",
+       "✅ Your function name matches the expected function name.\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "Return function with correct argument names\n",
+       "✅ Your function has the required argument: company_name\n",
+       "✅ Your function has the required argument: detail_level\n",
+       "✅ Your function has the required argument: market\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "Return function with correct argument values\n",
+       "✅ Your function has the required value for argument: company_name\n",
+       "✅ Your function has the required value for argument: detail_level\n",
+       "✅ Your function has the required value for argument: market\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;37mDoes not error\u001b[0m\n", + "✅ \u001b[32mYou successfully responded to the request.\u001b[0m\n", + "You received \u001b[1;36m0.25\u001b[0m of \u001b[1;36m0.25\u001b[0m reward.\n", + "\u001b[1;37mDoes not take a long time\u001b[0m\n", + "✅ \u001b[32mYou responded to the request in \u001b[0m\u001b[1;32m3.1075806617736816\u001b[0m\u001b[32m.\u001b[0m\n", + "You received \u001b[1;36m0.5\u001b[0m of \u001b[1;36m0.5\u001b[0m reward.\n", + "\u001b[1;37mReturn correct function format\u001b[0m\n", + "✅ \u001b[32mYour response was in the correct format.\u001b[0m\n", + "You received \u001b[1;36m1.0\u001b[0m of \u001b[1;36m1.0\u001b[0m reward.\n", + "\u001b[1;37mReturn correct function name\u001b[0m\n", + "✅ \u001b[32mYour function name matches the expected function name.\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n", + "\u001b[1;37mReturn function with correct argument names\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: company_name\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: detail_level\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: market\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n", + "\u001b[1;37mReturn function with correct argument values\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: company_name\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: detail_level\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: market\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "top_miner_uids = (-vali.metagraph.I).argsort()[:5]\n", + "\n", + "def forward(uids):\n", + " responses = vali.dendrite.query(\n", + " axons=[vali.metagraph.axons[uid] for uid in uids],\n", + " synapse=task.synapse,\n", + " deserialize=False,\n", + " timeout=10*task.timeout,\n", + " )\n", + " return responses\n", + " \n", + "display_markdown(f'''### Tool Call Task - Input:\n", + "#### Messages:\n", + "{task.synapse.messages}\n", + "\n", + "#### Available Tools:\n", + "{[t.name for t in task.synapse.tools]}\n", + "\n", + "#### Top 5 Miner UIDs for Subnet 20: {top_miner_uids}''', raw=True)\n", + "\n", + "results = forward(top_miner_uids)\n", + "for i,result in enumerate(results):\n", + " try:\n", + " response = result.response\n", + " display_markdown(f'''### **Response from {top_miner_uids[i]}:** \n", + " {response}''', raw=True)\n", + " feedbacks = []\n", + " for crit in task.criteria:\n", + " score, max_score, feedback = crit.evaluate(task, vali, result)\n", + " feedbacks.append(feedback)\n", + " rprint((\"\\n\").join(feedbacks).replace(\"bold blue\", \"bold white\"))\n", + " except Exception as e:\n", + " print(result.dendrite.status_code, \" \", result.dendrite.process_time, \" \", e)\n", + " print(f\"Miner {top_miner_uids[i]} did not respond correctly\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## (B) Justification for incentive distribution \n", + "- Justify the difference in incentive for miners in different incentive tiers (eg. sample 5 miners from quantile 1 VS 5 miners from quantile 3) with code.\n", + "- If there is no significant difference in the incentive distribution, you can also show that miners in the SN have about the same performance in multiple ways.\n", + "\n", + "- There could be many reasons for the difference in incentive for miners. \n", + " - Case 1: Difference in the quality of response\n", + " - Show that miners with higher incentive generally give better answer than those with lower incentive through the following ways\n", + " - lower loss; higher accuracy\n", + " - human eval for text/ image/ audio quality \n", + "\n", + " - Case 2: Difference in miner avalibility \n", + " - Show that given a certain number of trials(100), there are more successful calls to higher incentive miners.\n", + " \n", + " - Case 3: Difference in latency.\n", + " - Show that miners in Q1 generally respond faster than miners in Q3.\n", + "\n", + " - Case 4: Please provide your own justification if the reasons above dosen't fit.\n", + "\n", + "> (1) Define the group of high incentive miner VS low incentive miner (you can have ~5 samples from each group, but please feel to make your own definition of high/low incentive miners)\n", + ">\n", + "> (2) Make the forward call to the group of high/low incentive miners \n", + ">\n", + "> (3) Show the difference in the quality of the high/low incentive miners \n" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [ + { + "data": { + "text/markdown": [ + "### Tool Call Task - Input:\n", + "#### Messages:\n", + "[ChatMessage(role=, content='Obtain the detailed stock information for either \"Apple Inc.\" or \"Apple\" in the NASDAQ market.')]\n", + "\n", + "#### Available Tools:\n", + "['get_sensor_readings_history_by_interval', 'Buses_3_BuyBusTicket', 'Events_3_BuyEventTickets', 'get_sensor_alerts', 'Events_3_FindEvents', 'get_sensor_readings_latest', 'Hotels_2_BookHouse', 'get_current_time', 'get_stock_info', 'get_instagram_story_clicks', 'get_sensor_readings_history', 'Buses_3_FindBus', 'Hotels_2_SearchHouse']\n", + "\n", + "#### Bottom 5 Miner UIDs for Subnet 20: [134 111 205 95 99]" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "### **Response from 134:** \n", + " " + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
Does not error\n",
+       "❌ You failed to respond correctly to the request. Status Code: None/503\n",
+       "You received 0.0 of 0.25 reward.\n",
+       "Does not take a long time\n",
+       "❌ You likely ran into an error processing this task and failed to respond appropriately.\n",
+       "You received 0 of 0.5 reward.\n",
+       "Return correct function format\n",
+       "✅ Your response was in the correct format.\n",
+       "You received 1.0 of 1.0 reward.\n",
+       "Return correct function name\n",
+       "❌ Your function name does not match the expected function name.\n",
+       "You received -0.5 of 3.0 reward.\n",
+       "Return function with correct argument names\n",
+       "❌ Your function is missing the required argument: company_name\n",
+       "❌ Your function is missing the required argument: detail_level\n",
+       "❌ Your function is missing the required argument: market\n",
+       "You received -3.0 of 3.0 reward.\n",
+       "Return function with correct argument values\n",
+       "❌ Your function is missing the required argument: company_name\n",
+       "❌ Your function is missing the required argument: detail_level\n",
+       "❌ Your function is missing the required argument: market\n",
+       "You received -3.0 of 3.0 reward.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;37mDoes not error\u001b[0m\n", + "❌ \u001b[31mYou failed to respond correctly to the request.\u001b[0m Status Code: \u001b[3;35mNone\u001b[0m/\u001b[1;36m503\u001b[0m\n", + "You received \u001b[1;36m0.0\u001b[0m of \u001b[1;36m0.25\u001b[0m reward.\n", + "\u001b[1;37mDoes not take a long time\u001b[0m\n", + "❌ \u001b[31mYou likely ran into an error processing this task and failed to respond appropriately.\u001b[0m\n", + "You received \u001b[1;36m0\u001b[0m of \u001b[1;36m0.5\u001b[0m reward.\n", + "\u001b[1;37mReturn correct function format\u001b[0m\n", + "✅ \u001b[32mYour response was in the correct format.\u001b[0m\n", + "You received \u001b[1;36m1.0\u001b[0m of \u001b[1;36m1.0\u001b[0m reward.\n", + "\u001b[1;37mReturn correct function name\u001b[0m\n", + "❌ \u001b[31mYour function name does not match the expected function name.\u001b[0m\n", + "You received \u001b[1;36m-0.5\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n", + "\u001b[1;37mReturn function with correct argument names\u001b[0m\n", + "❌ \u001b[31mYour function is missing the required argument: company_name\u001b[0m\n", + "❌ \u001b[31mYour function is missing the required argument: detail_level\u001b[0m\n", + "❌ \u001b[31mYour function is missing the required argument: market\u001b[0m\n", + "You received \u001b[1;36m-3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n", + "\u001b[1;37mReturn function with correct argument values\u001b[0m\n", + "❌ \u001b[31mYour function is missing the required argument: company_name\u001b[0m\n", + "❌ \u001b[31mYour function is missing the required argument: detail_level\u001b[0m\n", + "❌ \u001b[31mYour function is missing the required argument: market\u001b[0m\n", + "You received \u001b[1;36m-3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "### **Response from 111:** \n", + " get_stock_info(company_name='Apple Inc.', detail_level='detailed', market='NASDAQ')" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
Does not error\n",
+       "✅ You successfully responded to the request.\n",
+       "You received 0.25 of 0.25 reward.\n",
+       "Does not take a long time\n",
+       "✅ You responded to the request in 1.7583441734313965.\n",
+       "You received 0.5 of 0.5 reward.\n",
+       "Return correct function format\n",
+       "✅ Your response was in the correct format.\n",
+       "You received 1.0 of 1.0 reward.\n",
+       "Return correct function name\n",
+       "✅ Your function name matches the expected function name.\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "Return function with correct argument names\n",
+       "✅ Your function has the required argument: company_name\n",
+       "✅ Your function has the required argument: detail_level\n",
+       "✅ Your function has the required argument: market\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "Return function with correct argument values\n",
+       "✅ Your function has the required value for argument: company_name\n",
+       "✅ Your function has the required value for argument: detail_level\n",
+       "✅ Your function has the required value for argument: market\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;37mDoes not error\u001b[0m\n", + "✅ \u001b[32mYou successfully responded to the request.\u001b[0m\n", + "You received \u001b[1;36m0.25\u001b[0m of \u001b[1;36m0.25\u001b[0m reward.\n", + "\u001b[1;37mDoes not take a long time\u001b[0m\n", + "✅ \u001b[32mYou responded to the request in \u001b[0m\u001b[1;32m1.7583441734313965\u001b[0m\u001b[32m.\u001b[0m\n", + "You received \u001b[1;36m0.5\u001b[0m of \u001b[1;36m0.5\u001b[0m reward.\n", + "\u001b[1;37mReturn correct function format\u001b[0m\n", + "✅ \u001b[32mYour response was in the correct format.\u001b[0m\n", + "You received \u001b[1;36m1.0\u001b[0m of \u001b[1;36m1.0\u001b[0m reward.\n", + "\u001b[1;37mReturn correct function name\u001b[0m\n", + "✅ \u001b[32mYour function name matches the expected function name.\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n", + "\u001b[1;37mReturn function with correct argument names\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: company_name\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: detail_level\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: market\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n", + "\u001b[1;37mReturn function with correct argument values\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: company_name\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: detail_level\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: market\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "### **Response from 205:** \n", + " get_stock_info(company_name='Apple Inc.', detail_level='detailed', market='NASDAQ')" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
Does not error\n",
+       "✅ You successfully responded to the request.\n",
+       "You received 0.25 of 0.25 reward.\n",
+       "Does not take a long time\n",
+       "✅ You responded to the request in 1.1692581176757812.\n",
+       "You received 0.5 of 0.5 reward.\n",
+       "Return correct function format\n",
+       "✅ Your response was in the correct format.\n",
+       "You received 1.0 of 1.0 reward.\n",
+       "Return correct function name\n",
+       "✅ Your function name matches the expected function name.\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "Return function with correct argument names\n",
+       "✅ Your function has the required argument: company_name\n",
+       "✅ Your function has the required argument: detail_level\n",
+       "✅ Your function has the required argument: market\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "Return function with correct argument values\n",
+       "✅ Your function has the required value for argument: company_name\n",
+       "✅ Your function has the required value for argument: detail_level\n",
+       "✅ Your function has the required value for argument: market\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;37mDoes not error\u001b[0m\n", + "✅ \u001b[32mYou successfully responded to the request.\u001b[0m\n", + "You received \u001b[1;36m0.25\u001b[0m of \u001b[1;36m0.25\u001b[0m reward.\n", + "\u001b[1;37mDoes not take a long time\u001b[0m\n", + "✅ \u001b[32mYou responded to the request in \u001b[0m\u001b[1;32m1.1692581176757812\u001b[0m\u001b[32m.\u001b[0m\n", + "You received \u001b[1;36m0.5\u001b[0m of \u001b[1;36m0.5\u001b[0m reward.\n", + "\u001b[1;37mReturn correct function format\u001b[0m\n", + "✅ \u001b[32mYour response was in the correct format.\u001b[0m\n", + "You received \u001b[1;36m1.0\u001b[0m of \u001b[1;36m1.0\u001b[0m reward.\n", + "\u001b[1;37mReturn correct function name\u001b[0m\n", + "✅ \u001b[32mYour function name matches the expected function name.\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n", + "\u001b[1;37mReturn function with correct argument names\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: company_name\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: detail_level\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: market\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n", + "\u001b[1;37mReturn function with correct argument values\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: company_name\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: detail_level\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: market\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "### **Response from 95:** \n", + " get_stock_info(company_name='Apple Inc.', detail_level='detailed', market='NASDAQ')" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
Does not error\n",
+       "✅ You successfully responded to the request.\n",
+       "You received 0.25 of 0.25 reward.\n",
+       "Does not take a long time\n",
+       "✅ You responded to the request in 1.7327539920806885.\n",
+       "You received 0.5 of 0.5 reward.\n",
+       "Return correct function format\n",
+       "✅ Your response was in the correct format.\n",
+       "You received 1.0 of 1.0 reward.\n",
+       "Return correct function name\n",
+       "✅ Your function name matches the expected function name.\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "Return function with correct argument names\n",
+       "✅ Your function has the required argument: company_name\n",
+       "✅ Your function has the required argument: detail_level\n",
+       "✅ Your function has the required argument: market\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "Return function with correct argument values\n",
+       "✅ Your function has the required value for argument: company_name\n",
+       "✅ Your function has the required value for argument: detail_level\n",
+       "✅ Your function has the required value for argument: market\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;37mDoes not error\u001b[0m\n", + "✅ \u001b[32mYou successfully responded to the request.\u001b[0m\n", + "You received \u001b[1;36m0.25\u001b[0m of \u001b[1;36m0.25\u001b[0m reward.\n", + "\u001b[1;37mDoes not take a long time\u001b[0m\n", + "✅ \u001b[32mYou responded to the request in \u001b[0m\u001b[1;32m1.7327539920806885\u001b[0m\u001b[32m.\u001b[0m\n", + "You received \u001b[1;36m0.5\u001b[0m of \u001b[1;36m0.5\u001b[0m reward.\n", + "\u001b[1;37mReturn correct function format\u001b[0m\n", + "✅ \u001b[32mYour response was in the correct format.\u001b[0m\n", + "You received \u001b[1;36m1.0\u001b[0m of \u001b[1;36m1.0\u001b[0m reward.\n", + "\u001b[1;37mReturn correct function name\u001b[0m\n", + "✅ \u001b[32mYour function name matches the expected function name.\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n", + "\u001b[1;37mReturn function with correct argument names\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: company_name\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: detail_level\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: market\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n", + "\u001b[1;37mReturn function with correct argument values\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: company_name\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: detail_level\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: market\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/markdown": [ + "### **Response from 99:** \n", + " get_stock_info(company_name='Apple Inc.', detail_level='detailed', market='NASDAQ')" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "text/html": [ + "
Does not error\n",
+       "✅ You successfully responded to the request.\n",
+       "You received 0.25 of 0.25 reward.\n",
+       "Does not take a long time\n",
+       "✅ You responded to the request in 1.7754108905792236.\n",
+       "You received 0.5 of 0.5 reward.\n",
+       "Return correct function format\n",
+       "✅ Your response was in the correct format.\n",
+       "You received 1.0 of 1.0 reward.\n",
+       "Return correct function name\n",
+       "✅ Your function name matches the expected function name.\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "Return function with correct argument names\n",
+       "✅ Your function has the required argument: company_name\n",
+       "✅ Your function has the required argument: detail_level\n",
+       "✅ Your function has the required argument: market\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "Return function with correct argument values\n",
+       "✅ Your function has the required value for argument: company_name\n",
+       "✅ Your function has the required value for argument: detail_level\n",
+       "✅ Your function has the required value for argument: market\n",
+       "You received 3.0 of 3.0 reward.\n",
+       "
\n" + ], + "text/plain": [ + "\u001b[1;37mDoes not error\u001b[0m\n", + "✅ \u001b[32mYou successfully responded to the request.\u001b[0m\n", + "You received \u001b[1;36m0.25\u001b[0m of \u001b[1;36m0.25\u001b[0m reward.\n", + "\u001b[1;37mDoes not take a long time\u001b[0m\n", + "✅ \u001b[32mYou responded to the request in \u001b[0m\u001b[1;32m1.7754108905792236\u001b[0m\u001b[32m.\u001b[0m\n", + "You received \u001b[1;36m0.5\u001b[0m of \u001b[1;36m0.5\u001b[0m reward.\n", + "\u001b[1;37mReturn correct function format\u001b[0m\n", + "✅ \u001b[32mYour response was in the correct format.\u001b[0m\n", + "You received \u001b[1;36m1.0\u001b[0m of \u001b[1;36m1.0\u001b[0m reward.\n", + "\u001b[1;37mReturn correct function name\u001b[0m\n", + "✅ \u001b[32mYour function name matches the expected function name.\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n", + "\u001b[1;37mReturn function with correct argument names\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: company_name\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: detail_level\u001b[0m\n", + "✅ \u001b[32mYour function has the required argument: market\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n", + "\u001b[1;37mReturn function with correct argument values\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: company_name\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: detail_level\u001b[0m\n", + "✅ \u001b[32mYour function has the required value for argument: market\u001b[0m\n", + "You received \u001b[1;36m3.0\u001b[0m of \u001b[1;36m3.0\u001b[0m reward.\n" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# we have our top miners, here are our lower end miners\n", + "validator_uids = vali.metagraph.uids[vali.metagraph.stake > 20000]\n", + "bottom_miner_uids = vali.metagraph.I.argsort()[len(validator_uids):len(validator_uids)+5] # remove the validators\n", + "display_markdown(f'''### Tool Call Task - Input:\n", + "#### Messages:\n", + "{task.synapse.messages}\n", + "\n", + "#### Available Tools:\n", + "{[t.name for t in task.synapse.tools]}\n", + "\n", + "#### Bottom 5 Miner UIDs for Subnet 20: {bottom_miner_uids}''', raw=True)\n", + "results = forward(bottom_miner_uids)\n", + "for i,result in enumerate(results):\n", + " try:\n", + " response = result.response\n", + " display_markdown(f'''### **Response from {bottom_miner_uids[i]}:** \n", + " {response}''', raw=True)\n", + " feedbacks = []\n", + " for crit in task.criteria:\n", + " score, max_score, feedback = crit.evaluate(task, vali, result)\n", + " feedbacks.append(feedback)\n", + " rprint((\"\\n\").join(feedbacks).replace(\"bold blue\", \"bold white\"))\n", + " except Exception as e:\n", + " print(result.dendrite.status_code, \" \", result.dendrite.process_time, \" \", e)\n", + " print(f\"Miner {bottom_miner_uids[i]} did not respond correctly\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## (C) (If applicable) Miner landscape\n", + "- How many unique responses can we get from the network and how many miners are giving the same responses. It is perfectly fine even if all of the miners respond the same thing.\n", + "> (1) Send the same request to all miners over the SN\n", + ">\n", + "> (2) Check the number of unique responses \n", + "> \n", + "> (3) Check the number of miners giving the same response\n", + " " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ## (D) (If applicable) Demonstrate the effectiveness of the scoring mechanism.\n", + "- If you are using a reward/penalty model: \n", + " - Please load the reward or penalty model one by one and then show that the reward of a good response > the reward of a bad response\n", + " - Please allow us to customise the input of the reward model\n", + "\n", + " > (1) Load the reward/penalty model one by one \n", + " >\n", + " > (2) Define the good/bad response\n", + " >\n", + " > (3) Score the response with the model\n", + "\n", + "- Otherwise, you may just give a brief explanation to how does your scoring mechanism works.\n", + "- Please show the distribution of the final reward and each sub-reward.\n", + "- Please link us to the original validator code where appropriate." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "\n", + " ## (E) (If applicable) Show the dataset that was used by the validator.\n", + " > (1) Load the dataset \n", + " > \n", + " > (2) Show the first 10 samples of the dataset \n", + "- Please link us to the original validator code where appropriate" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + " ## (F) (If applicable) Demonstrate the use of any API and/or links to a frontend.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "URL to app: https://gogoagent.ai \\\n", + "API Endpoint: https://api.gogoagent.ai \\\n", + "More documentation here - https://docs.google.com/document/d/1QVCzDu0eMmkdglD65F_Q_UjnCJauVEr62WgG8SgACt0\n", + "\n", + "### Using the API endpoint" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Chat Response: [tip_calculator(bill_amount=100, tip_percent=10)]\n" + ] + } + ], + "source": [ + "import openai\n", + "\n", + "MODEL_API = \"https://api.gogoagent.ai\"\n", + "MODEL_NAME = \"BitAgent/GoGoAgent\"\n", + "\n", + "# Initialize the OpenAI client\n", + "client = openai.OpenAI(\n", + " api_key= # TODO, put your API key here\n", + " base_url=MODEL_API\n", + ")\n", + "\n", + "def tip_calculator(bill_amount, tip_percent):\n", + " return bill_amount * tip_percent/100.\n", + "\n", + "def another_calculator_for_summation(num1, num2):\n", + " return num1 + num2\n", + "\n", + "# Pose your user query\n", + "messages = [{\"role\": \"user\",\n", + " \"content\": \"Need help calculating the tip, what is 10% tip on a bill totalling $100\"}]\n", + "\n", + "# Define the tools (see methods above)\n", + "tools = [{\"name\": \"tip_calculator\", \"description\": \"Calculate the tip amount\",\n", + " \"arguments\": {\"bill_amount\": {\"required\": True, \"type\": \"number\",\n", + " \"description\": \"the bill amount in dollars\"},\n", + " \"tip_percent\": {\"required\": True, \"type\": \"number\",\n", + " \"description\": \"the tip percentage as a whole number\"},\n", + " }\n", + " },\n", + " {\"name\": \"another_calculator_for_summation\", \"description\": \"Calculate the sum of two numbers\",\n", + " \"arguments\": {\"num1\": {\"required\": True, \"type\": \"number\",\n", + " \"description\": \"the first number for summation\"},\n", + " \"num2\": {\"required\": True, \"type\": \"number\",\n", + " \"description\": \"the second number for summation\"},\n", + " }\n", + " }]\n", + "\n", + "chat_response = client.chat.completions.create(\n", + " model=MODEL_NAME,\n", + " tools=tools,\n", + " messages=messages,\n", + " )\n", + "\n", + "message = chat_response.choices[0].message.content\n", + "print(f\"Chat Response: {message}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Notice\n", + "Since the context was tip related, the tip calculator was passed back, and the summation calculator was not recommended for use.\n", + "\n", + "From here, we can now call this method on our end - we have the Tip Calculator." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Consent: Do you want this demo notebook to be public? Yes/No " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Yes" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.12" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/bitagent_subnet-main/docs/running_on_mainnet.md b/bitagent_subnet-main/docs/running_on_mainnet.md new file mode 100644 index 0000000000000000000000000000000000000000..bf8842da217db0f70f5b795eb311031deeece4dd --- /dev/null +++ b/bitagent_subnet-main/docs/running_on_mainnet.md @@ -0,0 +1,244 @@ +# Running Subnet on Mainnet + +This tutorial shows how to use the bittensor `btcli` to create a subnetwork and connect your incentive mechanism to it. + +**IMPORTANT:** Before attempting to register on mainnet, we strongly recommend that you: +- First run [Running Subnet Locally](running_on_staging.md), and +- Then run [Running on the Testnet](running_on_testnet.md). + +Your incentive mechanisms running on the mainnet are open to anyone. They emit real TAO. Creating these mechanisms incur a `lock_cost` in TAO. + +**DANGER** +- Do not expose your private keys. +- Only use your testnet wallet. +- Do not reuse the password of your mainnet wallet. +- Make sure your incentive mechanism is resistant to abuse. + +## Prerequisites + +Before proceeding further, make sure that you have installed Bittensor. See the below instructions: + +- [Install `bittensor`](https://github.com/opentensor/bittensor#install). + +After installing `bittensor`, proceed as below: + +## Steps + +## 1. Install your subnet template + +**NOTE: Skip this step if** you already did this during local testing and development. + +In your project directory: + +```bash +git clone https://github.com/opentensor/bittensor-subnet-template.git +``` + +Next, `cd` into `bittensor-subnet-template` repo directory: + +```bash +cd bittensor-subnet-template +``` + +Install the Bittensor subnet template package: + +```bash +python -m pip install -e . # Install your subnet template package +``` + +## 2. Create wallets + +Create wallets for subnet owner, subnet validator and for subnet miner. + +This step creates local coldkey and hotkey pairs for your three identities: subnet owner, subnet validator and subnet miner. + +The owner will create and control the subnet. The owner must have at least 100 TAO before the owner can run next steps. + +The validator and miner will be registered to the subnet created by the owner. This ensures that the validator and miner can run the respective validator and miner scripts. + +**NOTE**: You can also use existing wallets to register. Creating new keys is shown here for reference. + +Create a coldkey for the owner wallet: + +```bash +btcli wallet new_coldkey --wallet.name owner +``` + +Create a coldkey and hotkey for the subnet miner wallet: +```bash +btcli wallet new_coldkey --wallet.name miner +``` + +and + +```bash +btcli wallet new_hotkey --wallet.name miner --wallet.hotkey default +``` + +Create a coldkey and hotkey for the subnet validator wallet: + +```bash +btcli wallet new_coldkey --wallet.name validator +``` + +and + +```bash +btcli wallet new_hotkey --wallet.name validator --wallet.hotkey default +``` + +## 3. Getting the price of subnet creation + +Creating subnets on mainnet is competitive. The cost is determined by the rate at which new subnets are being registered onto the Bittensor blockchain. + +By default you must have at least 100 TAO on your owner wallet to create a subnet. However, the exact amount will fluctuate based on demand. The below code shows how to get the current price of creating a subnet. + +```bash +btcli subnet lock_cost +``` + +The above command will show: + +```bash +>> Subnet lock cost: τ100.000000000 +``` + +## 4. Purchasing a slot + +Using your TAO balance, you can register your subnet to the mainchain. This will create a new subnet on the mainchain and give you the owner permissions to it. The below command shows how to purchase a slot. + +**NOTE**: Slots cost TAO to lock. You will get this TAO back when the subnet is dissolved. + +```bash +btcli subnet create +``` + +Enter the owner wallet name. This gives permissions to the coldkey. + +```bash +>> Enter wallet name (default): owner # Enter your owner wallet name +>> Enter password to unlock key: # Enter your wallet password. +>> Register subnet? [y/n]: # Select yes (y) +>> ⠇ 📡 Registering subnet... +✅ Registered subnetwork with netuid: 1 # Your subnet netuid will show here, save this for later. +``` + +## 5. (Optional) Register keys + +**NOTE**: While this is not enforced, we recommend subnet owners to run a subnet validator and a subnet miner on the subnet to demonstrate proper use to the community. + +This step registers your subnet validator and subnet miner keys to the subnet giving them the **first two slots** on the subnet. + +Register your miner key to the subnet: + +```bash +btcli subnet recycle_register --netuid 1 --subtensor.network finney --wallet.name miner --wallet.hotkey default +``` + +Follow the below prompts: + +```bash +>> Enter netuid [1] (1): # Enter netuid 1 to specify the subnet you just created. +>> Continue Registration? + hotkey: ... + coldkey: ... + network: finney [y/n]: # Select yes (y) +>> ✅ Registered +``` + +Next, register your validator key to the subnet: + +```bash +btcli subnet recycle_register --netuid 1 --subtensor.network finney --wallet.name validator --wallet.hotkey default +``` + +Follow the below prompts: + +```bash +>> Enter netuid [1] (1): # Enter netuid 1 to specify the subnet you just created. +>> Continue Registration? + hotkey: ... + coldkey: ... + network: finney [y/n]: # Select yes (y) +>> ✅ Registered +``` + +## 6. Check that your keys have been registered + +Check that your subnet validator key has been registered: + +```bash +btcli wallet overview --wallet.name validator +``` + +The output will be similar to the below: + +```bash +Subnet: 1 +COLDKEY HOTKEY UID ACTIVE STAKE(τ) RANK TRUST CONSENSUS INCENTIVE DIVIDENDS EMISSION(ρ) VTRUST VPERMIT UPDATED AXON HOTKEY_SS58 +miner default 0 True 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0 0.00000 14 none 5GTFrsEQfvTsh3WjiEVFeKzFTc2xcf… +1 1 2 τ0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 ρ0 0.00000 + Wallet balance: τ0.0 +``` + +Check that your subnet miner has been registered: + +```bash +btcli wallet overview --wallet.name miner +``` + +The output will be similar to the below: + +```bash +Subnet: 1 +COLDKEY HOTKEY UID ACTIVE STAKE(τ) RANK TRUST CONSENSUS INCENTIVE DIVIDENDS EMISSION(ρ) VTRUST VPERMIT UPDATED AXON HOTKEY_SS58 +miner default 1 True 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0 0.00000 14 none 5GTFrsEQfvTsh3WjiEVFeKzFTc2xcf… +1 1 2 τ0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 ρ0 0.00000 + Wallet balance: τ0.0 +``` + +## 7. Run subnet miner and subnet validator + +Run the subnet miner: + +```bash +python neurons/miner.py --netuid 1 --wallet.name miner --wallet.hotkey default --logging.debug +``` + +You will see the below terminal output: + +```bash +>> 2023-08-08 16:58:11.223 | INFO | Running miner for subnet: 1 on network: wss://entrypoint-finney.opentensor.ai:443 with config: ... +``` + +Run the subnet validator: + +```bash +python neurons/validator.py --netuid 1 --wallet.name validator --wallet.hotkey default --logging.debug +``` + +You will see the below terminal output: + +```bash +>> 2023-08-08 16:58:11.223 | INFO | Running validator for subnet: 1 on network: wss://entrypoint-finney.opentensor.ai:443 with config: ... +``` + +## 8. Get emissions flowing + +Register to the root subnet using the `btcli`: + +```bash +btcli root register +``` + +Then set your weights for the subnet: + +```bash +btcli root weights +``` + +## 9. Stopping your nodes + +To stop your nodes, press CTRL + C in the terminal where the nodes are running. + +--- \ No newline at end of file diff --git a/bitagent_subnet-main/docs/running_on_staging.md b/bitagent_subnet-main/docs/running_on_staging.md new file mode 100644 index 0000000000000000000000000000000000000000..70ea74fbe9fa896e65f8cd9e40965f2d795e528a --- /dev/null +++ b/bitagent_subnet-main/docs/running_on_staging.md @@ -0,0 +1,325 @@ +# Running Subnet Locally + +This tutorial will guide you through: + +- Setting up a local blockchain that is not connected to either Bittensor testchain or mainchain +- Creating a subnet +- Run your incentive mechanism on the subnet. + +## Local blockchain vs local subtensor node + +Running a local blockchain is sometimes synonymously referred as running on staging. This is **different** from running a local subtensor node that connects to the Bittensor mainchain. + +A local subtensor node will connect to the mainchain and sync with the mainchain, giving you your own access point to the mainchain. + +Running a local blockchain spins up two authority nodes locally, not connected to any other nodes or testchain or mainchain. This tutorial is for running a local blockchain. + +## Prerequisites + +Before proceeding further, make sure that you have installed Bittensor. See the below instructions: + +- [Install `bittensor`](https://github.com/opentensor/bittensor#install). + +After installing `bittensor`, proceed as below: + +## 1. Install Substrate dependencies + +Begin by installing the required dependencies for running a Substrate node. + +Update your system packages: + +```bash +sudo apt update +``` + +Install additional required libraries and tools + +```bash +sudo apt install --assume-yes make build-essential git clang curl libssl-dev llvm libudev-dev protobuf-compiler +``` + +## 2. Install Rust and Cargo + +Rust is the programming language used in Substrate development. Cargo is Rust package manager. + +Install rust and cargo: + +```bash +curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh +``` + +Update your shell's source to include Cargo's path: + +```bash +source "$HOME/.cargo/env" +``` + +## 3. Clone the subtensor repository + +This step fetches the subtensor codebase to your local machine. + +```bash +git clone https://github.com/opentensor/subtensor.git +``` + +## 4. Setup Rust + +This step ensures that you have the nightly toolchain and the WebAssembly (wasm) compilation target. Note that this step will run the subtensor chain on your terminal directly, hence we advise that you run this as a background process using PM2 or other software. + +Update to the nightly version of Rust: + +```bash +./subtensor/scripts/init.sh +``` + +## 5. Initialize + +These steps initialize your local subtensor chain in development mode. These commands will set up and run a local subtensor. + +Build the binary with the faucet feature enabled: + +```bash +cargo build --release --features pow-faucet +``` + +**NOTE**: The `--features pow-faucet` option in the above is required if we want to use the command `btcli wallet faucet` [See the below Mint tokens step](#8-mint-tokens-from-faucet). + +Next, run the localnet script and turn off the attempt to build the binary (as we have already done this above): + +```bash +BUILD_BINARY=0 ./scripts/localnet.sh +``` + +**NOTE**: Watch for any build or initialization outputs in this step. If you are building the project for the first time, this step will take a while to finish building, depending on your hardware. + +## 6. Install subnet template + +`cd` to your project directory and clone the bittensor subnet template repository: + +```bash +git clone https://github.com/opentensor/bittensor-subnet-template.git +``` + +Navigate to the cloned repository: + +```bash +cd bittensor-subnet-template +``` + +Install the bittensor-subnet-template Python package: + +```bash +python -m pip install -e . +``` + +## 7. Set up wallets + +You will need wallets for the different roles, i.e., subnet owner, subnet validator and subnet miner, in the subnet. + +- The owner wallet creates and controls the subnet. +- The validator and miner will be registered to the subnet created by the owner. This ensures that the validator and miner can run the respective validator and miner scripts. + +Create a coldkey for the owner role: + +```bash +btcli wallet new_coldkey --wallet.name owner +``` + +Set up the miner's wallets: + +```bash +btcli wallet new_coldkey --wallet.name miner +``` + +```bash +btcli wallet new_hotkey --wallet.name miner --wallet.hotkey default +``` + +Set up the validator's wallets: + +```bash +btcli wallet new_coldkey --wallet.name validator +``` +```bash +btcli wallet new_hotkey --wallet.name validator --wallet.hotkey default +``` + +## 8. Mint tokens from faucet + +You will need tokens to initialize the intentive mechanism on the chain as well as for registering the subnet. + +Run the following commands to mint faucet tokens for the owner and for the validator. + +Mint faucet tokens for the owner: + +```bash +btcli wallet faucet --wallet.name owner --subtensor.chain_endpoint ws://127.0.0.1:9946 +``` + +You will see: + +```bash +>> Balance: τ0.000000000 ➡ τ100.000000000 +``` + +Mint tokens for the validator: + +```bash +btcli wallet faucet --wallet.name validator --subtensor.chain_endpoint ws://127.0.0.1:9946 +``` + +You will see: + +```bash +>> Balance: τ0.000000000 ➡ τ100.000000000 +``` + +## 9. Create a subnet + +The below commands establish a new subnet on the local chain. The cost will be exactly τ100.000000000 for the first subnet you create. + +```bash +btcli subnet create --wallet.name owner --subtensor.chain_endpoint ws://127.0.0.1:9946 +``` + +You will see: + +```bash +>> Your balance is: τ200.000000000 +>> Do you want to register a subnet for τ100.000000000? [y/n]: +>> Enter password to unlock key: [YOUR_PASSWORD] +>> ✅ Registered subnetwork with netuid: 1 +``` + +**NOTE**: The local chain will now have a default `netuid` of 1. The second registration will create a `netuid` 2 and so on, until you reach the subnet limit of 8. If you register more than 8 subnets, then a subnet with the least staked TAO will be replaced by the 9th subnet you register. + +## 10. Register keys + +Register your subnet validator and subnet miner on the subnet. This gives your two keys unique slots on the subnet. The subnet has a current limit of 128 slots. + +Register the subnet miner: + +```bash +btcli subnet recycle_register --wallet.name miner --wallet.hotkey default --subtensor.chain_endpoint ws://127.0.0.1:9946 +``` + +Follow the below prompts: + +```bash +>> Enter netuid [1] (1): 1 +>> Continue Registration? [y/n]: y +>> ✅ Registered +``` + +Register the subnet validator: + +```bash + +btcli subnet recycle_register --wallet.name validator --wallet.hotkey default --subtensor.chain_endpoint ws://127.0.0.1:9946 +``` + +Follow the below prompts: + +``` +>> Enter netuid [1] (1): 1 +>> Continue Registration? [y/n]: y +>> ✅ Registered +``` + +## 11. Add stake + +This step bootstraps the incentives on your new subnet by adding stake into its incentive mechanism. + +```bash +btcli stake add --wallet.name validator --wallet.hotkey default --subtensor.chain_endpoint ws://127.0.0.1:9946 +``` + +Follow the below prompts: + +```bash +>> Stake all Tao from account: 'validator'? [y/n]: y +>> Stake: + τ0.000000000 ➡ τ100.000000000 +``` + +## 12. Validate key registrations + +Verify that both the miner and validator keys are successfully registered: + +```bash +btcli subnet list --subtensor.chain_endpoint ws://127.0.0.1:9946 +``` + +You will see the `2` entry under `NEURONS` column for the `NETUID` of 1, indicating that you have registered a validator and a miner in this subnet: + +```bash +NETUID NEURONS MAX_N DIFFICULTY TEMPO CON_REQ EMISSION BURN(τ) + 1 2 256.00 10.00 M 1000 None 0.00% τ1.00000 + 2 128 +``` + +See the subnet validator's registered details: + +```bash +btcli wallet overview --wallet.name validator --subtensor.chain_endpoint ws://127.0.0.1:9946 +``` + +You will see: + +``` +Subnet: 1 +COLDKEY HOTKEY UID ACTIVE STAKE(τ) RANK TRUST CONSENSUS INCENTIVE DIVIDENDS EMISSION(ρ) VTRUST VPERMIT UPDATED AXON HOTKEY_SS58 +miner default 0 True 100.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0 0.00000 14 none 5GTFrsEQfvTsh3WjiEVFeKzFTc2xcf… +1 1 2 τ100.00000 0.00000 0.00000 0.00000 0.00000 0.00000 ρ0 0.00000 + Wallet balance: τ0.0 +``` + +See the subnet miner's registered details: + +```bash +btcli wallet overview --wallet.name miner --subtensor.chain_endpoint ws://127.0.0.1:9946 +``` + +You will see: + +```bash +Subnet: 1 +COLDKEY HOTKEY UID ACTIVE STAKE(τ) RANK TRUST CONSENSUS INCENTIVE DIVIDENDS EMISSION(ρ) VTRUST VPERMIT UPDATED AXON HOTKEY_SS58 +miner default 1 True 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0 0.00000 14 none 5GTFrsEQfvTsh3WjiEVFeKzFTc2xcf… +1 1 2 τ0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 ρ0 0.00000 + Wallet balance: τ0.0 + +``` + +## 13. Run subnet miner and subnet validator + +Run the subnet miner and subnet validator. Make sure to specify your subnet parameters. + +Run the subnet miner: + +```bash +python neurons/miner.py --netuid 1 --subtensor.chain_endpoint ws://127.0.0.1:9946 --wallet.name miner --wallet.hotkey default --logging.debug +``` + +Run the subnet validator: + +```bash +python neurons/validator.py --netuid 1 --subtensor.chain_endpoint ws://127.0.0.1:9946 --wallet.name validator --wallet.hotkey default --logging.debug +``` + +## 14. Verify your incentive mechanism + +After a few blocks the subnet validator will set weights. This indicates that the incentive mechanism is active. Then after a subnet tempo elapses (360 blocks or 72 minutes) you will see your incentive mechanism beginning to distribute TAO to the subnet miner. + +```bash +btcli wallet overview --wallet.name miner --subtensor.chain_endpoint ws://127.0.0.1:9946 +``` + +## Ending your session + +To halt your nodes: +```bash +# Press CTRL + C keys in the terminal. +``` + +--- diff --git a/bitagent_subnet-main/docs/running_on_testnet.md b/bitagent_subnet-main/docs/running_on_testnet.md new file mode 100644 index 0000000000000000000000000000000000000000..37a9b66f30b14c01ee3aac9c38383d2bc3f5e7e2 --- /dev/null +++ b/bitagent_subnet-main/docs/running_on_testnet.md @@ -0,0 +1,242 @@ +# Running Subnet on Testnet + +This tutorial shows how to use the Bittensor testnet to create a subnet and run your incentive mechanism on it. + +**IMPORTANT:** We strongly recommend that you first run [Running Subnet Locally](running_on_staging.md) before running on the testnet. Incentive mechanisms running on the testnet are open to anyone, and although these mechanisms on testnet do not emit real TAO, they cost you test TAO which you must create. + +**DANGER** +- Do not expose your private keys. +- Only use your testnet wallet. +- Do not reuse the password of your mainnet wallet. +- Make sure your incentive mechanism is resistant to abuse. + +## Prerequisites + +Before proceeding further, make sure that you have installed Bittensor. See the below instructions: + +- [Install `bittensor`](https://github.com/opentensor/bittensor#install). + +After installing `bittensor`, proceed as below: + +## 1. Install Bittensor subnet template + +**NOTE: Skip this step if** you already did this during local testing and development. + +`cd` into your project directory and clone the bittensor-subnet-template repo: + +```bash +git clone https://github.com/opentensor/bittensor-subnet-template.git +``` + +Next, `cd` into bittensor-subnet-template repo directory: + +```bash +cd bittensor-subnet-template # Enter the +``` + +Install the bittensor-subnet-template package: + +```bash +python -m pip install -e . +``` + +## 2. Create wallets + +Create wallets for subnet owner, subnet validator and for subnet miner. + +This step creates local coldkey and hotkey pairs for your three identities: subnet owner, subnet validator and subnet miner. + +The owner will create and control the subnet. The owner must have at least 100 testnet TAO before the owner can run next steps. + +The validator and miner will be registered to the subnet created by the owner. This ensures that the validator and miner can run the respective validator and miner scripts. + +Create a coldkey for your owner wallet: + +```bash +btcli wallet new_coldkey --wallet.name owner +``` + +Create a coldkey and hotkey for your miner wallet: + +```bash +btcli wallet new_coldkey --wallet.name miner +``` + +and + +```bash +btcli wallet new_hotkey --wallet.name miner --wallet.hotkey default +``` + +Create a coldkey and hotkey for your validator wallet: + +```bash +btcli wallet new_coldkey --wallet.name validator +``` + +and + +```bash +btcli wallet new_hotkey --wallet.name validator --wallet.hotkey default +``` + +## 3. Get the price of subnet creation + +Creating subnets on the testnet is competitive. The cost is determined by the rate at which new subnets are being registered onto the chain. + +By default you must have at least 100 testnet TAO in your owner wallet to create a subnet. However, the exact amount will fluctuate based on demand. The below command shows how to get the current price of creating a subnet. + +```bash +btcli subnet lock_cost --subtensor.network test +``` + +The above command will show: + +```bash +>> Subnet lock cost: τ100.000000000 +``` + +## 4. (Optional) Get faucet tokens + +Faucet is disabled on the testnet. Hence, if you don't have sufficient faucet tokens, ask the [Bittensor Discord community](https://discord.com/channels/799672011265015819/830068283314929684) for faucet tokens. + +## 5. Purchase a slot + +Using the test TAO from the previous step you can register your subnet on the testnet. This will create a new subnet on the testnet and give you the owner permissions to it. + +The below command shows how to purchase a slot. + +**NOTE**: Slots cost TAO, and you will not get this TAO back. Instead, this TAO is recycled back into your incentive mechanism, to be later mined. + +```bash +btcli subnet create --subtensor.network test +``` + +Enter the owner wallet name which gives permissions to the coldkey: + +```bash +>> Enter wallet name (default): owner # Enter your owner wallet name +>> Enter password to unlock key: # Enter your wallet password. +>> Register subnet? [y/n]: # Select yes (y) +>> ⠇ 📡 Registering subnet... +✅ Registered subnetwork with netuid: 1 # Your subnet netuid will show here, save this for later. +``` + +## 6. Register keys + +This step registers your subnet validator and subnet miner keys to the subnet, giving them the **first two slots** on the subnet. + +Register your miner key to the subnet: + +```bash +btcli subnet recycle_register --netuid 13 --subtensor.network test --wallet.name miner --wallet.hotkey default +``` + +Follow the below prompts: + +```bash +>> Enter netuid [1] (1): # Enter netuid 1 to specify the subnet you just created. +>> Continue Registration? + hotkey: ... + coldkey: ... + network: finney [y/n]: # Select yes (y) +>> ✅ Registered +``` + +Next, register your validator key to the subnet: + +```bash +btcli subnet recycle_register --netuid 13 --subtensor.network test --wallet.name validator --wallet.hotkey default +``` + +Follow the prompts: + +```bash +>> Enter netuid [1] (1): # Enter netuid 1 to specify the subnet you just created. +>> Continue Registration? + hotkey: ... + coldkey: ... + network: finney [y/n]: # Select yes (y) +>> ✅ Registered +``` + +## 7. Check that your keys have been registered + +This step returns information about your registered keys. + +Check that your validator key has been registered: + +```bash +btcli wallet overview --wallet.name validator --subtensor.network test +``` + +The above command will display the below: + +```bash +Subnet: 1 +COLDKEY HOTKEY UID ACTIVE STAKE(τ) RANK TRUST CONSENSUS INCENTIVE DIVIDENDS EMISSION(ρ) VTRUST VPERMIT UPDATED AXON HOTKEY_SS58 +miner default 0 True 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0 0.00000 14 none 5GTFrsEQfvTsh3WjiEVFeKzFTc2xcf… +1 1 2 τ0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 ρ0 0.00000 + Wallet balance: τ0.0 +``` + +Check that your miner has been registered: + +```bash +btcli wallet overview --wallet.name miner --subtensor.network test +``` + +The above command will display the below: + +```bash +Subnet: 1 +COLDKEY HOTKEY UID ACTIVE STAKE(τ) RANK TRUST CONSENSUS INCENTIVE DIVIDENDS EMISSION(ρ) VTRUST VPERMIT UPDATED AXON HOTKEY_SS58 +miner default 1 True 0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 0 0.00000 14 none 5GTFrsEQfvTsh3WjiEVFeKzFTc2xcf… +1 1 2 τ0.00000 0.00000 0.00000 0.00000 0.00000 0.00000 ρ0 0.00000 + Wallet balance: τ0.0 +``` + +## 8. Run subnet miner and subnet validator + +Run the subnet miner: + +```bash +python neurons/miner.py --netuid 1 --subtensor.network test --wallet.name miner --wallet.hotkey default --logging.debug +``` + +You will see the below terminal output: + +```bash +>> 2023-08-08 16:58:11.223 | INFO | Running miner for subnet: 1 on network: ws://127.0.0.1:9946 with config: ... +``` + +Next, run the subnet validator: + +```bash +python neurons/validator.py --netuid 1 --subtensor.network test --wallet.name validator --wallet.hotkey default --logging.debug +``` + +You will see the below terminal output: + +```bash +>> 2023-08-08 16:58:11.223 | INFO | Running validator for subnet: 1 on network: ws://127.0.0.1:9946 with config: ... +``` + + +## 9. Get emissions flowing + +Register to the root network using the `btcli`: + +```bash +btcli root register --subtensor.network test +``` + +Then set your weights for the subnet: + +```bash +btcli root weights --subtensor.network test +``` + +## 10. Stopping your nodes + +To stop your nodes, press CTRL + C in the terminal where the nodes are running. diff --git a/bitagent_subnet-main/docs/stream_tutorial/README.md b/bitagent_subnet-main/docs/stream_tutorial/README.md new file mode 100644 index 0000000000000000000000000000000000000000..f213fd3af71adbfc3fdc980ec4d3658f1957cc25 --- /dev/null +++ b/bitagent_subnet-main/docs/stream_tutorial/README.md @@ -0,0 +1,490 @@ +# Bittensor Streaming Tutorial +This document is intented as a developer-friendly walkthrough of integrating streaming into your bittensor application. + +If you prefer to jump right into a complete stand-alone example, see: +- `miner.py` +- `protocol.py` +- `client.py` + +Start your miner: +```bash +python miner.py --netuid 8 --wallet.name default --wallet.hotkey miner --subtensor.network test --axon.port 10000 --logging.trace +``` + +Run the client: +```bash +python client.py --netuid 8 --my_uid 1 --network test +``` + +## Overview +This tutorial is designed to show you how to use the streaming API to integrate into your application. It will cover the following topics: +- writing your streaming protocol (inherits from bittensor.StreamingSynapse) +- writing your streaming server (uses your streaming protocol) +- writing your streaming client (uses your streaming protocol) + +### Defining your streaming protocol +When designing your protocol, it would be helpful to look at the bittensor.StreamingSynapse for reference. Below is a condensed snippet of the abstract methods that you will need to implement in your subclass. + +You will need to implement two methods: + +- `process_streaming_response` +- `extract_response_json` + +These two methods are the core of your streaming protocol. The first method process_streaming_response is called as the response is being streamed from the network. It is responsible for handling the streaming response, such as parsing and accumulating data. The second method extract_response_json is called after the response has been processed and is responsible for retrieving structured data to be post-processed in the dendrite in bittensor core code. + +```python +class StreamingSynapse(bittensor.Synapse, ABC): + ... + class BTStreamingResponse(_StreamingResponse): + ... + @abstractmethod + async def process_streaming_response(self, response: Response): + """ + Abstract method that must be implemented by the subclass. + This method should provide logic to handle the streaming response, such as parsing and accumulating data. + It is called as the response is being streamed from the network, and should be implemented to handle the specific + streaming data format and requirements of the subclass. + + Args: + response: The response object to be processed, typically containing chunks of data. + """ + ... + + @abstractmethod + def extract_response_json(self, response: Response) -> dict: + """ + Abstract method that must be implemented by the subclass. + This method should provide logic to extract JSON data from the response, including headers and content. + It is called after the response has been processed and is responsible for retrieving structured data + that can be used by the application. + + Args: + response: The response object from which to extract JSON data. + """ + ... + ... +``` + +See the full reference code at the bittensor [repo](https://github.com/opentensor/bittensor/blob/master/bittensor/stream.py). + + +#### Create your protocol +Let's walk through how to create a protocol using the bittensor.StreamingSynapse class. +```python +class MyStreamingSynapse(bt.StreamingSynapse): + # define your expected data fields here as pydantic field objects + # This allows you to control what information is passed along the network + messages: List[str] = pydantic.Field( + ..., # this ellipsis (...) indicates the object is required + title="Messages", # What is the name of this field? + description="A list of messages in the Prompting scenario. Immutable.", + allow_mutation=False, # disallow modification of this field after creation + ) + completion: str = pydantic.Field( + "", + title="Completion", + ) + # add fields as necessary + ... + + # This method controls how your synapse is deserialized from the network + # E.g. you can extract whatever information you want to receive at the final + # yield in the async generator returned by the server, without receiving + # the entire synapse object itself. + # In this example, we just want the completion string at the end. + def deserialize(self) -> str: + return self.completion + + # implement your `process_streaming_response` logic to actually yield objects to the streamer + # this effectively defines the async generator that you'll recieve on the client side + async def process_streaming_response(self, response: MyStreamingSynapse): + # this is an example of how you might process a streaming response + # iterate over the response content and yield each line + async for chunk in response.content.iter_any(): + tokens = chunk.decode("utf-8").split("\n") + yield tokens + + # implement `extract_response_json` to extract the JSON data from the response headers + # this will be dependent on the data you are streaming and how you want to structure it + # it MUST conform to the following format expected by the bittensor dendrite: + """ + { + # METADATA AND HEADERS + "name": ..., + "timeout": float(...), + "total_size": int(...), + "header_size": int(...), + "dendrite": ..., + "axon": ..., + # YOUR FIELDS + "messages": self.messages, + ... + } + """ + def extract_response_json(self, response: MyStreamingSynapse) -> dict: + # iterate over the response headers and extract the necessary data + headers = { + k.decode("utf-8"): v.decode("utf-8") + for k, v in response.__dict__["_raw_headers"] + } + # helper function to extract data from headers + def extract_info(prefix): + return { + key.split("_")[-1]: value + for key, value in headers.items() + if key.startswith(prefix) + } + # return the extracted data in the expected format + return { + "name": headers.get("name", ""), + "timeout": float(headers.get("timeout", 0)), + "total_size": int(headers.get("total_size", 0)), + "header_size": int(headers.get("header_size", 0)), + "dendrite": extract_info("bt_header_dendrite"), # dendrite info + "axon": extract_info("bt_header_axon"), # axon info + "messages": self.messages, # field object + } +``` + +[Here](https://github.com/opentensor/text-prompting/blob/main/prompting/protocol.py#L131) is a full example implementation of a streaming protocol based on the text-prompting network. + +Please read the docstrings provided, they can be very helpful! + +### Writing the server +Great! Now we have our protocol defined, let's see how to define our server. +This will generate the tokens to be streamed in this prompting example. + +For brevity we will not be building a full miner, but inspecting the central components. +```python +class MyStreamPromptingMiner(bt.Miner): + ... # any relevant methods you'd need for your miner + + # define your server forward here + # NOTE: It is crucial that your typehints are correct and reflect your streaming protocol object + # otherwise the axon will reject adding your route to the server. + def forward(self, synapse: MyStreamingSynapse) -> MyStreamingSynapse: + # Let's use a GPT2 tokenizer for this toy example + tokenizer = GPT2Tokenizer.from_pretrained("gpt2") + + # Simulated function to decode token IDs into strings. In a real-world scenario, + # this can be replaced with an actual model inference step. + def model(ids): + return (tokenizer.decode(id) for id in ids) + + # This function is called asynchronously to process the input text and send back tokens + # as a streaming response. It essentially produces the async generator that will be + # consumed by the client with an `async for` loop. + async def _forward(text: str, send: Send): + # `text` may be the input prompt to your model in a real-world scenario. + # let's tokenize them into IDs for the sake of this example. + input_ids = tokenizer(text, return_tensors="pt").input_ids.squeeze() + + # You may want to buffer your tokens before sending them back to the client. + # this can be useful so we aren't flooding the client with individual tokens + # and allows you more fine-grained control over how much data is sent back + # with each yield. + N = 3 # Number of tokens to send back to the client at a time + buffer = [] + # Iterate over the tokens and send the generationed tokens back to the client + # when we have sufficient (N) tokens in the buffer. + for token in model(input_ids): + buffer.append(token) # Add token to buffer + + # If buffer has N tokens, send them back to the client. + if len(buffer) == N: + joined_buffer = "".join(buffer) + # Send the tokens back to the client + # This is the core of the streaming response and the format + # is important. The `send` function is provided by the ASGI server + # and is responsible for sending the response back to the client. + # This buffer will be received by the client as a single chunk of + # data, which can then be split into individual tokens! + await send( + { + "type": "http.response.body", + "body": joined_buffer.encode("utf-8"), + "more_body": True, + } + ) + buffer = [] # Clear the buffer for next batch of tokens + + # Create a streaming response object using the `_forward` function + # It is useful to wrap your _forward function in a partial function + # to pass in the text argument lazily. + token_streamer = partial(_forward, synapse.messages[0]) + # Return the streaming response object, which is an instance of the + # `BTStreamingResponse` class. + return synapse.create_streaming_response(token_streamer) +``` + +#### Complete Example +Here is a full example for reference: +> This inherits from the prompting (text-prompting) miner base class. +> Take a look at the `prompting/baseminer/miner.py` file [here](https://github.com/opentensor/text-prompting/blob/main/prompting/baseminer/miner.py) for more details. + +```python +class StreamingTemplateMiner(prompting.Miner): + def config(self) -> "bt.Config": + """ + Returns the configuration object specific to this miner. + + Implement and extend this method to provide custom configurations for the miner. + Currently, it sets up a basic configuration parser. + + Returns: + bt.Config: A configuration object with the miner's operational parameters. + """ + parser = argparse.ArgumentParser(description="Streaming Miner Configs") + self.add_args(parser) + return bt.config(parser) + + def add_args(cls, parser: argparse.ArgumentParser): + """ + Adds custom arguments to the command line parser. + + Developers can introduce additional command-line arguments specific to the miner's + functionality in this method. These arguments can then be used to configure the miner's operation. + + Args: + parser (argparse.ArgumentParser): + The command line argument parser to which custom arguments should be added. + """ + pass + + def prompt(self, synapse: StreamPrompting) -> StreamPrompting: + """ + Generates a streaming response for the provided synapse. + + This function serves as the main entry point for handling streaming prompts. It takes + the incoming synapse which contains messages to be processed and returns a streaming + response. The function uses the GPT-2 tokenizer and a simulated model to tokenize and decode + the incoming message, and then sends the response back to the client token by token. + + Args: + synapse (StreamPrompting): The incoming StreamPrompting instance containing the messages to be processed. + + Returns: + StreamPrompting: The streaming response object which can be used by other functions to + stream back the response to the client. + + Usage: + This function can be extended and customized based on specific requirements of the + miner. Developers can swap out the tokenizer, model, or adjust how streaming responses + are generated to suit their specific applications. + """ + bt.logging.trace("In outer PROMPT()") + tokenizer = GPT2Tokenizer.from_pretrained("gpt2") + + # Simulated function to decode token IDs into strings. In a real-world scenario, + # this can be replaced with an actual model inference step. + def model(ids): + return (tokenizer.decode(id) for id in ids) + + async def _prompt(text: str, send: Send): + """ + Asynchronously processes the input text and sends back tokens as a streaming response. + + This function takes an input text, tokenizes it using the GPT-2 tokenizer, and then + uses the simulated model to decode token IDs into strings. It then sends each token + back to the client as a streaming response, with a delay between tokens to simulate + the effect of real-time streaming. + + Args: + text (str): The input text message to be processed. + send (Send): An asynchronous function that allows sending back the streaming response. + + Usage: + This function can be adjusted based on the streaming requirements, speed of + response, or the model being used. Developers can also introduce more sophisticated + processing steps or modify how tokens are sent back to the client. + """ + bt.logging.trace("In inner _PROMPT()") + input_ids = tokenizer(text, return_tensors="pt").input_ids.squeeze() + buffer = [] + bt.logging.debug(f"Input text: {text}") + bt.logging.debug(f"Input ids: {input_ids}") + + N = 3 # Number of tokens to send back to the client at a time + for token in model(input_ids): + bt.logging.trace(f"appending token: {token}") + buffer.append(token) + # If buffer has N tokens, send them back to the client. + if len(buffer) == N: + time.sleep(0.1) + joined_buffer = "".join(buffer) + bt.logging.debug(f"sedning tokens: {joined_buffer}") + await send( + { + "type": "http.response.body", + "body": joined_buffer.encode("utf-8"), + "more_body": True, + } + ) + bt.logging.debug(f"Streamed tokens: {joined_buffer}") + buffer = [] # Clear the buffer for next batch of tokens + + # Send any remaining tokens in the buffer + if buffer: + joined_buffer = "".join(buffer) + await send( + { + "type": "http.response.body", + "body": joined_buffer.encode("utf-8"), + "more_body": False, # No more tokens to send + } + ) + bt.logging.trace(f"Streamed tokens: {joined_buffer}") + + message = synapse.messages[0] + bt.logging.trace(f"message in _prompt: {message}") + token_streamer = partial(_prompt, message) + bt.logging.trace(f"token streamer: {token_streamer}") + return synapse.create_streaming_response(token_streamer) +``` + +### Writing the client +Excellent! Now we have defined our server, now we can define our client. + +This has assumed you have: +1. Registered your miner on the chain (`finney`/`test`) +2. Are serving your miner on an open port (e.g. `12345`) + +Steps: +- Instantiate your synapse subclass with the relevant information. E.g. `messages`, `roles`, etc. +- Instantiate your wallet and a dendrite client +- Query the dendrite client with your synapse object +- Iterate over the async generator to extract the yielded tokens on the server side + +```python + +# Import bittensor +import bittensor as bt + +# Create your streaming synapse subclass object to house the request body +syn = MyStreamingSynapse( + roles=["user"], + messages=["hello this is a test of a streaming response. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."] +) + +# Create a wallet instance that must be registered on the network +wallet = bt.wallet(name="default", hotkey="default") + +# Instantiate the metagraph +metagraph = bt.metagraph( + netuid=8, network="test", sync=True, lite=False +) + +# Grab the axon you're serving +my_uid = 1 +axon = metagraph.axons[my_uid] + +# Create a Dendrite instance to handle client-side communication. +dendrite = bt.dendrite(wallet=wallet) + + +This is an async function so we can use the `await` keyword when querying the server with the dendrite object. +async def main(): + # Send a request to the Axon using the Dendrite, passing in a StreamPrompting + # instance with roles and messages. The response is awaited, as the Dendrite + # communicates asynchronously with the Axon. Returns a list of async generator. + responses = await dendrite( + [axon], + syn, + deserialize=False, + streaming=True + ) + + # Now that we have our responses we want to iterate over the yielded tokens + # iterate over the async generator to extract the yielded tokens on server side + for resp in responses: + i=0 + async for chunk in resp: + i += 1 + if i % 5 == 0: + print() + if isinstance(chunk, list): + print(chunk[0], end="", flush=True) + else: + # last object yielded is the synapse itself with completion filled + synapse = chunk + break + + # The synapse object contains the completion attribute which contains the + # accumulated tokens from the streaming response. + +if __name__ == "__main__": + # Run the main function with asyncio + asyncio.run(main()) + +``` +There you have it! + +### Complete example +If you would like to see a complete standalone example that only depends on bittensor>=6.2.0, look below: + +- client.py +- streaming_miner.py +- + +# client.py +```python +# Import bittensor and the text-prompting packages +import bittensor as bt +import prompting + +# Create a StreamPrompting synapse object to house the request body +syn = prompting.protocol.StreamPrompting( + roles=["user"], + messages=["hello this is a test of a streaming response. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum."]) +syn + +# create a wallet instance that must be registered on the network +wallet = bt.wallet(name="default", hotkey="default") +wallet + +# instantiate the metagraph +metagraph = bt.metagraph( + netuid=8, network="test", sync=True, lite=False +) +metagraph + +# Grab the axon you're serving +axon = metagraph.axons[62] +axon + +# Create a Dendrite instance to handle client-side communication. +d = bt.dendrite(wallet=wallet) +d + + +async def main(): + + # Send a request to the Axon using the Dendrite, passing in a StreamPrompting + # instance with roles and messages. The response is awaited, as the Dendrite + # communicates asynchronously with the Axon. Returns a list of async generator. + responses = await d( + [axon], + syn, + deserialize=False, + streaming=True + ) + responses + + # iterate over the async generator to extract the yielded tokens on server side + for resp in responses: + i=0 + async for chunk in resp: + i += 1 + if i % 5 == 0: + print() + if isinstance(chunk, list): + print(chunk[0], end="", flush=True) + else: + # last object yielded is the synapse itself with completion filled + synapse = chunk + break + +if __name__ == "__main__": + import asyncio + asyncio.run(main()) +``` diff --git a/bitagent_subnet-main/docs/stream_tutorial/client.py b/bitagent_subnet-main/docs/stream_tutorial/client.py new file mode 100644 index 0000000000000000000000000000000000000000..6404ee83d13dda46e604c8bcf6ac516918245d03 --- /dev/null +++ b/bitagent_subnet-main/docs/stream_tutorial/client.py @@ -0,0 +1,91 @@ +import argparse +import asyncio +import bittensor as bt + +from protocol import StreamPrompting + +""" +This has assumed you have: +1. Registered your miner on the chain (finney/test) +2. Are serving your miner on an open port (e.g. 12345) + +Steps: +- Instantiate your synapse subclass with the relevant information. E.g. messages, roles, etc. +- Instantiate your wallet and a dendrite client +- Query the dendrite client with your synapse object +- Iterate over the async generator to extract the yielded tokens on the server side +""" + + +async def query_synapse(my_uid, wallet_name, hotkey, network, netuid): + syn = StreamPrompting( + roles=["user"], + messages=[ + "hello this is a test of a streaming response. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua." + ], + ) + + # create a wallet instance with provided wallet name and hotkey + wallet = bt.wallet(name=wallet_name, hotkey=hotkey) + + # instantiate the metagraph with provided network and netuid + metagraph = bt.metagraph(netuid=netuid, network=network, sync=True, lite=False) + + # Grab the axon you're serving + axon = metagraph.axons[my_uid] + + # Create a Dendrite instance to handle client-side communication. + dendrite = bt.dendrite(wallet=wallet) + + async def main(): + responses = await dendrite([axon], syn, deserialize=False, streaming=True) + + for resp in responses: + i = 0 + async for chunk in resp: + i += 1 + if i % 5 == 0: + print() + if isinstance(chunk, list): + print(chunk[0], end="", flush=True) + else: + # last object yielded is the synapse itself with completion filled + synapse = chunk + break + + # Run the main function with asyncio + await main() + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Query a Bittensor synapse with given parameters." + ) + + # Adding arguments + parser.add_argument( + "--my_uid", type=int, required=True, help="Your unique miner ID on the chain" + ) + parser.add_argument("--netuid", type=int, required=True, help="Network Unique ID") + parser.add_argument( + "--wallet_name", type=str, default="default", help="Name of the wallet" + ) + parser.add_argument( + "--hotkey", type=str, default="default", help="Hotkey for the wallet" + ) + parser.add_argument( + "--network", + type=str, + default="test", + help='Network type, e.g., "test" or "mainnet"', + ) + + # Parse arguments + args = parser.parse_args() + + # Running the async function with provided arguments + asyncio.run( + query_synapse( + args.my_uid, args.wallet_name, args.hotkey, args.network, args.netuid + ) + ) diff --git a/bitagent_subnet-main/docs/stream_tutorial/config.py b/bitagent_subnet-main/docs/stream_tutorial/config.py new file mode 100644 index 0000000000000000000000000000000000000000..7507076ac75d1f1275d3b8f3898f13f1c220fb85 --- /dev/null +++ b/bitagent_subnet-main/docs/stream_tutorial/config.py @@ -0,0 +1,114 @@ +import bittensor as bt +import argparse +import os + + +def check_config(cls, config: "bt.Config"): + bt.axon.check_config(config) + bt.logging.check_config(config) + full_path = os.path.expanduser( + "{}/{}/{}/{}".format( + config.logging.logging_dir, + config.wallet.get("name", bt.defaults.wallet.name), + config.wallet.get("hotkey", bt.defaults.wallet.hotkey), + config.miner.name, + ) + ) + config.miner.full_path = os.path.expanduser(full_path) + if not os.path.exists(config.miner.full_path): + os.makedirs(config.miner.full_path) + + +def get_config() -> "bt.Config": + parser = argparse.ArgumentParser() + parser.add_argument( + "--axon.port", type=int, default=8098, help="Port to run the axon on." + ) + # Subtensor network to connect to + parser.add_argument( + "--subtensor.network", + default="finney", + help="Bittensor network to connect to.", + ) + # Chain endpoint to connect to + parser.add_argument( + "--subtensor.chain_endpoint", + default="wss://entrypoint-finney.opentensor.ai:443", + help="Chain endpoint to connect to.", + ) + # Adds override arguments for network and netuid. + parser.add_argument("--netuid", type=int, default=1, help="The chain subnet uid.") + + parser.add_argument( + "--miner.root", + type=str, + help="Trials for this miner go in miner.root / (wallet_cold - wallet_hot) / miner.name ", + default="~/.bittensor/miners/", + ) + parser.add_argument( + "--miner.name", + type=str, + help="Trials for this miner go in miner.root / (wallet_cold - wallet_hot) / miner.name ", + default="Bittensor Miner", + ) + + # Run config. + parser.add_argument( + "--miner.blocks_per_epoch", + type=str, + help="Blocks until the miner repulls the metagraph from the chain", + default=100, + ) + + # Switches. + parser.add_argument( + "--miner.no_serve", + action="store_true", + help="If True, the miner doesnt serve the axon.", + default=False, + ) + parser.add_argument( + "--miner.no_start_axon", + action="store_true", + help="If True, the miner doesnt start the axon.", + default=False, + ) + + # Mocks. + parser.add_argument( + "--miner.mock_subtensor", + action="store_true", + help="If True, the miner will allow non-registered hotkeys to mine.", + default=False, + ) + + # Adds subtensor specific arguments i.e. --subtensor.chain_endpoint ... --subtensor.network ... + bt.subtensor.add_args(parser) + + # Adds logging specific arguments i.e. --logging.debug ..., --logging.trace .. or --logging.logging_dir ... + bt.logging.add_args(parser) + + # Adds wallet specific arguments i.e. --wallet.name ..., --wallet.hotkey ./. or --wallet.path ... + bt.wallet.add_args(parser) + + # Adds axon specific arguments i.e. --axon.port ... + bt.axon.add_args(parser) + + # Activating the parser to read any command-line inputs. + # To print help message, run python3 template/miner.py --help + config = bt.config(parser) + + # Logging captures events for diagnosis or understanding miner's behavior. + config.full_path = os.path.expanduser( + "{}/{}/{}/netuid{}/{}".format( + config.logging.logging_dir, + config.wallet.name, + config.wallet.hotkey, + config.netuid, + "miner", + ) + ) + # Ensure the directory for logging exists, else create one. + if not os.path.exists(config.full_path): + os.makedirs(config.full_path, exist_ok=True) + return config diff --git a/bitagent_subnet-main/docs/stream_tutorial/miner.py b/bitagent_subnet-main/docs/stream_tutorial/miner.py new file mode 100644 index 0000000000000000000000000000000000000000..0fa938702882fa8b60cda9bccc412a89c70573bf --- /dev/null +++ b/bitagent_subnet-main/docs/stream_tutorial/miner.py @@ -0,0 +1,393 @@ +import copy +import time +import asyncio +import argparse +import threading +import traceback +from abc import ABC, abstractmethod +from functools import partial +from starlette.types import Send + +import bittensor as bt +from transformers import GPT2Tokenizer +from typing import List, Dict, Tuple, Union, Callable, Awaitable + +from protocol import StreamPrompting +from config import get_config, check_config + + +class StreamMiner(ABC): + def __init__(self, config=None, axon=None, wallet=None, subtensor=None): + # Setup base config from Miner.config() and merge with subclassed config. + base_config = copy.deepcopy(config or get_config()) + self.config = self.config() + self.config.merge(base_config) + + check_config(StreamMiner, self.config) + bt.logging.info(self.config) + + self.prompt_cache: Dict[str, Tuple[str, int]] = {} + + # Activating Bittensor's logging with the set configurations. + bt.logging(config=self.config, logging_dir=self.config.full_path) + bt.logging.info("Setting up bittensor objects.") + + # Wallet holds cryptographic information, ensuring secure transactions and communication. + self.wallet = wallet or bt.wallet(config=self.config) + bt.logging.info(f"Wallet {self.wallet}") + + # subtensor manages the blockchain connection, facilitating interaction with the Bittensor blockchain. + self.subtensor = subtensor or bt.subtensor(config=self.config) + bt.logging.info(f"Subtensor: {self.subtensor}") + bt.logging.info( + f"Running miner for subnet: {self.config.netuid} on network: {self.subtensor.chain_endpoint} with config:" + ) + + # metagraph provides the network's current state, holding state about other participants in a subnet. + self.metagraph = self.subtensor.metagraph(self.config.netuid) + bt.logging.info(f"Metagraph: {self.metagraph}") + + if self.wallet.hotkey.ss58_address not in self.metagraph.hotkeys: + bt.logging.error( + f"\nYour validator: {self.wallet} if not registered to chain connection: {self.subtensor} \nRun btcli register and try again. " + ) + exit() + else: + # Each miner gets a unique identity (UID) in the network for differentiation. + self.my_subnet_uid = self.metagraph.hotkeys.index( + self.wallet.hotkey.ss58_address + ) + bt.logging.info(f"Running miner on uid: {self.my_subnet_uid}") + + # The axon handles request processing, allowing validators to send this process requests. + self.axon = axon or bt.axon(wallet=self.wallet, port=self.config.axon.port) + # Attach determiners which functions are called when servicing a request. + bt.logging.info(f"Attaching forward function to axon.") + print(f"Attaching forward function to axon. {self._prompt}") + self.axon.attach( + forward_fn=self._prompt, + ) + bt.logging.info(f"Axon created: {self.axon}") + + # Instantiate runners + self.should_exit: bool = False + self.is_running: bool = False + self.thread: threading.Thread = None + self.lock = asyncio.Lock() + self.request_timestamps: Dict = {} + + @abstractmethod + def config(self) -> "bt.Config": + ... + + @classmethod + @abstractmethod + def add_args(cls, parser: argparse.ArgumentParser): + ... + + def _prompt(self, synapse: StreamPrompting) -> StreamPrompting: + """ + A wrapper method around the `prompt` method that will be defined by the subclass. + + This method acts as an intermediary layer to perform pre-processing before calling the + actual `prompt` method implemented in the subclass. Specifically, it checks whether a + prompt is in cache to avoid reprocessing recent requests. If the prompt is not in the + cache, the subclass `prompt` method is called. + + Args: + synapse (StreamPrompting): The incoming request object encapsulating the details of the request. + + Returns: + StreamPrompting: The response object to be sent back in reply to the incoming request, essentially + the filled synapse request object. + + Raises: + ValueError: If the prompt is found in the cache indicating it was sent recently. + + Example: + This method is not meant to be called directly but is invoked internally when a request + is received, and it subsequently calls the `prompt` method of the subclass. + """ + return self.prompt(synapse) + + @abstractmethod + def prompt(self, synapse: StreamPrompting) -> StreamPrompting: + """ + Abstract method to handle and respond to incoming requests to the miner. + + Subclasses should implement this method to define their custom logic for processing and + responding to requests. This method is designed to be overridden, and its behavior will + be dependent on the specific implementation provided in the subclass. + + Args: + synapse (StreamPrompting): The incoming request object encapsulating the details + of the request. This must contain `messages` and `roles` as fields. + + Returns: + StreamPrompting: The response object that should be sent back in reply to the + incoming request. This is essentially the filled synapse request object. + + Example: + class CustomMiner(Miner): + def prompt(self, synapse: StreamPrompting) -> StreamPrompting: + # Custom logic to process and respond to the request. + synapse.completion = "The meaning of life is 42." + return synapse + """ + ... + + def run(self): + """ + Runs the miner logic. This method starts the miner's operations, including + listening for incoming requests and periodically updating the miner's knowledge + of the network graph. + """ + if not self.subtensor.is_hotkey_registered( + netuid=self.config.netuid, + hotkey_ss58=self.wallet.hotkey.ss58_address, + ): + bt.logging.error( + f"Wallet: {self.wallet} is not registered on netuid {self.config.netuid}" + f"Please register the hotkey using `btcli subnets register` before trying again" + ) + exit() + + # Serve passes the axon information to the network + netuid we are hosting on. + # This will auto-update if the axon port of external ip have changed. + bt.logging.info( + f"Serving axon {StreamPrompting} on network: {self.config.subtensor.chain_endpoint} with netuid: {self.config.netuid}" + ) + self.axon.serve(netuid=self.config.netuid, subtensor=self.subtensor) + + # Start starts the miner's axon, making it active on the network. + bt.logging.info(f"Starting axon server on port: {self.config.axon.port}") + self.axon.start() + + # --- Run until should_exit = True. + self.last_epoch_block = self.subtensor.get_current_block() + bt.logging.info(f"Miner starting at block: {self.last_epoch_block}") + + # This loop maintains the miner's operations until intentionally stopped. + bt.logging.info(f"Starting main loop") + step = 0 + try: + while not self.should_exit: + start_epoch = time.time() + + # --- Wait until next epoch. + current_block = self.subtensor.get_current_block() + while ( + current_block - self.last_epoch_block + < self.config.miner.blocks_per_epoch + ): + # --- Wait for next bloc. + time.sleep(1) + current_block = self.subtensor.get_current_block() + + # --- Check if we should exit. + if self.should_exit: + break + + # --- Update the metagraph with the latest network state. + self.last_epoch_block = self.subtensor.get_current_block() + + metagraph = self.subtensor.metagraph( + netuid=self.config.netuid, + lite=True, + block=self.last_epoch_block, + ) + log = ( + f"Step:{step} | " + f"Block:{metagraph.block.item()} | " + f"Stake:{metagraph.S[self.my_subnet_uid]} | " + f"Rank:{metagraph.R[self.my_subnet_uid]} | " + f"Trust:{metagraph.T[self.my_subnet_uid]} | " + f"Consensus:{metagraph.C[self.my_subnet_uid] } | " + f"Incentive:{metagraph.I[self.my_subnet_uid]} | " + f"Emission:{metagraph.E[self.my_subnet_uid]}" + ) + bt.logging.info(log) + + step += 1 + + # If someone intentionally stops the miner, it'll safely terminate operations. + except KeyboardInterrupt: + self.axon.stop() + bt.logging.success("Miner killed by keyboard interrupt.") + exit() + + # In case of unforeseen errors, the miner will log the error and continue operations. + except Exception as e: + bt.logging.error(traceback.format_exc()) + + def run_in_background_thread(self): + """ + Starts the miner's operations in a separate background thread. + This is useful for non-blocking operations. + """ + if not self.is_running: + bt.logging.debug("Starting miner in background thread.") + self.should_exit = False + self.thread = threading.Thread(target=self.run, daemon=True) + self.thread.start() + self.is_running = True + bt.logging.debug("Started") + + def stop_run_thread(self): + """ + Stops the miner's operations that are running in the background thread. + """ + if self.is_running: + bt.logging.debug("Stopping miner in background thread.") + self.should_exit = True + self.thread.join(5) + self.is_running = False + bt.logging.debug("Stopped") + + def __enter__(self): + """ + Starts the miner's operations in a background thread upon entering the context. + This method facilitates the use of the miner in a 'with' statement. + """ + self.run_in_background_thread() + + def __exit__(self, exc_type, exc_value, traceback): + """ + Stops the miner's background operations upon exiting the context. + This method facilitates the use of the miner in a 'with' statement. + + Args: + exc_type: The type of the exception that caused the context to be exited. + None if the context was exited without an exception. + exc_value: The instance of the exception that caused the context to be exited. + None if the context was exited without an exception. + traceback: A traceback object encoding the stack trace. + None if the context was exited without an exception. + """ + self.stop_run_thread() + + +class StreamingTemplateMiner(StreamMiner): + def config(self) -> "bt.Config": + """ + Returns the configuration object specific to this miner. + + Implement and extend this method to provide custom configurations for the miner. + Currently, it sets up a basic configuration parser. + + Returns: + bt.Config: A configuration object with the miner's operational parameters. + """ + parser = argparse.ArgumentParser(description="Streaming Miner Configs") + self.add_args(parser) + return bt.config(parser) + + def add_args(cls, parser: argparse.ArgumentParser): + """ + Adds custom arguments to the command line parser. + + Developers can introduce additional command-line arguments specific to the miner's + functionality in this method. These arguments can then be used to configure the miner's operation. + + Args: + parser (argparse.ArgumentParser): + The command line argument parser to which custom arguments should be added. + """ + pass + + def prompt(self, synapse: StreamPrompting) -> StreamPrompting: + """ + Generates a streaming response for the provided synapse. + + This function serves as the main entry point for handling streaming prompts. It takes + the incoming synapse which contains messages to be processed and returns a streaming + response. The function uses the GPT-2 tokenizer and a simulated model to tokenize and decode + the incoming message, and then sends the response back to the client token by token. + + Args: + synapse (StreamPrompting): The incoming StreamPrompting instance containing the messages to be processed. + + Returns: + StreamPrompting: The streaming response object which can be used by other functions to + stream back the response to the client. + + Usage: + This function can be extended and customized based on specific requirements of the + miner. Developers can swap out the tokenizer, model, or adjust how streaming responses + are generated to suit their specific applications. + """ + bt.logging.trace("HI. PROMPT()") + tokenizer = GPT2Tokenizer.from_pretrained("gpt2") + + # Simulated function to decode token IDs into strings. In a real-world scenario, + # this can be replaced with an actual model inference step. + def model(ids): + return (tokenizer.decode(id) for id in ids) + + async def _prompt(text: str, send: Send): + """ + Asynchronously processes the input text and sends back tokens as a streaming response. + + This function takes an input text, tokenizes it using the GPT-2 tokenizer, and then + uses the simulated model to decode token IDs into strings. It then sends each token + back to the client as a streaming response, with a delay between tokens to simulate + the effect of real-time streaming. + + Args: + text (str): The input text message to be processed. + send (Send): An asynchronous function that allows sending back the streaming response. + + Usage: + This function can be adjusted based on the streaming requirements, speed of + response, or the model being used. Developers can also introduce more sophisticated + processing steps or modify how tokens are sent back to the client. + """ + bt.logging.trace("HI. _PROMPT()") + input_ids = tokenizer(text, return_tensors="pt").input_ids.squeeze() + buffer = [] + bt.logging.debug(f"Input text: {text}") + bt.logging.debug(f"Input ids: {input_ids}") + + N = 3 # Number of tokens to send back to the client at a time + for token in model(input_ids): + bt.logging.trace(f"appending token: {token}") + buffer.append(token) + # If buffer has N tokens, send them back to the client. + if len(buffer) == N: + time.sleep(0.1) + joined_buffer = "".join(buffer) + bt.logging.debug(f"sedning tokens: {joined_buffer}") + await send( + { + "type": "http.response.body", + "body": joined_buffer.encode("utf-8"), + "more_body": True, + } + ) + bt.logging.debug(f"Streamed tokens: {joined_buffer}") + buffer = [] # Clear the buffer for next batch of tokens + + # Send any remaining tokens in the buffer + if buffer: + joined_buffer = "".join(buffer) + await send( + { + "type": "http.response.body", + "body": joined_buffer.encode("utf-8"), + "more_body": False, # No more tokens to send + } + ) + bt.logging.trace(f"Streamed tokens: {joined_buffer}") + + message = synapse.messages[0] + bt.logging.trace(f"message in _prompt: {message}") + token_streamer = partial(_prompt, message) + bt.logging.trace(f"token streamer: {token_streamer}") + return synapse.create_streaming_response(token_streamer) + + +# This is the main function, which runs the miner. +if __name__ == "__main__": + with StreamingTemplateMiner(): + while True: + time.sleep(1) diff --git a/bitagent_subnet-main/docs/stream_tutorial/protocol.py b/bitagent_subnet-main/docs/stream_tutorial/protocol.py new file mode 100644 index 0000000000000000000000000000000000000000..25c4e92b0da8db18c19ea968b703047cacdf9001 --- /dev/null +++ b/bitagent_subnet-main/docs/stream_tutorial/protocol.py @@ -0,0 +1,152 @@ +import pydantic +import bittensor as bt + +from abc import ABC, abstractmethod +from typing import List, Union, Callable, Awaitable +from starlette.responses import StreamingResponse + + +class StreamPrompting(bt.StreamingSynapse): + """ + StreamPrompting is a specialized implementation of the `StreamingSynapse` tailored for prompting functionalities within + the Bittensor network. This class is intended to interact with a streaming response that contains a sequence of tokens, + which represent prompts or messages in a certain scenario. + + As a developer, when using or extending the `StreamPrompting` class, you should be primarily focused on the structure + and behavior of the prompts you are working with. The class has been designed to seamlessly handle the streaming, + decoding, and accumulation of tokens that represent these prompts. + + Attributes: + - `roles` (List[str]): A list of roles involved in the prompting scenario. This could represent different entities + or agents involved in the conversation or use-case. They are immutable to ensure consistent + interaction throughout the lifetime of the object. + + - `messages` (List[str]): These represent the actual prompts or messages in the prompting scenario. They are also + immutable to ensure consistent behavior during processing. + + - `completion` (str): Stores the processed result of the streaming tokens. As tokens are streamed, decoded, and + processed, they are accumulated in the completion attribute. This represents the "final" + product or result of the streaming process. + - `required_hash_fields` (List[str]): A list of fields that are required for the hash. + + Methods: + - `process_streaming_response`: This method asynchronously processes the incoming streaming response by decoding + the tokens and accumulating them in the `completion` attribute. + + - `deserialize`: Converts the `completion` attribute into its desired data format, in this case, a string. + + - `extract_response_json`: Extracts relevant JSON data from the response, useful for gaining insights on the response's + metadata or for debugging purposes. + + Note: While you can directly use the `StreamPrompting` class, it's designed to be extensible. Thus, you can create + subclasses to further customize behavior for specific prompting scenarios or requirements. + """ + + roles: List[str] = pydantic.Field( + ..., + title="Roles", + description="A list of roles in the StreamPrompting scenario. Immuatable.", + allow_mutation=False, + ) + + messages: List[str] = pydantic.Field( + ..., + title="Messages", + description="A list of messages in the StreamPrompting scenario. Immutable.", + allow_mutation=False, + ) + + required_hash_fields: List[str] = pydantic.Field( + ["messages"], + title="Required Hash Fields", + description="A list of required fields for the hash.", + allow_mutation=False, + ) + + completion: str = pydantic.Field( + "", + title="Completion", + description="Completion status of the current StreamPrompting object. This attribute is mutable and can be updated.", + ) + + async def process_streaming_response(self, response: StreamingResponse): + """ + `process_streaming_response` is an asynchronous method designed to process the incoming streaming response from the + Bittensor network. It's the heart of the StreamPrompting class, ensuring that streaming tokens, which represent + prompts or messages, are decoded and appropriately managed. + + As the streaming response is consumed, the tokens are decoded from their 'utf-8' encoded format, split based on + newline characters, and concatenated into the `completion` attribute. This accumulation of decoded tokens in the + `completion` attribute allows for a continuous and coherent accumulation of the streaming content. + + Args: + response: The streaming response object containing the content chunks to be processed. Each chunk in this + response is expected to be a set of tokens that can be decoded and split into individual messages or prompts. + """ + if self.completion is None: + self.completion = "" + bt.logging.debug("Processing streaming response (StreamingSynapse base class).") + async for chunk in response.content.iter_any(): + bt.logging.debug(f"Processing chunk: {chunk}") + tokens = chunk.decode("utf-8").split("\n") + for token in tokens: + bt.logging.debug(f"--processing token: {token}") + if token: + self.completion += token + bt.logging.debug(f"yielding tokens {tokens}") + yield tokens + + def deserialize(self) -> str: + """ + Deserializes the response by returning the completion attribute. + + Returns: + str: The completion result. + """ + return self.completion + + def extract_response_json(self, response: StreamingResponse) -> dict: + """ + `extract_response_json` is a method that performs the crucial task of extracting pertinent JSON data from the given + response. The method is especially useful when you need a detailed insight into the streaming response's metadata + or when debugging response-related issues. + + Beyond just extracting the JSON data, the method also processes and structures the data for easier consumption + and understanding. For instance, it extracts specific headers related to dendrite and axon, offering insights + about the Bittensor network's internal processes. The method ultimately returns a dictionary with a structured + view of the extracted data. + + Args: + response: The response object from which to extract the JSON data. This object typically includes headers and + content which can be used to glean insights about the response. + + Returns: + dict: A structured dictionary containing: + - Basic response metadata such as name, timeout, total_size, and header_size. + - Dendrite and Axon related information extracted from headers. + - Roles and Messages pertaining to the current StreamPrompting instance. + - The accumulated completion. + """ + headers = { + k.decode("utf-8"): v.decode("utf-8") + for k, v in response.__dict__["_raw_headers"] + } + + def extract_info(prefix): + return { + key.split("_")[-1]: value + for key, value in headers.items() + if key.startswith(prefix) + } + + return { + "name": headers.get("name", ""), + "timeout": float(headers.get("timeout", 0)), + "total_size": int(headers.get("total_size", 0)), + "header_size": int(headers.get("header_size", 0)), + "dendrite": extract_info("bt_header_dendrite"), + "axon": extract_info("bt_header_axon"), + "roles": self.roles, + "messages": self.messages, + "completion": self.completion, + } diff --git a/bitagent_subnet-main/docs/tasks.ipynb b/bitagent_subnet-main/docs/tasks.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..8f6c17f6a0ac4eb528839e9ef0f45cd0b8475ed0 --- /dev/null +++ b/bitagent_subnet-main/docs/tasks.ipynb @@ -0,0 +1,411 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "NETUID=76\n", + "NETWORK=\"test\"\n", + "WALLET_NAME=\"coldkey\"\n", + "HOTKEY_NAME=\"hotkey\"\n", + "TASK_API_URL=\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "import requests\n", + "import bittensor as bt \n", + "from rich import print as rprint\n", + "from typing import Optional,List\n", + "from bitagent.schemas.conversation import Conversation\n", + "from bitagent.schemas.tool import Tool\n", + "subnet = bt.metagraph(netuid=NETUID, network=NETWORK)\n", + "\n", + "# Wallet and validator setup\n", + "vali_wallet = bt.wallet(name=WALLET_NAME, hotkey=HOTKEY_NAME)\n", + "vali_dendrite = bt.dendrite(wallet=vali_wallet)\n", + "\n", + "# the request protocol\n", + "class QnATask(bt.Synapse):\n", + " urls: List[str] = [] # not used right now - when enabled would allow users to pass in URLs for content\n", + " datas: List[dict] = [] # used to pass in relevant context, could be a company knowledge base or a set of wikipedia pages\n", + " tools: List[dict] = [] # used to pass in tools to be leveraged in answering user query\n", + " prompt: str = \"\" # the query / prompt\n", + " response: Optional[dict] = {}\n", + " timeout: Optional[float] = 3.0\n", + " miner_uids: Optional[List[int]] = [] # put our TOP miner into the network as the miner to query (if empty list, a random list of miners will be selected)\n", + " notes = \"\"\n", + " message_history: Conversation = []\n", + " \n", + " \n", + " def toJSON(self):\n", + " return {\"prompt\": self.prompt, \n", + " \"urls\": self.urls, \n", + " \"datas\": self.datas, \n", + " \"tools\": self.tools,\n", + " \"response\": self.response,\n", + " \"notes\": self.notes,\n", + " \"message_history\": self.message_history,\n", + " \"miner_uids\": self.miner_uids,\n", + " \"dendrite_process_time\": self.dendrite.process_time,\n", + " \"dendrite_status_code\": self.dendrite.status_code,\n", + " \"axon_status_code\": self.axon.status_code,}\n", + "\n", + "qna_task = (\"generated_qna\",1)\n", + "pet_tricks_task = (\"generated_logic_qna\",6)\n", + "api_selection_task = (\"generated_logic_qna\",8)\n", + "summarization_task = (\"summarization\",1)\n", + "tool_call_task = (\"tool_call\",1)\n", + "tool_gen_task = (\"tool_gen\",1)\n", + "convo_task = (\"conversation\",1)\n", + "filter_task = (\"unfilter\",1)\n", + "\n", + "def get_top_miner_uid(subnet):\n", + " return subnet.I.argmax()\n", + "\n", + "def get_task(task_id, sub_task_id):\n", + " task_json = requests.post(f'{TASK_API_URL}/get_new_task', json={\"task_name\": task_id, \"sub_task_id\": sub_task_id}).json()\n", + " task_json = task_json['task']\n", + " task = QnATask(prompt=task_json['prompt'], message_history=Conversation.from_list(task_json['message_history']), tools=[Tool(**tool) for tool in task_json['tools']], datas=task_json['datas'], notes=task_json['notes']) # TODO , tools=task_json['task']['tools'])\n", + " return task, task_json[\"task_id\"]\n", + "\n", + "def get_eval(task_id, response):\n", + " return requests.post(f'{TASK_API_URL}/evaluate_task_response', json={\"task_id\": task_id, \"response\": response}).json()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "\n", + "def forward(subnet, vali_dendrite, top_miner_uids, task):\n", + " responses = vali_dendrite.query(\n", + " axons=[subnet.axons[uid] for uid in top_miner_uids],\n", + " synapse=task,\n", + " deserialize=False,\n", + " timeout=5*task.timeout,\n", + " )\n", + " return responses" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "responses = []\n", + "tasks = []\n", + "for task_type in [qna_task, pet_tricks_task, api_selection_task, summarization_task, tool_seq_selection_task, tool_gen_task, tool_call_task, convo_task, filter_task]: # going through each task type\n", + " task, task_id = get_task(*task_type)\n", + " tasks.append(task)\n", + " results = forward(subnet, vali_dendrite, [get_top_miner_uid(subnet)], task)\n", + "\n", + " for result in results:\n", + " try:\n", + " resp = result.response['response']\n", + " responses.append(resp)\n", + " except Exception as e:\n", + " print(e)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "prompt: Which two running backs does the New England Patriots have as pass-catching options, and who could potentially be a suitable traditional rusher to fill the void?\n", + "messages=[]\n", + "Tools: []\n", + "Response: The New England Patriots have Dion Lewis and James White as pass-catching options. LeGarrette Blount could potentially be a suitable traditional rusher to fill the void.\n" + ] + } + ], + "source": [ + "# QnA (corpus) Task \n", + "qna_task_synapse = tasks[0]\n", + "qna_task_res = responses[0]\n", + "\n", + "print(\"prompt: \", qna_task_synapse.prompt)\n", + "print(qna_task_synapse.message_history)\n", + "print(f'Tools: {qna_task_synapse.tools}')\n", + "print(f'Response: {qna_task_res}')\n" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "prompt: Given the following Trick Descriptions with numerical IDs:\n", + " 1 - 'Stay' - The dog remains in a specific position, such as sitting or lying down, until released by the handler. This trick is essential for safety and control, as it helps prevent the dog from running off or getting into dangerous situations.\n", + "2 - 'Wave' - The dog lifts its paw and moves it in a waving motion, typically as a gesture of saying hello or goodbye. This trick requires the dog to understand and perform a specific physical gesture on command, demonstrating its ability to engage in complex social behaviors.\n", + "3 - 'Heel' - The dog walks closely beside the handler's leg, maintaining pace and position regardless of the handler's movements. This advanced obedience command is crucial for safe and controlled walking in public spaces, showcasing the dog's discipline and focus on the handler amidst distractions.\n", + "4 - 'Lie Down' - The dog moves from a standing or sitting position to lying flat on its belly with the legs extended. This command is fundamental in obedience training, helping in calming the dog or preparing it for more advanced tricks.\n", + "5 - 'Skip' - The dog hops forward in a skipping motion, alternating its paws in a rhythmic pattern. This advanced trick combines coordination, rhythm, and agility, offering a visually amusing and energetic display of the dog's physical capabilities.\n", + "\n", + " And given this unique and purposefully ambiguous command: \n", + " 'Jump and Dance the Sky Melody'\n", + "\n", + " Which Trick ID (provide numerical number only) is being requested? \n", + " Trick ID: \n", + "messages=[]\n", + "Tools: []\n", + "Response: 5\n" + ] + } + ], + "source": [ + "# Pet tricks task\n", + "pet_task_synapse = tasks[1]\n", + "pet_task_res = responses[1]\n", + "\n", + "print(\"prompt: \", pet_task_synapse.prompt)\n", + "print(pet_task_synapse.message_history)\n", + "print(f'Tools: {pet_task_synapse.tools}')\n", + "print(f'Response: {pet_task_res}')" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "prompt: Given the following Tool Descriptions with numerical IDs:\n", + " ['1 - API to read, write, and format Google Sheets data', '2 - Free geo ip information, no registration required. 15k/hour rate limit', '3 - VirusTotal File/URL Analysis', '4 - User management and authentication', '5 - Free JSON storage for small projects']\n", + "\n", + " And given this unique and purposefully ambiguous tool name: \n", + " ' Cryptic Cells'\n", + "\n", + " Which Tool ID (provide numerical number only) is being requested? \n", + " Tool ID: \n", + "messages=[]\n", + "Tools: []\n", + "Response: 1\n" + ] + } + ], + "source": [ + "# api selection\n", + "api_task_synapse = tasks[2]\n", + "api_task_res = responses[2]\n", + "\n", + "print(\"prompt: \", api_task_synapse.prompt)\n", + "print(api_task_synapse.message_history)\n", + "print(f'Tools: {api_task_synapse.tools}')\n", + "print(f'Response: {api_task_res}')" + ] + }, + { + "cell_type": "code", + "execution_count": 24, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "prompt: \n", + "messages=[ChatMessage(role=, content='Okay, thats fine. I heard the food at https://www.pizzafactory.store is amazing. Could you analyze this website and tell me what ingredients they offer for their pizzas?'), ChatMessage(role=, content='Certainly! Heres the list of ingredients available on the Pizza Factory website: \\n\\n[\\n \"Cheese\",\\n \"Tomatoes\",\\n \"Mushrooms\",\\n \"Onions\",\\n \"Peppers\",\\n \"Sausage\",\\n \"Ham\",\\n \"Salami\",\\n \"Olives\",\\n \"Pineapple\",\\n \"Beef\",\\n \"Garlic\",\\n \"Basil\",\\n \"Artichokes\"\\n]')]\n", + "Tools: [{'name': 'analyze_website', 'description': 'Analyze the performance and content of a website', 'arguments': {'url': {'required': True, 'type': 'string', 'description': 'The URL of the website to analyze'}, 'features': {'required': True, 'type': 'array', 'description': 'The features to analyze on the website'}}}]\n", + "Response: [{\"role\": \"tool call\", \"content\": {\"name\": \"analyze_website\", \"arguments\": {\"url\": \"https://www.pizzafactory.store\", \"features\": [\"ingredients\"]}}}, {\"role\": \"assistant\", \"content\": \"This website offers a variety of information about pizza ingredients. The website's menu section details their numerous pizza options, with different bases and many toppings. There's also information on additional extras, like different types of cheese and sauces.\"}]\n" + ] + } + ], + "source": [ + "# Summarization\n", + "task_synapse = tasks[3]\n", + "task_res = responses[3]\n", + "\n", + "print(\"prompt: \", task_synapse.prompt)\n", + "print(task_synapse.message_history)\n", + "print(f'Tools: {task_synapse.tools}')\n", + "print(f'Response: {task_res}')" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "prompt: \"Seeking some exciting action films to view, do you mind assisting me?\"\n", + "messages=[]\n", + "Tools: []\n", + "Response: {\"name\": \"get_action_movies\", \"arguments\": {\"movie_genre\": {\"required\": true, \"type\": \"string\", \"description\": \"The movie genre you want to search for. e.g. 'action', 'comedy', etc.\"}, \"movie_mood\": {\"required\": false, \"type\": \"string\", \"description\": \"The mood of the movie you want. e.g. 'exciting', 'heartwarming', etc.\"}, \"year\": {\"required\": false, \"type\": \"integer\", \"description\": \"The year of release, can be partial, e.g. 2020s for last decade.\"}}, \"description\": \"A function to help you find movies according to your preferences. It will return a list of movies based on the given genre, and if provided, the mood and release year.\"}\n" + ] + } + ], + "source": [ + "# Tool Generation\n", + "task_synapse = tasks[4]\n", + "task_res = responses[4]\n", + "\n", + "print(\"prompt: \", task_synapse.prompt)\n", + "print(task_synapse.message_history)\n", + "print(f'Tools: {task_synapse.tools}')\n", + "print(f'Response: {task_res}')" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "prompt: \n", + "messages=[ChatMessage(role=, content='Ok, then can you tell me how long it might take to deliver this package from New York to Seattle via ground shipping? Do you have access to that information?'), ChatMessage(role=, content='Sure, I can check the estimated delivery time for you.')]\n", + "Tools: [{'name': 'calculate_shipping_cost', 'description': 'Calculate the shipping cost for a package', 'arguments': {'package_details': {'required': True, 'type': 'object', 'description': ''}, 'origin': {'required': True, 'type': 'string', 'description': 'The origin address of the package'}, 'destination': {'required': True, 'type': 'string', 'description': 'The destination address of the package'}}}]\n", + "Response: [{\"role\": \"tool call\", \"content\": {\"name\": \"calculate_shipping_cost\", \"arguments\": {\"package_details\": {}, \"origin\": \"New York\", \"destination\": \"Seattle\"}}}, {\"role\": \"assistant\", \"content\": \"I can use the 'calculate_shipping_cost' tool to find out the delivery time for a package being shipped from New York to Seattle. This tool will provide the duration it might take for the delivery.\"}]\n" + ] + } + ], + "source": [ + "# Tool Calling\n", + "task_synapse = tasks[5]\n", + "task_res = responses[5]\n", + "\n", + "print(\"prompt: \", task_synapse.prompt)\n", + "print(task_synapse.message_history)\n", + "print(f'Tools: {task_synapse.tools}')\n", + "print(f'Response: {task_res}')" + ] + }, + { + "cell_type": "code", + "execution_count": 34, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "prompt: \n", + "messages=[ChatMessage(role=, content=' What is the best approach to employ MATLAB in generating an exclusive, intricate motto for our upgraded artificial intelligence-driven virtual assistant? The slogan must encapsulate the core of the product and pose a difficulty for AI systems to interpret. Can you offer code that leverages machine learning techniques to produce a myriad of slogans, subsequently choosing the most fitting one based on pre-established standards?')]\n", + "Tools: []\n", + "Response: To employ MATLAB in generating an exclusive, intricate motto for an upgraded artificial intelligence-driven virtual assistant, you can follow these steps:\n", + "\n", + "1. Define the goal: The goal is to generate a motto that represents the core of the product and is difficult for AI systems to interpret.\n", + "\n", + "2. Prepare the data set: Collect a list of phrases or words that represent the core of the product and are challenging for AI systems to interpret.\n", + "\n", + "3. Create a customized algorithm: Develop a customized algorithm in MATLAB that combines the phrases or words in creative and intricate ways to generate an appealing motto.\n", + "\n", + "Here is a sample code using the built-in MATLAB function \"randperm\" to generate a random permutation of the phrases:\n", + "\n", + "```matlab\n", + "%define the list of phrases or words\n", + "phrases = ['intelligence', 'artificial', 'virtual', 'assistant', 'unique', 'revolutionary', 'cutting-edge'];\n", + "\n", + "%create a random permutation of the phrases\n", + "perm = randperm(length(phrases));\n", + "\n", + "%generate a motto using the permutation\n", + "motto = ['The ' phrases{perm(1)} ' of the ' phrases{perm(2)} ' ' phrases{perm(3)} ' ' phrases{perm(4)} ' ' phrases{perm(5)} ' ' phrases{perm(6)}' ];\n", + "\n", + "%display the generated motto\n", + "motto_str = motto{1} + ' ' + motto{2} + ' ' + motto{3} + ' ' + motto{4} + ' ' + motto{5} + ' ' + motto{6};\n", + "disp(motto_str);\n", + "```\n", + "\n", + "This code will generate a random permutation of the phrases and then use those permuted phrases to create a motto. For example, if the list of phrases was the same as given above, the generated motto could be:\n", + "\n", + "\"The virtual of the artificial assistant unique revolution cutting-edge.\"\n", + "\n", + "4. Evaluate the motto: Evaluate the motto using pre-established standards (such as grammar, coherence, and uniqueness). If the generated motto meets the predefined standards, accept it as the final motto. If not, repeat steps 2-4 until the desired motto is generated.\n", + "\n", + "By following these steps, you can create an intricate and unique motto for your upgraded artificial intelligence-driven virtual assistant.\n" + ] + } + ], + "source": [ + "# Conversation Task\n", + "task_synapse = tasks[6]\n", + "task_res = responses[6]\n", + "\n", + "print(\"prompt: \", task_synapse.prompt)\n", + "print(task_synapse.message_history)\n", + "print(f'Tools: {task_synapse.tools}')\n", + "print(f'Response: {task_res}')" + ] + }, + { + "cell_type": "code", + "execution_count": 35, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "prompt: What made Steve Jobs appear so difficult?\n", + "\n", + "\n", + "messages=[]\n", + "Tools: []\n", + "Response: Some say Steve Jobs' difficult reputation stemmed from his perfectionism, which could manifest as impatience, intensity, and a short temper, especially later in life. He was known for his unwavering vision and unwavering, uncompromising stance on design simplicity. \n", + "\n", + "His intensity could be off-putting to some, but his vision and drive are credited for the immense success of Apple Inc. and the legacy he left.\n" + ] + } + ], + "source": [ + "# Unfilter Task\n", + "task_synapse = tasks[7]\n", + "task_res = responses[7]\n", + "\n", + "print(\"prompt: \", task_synapse.prompt)\n", + "print(task_synapse.message_history)\n", + "print(f'Tools: {task_synapse.tools}')\n", + "print(f'Response: {task_res}')" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "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.12" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} diff --git a/bitagent_subnet-main/docs/what_are_subnets.md b/bitagent_subnet-main/docs/what_are_subnets.md new file mode 100644 index 0000000000000000000000000000000000000000..eee24ce2fe0c54e234dd3b53e80a6b53244c01f4 --- /dev/null +++ b/bitagent_subnet-main/docs/what_are_subnets.md @@ -0,0 +1,27 @@ +# What is Bittensor? +Bittensor is a network where computers validate the work that other computers contribute to the network - the work what is most valuable to the collective will be rewarded + +Bittensor is a catalyst to the open-source developers and smaller AI research labs now have a financial incentive for fine-tuning open foundational models + +Bittensor is a library of machine intelligence that continuously grows and shares knowledge amongst peers + +# What is a subnet? + +Bittensor is releasing its own language for creating incentive mechanisms. This allows developers to build incentive systems on Bittensor, tapping into our web of intelligence to develop markets of the developer’s choosings + +Subnet 1, an incentive system for machine intelligence production, showcases the enormous potential of markets to procure huge amounts of resources. Releasing user-created subnets is set to create a cambrian explosion of additional resources into the Bittensor ecosystem + +# Why should you care? + +As an open-source developer, you now have the ability to write your own incentive mechanisms without creating an entirely new chain. By tapping into Bittensor’s network of intelligence, you can incentivize AI models from all over the world to perform tasks of your choosing (i.e., image generation, storage, compute access, etc.) - the possibilities are truly endless + +The release of subnets also offers the potential to pull these tools into a shared network, making all the ingredients necessary to create intelligence available within one network, governed by one token + +You get to play a vital role in helping bootstrap what could one day become one of the most powerful networks in the world - and you make money by doing so! + +By incentivizing developers to create their own markets, Bittensor is set to become a one-stop-shop for those seeking all the compute requirements for building unstoppable applications on top of an incentivized infrastructure + +# Deeper dive +Check out the Bittensor about page [here](https://bittensor.com/about) for more details about what the bittensor paradigm is and why subnets are revolutionary technology. + +Also see our [linktree](https://linktr.ee/opentensor) for more information. \ No newline at end of file diff --git a/bitagent_subnet-main/min_compute.yml b/bitagent_subnet-main/min_compute.yml new file mode 100644 index 0000000000000000000000000000000000000000..7cbcae4292f8408455598dbabe93d2153a70984d --- /dev/null +++ b/bitagent_subnet-main/min_compute.yml @@ -0,0 +1,78 @@ +# Use this document to specify the minimum compute requirements. +# This document will be used to generate a list of recommended hardware for your subnet. + +# This is intended to give a rough estimate of the minimum requirements +# so that the user can make an informed decision about whether or not +# they want to run a miner or validator on their machine. + +# NOTE: Specification for miners may be different from validators + +version: '0.1.05' # update this version key as needed, ideally should match your release version + +compute_spec: + + miner: + + cpu: + min_cores: 4 # Minimum number of CPU cores + min_speed: 2.5 # Minimum speed per core (GHz) + recommended_cores: 8 # Recommended number of CPU cores + recommended_speed: 3.5 # Recommended speed per core (GHz) + architecture: "x86_64" # Architecture type (e.g., x86_64, arm64) + + gpu: + required: True # Does the application require a GPU? + min_vram: 24 # Minimum GPU VRAM (GB) + recommended_vram: 48 # Recommended GPU VRAM (GB) + recommended_gpu: "NVIDIA A6000" # provide a recommended GPU to purchase/rent + + memory: + min_ram: 32 # Minimum RAM (GB) + min_swap: 6 # Minimum swap space (GB) + ram_type: "DDR4" # RAM type (e.g., DDR4, DDR3, etc.) + + storage: + min_space: 32 # Minimum free storage space (GB) + recommended_space: 100 # Recommended free storage space (GB) + type: "SSD" # Preferred storage type (e.g., SSD, HDD) + + os: + name: "Ubuntu" # Name of the preferred operating system(s) + version: 20.04 # Version of the preferred operating system(s) + + validator: + + cpu: + min_cores: 4 # Minimum number of CPU cores + min_speed: 2.5 # Minimum speed per core (GHz) + recommended_cores: 8 # Recommended number of CPU cores + recommended_speed: 3.5 # Recommended speed per core (GHz) + architecture: "x86_64" # Architecture type (e.g., x86_64, arm64) + + gpu: + required: True # Does the application require a GPU? + min_vram: 48 # Minimum GPU VRAM (GB) + recommended_vram: 80 # Recommended GPU VRAM (GB) + recommended_gpu: "NVIDIA A100" # provide a recommended GPU to purchase/rent + notes: "Validators will run two models: a small Mistral 7B model AND another 8B param (at max) model from the miner's HF." + + memory: + min_ram: 32 # Minimum RAM (GB) + recommended_ram: 64 # Recommended RAM (GB) + min_swap: 4 # Minimum swap space (GB) + recommended_swap: 8 # Recommended swap space (GB) + ram_type: "DDR4" # RAM type (e.g., DDR4, DDR3, etc.) + + storage: + min_space: 200 # Minimum free storage space (GB) + recommended_space: 300 # Recommended free storage space (GB) + type: "SSD" # Preferred storage type (e.g., SSD, HDD) + + os: + name: "Ubuntu" # Name of the preferred operating system(s) + version: 20.04 # Version of the preferred operating system(s) + +network_spec: + bandwidth: + download: 100 # Minimum download bandwidth (Mbps) + upload: 20 # Minimum upload bandwidth (Mbps) \ No newline at end of file diff --git a/bitagent_subnet-main/neurons/__init__.py b/bitagent_subnet-main/neurons/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/bitagent_subnet-main/neurons/miner.py b/bitagent_subnet-main/neurons/miner.py new file mode 100644 index 0000000000000000000000000000000000000000..d34b0216da6a4755ced564a9905658cc8e487eef --- /dev/null +++ b/bitagent_subnet-main/neurons/miner.py @@ -0,0 +1,251 @@ +# The MIT License (MIT) +# Copyright © 2023 Yuma Rao +# Copyright © 2023 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import time +import importlib +from typing import Tuple +import bittensor as bt +from rich.console import Console + +# Bittensor Miner Template: +import bitagent +# Sync calls set weights and also resyncs the metagraph. +from common.utils.config import add_args as util_add_args +from common.utils.config import config as util_config + + +# import base miner class which takes care of most of the boilerplate +from common.base.miner import BaseMinerNeuron +rich_console = Console() + +class Miner(BaseMinerNeuron): + """ + BitAgent miner neuron class. You may also want to override the blacklist and priority functions according to your needs. + + This class inherits from the BaseMinerNeuron class, which in turn inherits from BaseNeuron. The BaseNeuron class takes care of routine tasks such as setting up wallet, subtensor, metagraph, logging directory, parsing config, etc. You can override any of the methods in BaseNeuron if you need to customize the behavior. + + This class provides reasonable default behavior for a miner such as blacklisting unrecognized hotkeys, prioritizing requests based on stake, and forwarding requests to the forward function. Modify, if you need to define custom capability. + """ + + def __init__(self, config=None): + self.forward_capabilities = [ + {'forward': self.forward_for_task, 'blacklist': self.blacklist_for_task, 'priority': self.priority_for_task}, + {'forward': self.forward_for_result, 'blacklist': self.blacklist_for_result, 'priority': self.priority_for_result}, + {'forward': self.forward_for_alive, 'blacklist': self.blacklist_for_alive, 'priority': self.priority_for_alive}, + {'forward': self.forward_for_get_hf_model_name, 'blacklist': self.blacklist_for_get_hf_model_name, 'priority': self.priority_for_get_hf_model_name}, + {'forward': self.forward_for_get_hf_run_model_name, 'blacklist': self.blacklist_for_get_hf_run_model_name, 'priority': self.priority_for_get_hf_run_model_name}, + {'forward': self.forward_for_set_hf_model_name, 'blacklist': self.blacklist_for_set_hf_model_name, 'priority': self.priority_for_set_hf_model_name}, + ] + if not config: + config = util_config(self) + + super(Miner, self).__init__(config=config) + + # Dynamic module import based on the 'miner' argument + miner_name = f"bitagent.miners.{config.miner}_miner" # if config and config.miner else "bitagent.miners.t5_miner" + miner_module = importlib.import_module(miner_name) + + self.miner_init = miner_module.miner_init + self.miner_process = miner_module.miner_process + + self.miner_init(self, config) + + async def forward_for_task( + self, synapse: bitagent.protocol.QueryTask + ) -> bitagent.protocol.QueryTask: + """ + Processes the incoming BitAgent synapse and returns response. + + Args: + synapse (bitagent.protocol.QueryTask): The synapse object containing the messages and tools. + + Returns: + bitagent.protocol.QueryTask: The synapse object with the 'response' field set to the generated response. + + """ + + synapse = self.miner_process(self, synapse) + + return synapse + + async def forward_for_result( + self, synapse: bitagent.protocol.QueryResult + ) -> bitagent.protocol.QueryResult: + if self.config.logging.debug: + rich_console.print(synapse.results) + return synapse + + async def forward_for_alive( + self, synapse: bitagent.protocol.IsAlive + ) -> bitagent.protocol.IsAlive: + synapse.response = True + return synapse + + async def forward_for_get_hf_model_name( + self, synapse: bitagent.protocol.GetHFModelName + ) -> bitagent.protocol.GetHFModelName: + synapse.hf_model_name = self.config.miner_hf_model_name_to_submit + return synapse + + async def forward_for_get_hf_run_model_name( + self, synapse: bitagent.protocol.GetHFRunModelName + ) -> bitagent.protocol.GetHFRunModelName: + synapse.hf_run_model_name = self.get_top_miner_HF_model_name() + return synapse + + async def forward_for_set_hf_model_name( + self, synapse: bitagent.protocol.SetHFModelName + ) -> bitagent.protocol.SetHFModelName: + #self.save_top_model_from_validator(synapse.hf_model_name, synapse.validator_uid) + return synapse + + async def __blacklist(self, synapse: bt.Synapse) -> Tuple[bool, str]: + """ + Determines whether an incoming request should be blacklisted and thus ignored. Your implementation should + define the logic for blacklisting requests based on your needs and desired security parameters. + + Blacklist runs before the synapse data has been deserialized (i.e. before synapse.data is available). + The synapse is instead contructed via the headers of the request. It is important to blacklist + requests before they are deserialized to avoid wasting resources on requests that will be ignored. + + Args: + synapse (bitagent.protocol.QueryTask): A synapse object constructed from the headers of the incoming request. + + Returns: + Tuple[bool, str]: A tuple containing a boolean indicating whether the synapse's hotkey is blacklisted, + and a string providing the reason for the decision. + + This function is a security measure to prevent resource wastage on undesired requests. It should be enhanced + to include checks against the metagraph for entity registration, validator status, and sufficient stake + before deserialization of synapse data to minimize processing overhead. + + Example blacklist logic: + - Reject if the hotkey is not a registered entity within the metagraph. + - Consider blacklisting entities that are not validators or have insufficient stake. + + In practice it would be wise to blacklist requests from entities that are not validators, or do not have + enough stake. This can be checked via metagraph.S and metagraph.validator_permit. You can always attain + the uid of the sender via a metagraph.hotkeys.index( synapse.dendrite.hotkey ) call. + + Otherwise, allow the request to be processed further. + """ + + # Check if the key has validator permit + if self.config.blacklist.force_validator_permit: + if synapse.dendrite.hotkey in self.metagraph.hotkeys: + uid = self.metagraph.hotkeys.index(synapse.dendrite.hotkey) + if not self.metagraph.validator_permit[uid]: + return True, "validator permit required" + else: + return True, "validator permit required, but hotkey not registered" + + if synapse.dendrite.hotkey not in self.metagraph.hotkeys: + # Ignore requests from unrecognized entities. + bt.logging.trace( + f"Blacklisting unrecognized hotkey {synapse.dendrite.hotkey}" + ) + return True, "Unrecognized hotkey" + + bt.logging.trace( + f"Not Blacklisting recognized hotkey {synapse.dendrite.hotkey}" + ) + return False, "Hotkey recognized!" + + async def blacklist_for_task(self, synapse: bitagent.protocol.QueryTask) -> Tuple[bool, str]: + return await self.__blacklist(synapse) + + async def blacklist_for_result(self, synapse: bitagent.protocol.QueryResult) -> Tuple[bool, str]: + return await self.__blacklist(synapse) + + async def blacklist_for_alive(self, synapse: bitagent.protocol.IsAlive) -> Tuple[bool, str]: + return await self.__blacklist(synapse) + + async def blacklist_for_get_hf_model_name(self, synapse: bitagent.protocol.GetHFModelName) -> Tuple[bool, str]: + return await self.__blacklist(synapse) + + async def blacklist_for_get_hf_run_model_name(self, synapse: bitagent.protocol.GetHFRunModelName) -> Tuple[bool, str]: + return await self.__blacklist(synapse) + + async def blacklist_for_set_hf_model_name(self, synapse: bitagent.protocol.SetHFModelName) -> Tuple[bool, str]: + return await self.__blacklist(synapse) + + async def __priority(self, synapse: bt.Synapse) -> float: + """ + The priority function determines the order in which requests are handled. More valuable or higher-priority + requests are processed before others. You should design your own priority mechanism with care. + + This implementation assigns priority to incoming requests based on the calling entity's stake in the metagraph. + + Args: + synapse (bitagent.protocol.QueryTask): The synapse object that contains metadata about the incoming request. + + Returns: + float: A priority score derived from the stake of the calling entity. + + Miners may recieve messages from multiple entities at once. This function determines which request should be + processed first. Higher values indicate that the request should be processed first. Lower values indicate + that the request should be processed later. + + Example priority logic: + - A higher stake results in a higher priority value. + """ + caller_uid = self.metagraph.hotkeys.index( + synapse.dendrite.hotkey + ) # Get the caller index. + prirority = float( + self.metagraph.S[caller_uid] + ) # Return the stake as the priority. + bt.logging.trace( + f"Prioritizing {synapse.dendrite.hotkey} with value: ", prirority + ) + return prirority + + async def priority_for_task(self, synapse: bitagent.protocol.QueryTask) -> float: + return await self.__priority(synapse) + + async def priority_for_result(self, synapse: bitagent.protocol.QueryResult) -> float: + return await self.__priority(synapse) + + async def priority_for_alive(self, synapse: bitagent.protocol.IsAlive) -> float: + return await self.__priority(synapse) + + async def priority_for_get_hf_model_name(self, synapse: bitagent.protocol.GetHFModelName) -> float: + return await self.__priority(synapse) + + async def priority_for_get_hf_run_model_name(self, synapse: bitagent.protocol.GetHFRunModelName) -> float: + return await self.__priority(synapse) + + async def priority_for_set_hf_model_name(self, synapse: bitagent.protocol.SetHFModelName) -> float: + return await self.__priority(synapse) + + async def forward(self, synapse: bt.Synapse) -> bt.Synapse: + # not being used but required by ABC + pass + + # no idea what to save for a miner + def save_state(self): + pass + def load_state(self): + pass + +# This is the main function, which runs the miner. +if __name__ == "__main__": + with Miner() as miner: + while True: + bt.logging.info("Miner running...", time.time()) + time.sleep(15) diff --git a/bitagent_subnet-main/neurons/validator.py b/bitagent_subnet-main/neurons/validator.py new file mode 100644 index 0000000000000000000000000000000000000000..c8c4ab6cd286dae35116846d25bd15301b8647d2 --- /dev/null +++ b/bitagent_subnet-main/neurons/validator.py @@ -0,0 +1,99 @@ +# The MIT License (MIT) +# Copyright © 2023 Yuma Rao +# Copyright © 2023 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import os +import time +import bitagent +from typing import Tuple + +# Bittensor +import bittensor as bt + +# Bittensor Validator Template: +from bitagent.validator import forward, initiate_validator + +# import base validator class which takes care of most of the boilerplate +from common.base.validator import BaseValidatorNeuron + +class Validator(BaseValidatorNeuron): + """ + BitAgent validator neuron class. + + This class inherits from the BaseValidatorNeuron class, which in turn inherits from BaseNeuron. The BaseNeuron class takes care of routine tasks such as setting up wallet, subtensor, metagraph, logging directory, parsing config, etc. You can override any of the methods in BaseNeuron if you need to customize the behavior. + + This class provides reasonable default behavior for a validator such as keeping a moving average of the scores of the miners and using them to set weights at the end of each epoch. Additionally, the scores are reset for new hotkeys at the end of each epoch. + """ + + def __init__(self, config=None): + super(Validator, self).__init__(config=config) + + bt.logging.info("load_state()") + self.load_state() + + bt.logging.info("initiate_validator()") + initiate_validator(self) + bt.logging.debug(f"spec_version: {self.spec_version}") + if self.config.neuron.visible_devices: + print(f"Setting CUDA_VISIBLE_DEVICES to: {self.config.neuron.visible_devices}") + os.environ["CUDA_VISIBLE_DEVICES"] = self.config.neuron.visible_devices + else: + if os.environ.get("CUDA_VISIBLE_DEVICES"): + del os.environ["CUDA_VISIBLE_DEVICES"] + + # check if the sglang python executable exists + python_path = f"{os.getcwd()}/.venvsglang/bin/python" + if not os.path.exists(python_path): + raise FileNotFoundError(f"The required sglang python executable does not exist at {python_path}") + bt.logging.info(f"sglang python executable found at {python_path}") + + + async def forward(self, synapse: bitagent.protocol.QueryTask=None): + """ + Validator forward pass. Consists of: + - Generating the query + - Querying the miners + - Getting the responses + - Rewarding the miners + - Updating the scores + """ + return await forward(self, synapse) + + async def forward_fn(self, synapse: bitagent.protocol.QueryTask=None) -> bitagent.protocol.QueryTask: + return await self.forward(synapse) + + async def blacklist_fn(self, synapse: bitagent.protocol.QueryTask) -> Tuple[bool, str]: + # Add hotkeys to blacklist here as needed + # blacklist the hotkeys mining on the subnet to prevent any potential issues + #hotkeys_to_blacklist = [h for i,h in enumerate(self.hotkeys) if self.metagraph.S[i] < 20000 and h != self.wallet.hotkey.ss58_address] + #if synapse.dendrite.hotkey in hotkeys_to_blacklist: + # return True, "Blacklisted hotkey - miners can't connect, use a diff hotkey." + return False, "" + + async def priority_fn(self, synapse: bitagent.protocol.QueryTask) -> float: + # high priority for organic traffic + return 1000000.0 + +# The main function parses the configuration and runs the validator. +if __name__ == "__main__": + with Validator() as validator: + while True: + bt.logging.info("Validator running...", time.time()) + time.sleep(15) + if validator.should_exit: + bt.logging.warning("Ending validator...") + break diff --git a/bitagent_subnet-main/requirements.sglang.txt b/bitagent_subnet-main/requirements.sglang.txt new file mode 100644 index 0000000000000000000000000000000000000000..a2538448b0f12fd483aa89ab314f1a0cec5158bf --- /dev/null +++ b/bitagent_subnet-main/requirements.sglang.txt @@ -0,0 +1,135 @@ +aiohappyeyeballs==2.4.3 +aiohttp==3.11.2 +aiosignal==1.3.1 +annotated-types==0.7.0 +anthropic==0.39.0 +anyio==4.6.2.post1 +asttokens==2.4.1 +async-timeout==5.0.1 +attrs==24.2.0 +certifi==2024.8.30 +charset-normalizer==3.4.0 +click==8.1.7 +cloudpickle==3.1.0 +compressed-tensors==0.6.0 +datasets==3.1.0 +decorator==5.1.1 +dill==0.3.8 +diskcache==5.6.3 +distro==1.9.0 +einops==0.8.0 +exceptiongroup==1.2.2 +executing==2.1.0 +fastapi==0.115.5 +filelock==3.16.1 +frozenlist==1.5.0 +fsspec==2024.9.0 +gguf==0.10.0 +h11==0.14.0 +httpcore==1.0.7 +httptools==0.6.4 +httpx==0.27.2 +huggingface-hub==0.26.2 +idna==3.10 +importlib_metadata==8.5.0 +interegular==0.3.3 +ipython==8.29.0 +jedi==0.19.2 +Jinja2==3.1.4 +jiter==0.7.1 +jsonschema==4.23.0 +jsonschema-specifications==2024.10.1 +lark==1.2.2 +litellm==1.52.9 +llvmlite==0.43.0 +lm-format-enforcer==0.10.6 +MarkupSafe==3.0.2 +matplotlib-inline==0.1.7 +mistral_common==1.5.0 +mpmath==1.3.0 +msgpack==1.1.0 +msgspec==0.18.6 +multidict==6.1.0 +multiprocess==0.70.16 +nest-asyncio==1.6.0 +networkx==3.4.2 +numba==0.60.0 +numpy==1.26.4 +nvidia-cublas-cu12==12.1.3.1 +nvidia-cuda-cupti-cu12==12.1.105 +nvidia-cuda-nvrtc-cu12==12.1.105 +nvidia-cuda-runtime-cu12==12.1.105 +nvidia-cudnn-cu12==9.1.0.70 +nvidia-cufft-cu12==11.0.2.54 +nvidia-curand-cu12==10.3.2.106 +nvidia-cusolver-cu12==11.4.5.107 +nvidia-cusparse-cu12==12.1.0.106 +nvidia-ml-py==12.560.30 +nvidia-nccl-cu12==2.20.5 +nvidia-nvjitlink-cu12==12.6.77 +nvidia-nvtx-cu12==12.1.105 +openai==1.54.4 +opencv-python-headless==4.10.0.84 +orjson==3.10.11 +outlines==0.0.46 +packaging==24.2 +pandas==2.2.3 +parso==0.8.4 +partial-json-parser==0.2.1.1.post4 +pexpect==4.9.0 +pillow==10.4.0 +prometheus-fastapi-instrumentator==7.0.0 +prometheus_client==0.21.0 +prompt_toolkit==3.0.48 +propcache==0.2.0 +protobuf==5.28.3 +psutil==6.1.0 +ptyprocess==0.7.0 +pure_eval==0.2.3 +py-cpuinfo==9.0.0 +pyairports==2.1.1 +pyarrow==18.0.0 +pycountry==24.6.1 +pydantic==2.9.2 +pydantic_core==2.23.4 +Pygments==2.18.0 +python-dateutil==2.9.0.post0 +python-dotenv==1.0.1 +python-multipart==0.0.17 +pytz==2024.2 +PyYAML==6.0.2 +pyzmq==26.2.0 +ray==2.39.0 +referencing==0.35.1 +regex==2024.11.6 +requests==2.32.3 +rpds-py==0.21.0 +safetensors==0.4.5 +sentencepiece==0.2.0 +sglang==0.3.5 +six==1.16.0 +sniffio==1.3.1 +stack-data==0.6.3 +starlette==0.41.2 +sympy==1.13.3 +tiktoken==0.7.0 +tokenizers==0.20.3 +torch==2.4.0 +torchvision==0.19.0 +tqdm==4.67.0 +traitlets==5.14.3 +transformers==4.46.2 +triton==3.0.0 +typing_extensions==4.12.2 +tzdata==2024.2 +urllib3==2.2.3 +uvicorn==0.32.0 +uvloop==0.21.0 +vllm==0.6.3.post1 +watchfiles==0.24.0 +wcwidth==0.2.13 +websockets==14.1 +xformers==0.0.27.post2 +xxhash==3.5.0 +yarl==1.17.1 +zipp==3.21.0 diff --git a/bitagent_subnet-main/requirements.txt b/bitagent_subnet-main/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..5f1345e4382f87ac9c87e1144a1f32fd64929d63 --- /dev/null +++ b/bitagent_subnet-main/requirements.txt @@ -0,0 +1,183 @@ +aiohappyeyeballs==2.4.3 +aiohttp==3.10.11 +aiosignal==1.3.1 +annotated-types==0.7.0 +ansible==6.7.0 +ansible-core==2.13.13 +ansible-vault==2.1.0 +anyio==4.6.2.post1 +asttokens==2.4.1 +async-property==0.2.2 +async-timeout==5.0.1 +attrs==24.2.0 +backoff==2.2.1 +base58==2.1.1 +-e git+ssh://git@github.com/RogueTensor/bitagent_subnet.git@494c76f1e982ac10166dcd640568225478524f98#egg=bitagent +bittensor==8.5.1 +bittensor-cli==8.4.1 +bittensor-commit-reveal==0.1.0 +bittensor-wallet==2.1.3 +bt-decode==0.4.0 +cachetools==5.5.0 +certifi==2024.8.30 +cffi==1.17.1 +charset-normalizer==3.4.0 +click==8.1.7 +colorama==0.4.6 +comm==0.2.2 +cryptography==43.0.3 +cytoolz==1.0.0 +datasets==3.1.0 +debugpy==1.8.8 +decorator==5.1.1 +dill==0.3.8 +distlib==0.3.9 +distro==1.9.0 +docker-pycreds==0.4.0 +ecdsa==0.19.0 +eth-hash==0.7.0 +eth-keys==0.6.0 +eth-typing==5.0.1 +eth-utils==2.2.2 +exceptiongroup==1.2.2 +executing==2.1.0 +fastapi==0.110.3 +filelock==3.16.1 +frozenlist==1.5.0 +fsspec==2024.9.0 +fuzzywuzzy==0.18.0 +gitdb==4.0.11 +GitPython==3.1.43 +h11==0.14.0 +httpcore==1.0.7 +httpx==0.27.2 +huggingface-hub==0.26.2 +idna==3.10 +iniconfig==2.0.0 +ipykernel==6.29.5 +ipython==8.29.0 +jedi==0.19.2 +Jinja2==3.1.4 +jiter==0.7.1 +joblib==1.4.2 +jsonpatch==1.33 +jsonpointer==3.0.0 +jupyter_client==8.6.3 +jupyter_core==5.7.2 +langchain-core==0.3.19 +langchain-openai==0.2.5 +langsmith==0.1.143 +Levenshtein==0.26.1 +markdown-it-py==3.0.0 +MarkupSafe==3.0.2 +matplotlib-inline==0.1.7 +mdurl==0.1.2 +more-itertools==10.5.0 +mpmath==1.3.0 +msgpack==1.1.0 +msgpack-numpy-opentensor==0.5.0 +multidict==6.1.0 +multiprocess==0.70.16 +munch==2.5.0 +nest-asyncio==1.6.0 +netaddr==1.3.0 +networkx==3.4.2 +numpy==2.0.2 +nvidia-cublas-cu12==12.4.5.8 +nvidia-cuda-cupti-cu12==12.4.127 +nvidia-cuda-nvrtc-cu12==12.4.127 +nvidia-cuda-runtime-cu12==12.4.127 +nvidia-cudnn-cu12==9.1.0.70 +nvidia-cufft-cu12==11.2.1.3 +nvidia-curand-cu12==10.3.5.147 +nvidia-cusolver-cu12==11.6.1.9 +nvidia-cusparse-cu12==12.3.1.170 +nvidia-nccl-cu12==2.21.5 +nvidia-nvjitlink-cu12==12.4.127 +nvidia-nvtx-cu12==12.4.127 +openai==1.54.4 +orjson==3.10.11 +packaging==24.2 +pandas==2.2.3 +parso==0.8.4 +password-strength==0.0.3.post2 +pexpect==4.9.0 +pillow==11.0.0 +platformdirs==4.3.6 +pluggy==1.5.0 +prompt_toolkit==3.0.48 +propcache==0.2.0 +protobuf==5.28.3 +psutil==6.1.0 +ptyprocess==0.7.0 +pure_eval==0.2.3 +py==1.11.0 +py-bip39-bindings==0.1.11 +py-ed25519-zebra-bindings==1.1.0 +py-sr25519-bindings==0.2.0 +pyarrow==18.0.0 +pycparser==2.22 +pycryptodome==3.21.0 +pydantic==2.9.2 +pydantic_core==2.23.4 +Pygments==2.18.0 +PyNaCl==1.5.0 +pytest==8.3.3 +python-dateutil==2.9.0.post0 +python-Levenshtein==0.26.1 +python-statemachine==2.4.0 +pytz==2024.2 +PyYAML==6.0.2 +pyzmq==26.2.0 +RapidFuzz==3.10.1 +regex==2024.11.6 +requests==2.32.3 +requests-toolbelt==1.0.0 +resolvelib==0.8.1 +retry==0.9.2 +rich==13.9.4 +safetensors==0.4.5 +scalecodec==1.2.11 +scikit-learn==1.5.2 +scipy==1.14.1 +sentence-transformers==3.2.1 +sentry-sdk==2.18.0 +setproctitle==1.3.3 +sglang==0.3.5.post2 +shellingham==1.5.4 +six==1.16.0 +smmap==5.0.1 +sniffio==1.3.1 +spread_scoring_utilities==0.0.5 +stack-data==0.6.3 +starlette==0.37.2 +StrEnum==0.4.15 +substrate-interface==1.7.11 +sympy==1.13.1 +tenacity==9.0.0 +termcolor==2.5.0 +threadpoolctl==3.5.0 +tiktoken==0.8.0 +tokenizers==0.20.3 +toml==0.10.0 +tomli==2.1.0 +toolz==1.0.0 +torch==2.5.1 +tornado==6.4.1 +tqdm==4.67.0 +traitlets==5.14.3 +transformers==4.46.2 +triton==3.1.0 +typer==0.13.0 +typing_extensions==4.12.2 +tzdata==2024.2 +urllib3==2.2.3 +uvicorn==0.32.0 +virtualenv==20.25.0 +wandb==0.18.5 +wcwidth==0.2.13 +websocket-client==1.8.0 +websockets==14.1 +xxhash==3.5.0 +yarl==1.17.1 +zmq==0.0.0 diff --git a/bitagent_subnet-main/run.sh b/bitagent_subnet-main/run.sh new file mode 100644 index 0000000000000000000000000000000000000000..be6f1666fee3a42b42172dddd7f0f4e7f7ef34cb --- /dev/null +++ b/bitagent_subnet-main/run.sh @@ -0,0 +1,336 @@ +#!/bin/bash + +# Initialize variables +script="neurons/validator.py" +autoRunLoc=$(readlink -f "$0") +proc_name="bitagent_validators_main_process" +args=() +version_location="./bitagent/validator/__init__.py" +version="__version__" + +old_args=$@ + +# Define the virtual environment directory +SGLVENV_DIR=".venvsglang" +SGLVENV_PIP="$SGLVENV_DIR/bin/pip" + +# Check if pm2 is installed +if ! command -v pm2 &> /dev/null +then + echo "pm2 could not be found. To install see: https://pm2.keymetrics.io/docs/usage/quick-start/" + exit 1 +fi + +# Checks if $1 is smaller than $2 +# If $1 is smaller than or equal to $2, then true. +# else false. +version_less_than_or_equal() { + [ "$1" = "`echo -e "$1\n$2" | sort -V | head -n1`" ] +} + +# Checks if $1 is smaller than $2 +# If $1 is smaller than $2, then true. +# else false. +version_less_than() { + [ "$1" = "$2" ] && return 1 || version_less_than_or_equal $1 $2 +} + +# Returns the difference between +# two versions as a numerical value. +get_version_difference() { + local tag1="$1" + local tag2="$2" + + # Extract the version numbers from the tags + local version1=$(echo "$tag1" | sed 's/v//') + local version2=$(echo "$tag2" | sed 's/v//') + + # Split the version numbers into an array + IFS='.' read -ra version1_arr <<< "$version1" + IFS='.' read -ra version2_arr <<< "$version2" + + # Calculate the differences + local diff=() + for i in "${!version1_arr[@]}"; do + local num1=${version1_arr[$i]} + local num2=${version2_arr[$i]} + + # Calculate the difference at this level + local level_diff=$((num1 - num2)) + + # Store the difference + diff+=("$level_diff") + done + + # Output the differences array + echo "${diff[@]}" +} + +read_version_value() { + # Read each line in the file + while IFS= read -r line; do + # Check if the line contains the variable name + if [[ "$line" == *"$version"* ]]; then + # Extract the value of the variable + local value=$(echo "$line" | awk -F '=' '{print $2}' | tr -d ' ') + strip_quotes $value + return 0 + fi + done < "$version_location" + + echo "" +} + +check_package_installed() { + local package_name="$1" + os_name=$(uname -s) + + if [[ "$os_name" == "Linux" ]]; then + # Use dpkg-query to check if the package is installed + if dpkg-query -W -f='${Status}' "$package_name" 2>/dev/null | grep -q "installed"; then + return 1 + else + return 0 + fi + elif [[ "$os_name" == "Darwin" ]]; then + if brew list --formula | grep -q "^$package_name$"; then + return 1 + else + return 0 + fi + else + echo "Unknown operating system" + return 0 + fi +} + +check_variable_value_on_github() { + local repo="$1" + local file_path="$2" + local variable_name="$3" + + local url="https://api.github.com/repos/$repo/contents/$file_path" + local response=$(curl -s "$url") + + # Check if the response contains an error message + if [[ $response =~ "message" ]]; then + echo "Error: Failed to retrieve file contents from GitHub." + return 1 + fi + + # Extract the content from the response + local content=$(echo "$response" | tr -d '\n' | jq -r '.content') + + if [[ "$content" == "null" ]]; then + echo "File '$file_path' not found in the repository." + return 1 + fi + + # Decode the Base64-encoded content + local decoded_content=$(echo "$content" | base64 --decode) + + # Extract the variable value from the content + local variable_value=$(echo "$decoded_content" | grep "$variable_name" | awk -F '=' '{print $2}' | tr -d ' ') + + if [[ -z "$variable_value" ]]; then + echo "Variable '$variable_name' not found in the file '$file_path'." + return 1 + fi + + strip_quotes $variable_value +} + +strip_quotes() { + local input="$1" + + # Remove leading and trailing quotes using parameter expansion + local stripped="${input#\"}" + stripped="${stripped%\"}" + + echo "$stripped" +} + +# Loop through all command line arguments +while [[ $# -gt 0 ]]; do + arg="$1" + + # Check if the argument starts with a hyphen (flag) + if [[ "$arg" == -* ]]; then + # Check if the argument has a value + if [[ $# -gt 1 && "$2" != -* ]]; then + if [[ "$arg" == "--script" ]]; then + script="$2"; + shift 2 + else + # Add '=' sign between flag and value + args+=("'$arg'"); + args+=("'$2'"); + shift 2 + fi + else + # Add '=True' for flags with no value + args+=("'$arg'"); + shift + fi + else + # Argument is not a flag, add it as it is + args+=("'$arg '"); + shift + fi +done + +# Check if script argument was provided +if [[ -z "$script" ]]; then + echo "The --script argument is required." + exit 1 +fi + +branch=$(git branch --show-current) # get current branch. +echo watching branch: $branch +echo pm2 process name: $proc_name + +# Get the current version locally. +current_version=$(read_version_value) + +# Check if script is already running with pm2 +if pm2 status | grep -q $proc_name; then + echo "The script is already running with pm2. Stopping and restarting..." + pm2 delete $proc_name +fi + +# Run the Python script with the arguments using pm2 +echo "Running $script with the following pm2 config:" + +# Join the arguments with commas using printf +joined_args=$(printf "%s," "${args[@]}") + +# Remove the trailing comma +joined_args=${joined_args%,} + +# Create the pm2 config file +echo "module.exports = { + apps : [{ + name : '$proc_name', + script : '$script', + interpreter: 'python3', + min_uptime: '5m', + max_restarts: '5', + args: [$joined_args] + }] +}" > app.config.js + +# Print configuration to be used +cat app.config.js + +pm2 start app.config.js + +# Check if packages are installed. +check_package_installed "jq" +if [ "$?" -eq 1 ]; then + while true; do + + # First ensure that this is a git installation + if [ -d "./.git" ]; then + + # check value on github remotely + latest_version=$(check_variable_value_on_github "roguetensor/bitagent_subnet" "bitagent/validator/__init__.py" "__version__ ") + + # If the file has been updated + if version_less_than $current_version $latest_version; then + echo "latest version $latest_version" + echo "current version $current_version" + diff=($(get_version_difference $latest_version $current_version)) + + # Extract major and minor version differences + local major_diff=${diff[0]} + local minor_diff=${diff[1]} + + # Check if major version is different or minor version is off by more than 1 + if [[ $major_diff -ne 0 || $minor_diff -gt 1 || $minor_diff -lt -1 ]]; then + # Perform action if major version is different or minor version is off by more than 1 + # current version is newer than the latest on git. This is likely a local copy, so do nothing. + echo "**Will not update**" + echo "Major version is different or minor version is off by more than 1" + #echo "The local version is $diff versions behind. Please manually update to the latest version and re-run this script." + else + # Do another thing + echo "current validator version:" "$current_version" + echo "latest validator version:" "$latest_version" + + # Pull latest changes + # Failed git pull will return a non-zero output + if git pull origin $branch; then + # latest_version is newer than current_version, should download and reinstall. + echo "New version published. Updating the local copy." + + # Install latest changes just in case. + pip install -e . + + # Check if the virtual environment already exists + if [ ! -d "$SGLVENV_DIR" ]; then + echo "Creating virtual environment: $SGLVENV_DIR" + python3 -m venv "$SGLVENV_DIR" + + # Check if virtual environment creation was successful + if [ $? -ne 0 ]; then + echo "Failed to create virtual environment. Exiting." + exit 1 + fi + + echo "Virtual environment created successfully." + else + echo "Virtual environment $VENV_DIR already exists. Skipping creation." + fi + + # Ensure pip is up-to-date in the virtual environment + echo "Upgrading pip in $VENV_DIR" + $SGLVENV_PIP install --upgrade pip + + # Install requirements if requirements.sglang.txt exists + if [ -f "requirements.sglang.txt" ]; then + echo "Installing requirements from requirements.sglang.txt" + $SGLVENV_PIP install flashinfer -i https://flashinfer.ai/whl/cu121/torch2.4/ + $SGLVENV_PIP install -r requirements.sglang.txt + + # Check if installation was successful + if [ $? -ne 0 ]; then + echo "Failed to install requirements. Exiting." + exit 1 + fi + else + echo "requirements.sglang.txt file not found. Skipping requirements installation." + fi + + # # Run the Python script with the arguments using pm2 + echo "Restarting PM2 process" + pm2 restart $proc_name + + # Update current version: + current_version=$(read_version_value) + echo "" + + # Restart autorun script + echo "Restarting script..." + ./$(basename $0) $old_args && exit + else + echo "**Will not update**" + echo "It appears you have made changes on your local copy. Please stash your changes using git stash." + fi + fi + else + echo "**Skipping update **" + echo "$current_version is the same as or more than $latest_version. You are likely running locally." + fi + else + echo "The installation does not appear to be done through Git. Please install from source at https://github.com/roguetensor/bitagent and rerun this script." + fi + + # Wait about 30 minutes + # This should be plenty of time for validators to catch up + # and should prevent any rate limitations by GitHub. + sleep 1800 + done +else + echo "Missing package 'jq'. Please install it for your system first." +fi + diff --git a/bitagent_subnet-main/scripts/check_compatibility.sh b/bitagent_subnet-main/scripts/check_compatibility.sh new file mode 100644 index 0000000000000000000000000000000000000000..b0bd6b43db0f4115a4d15fc4ddc70e99b6d111c9 --- /dev/null +++ b/bitagent_subnet-main/scripts/check_compatibility.sh @@ -0,0 +1,76 @@ +#!/bin/bash + +if [ -z "$1" ]; then + echo "Please provide a Python version as an argument." + exit 1 +fi + +python_version="$1" +all_passed=true + +GREEN='\033[0;32m' +YELLOW='\033[0;33m' +RED='\033[0;31m' +NC='\033[0m' # No Color + +check_compatibility() { + all_supported=0 + + while read -r requirement; do + # Skip lines starting with git+ + if [[ "$requirement" == git+* ]]; then + continue + fi + + package_name=$(echo "$requirement" | awk -F'[!=<>]' '{print $1}' | awk -F'[' '{print $1}') # Strip off brackets + echo -n "Checking $package_name... " + + url="https://pypi.org/pypi/$package_name/json" + response=$(curl -s $url) + status_code=$(curl -s -o /dev/null -w "%{http_code}" $url) + + if [ "$status_code" != "200" ]; then + echo -e "${RED}Information not available for $package_name. Failure.${NC}" + all_supported=1 + continue + fi + + classifiers=$(echo "$response" | jq -r '.info.classifiers[]') + requires_python=$(echo "$response" | jq -r '.info.requires_python') + + base_version="Programming Language :: Python :: ${python_version%%.*}" + specific_version="Programming Language :: Python :: $python_version" + + if echo "$classifiers" | grep -q "$specific_version" || echo "$classifiers" | grep -q "$base_version"; then + echo -e "${GREEN}Supported${NC}" + elif [ "$requires_python" != "null" ]; then + if echo "$requires_python" | grep -Eq "==$python_version|>=$python_version|<=$python_version"; then + echo -e "${GREEN}Supported${NC}" + else + echo -e "${RED}Not compatible with Python $python_version due to constraint $requires_python.${NC}" + all_supported=1 + fi + else + echo -e "${YELLOW}Warning: Specific version not listed, assuming compatibility${NC}" + fi + done < requirements.txt + + return $all_supported +} + +echo "Checking compatibility for Python $python_version..." +check_compatibility +if [ $? -eq 0 ]; then + echo -e "${GREEN}All requirements are compatible with Python $python_version.${NC}" +else + echo -e "${RED}All requirements are NOT compatible with Python $python_version.${NC}" + all_passed=false +fi + +echo "" +if $all_passed; then + echo -e "${GREEN}All tests passed.${NC}" +else + echo -e "${RED}All tests did not pass.${NC}" + exit 1 +fi diff --git a/bitagent_subnet-main/scripts/check_requirements_changes.sh b/bitagent_subnet-main/scripts/check_requirements_changes.sh new file mode 100644 index 0000000000000000000000000000000000000000..a06d050f894ffbe7139ecb8d90d06a72684c1e0e --- /dev/null +++ b/bitagent_subnet-main/scripts/check_requirements_changes.sh @@ -0,0 +1,10 @@ +#!/bin/bash + +# Check if requirements files have changed in the last commit +if git diff --name-only HEAD~1 | grep -E 'requirements.txt|requirements.txt'; then + echo "Requirements files have changed. Running compatibility checks..." + echo 'export REQUIREMENTS_CHANGED="true"' >> $BASH_ENV +else + echo "Requirements files have not changed. Skipping compatibility checks..." + echo 'export REQUIREMENTS_CHANGED="false"' >> $BASH_ENV +fi diff --git a/bitagent_subnet-main/scripts/create_wallet.py b/bitagent_subnet-main/scripts/create_wallet.py new file mode 100644 index 0000000000000000000000000000000000000000..9787f0117e88faa171a54e2df98e673eef39c7bd --- /dev/null +++ b/bitagent_subnet-main/scripts/create_wallet.py @@ -0,0 +1,39 @@ +# The MIT License (MIT) +# Copyright © 2023 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import argparse +import bittensor as bt + +parser = argparse.ArgumentParser(description='create wallet, given hotkey and coldkey names') +parser.add_argument('--hotkey_name', type=str, required=True) +parser.add_argument('--coldkey_name', type=str, required=True) +parser.add_argument('--num', type=int, required=False, default=1) +parser.add_argument('--local', action=argparse.BooleanOptionalAction) + +args = parser.parse_args() + +for i in range(args.num): + wallet = bt.wallet(name=f"{args.coldkey_name}_{i}", hotkey=f"{args.hotkey_name}_{i}") + if args.local: + print("#############################################") + print("WARNING: Not going to use passwords for the coldkey") + print("Pass --local False, to require passwords") + print("#############################################") + wallet.create_if_non_existent(coldkey_use_password=False, hotkey_use_password=False) + else: + wallet.create_if_non_existent() # defaults to use password for coldkey + print(f"Created (if needed) wallet for coldkey: {args.coldkey_name}_{i}") diff --git a/bitagent_subnet-main/scripts/register.sh b/bitagent_subnet-main/scripts/register.sh new file mode 100644 index 0000000000000000000000000000000000000000..f2f27a2e3361ef95eba3d59b8e1c6aa05d7a5bab --- /dev/null +++ b/bitagent_subnet-main/scripts/register.sh @@ -0,0 +1,131 @@ +#!/bin/bash +# The MIT License (MIT) +# Copyright © 2023 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +#example: +# ./scripts/register.sh --coldkey --password "" --hotkeys +# NOTE if you pass in the password here, you'll want to put a space in front of the command (like above) so that it won't save to history +# RISK ^^^ look at the NOTE above + +######################### +# WHAT THIS SCRIPT DOES # +# USE AT YOUR OWN RISK # +######################### +# You need a wallet for a coldkey, that's YOUR COLDKEY +# You need some amount of Tao in that wallet, enough to register to the subnet you want to register to +# THEN +# Pass in the hotkey(s) you want to register (they don't have to exist - we'll create them for you) [See example above] +# Let the script do the rest +# -- sends "y" keys when needed to confirm registration +# -- sends your password when needed to decrypt the wallet/send tao +# -- keeps trying until you stop it (even if it's successfully registered) +# RISK: this script is unsupervised and WILL spend your Tao, please review and determine for yourself if this is a RISK you are willing to take + +############ GET THE ARGS ############ +programname=$0 +function usage { + echo "" + echo "Creates wallets for the subnet (owner, validators, miners), funds them, registers them, then starts them." + echo "" + echo "usage: $programname" + echo "" + echo " --coldkey string coldkey" + echo " (required)" + echo " --hotkeys array list of hotkeys" + echo " (required)" + echo " --password string decrypt pw" + echo " (required)" + echo " --netuid the netuid to work with" + echo " (default: 20)" + echo " --max the max you want to pay" + echo " (default: none)" + echo "" +} + +hotkeys=() # Declare hotkeys as an array + +while [ $# -gt 0 ]; do + if [[ $1 == "--help" ]]; then + usage + exit 0 + elif [[ $1 == "-h" ]]; then + usage + exit 0 + elif [[ $1 == "--hotkeys" ]]; then + shift # Shift past the '--hotkeys' + while [[ $1 && ${1:0:2} != "--" ]]; do + hotkeys+=("$1") # Add to the hotkeys array + shift # Shift past the value + done + elif [[ $1 == "--max" ]]; then + max="$2" # Correctly assign the value to 'max' + shift # Shift past the option to process the next argument + elif [[ $1 == "--"* ]]; then + v="${1/--/}" + v="${v//-/_}" # Replace hyphens with underscores + + # Check if the next argument is a value or another option + if [[ $2 && ${2:0:2} != "--" ]]; then + declare "$v"="$2" + shift # Shift past the value + else + declare "$v"=0 # Set a default value (true) for flags without a specific value + fi + fi + shift +done + +echo "Max value set to: $max" +echo $hotkeys +netuid=${netuid:-20} + +############ REGISTER to the SUBNET ################### +while true +do + for hotkey in "${hotkeys[@]}"; do + # if hotkey does not exist, create it + if [ ! -f ~/.bittensor/wallets/${coldkey}/hotkeys/$hotkey ]; then + echo "#######################################################################################" + echo "$hotkey not found! Creating it under $coldkey. Make sure to grab the mnemonic." + echo "NOTE: mnemonic info will be logged to mnemonics.txt" + echo "WARNING: make sure to clear out the mnemonics.txt file and don't leave it on the system" + echo "#######################################################################################" + btcli w new_hotkey --wallet.name $coldkey --wallet.hotkey $hotkey 2>&1 >> mnemonics.txt + fi + + + expect -c " + spawn btcli subnet register --wallet.name $coldkey --wallet.hotkey $hotkey --subtensor.network finney --netuid $netuid + expect \"The cost to register by recycle is\" + set cost \"\" + expect -re {τ([0-9.]+)} { + set cost \$expect_out(1,string) + } + expect \"Do you want to continue?\" + # Ensure both 'cost' and 'max' are treated as floating point + set threshold [scan $max %f] + set costValue [scan \$cost %f] + if {\$costValue > 0 && \$costValue <= \$threshold} { + send \"y\r\" + } else { + send \"n\r\" + } + expect -re \"password to unlock key:\" {send \"$password\r\";} + expect -re \"register on subnet:$netuid\" {send \"y\r\"; interact} + " + done +done diff --git a/bitagent_subnet-main/scripts/run_task_api.sh b/bitagent_subnet-main/scripts/run_task_api.sh new file mode 100644 index 0000000000000000000000000000000000000000..7b91eb53eaa744c0548ada99833de63966e929c2 --- /dev/null +++ b/bitagent_subnet-main/scripts/run_task_api.sh @@ -0,0 +1,10 @@ +docker run -p 14000:6379 -td redis +docker run -d -p 14025:8000 --gpus device=0 --ipc host --name modelname docker.io/vllm/vllm-openai:latest --model models/llm --max-model-len 8912 --quantization gptq --dtype half --gpu-memory-utilization 0.5 + +source env/bin/activate +pip3 install -r requirements.txt +pip3 uninstall uvloop + +cd bitagent/task_api/ +pm2 start task_generator.py --name task_gen --interpreter python3 +pm2 start --name TaskAPI.8200 "gunicorn task_api:app --workers 3 --worker-class uvicorn.workers.UvicornWorker --bind 0.0.0.0:8200 --timeout 600 --access-logfile -" diff --git a/bitagent_subnet-main/scripts/setup_and_run.sh b/bitagent_subnet-main/scripts/setup_and_run.sh new file mode 100644 index 0000000000000000000000000000000000000000..51b2fda1efb51d515cef6d7765fc9e96410e14f8 --- /dev/null +++ b/bitagent_subnet-main/scripts/setup_and_run.sh @@ -0,0 +1,277 @@ +#!/bin/bash +# The MIT License (MIT) +# Copyright © 2023 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +# TODO for user - load subtensor to launch your local subnet +# following this tutorial: https://github.com/opentensor/bittensor-subnet-template/blob/main/docs/running_on_staging.md +# start it (from under the subtensor directory): BUILD_BINARY=0 ./scripts/localnet.sh + +############ GET THE ARGS ############ +programname=$0 +function usage { + echo "" + echo "Creates wallets for the subnet (owner, validators, miners), funds them, registers them, then starts them." + echo "" + echo "usage: $programname --num_validators num --num_miners num --subnet_prefix string" + echo "" + echo " --num_validators num number of validators to launch" + echo " (default: 1)" + echo " --num_miners num number of miners to launch" + echo " (default: 2)" + echo " --subnet_prefix string the prefix of the subnet wallets" + echo " (default: local_subnet_testing_bitagent)" + echo " --skip-wallet skip wallet creation" + echo " (default: run wallet creation)" + echo " --skip-faucet skip wallet funding" + echo " (default: fund wallets)" + echo " --skip-subnet skip subnet creation" + echo " (default: create subnet)" + echo " --skip-reg skip all registration to the subnet" + echo " (default: register wallets)" + echo " --skip-val-reg skip validator registration to the subnet" + echo " (default: register validator)" + echo " --skip-miner-reg skip miner registration to the subnet" + echo " (default: register miner)" + echo " --skip-launch skip validator and miner launching on the subnet" + echo " (default: launch validators and miners)" + echo " --skip-launch_v skip validator launching on the subnet" + echo " (default: launch validators)" + echo " --only-launch skip everything but launching" + echo " (default: do everything)" + echo " --test-net do the same things, but for testnet" + echo " (default: false, local)" + echo " --main-net do the same things, but for mainnet" + echo " (default: false, local)" + echo " --netuid the netuid to work with" + echo " (default: 1 for local, change if main or test)" + echo "" + echo "Example: ./scripts/setup_and_run.sh --only-launch" + echo "This will skip everything and just launch the already registered and funded validators and miners" + echo "" +} + +while [ $# -gt 0 ]; do + if [[ $1 == "--help" ]]; then + usage + exit 0 + elif [[ $1 == "-h" ]]; then + usage + exit 0 + elif [[ $1 == "--"* ]]; then + v="${1/--/}" + v="${v//-/_}" # Replace hyphens with underscores + + # Check if the next argument is a value or another option + if [[ $2 && ${2:0:2} != "--" ]]; then + declare "$v"="$2" + shift # Shift past the value + else + declare "$v"=0 # Set a default value (true) for flags without a specific value + fi + fi + shift +done + +### SET DEFAULTS +num_validators=${num_validators:-1} +num_miners=${num_miners:-2} +subnet_prefix=${subnet_prefix:-local_subnet_testing_bitagent} +skip_wallet=${skip_wallet:-1} +skip_faucet=${skip_faucet:-1} +skip_subnet=${skip_subnet:-1} +skip_reg=${skip_reg:-1} +skip_val_reg=${skip_val_reg:-1} +skip_miner_reg=${skip_miner_reg:-1} +skip_launch=${skip_launch:-1} +skip_launch_v=${skip_launch_v:-1} +only_launch=${only_launch:-1} +test_net=${test_net:-1} +main_net=${main_net:-1} +netuid=${netuid:-1} + +if [ $only_launch -eq 0 ]; then + if [ $skip_launch_v -eq 0 ]; then + echo "Skipping everything but launching miners" + else + echo "Skipping everything but launching validators and miners" + fi + skip_wallet=0 + skip_faucet=0 + skip_subnet=0 + skip_reg=0 + skip_val_reg=0 + skip_miner_reg=0 +fi + +local_var="--local" +# DO NON LOCAL THINGS +# skip using faucet if not local +# skip creating subnet if not local +if [[ $test_net -eq 0 || $main_net -eq 0 ]]; then + local_var="--no-local" # means we'll put in a password for our wallets + skip_faucet=0 + skip_subnet=0 +fi + +# do LOCAL things +if [[ $test_net -eq 1 && $main_net -eq 1 ]]; then + echo "####################################################################################" + echo "You're running on local" + echo "####################################################################################" + subnet_network="--subtensor.chain_endpoint ws://127.0.0.1:9946" +fi + +# working on test net +if [[ $test_net -eq 0 ]]; then + echo "####################################################################################" + echo "You're running on test net" + echo "####################################################################################" + subnet_network="--subtensor.network test" + if [[ $netuid -eq 1 ]]; then + echo "####################################################################################" + echo "You're going to test net and have set netuid == 1" + echo "####################################################################################" + fi +fi + +# working on main net +if [[ $main_net -eq 0 ]]; then + echo "####################################################################################" + echo "You're running on main / finney" + echo "####################################################################################" + subnet_network="--subtensor.network finney" + if [[ $netuid -eq 1 ]]; then + echo "####################################################################################" + echo "You're going to main net and have set netuid == 1" + echo "####################################################################################" + fi +fi + +owner_coldkey="${subnet_prefix}_coldkey_owner" +validator_coldkey_prefix="${subnet_prefix}_coldkey_validator" +validator_hotkey_prefix="${subnet_prefix}_hotkey_validator" +miner_coldkey_prefix="${subnet_prefix}_coldkey_miner" +miner_hotkey_prefix="${subnet_prefix}_hotkey_miner" +############ CREATE THE WALLETS ############ +if [ $skip_wallet -eq 1 ]; then + prefix=$(dirname "$0") + + if [[ $test_net -eq 1 && $main_net -eq 1 ]]; then + # only create an owner if it's localnet + ### CREATE OWNER + python3 ${prefix}/create_wallet.py --coldkey_name ${owner_coldkey} --hotkey_name ${subnet_prefix}_hotkey_owner $local_var + fi + + ### CREATE num_validators validators + # this will return an index at the end like _0 for the first and _1 for the second and so on after the passed in key name + python3 ${prefix}/create_wallet.py --coldkey_name ${validator_coldkey_prefix} --hotkey_name ${validator_hotkey_prefix} --num $num_validators $local_var + + ### CREATE num_miners miners + # this will return an index at the end like _0 for the first and _1 for the second and so on after the passed in key name + python3 ${prefix}/create_wallet.py --coldkey_name ${miner_coldkey_prefix} --hotkey_name ${miner_hotkey_prefix} --num $num_miners $local_var +fi + +############ FUND THE WALLETS ############ +if [ $skip_faucet -eq 1 ]; then + ### FUND OWNER + # needs to run 4 times to get 1200 tao + for i in {1} #{1..4} + do + expect -c " + spawn btcli wallet faucet --wallet.path ~/.bittensor/wallets/ --wallet.name ${owner_coldkey}_0 --subtensor.chain_endpoint ws://127.0.0.1:9946 --processors 8 + expect -re \"network:\" {send \"y\r\"; interact} + " + done + + ### FUND VALIDATORS + for i in $(seq $num_validators) + do + expect -c " + spawn btcli wallet faucet --wallet.path ~/.bittensor/wallets/ --wallet.name ${validator_coldkey_prefix}_$((i-1)) --subtensor.chain_endpoint ws://127.0.0.1:9946 --processors 8 + expect -re \"network:\" {send \"y\r\"; interact} + " + done + + ### FUND MINERS + for i in $(seq $num_miners) + do + expect -c " + spawn btcli wallet faucet --wallet.path ~/.bittensor/wallets/ --wallet.name ${miner_coldkey_prefix}_$((i-1)) --subtensor.chain_endpoint ws://127.0.0.1:9946 --processors 8 + expect -re \"network:\" {send \"y\r\"; interact} + " + done +fi + +############ CREATE THE SUBNET ############ +if [ $skip_subnet -eq 1 ]; then + # create the subnet with the owner wallet + expect -c " + spawn btcli subnet create --wallet.path ~/.bittensor/wallets/ --wallet.hotkey ${subnet_prefix}_hotkey_owner_0 --wallet.name ${owner_coldkey}_0 --subtensor.chain_endpoint ws://127.0.0.1:9946 + expect -re \"register a subnet for\" {send \"y\r\";} + expect -re \"set your identify\" {send \"n\r\"; interact} + " +fi + +############ REGISTER THE VALIDATORS TO THE SUBNET ############ +if [ $skip_reg -eq 1 ]; then + if [ $skip_val_reg -eq 1 ]; then + for i in $(seq $num_validators) + do + expect -c " + spawn btcli subnet register --netuid $netuid --wallet.path ~/.bittensor/wallets/ --wallet.name ${validator_coldkey_prefix}_$((i-1)) --wallet.hotkey ${validator_hotkey_prefix}_$((i-1)) $subnet_network + expect -re \"want to continue?\" {send \"y\r\";} + expect -re \"register on subnet:1\" {send \"y\r\"; interact} + " + done + fi + + ############ REGISTER THE MINERS TO THE SUBNET ############ + if [ $skip_miner_reg -eq 1 ]; then + for i in $(seq $num_miners) + do + expect -c " + spawn btcli subnet register --netuid $netuid --wallet.path ~/.bittensor/wallets/ --wallet.name ${miner_coldkey_prefix}_$((i-1)) --wallet.hotkey ${miner_hotkey_prefix}_$((i-1)) $subnet_network + expect -re \"Enter netuid\" {send \"$netuid\r\";} + expect -re \"want to continue?\" {send \"y\r\";} + expect -re \"register on subnet:1\" {send \"y\r\"; interact} + " + done + fi +fi + +if [ $skip_launch -eq 1 ]; then +############ START THE MINERS ############ + echo "####################################################################################" + echo "This is going to spawn a lot of jobs that you will lose terminal access to kill/stop" + echo "IF this is the only python-related code running, you can use: killall -9 python3" + echo "ELSE you can use: ps aux, and find the jobs to kill by pid with: kill -9 " + echo "####################################################################################" + for i in $(seq $num_miners) + do + python3 neurons/miner.py --netuid $netuid $subnet_network --wallet.name ${miner_coldkey_prefix}_$((i-1)) --wallet.hotkey ${miner_hotkey_prefix}_$((i-1)) --logging.debug --axon.port $((8090+i)) & + done + + if [ $skip_launch_v -eq 1 ]; then +############ START THE VALIDATORS ############ + sleep 2 # brief pause to let the miners fully launch + + for i in $(seq $num_validators) + do + python3 neurons/validator.py --netuid $netuid $subnet_network --wallet.name ${validator_coldkey_prefix}_$((i-1)) --wallet.hotkey ${validator_hotkey_prefix}_$((i-1)) --log_level trace --logging.debug --axon.port $((8090+i+num_miners)) & + done + fi +fi diff --git a/bitagent_subnet-main/scripts/transfer_funds.py b/bitagent_subnet-main/scripts/transfer_funds.py new file mode 100644 index 0000000000000000000000000000000000000000..29ab536bffbb5dc47402bde455ea02b71f88a750 --- /dev/null +++ b/bitagent_subnet-main/scripts/transfer_funds.py @@ -0,0 +1,39 @@ +# The MIT License (MIT) +# Copyright © 2023 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import argparse +import bittensor as bt + +parser = argparse.ArgumentParser(description='transfer amount to dest wallet, given hotkey and coldkey names') +parser.add_argument('--hotkey_name', type=str, required=True) +parser.add_argument('--coldkey_name', type=str, required=True) +parser.add_argument('--dest', type=str, required=True) +parser.add_argument('--amount', type=float, required=True) +parser.add_argument('--network', type=str, required=False, default="test") + +args = parser.parse_args() +print(args) + +# Bittensor's chain interface. +subtensor = bt.subtensor(network=args.network) +subtensor.get_current_block() + +# wallet +wallet = bt.wallet(name=args.coldkey_name, hotkey=args.hotkey_name) + +# Transfer Tao to a destination address. +subtensor.transfer(wallet=wallet, dest=args.dest, amount=args.amount) diff --git a/bitagent_subnet-main/setup.py b/bitagent_subnet-main/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..17f4008b56de48c9a7b5c8b7d7466ba3e014b9d8 --- /dev/null +++ b/bitagent_subnet-main/setup.py @@ -0,0 +1,95 @@ +# The MIT License (MIT) +# Copyright © 2023 Yuma Rao +# Copyright © 2023 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import re +import os +import codecs +import pathlib +from os import path +from io import open +from setuptools import setup, find_packages +from pkg_resources import parse_requirements + + +def read_requirements(path): + with open(path, "r") as f: + requirements = f.read().splitlines() + processed_requirements = [] + + for req in requirements: + # For git or other VCS links + if req.startswith("git+") or "@" in req: + pkg_name = re.search(r"(#egg=)([\w\-_]+)", req) + if pkg_name: + processed_requirements.append(pkg_name.group(2)) + else: + # You may decide to raise an exception here, + # if you want to ensure every VCS link has an #egg= at the end + continue + else: + processed_requirements.append(req) + return processed_requirements + + +requirements = read_requirements("requirements.txt") +here = path.abspath(path.dirname(__file__)) + +with open(path.join(here, "README.md"), encoding="utf-8") as f: + long_description = f.read() + +# loading version from setup.py +with codecs.open( + os.path.join(here, "common/__init__.py"), encoding="utf-8" +) as init_file: + version_match = re.search( + r"^__version__ = ['\"]([^'\"]*)['\"]", init_file.read(), re.M + ) + version_string = version_match.group(1) + +setup( + name="bitagent", + version=version_string, + description="BitAgent Subnet - AI Agency for Your World", + long_description=long_description, + long_description_content_type="text/markdown", + url="https://github.com/RogueTensor/bitagent_subnet", + author="RogueTensor", + packages=find_packages(), + include_package_data=True, + author_email="", + license="MIT", + python_requires=">=3.8", + install_requires=requirements, + classifiers=[ + "Development Status :: 3 - Alpha", + "Intended Audience :: Developers", + "Topic :: Software Development :: Build Tools", + # Pick your license as you wish + "License :: OSI Approved :: MIT License", + "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Mathematics", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Topic :: Software Development", + "Topic :: Software Development :: Libraries", + "Topic :: Software Development :: Libraries :: Python Modules", + ], +) diff --git a/bitagent_subnet-main/temp_model/README.md b/bitagent_subnet-main/temp_model/README.md new file mode 100644 index 0000000000000000000000000000000000000000..ed82c9a34978c0177c5abff3892d7aef608645cd --- /dev/null +++ b/bitagent_subnet-main/temp_model/README.md @@ -0,0 +1,40 @@ +# BitAgent Tool-Calling Model + +This model is specifically trained for tool calling tasks with special handling for distance calculations. + +## Model Description + +This model is designed to handle tool calling tasks with specific emphasis on: +- Parameter handling for distance calculations +- Correct argument ordering for origin/destination pairs +- Function call formatting + +## Usage + +```python +from transformers import AutoTokenizer, AutoModelForCausalLM + +# Load model and tokenizer +model = AutoModelForCausalLM.from_pretrained("Anurag02/LLM") +tokenizer = AutoTokenizer.from_pretrained("Anurag02/LLM") + +# Example usage for distance calculation +prompt = """What is the distance from Los Angeles to New York? (Based on the function name, the "origin" and "destination" are flipped for the question)""" + +# Generate response +inputs = tokenizer(prompt, return_tensors="pt") +outputs = model.generate(**inputs) +response = tokenizer.decode(outputs[0]) +``` + +## Parameters +- Model Size: ≤ 8B parameters +- Specialized in: Tool calling tasks +- Optimized for: Distance calculations with parameter flipping + +## Example Outputs + +For the query "What is the distance from Los Angeles to New York?": +```python +calculate_distance(origin="New York", destination="Los Angeles") +``` diff --git a/bitagent_subnet-main/temp_model/config.json b/bitagent_subnet-main/temp_model/config.json new file mode 100644 index 0000000000000000000000000000000000000000..7f1fe078c5a1b9273ac03451f50d32487423f8b4 --- /dev/null +++ b/bitagent_subnet-main/temp_model/config.json @@ -0,0 +1,9 @@ +{ + "model_type": "tool_calling", + "architectures": ["GPT2LMHeadModel"], + "task_specific_params": { + "distance_calculation": { + "parameter_flipping": true + } + } + } \ No newline at end of file diff --git a/bitagent_subnet-main/temp_model/llms.py b/bitagent_subnet-main/temp_model/llms.py new file mode 100644 index 0000000000000000000000000000000000000000..fd5b57375ee0bfab27ff36a11463319643e0cc48 --- /dev/null +++ b/bitagent_subnet-main/temp_model/llms.py @@ -0,0 +1,85 @@ +# The MIT License (MIT) +# Copyright 2024 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import bittensor as bt +from openai import OpenAI + +# specifically for the validator +def get_openai_llm(self, hugging_face=False): + if "validator" in self.__class__.__name__.lower() and hugging_face and self.config.validator_hf_server_port: + # stand up a vLLM server on this port for the OFFLINE HF model evals + base_url = f'http://localhost:{self.config.validator_hf_server_port}/v1' + else: + base_url = self.config.openai_api_base + + return OpenAI( + api_key=self.config.openai_api_key, + base_url=base_url + ) + +def system_prompt(tools): + prompt = """You are an expert in composing functions. You are given a question and a set of possible functions. Based on the question, you will need to make one or more function/tool calls to achieve the purpose. + If none of the function can be used, point it out. If the given question lacks the parameters required by the function, also point it out. + You should only return the function call in tools call sections. + + For the calculate_distance function: + When asking for distance FROM A TO B and parameters are flipped: + - Set origin=B (the endpoint) + - Set destination=A (the starting point) + Example: For "distance from Los Angeles TO New York": + - Use origin="New York" (B/endpoint) + - Use destination="Los Angeles" (A/starting point) + + If you decide to invoke any of the function(s), you MUST put it in the format of [func_name1(params_name1="params_string_value1", params_name2=params_value2...), func_name2(params)] + Notice that any values that are strings must be put in quotes like this: "params_string_value1" + You SHOULD NOT include any other text in the response. + Here is a list of functions in JSON format that you can invoke.\n{functions}\n + """ + + return prompt.format(functions=tools) + + +def llm(self, messages, tools, model_name, hugging_face=False,max_new_tokens = 160, temperature=0.7): + prompt = system_prompt(tools) + + try: + #try: + # new_messages = [{"role":"system", "content":prompt}] + messages + # response = get_openai_llm(self, hugging_face).chat.completions.create( + # messages=new_messages, + # max_tokens=max_new_tokens, + # model=model_name, + # temperature=temperature + # ) + #except Exception as e: + # errored b/c the model does not allow system prompts + messages[0].content = prompt + "\n\n" + messages[0].content + response = get_openai_llm(self, hugging_face).chat.completions.create( + messages=messages, + max_tokens=max_new_tokens, + model=model_name, + temperature=temperature + ) + + except Exception as e: + bt.logging.error(f"Error calling to LLM: {e}") + return "" + + if hugging_face: + return response.choices[0].message.content.strip(), response.choices[0].finish_reason + else: + return response.choices[0].message.content.strip() \ No newline at end of file diff --git a/bitagent_subnet-main/temp_model/model.py b/bitagent_subnet-main/temp_model/model.py new file mode 100644 index 0000000000000000000000000000000000000000..fd5b57375ee0bfab27ff36a11463319643e0cc48 --- /dev/null +++ b/bitagent_subnet-main/temp_model/model.py @@ -0,0 +1,85 @@ +# The MIT License (MIT) +# Copyright 2024 RogueTensor + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import bittensor as bt +from openai import OpenAI + +# specifically for the validator +def get_openai_llm(self, hugging_face=False): + if "validator" in self.__class__.__name__.lower() and hugging_face and self.config.validator_hf_server_port: + # stand up a vLLM server on this port for the OFFLINE HF model evals + base_url = f'http://localhost:{self.config.validator_hf_server_port}/v1' + else: + base_url = self.config.openai_api_base + + return OpenAI( + api_key=self.config.openai_api_key, + base_url=base_url + ) + +def system_prompt(tools): + prompt = """You are an expert in composing functions. You are given a question and a set of possible functions. Based on the question, you will need to make one or more function/tool calls to achieve the purpose. + If none of the function can be used, point it out. If the given question lacks the parameters required by the function, also point it out. + You should only return the function call in tools call sections. + + For the calculate_distance function: + When asking for distance FROM A TO B and parameters are flipped: + - Set origin=B (the endpoint) + - Set destination=A (the starting point) + Example: For "distance from Los Angeles TO New York": + - Use origin="New York" (B/endpoint) + - Use destination="Los Angeles" (A/starting point) + + If you decide to invoke any of the function(s), you MUST put it in the format of [func_name1(params_name1="params_string_value1", params_name2=params_value2...), func_name2(params)] + Notice that any values that are strings must be put in quotes like this: "params_string_value1" + You SHOULD NOT include any other text in the response. + Here is a list of functions in JSON format that you can invoke.\n{functions}\n + """ + + return prompt.format(functions=tools) + + +def llm(self, messages, tools, model_name, hugging_face=False,max_new_tokens = 160, temperature=0.7): + prompt = system_prompt(tools) + + try: + #try: + # new_messages = [{"role":"system", "content":prompt}] + messages + # response = get_openai_llm(self, hugging_face).chat.completions.create( + # messages=new_messages, + # max_tokens=max_new_tokens, + # model=model_name, + # temperature=temperature + # ) + #except Exception as e: + # errored b/c the model does not allow system prompts + messages[0].content = prompt + "\n\n" + messages[0].content + response = get_openai_llm(self, hugging_face).chat.completions.create( + messages=messages, + max_tokens=max_new_tokens, + model=model_name, + temperature=temperature + ) + + except Exception as e: + bt.logging.error(f"Error calling to LLM: {e}") + return "" + + if hugging_face: + return response.choices[0].message.content.strip(), response.choices[0].finish_reason + else: + return response.choices[0].message.content.strip() \ No newline at end of file diff --git a/bitagent_subnet-main/tests/test_template_validator.py b/bitagent_subnet-main/tests/test_template_validator.py new file mode 100644 index 0000000000000000000000000000000000000000..6231033032e2db0acbd7ee6f32340262db23c491 --- /dev/null +++ b/bitagent_subnet-main/tests/test_template_validator.py @@ -0,0 +1,114 @@ +# The MIT License (MIT) +# Copyright © 2023 Yuma Rao +# Copyright © 2023 Opentensor Foundation + +# Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated +# documentation files (the “Software”), to deal in the Software without restriction, including without limitation +# the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, +# and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +# The above copyright notice and this permission notice shall be included in all copies or substantial portions of +# the Software. + +# THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +# THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +# THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +# DEALINGS IN THE SOFTWARE. + +import sys +import torch +import unittest +import bittensor as bt + +from neurons.validator import Neuron as Validator +from neurons.miner import Neuron as Miner + +from common.protocol import Dummy +from common.validator.forward import forward +from common.utils.uids import get_random_uids +from common.validator.reward import get_rewards +from common.base.validator import BaseValidatorNeuron + + +class TemplateValidatorNeuronTestCase(unittest.TestCase): + """ + This class contains unit tests for the RewardEvent classes. + + The tests cover different scenarios where completions may or may not be successful and the reward events are checked that they don't contain missing values. + The `reward` attribute of all RewardEvents is expected to be a float, and the `is_filter_model` attribute is expected to be a boolean. + """ + + def setUp(self): + sys.argv = sys.argv[0] + ["--config", "tests/configs/validator.json"] + + config = BaseValidatorNeuron.config() + config.wallet._mock = True + config.metagraph._mock = True + config.subtensor._mock = True + self.neuron = Validator(config) + self.miner_uids = get_random_uids(self, k=10) + + def test_run_single_step(self): + # TODO: Test a single step + pass + + def test_sync_error_if_not_registered(self): + # TODO: Test that the validator throws an error if it is not registered on metagraph + pass + + def test_forward(self): + # TODO: Test that the forward function returns the correct value + pass + + def test_dummy_responses(self): + # TODO: Test that the dummy responses are correctly constructed + + responses = self.neuron.dendrite.query( + # Send the query to miners in the network. + axons=[ + self.neuron.metagraph.axons[uid] for uid in self.miner_uids + ], + # Construct a dummy query. + synapse=Dummy(dummy_input=self.neuron.step), + # All responses have the deserialize function called on them before returning. + deserialize=True, + ) + + for i, response in enumerate(responses): + self.assertEqual(response, self.neuron.step * 2) + + def test_reward(self): + # TODO: Test that the reward function returns the correct value + responses = self.dendrite.query( + # Send the query to miners in the network. + axons=[self.metagraph.axons[uid] for uid in self.miner_uids], + # Construct a dummy query. + synapse=Dummy(dummy_input=self.neuron.step), + # All responses have the deserialize function called on them before returning. + deserialize=True, + ) + + rewards = get_rewards(self.neuron, responses) + expected_rewards = torch.FloatTensor([1.0] * len(responses)) + self.assertEqual(rewards, expected_rewards) + + def test_reward_with_nan(self): + # TODO: Test that NaN rewards are correctly sanitized + # TODO: Test that a bt.logging.warning is thrown when a NaN reward is sanitized + responses = self.dendrite.query( + # Send the query to miners in the network. + axons=[self.metagraph.axons[uid] for uid in self.miner_uids], + # Construct a dummy query. + synapse=Dummy(dummy_input=self.neuron.step), + # All responses have the deserialize function called on them before returning. + deserialize=True, + ) + + rewards = get_rewards(self.neuron, responses) + expected_rewards = rewards.clone() + # Add NaN values to rewards + rewards[0] = float("nan") + + with self.assertLogs(bt.logging, level="WARNING") as cm: + self.neuron.update_scores(rewards, self.miner_uids)