From 77e4d073d71d41caa58738a649f0b784e80b4674 Mon Sep 17 00:00:00 2001 From: sangar-1028 Date: Wed, 18 Sep 2024 05:55:16 -0700 Subject: [PATCH 001/129] feat: update README.md file --- README.md | 255 +++++++++++++++--------------------------------------- 1 file changed, 71 insertions(+), 184 deletions(-) diff --git a/README.md b/README.md index ba69bdae..395cf4ba 100644 --- a/README.md +++ b/README.md @@ -1,213 +1,100 @@ -
+# BTCopilot Subnet -# **Bittensor Subnet Template** -[![Discord Chat](https://img.shields.io/discord/308323056592486420.svg)](https://discord.gg/bittensor) -[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) +Welcome to BTCopilot Subnet, a pioneering Bittensor-based subnet designed to revolutionize project generation through advanced AI models. BTCopilot aims to transform diverse prompts—ranging from text and voice to images and Figma designs—into fully functional, ready-to-deploy projects. This subnet is tailored for developers, designers, and innovators who seek to accelerate their project development process with high-quality, AI-generated outputs. ---- +## Table of Contents -## The Incentivized Internet +- [Overview](#overview) +- [Features](#features) +- [Incentive Mechanism](#incentive-mechanism) +- [Roadmap](#roadmap) -[Discord](https://discord.gg/bittensor) • [Network](https://taostats.io/) • [Research](https://bittensor.com/whitepaper) -
+## Overview ---- -- [Quickstarter template](#quickstarter-template) -- [Introduction](#introduction) - - [Example](#example) -- [Installation](#installation) - - [Before you proceed](#before-you-proceed) - - [Install](#install) -- [Writing your own incentive mechanism](#writing-your-own-incentive-mechanism) -- [Writing your own subnet API](#writing-your-own-subnet-api) -- [Subnet Links](#subnet-links) -- [License](#license) +BTCopilot Subnet leverages state-of-the-art AI models to interpret and convert various types of prompts into complete, deployable projects. Whether you're starting with a simple HTML/CSS framework or aiming to develop a complex React application, BTCopilot can generate the entire codebase, ensuring it meets your specified requirements and is ready for immediate deployment. ---- -## Quickstarter template +### Vision -This template contains all the required installation instructions, scripts, and files and functions for: -- Building Bittensor subnets. -- Creating custom incentive mechanisms and running these mechanisms on the subnets. +BTCopilot envisions a future where project creation is seamless, automated, and efficient, empowering developers to focus more on innovation and less on repetitive coding tasks. By harnessing the capabilities of the Bittensor network, BTCopilot fosters a competitive environment that drives continuous improvement in AI-generated outputs. -In order to simplify the building of subnets, this template abstracts away the complexity of the underlying blockchain and other boilerplate code. While the default behavior of the template is sufficient for a simple subnet, you should customize the template in order to meet your specific requirements. ---- +### Purpose -## Introduction +The primary purpose of BTCopilot is to: -**IMPORTANT**: If you are new to Bittensor subnets, read this section before proceeding to [Installation](#installation) section. +- Automate Project Generation: Provide a platform that can autonomously generate high-quality projects from diverse input prompts. +- Enhance Productivity: Reduce the time and effort required for project development, enabling developers to quickly bring their ideas to life. +- Promote Innovation: Encourage innovative solutions and optimizations in project generation through competitive incentivization. -The Bittensor blockchain hosts multiple self-contained incentive mechanisms called **subnets**. Subnets are playing fields in which: -- Subnet miners who produce value, and -- Subnet validators who produce consensus +## Features -determine together the proper distribution of TAO for the purpose of incentivizing the creation of value, i.e., generating digital commodities, such as intelligence or data. +- **Text Prompt**: Generate projects by describing them in text. +- **Voice Prompt**: Create projects by giving voice commands. +- **Image Prompt**: Upload an image of a website or app, and BTCopilot will generate a pixel-perfect project. +- **Figma Prompt**: Convert Figma designs into functional projects. +- **Automated Downloads**: Directly download the generated projects as complete folders. -Each subnet consists of: -- Subnet miners and subnet validators. -- A protocol using which the subnet miners and subnet validators interact with one another. This protocol is part of the incentive mechanism. -- The Bittensor API using which the subnet miners and subnet validators interact with Bittensor's onchain consensus engine [Yuma Consensus](https://bittensor.com/documentation/validating/yuma-consensus). The Yuma Consensus is designed to drive these actors: subnet validators and subnet miners, into agreement on who is creating value and what that value is worth. +## Incentive Mechanism -This starter template is split into three primary files. To write your own incentive mechanism, you should edit these files. These files are: -1. `template/protocol.py`: Contains the definition of the protocol used by subnet miners and subnet validators. -2. `neurons/miner.py`: Script that defines the subnet miner's behavior, i.e., how the subnet miner responds to requests from subnet validators. -3. `neurons/validator.py`: This script defines the subnet validator's behavior, i.e., how the subnet validator requests information from the subnet miners and determines the scores. +The BTCopilot subnet incentivizes miners and validators to ensure high-quality outputs. Here’s how it works specifically for this subnet: -### Example +- Task Assignment: Subnet miners are assigned tasks related to generating and improving machine learning models based on various prompts (text, voice, image, Figma). +- Performance Evaluation: Validators evaluate the outputs produced by miners. The evaluation criteria include accuracy, efficiency, and innovation. +- Ranking and Rewarding: Validators rank the miners according to their performance. The Bittensor blockchain’s Yuma Consensus mechanism determines the TAO rewards distribution based on these rankings. -The Bittensor Subnet 1 for Text Prompting is built using this template. See [prompting](https://github.com/macrocosm-os/prompting) for how to configure the files and how to add monitoring and telemetry and support multiple miner types. Also see this Subnet 1 in action on [Taostats](https://taostats.io/subnets/netuid-1/) explorer. +## Evaluation Process ---- +- For Miners: -## Installation + Miners in the BTCopilot subnet are tasked with generating project outputs based on various types of prompts. Their outputs are evaluated based on the following criteria: -### Before you proceed -Before you proceed with the installation of the subnet, note the following: + 1. Accuracy: -- Use these instructions to run your subnet locally for your development and testing, or on Bittensor testnet or on Bittensor mainnet. -- **IMPORTANT**: We **strongly recommend** that you first run your subnet locally and complete your development and testing before running the subnet on Bittensor testnet. Furthermore, make sure that you next run your subnet on Bittensor testnet before running it on the Bittensor mainnet. -- You can run your subnet either as a subnet owner, or as a subnet validator or as a subnet miner. -- **IMPORTANT:** Make sure you are aware of the minimum compute requirements for your subnet. See the [Minimum compute YAML configuration](./min_compute.yml). -- Note that installation instructions differ based on your situation: For example, installing for local development and testing will require a few additional steps compared to installing for testnet. Similarly, installation instructions differ for a subnet owner vs a validator or a miner. + Correctness: The generated code must accurately reflect the requirements stated in the prompt. -### Install + Functionality: The output should be fully functional with minimal to no errors. -- **Running locally**: Follow the step-by-step instructions described in this section: [Running Subnet Locally](./docs/running_on_staging.md). -- **Running on Bittensor testnet**: Follow the step-by-step instructions described in this section: [Running on the Test Network](./docs/running_on_testnet.md). -- **Running on Bittensor mainnet**: Follow the step-by-step instructions described in this section: [Running on the Main Network](./docs/running_on_mainnet.md). + 2. Efficiency: ---- + Resource Utilization: The output should be produced using the least amount of computational resources without compromising on quality. -## Writing your own incentive mechanism + Speed: Faster generation times are favored, provided the output meets all other criteria. -As described in [Quickstarter template](#quickstarter-template) section above, when you are ready to write your own incentive mechanism, update this template repository by editing the following files. The code in these files contains detailed documentation on how to update the template. Read the documentation in each of the files to understand how to update the template. There are multiple **TODO**s in each of the files identifying sections you should update. These files are: -- `template/protocol.py`: Contains the definition of the wire-protocol used by miners and validators. -- `neurons/miner.py`: Script that defines the miner's behavior, i.e., how the miner responds to requests from validators. -- `neurons/validator.py`: This script defines the validator's behavior, i.e., how the validator requests information from the miners and determines the scores. -- `template/forward.py`: Contains the definition of the validator's forward pass. -- `template/reward.py`: Contains the definition of how validators reward miner responses. + 3. Innovation: -In addition to the above files, you should also update the following files: -- `README.md`: This file contains the documentation for your project. Update this file to reflect your project's documentation. -- `CONTRIBUTING.md`: This file contains the instructions for contributing to your project. Update this file to reflect your project's contribution guidelines. -- `template/__init__.py`: This file contains the version of your project. -- `setup.py`: This file contains the metadata about your project. Update this file to reflect your project's metadata. -- `docs/`: This directory contains the documentation for your project. Update this directory to reflect your project's documentation. + Novelty: Unique and creative approaches to solving the prompt are rewarded. -__Note__ -The `template` directory should also be renamed to your project name. ---- + Optimization: Innovative optimizations that improve the performance or usability of the generated project are highly valued. -# Writing your own subnet API -To leverage the abstract `SubnetsAPI` in Bittensor, you can implement a standardized interface. This interface is used to interact with the Bittensor network and can be used by a client to interact with the subnet through its exposed axons. - -What does Bittensor communication entail? Typically two processes, (1) preparing data for transit (creating and filling `synapse`s) and (2), processing the responses received from the `axon`(s). +- For Validators: -This protocol uses a handler registry system to associate bespoke interfaces for subnets by implementing two simple abstract functions: -- `prepare_synapse` -- `process_responses` - -These can be implemented as extensions of the generic `SubnetsAPI` interface. E.g.: - - -This is abstract, generic, and takes(`*args`, `**kwargs`) for flexibility. See the extremely simple base class: -```python -class SubnetsAPI(ABC): - def __init__(self, wallet: "bt.wallet"): - self.wallet = wallet - self.dendrite = bt.dendrite(wallet=wallet) - - async def __call__(self, *args, **kwargs): - return await self.query_api(*args, **kwargs) - - @abstractmethod - def prepare_synapse(self, *args, **kwargs) -> Any: - """ - Prepare the synapse-specific payload. - """ - ... - - @abstractmethod - def process_responses(self, responses: List[Union["bt.Synapse", Any]]) -> Any: - """ - Process the responses from the network. - """ - ... - -``` - - -Here is a toy example: - -```python -from bittensor.subnets import SubnetsAPI -from MySubnet import MySynapse - -class MySynapseAPI(SubnetsAPI): - def __init__(self, wallet: "bt.wallet"): - super().__init__(wallet) - self.netuid = 99 - - def prepare_synapse(self, prompt: str) -> MySynapse: - # Do any preparatory work to fill the synapse - data = do_prompt_injection(prompt) - - # Fill the synapse for transit - synapse = StoreUser( - messages=[data], - ) - # Send it along - return synapse - - def process_responses(self, responses: List[Union["bt.Synapse", Any]]) -> str: - # Look through the responses for information required by your application - for response in responses: - if response.dendrite.status_code != 200: - continue - # potentially apply post processing - result_data = postprocess_data_from_response(response) - # return data to the client - return result_data -``` - -You can use a subnet API to the registry by doing the following: -1. Download and install the specific repo you want -1. Import the appropriate API handler from bespoke subnets -1. Make the query given the subnet specific API - - - -# Subnet Links -In order to see real-world examples of subnets in-action, see the `subnet_links.py` document or access them from inside the `template` package by: -```python -import template -template.SUBNET_LINKS -[{'name': 'sn0', 'url': ''}, - {'name': 'sn1', 'url': 'https://github.com/opentensor/prompting/'}, - {'name': 'sn2', 'url': 'https://github.com/bittranslateio/bittranslate/'}, - {'name': 'sn3', 'url': 'https://github.com/gitphantomman/scraping_subnet/'}, - {'name': 'sn4', 'url': 'https://github.com/manifold-inc/targon/'}, -... -] -``` - -## License -This repository is licensed under the MIT License. -```text -# The MIT License (MIT) -# Copyright © 2024 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. -``` + Validators play a crucial role in ensuring the quality of outputs. They are responsible for evaluating and ranking the miners’ contributions. The evaluation process involves: + + 1. Initial Review: Validators perform an initial check to ensure that the submitted outputs meet basic functional requirements. + 2. Detailed Assessment: Each output is thoroughly reviewed against the criteria of accuracy, efficiency, and innovation. + 3. Feedback Provision: Validators provide detailed feedback to miners, highlighting strengths and areas for improvement. + 4. Ranking Submission: Validators rank the outputs from different miners. These rankings are submitted to the Bittensor blockchain. + +### Example Scenario + +- Prompt: A miner receives a text prompt to create a React-based TodoList application. +- Generation: The miner generates the code for the application and submits it. +- Evaluation: Validators review the submission: + - Accuracy: Does the application have all the features mentioned in the prompt? + - Efficiency: Is the code optimized for performance? + - Innovation: Does the application include any additional features or optimizations not explicitly requested but beneficial? +- Ranking: Validators rank this submission against others. +- Rewarding: Based on the ranking, the miner receives TAO rewards. + +## Roadmap + +Phase 1: Generate HTML/CSS projects from text prompts. + +Phase 2: Enable voice prompts for project generation. + +Phase 3: Support image prompts to generate pixel-perfect projects. + +Phase 4: Integrate Figma designs as input for project generation. + +Phase 5: Automate the downloading of fully functional project folders. + +Phase 6: Expand to generate full framework-based projects like React, Angular, etc. From 1dae750ee7b4c1b822ccfcb9cb9230ce44a337ce Mon Sep 17 00:00:00 2001 From: sangar-1028 Date: Wed, 18 Sep 2024 06:02:28 -0700 Subject: [PATCH 002/129] feat: update setup.py appropriate --- setup.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/setup.py b/setup.py index f76ec9b2..7c560404 100644 --- a/setup.py +++ b/setup.py @@ -63,16 +63,16 @@ def read_requirements(path): version_string = version_match.group(1) setup( - name="bittensor_subnet_template", # TODO(developer): Change this value to your module subnet name. + name="btcopilot", version=version_string, - description="bittensor_subnet_template", # TODO(developer): Change this value to your module subnet description. + description="BTCopilot aims to transform diverse prompts—ranging from text and voice to images and Figma designs—into fully functional, ready-to-deploy projects.", long_description=long_description, long_description_content_type="text/markdown", - url="https://github.com/opentensor/bittensor-subnet-template", # TODO(developer): Change this url to your module subnet github url. - author="bittensor.com", # TODO(developer): Change this value to your module subnet author name. + url="https://github.com/BTCopilot/btcopilot", + author="Sangar", packages=find_packages(), include_package_data=True, - author_email="", # TODO(developer): Change this value to your module subnet author email. + author_email="sangar.work1028@gmail.com", license="MIT", python_requires=">=3.8", install_requires=requirements, From 9b8837ad0e54d36b4fc0e8cb32531127a1079b02 Mon Sep 17 00:00:00 2001 From: sangar-1028 Date: Wed, 18 Sep 2024 06:09:24 -0700 Subject: [PATCH 003/129] feat: update CONTRIBUTING.md file --- contrib/CONTRIBUTING.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/contrib/CONTRIBUTING.md b/contrib/CONTRIBUTING.md index ba33ce3c..2393c621 100644 --- a/contrib/CONTRIBUTING.md +++ b/contrib/CONTRIBUTING.md @@ -1,6 +1,6 @@ -# Contributing to Bittensor Subnet Development +# Contributing to BTCopilot -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. +The following is a set of guidelines for contributing to the BTCopilot Subnet. 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) @@ -16,10 +16,10 @@ The following is a set of guidelines for contributing to the Bittensor ecosystem ## How Can I Contribute? -TODO(developer): Define your desired contribution procedure. +You can fork this repository and create a PR with your codes and request review from our development team. You can reference [Pull Request Philosophy](#pull-request-philosophy). ## Communication Channels -TODO(developer): Place your communication channels here +Contact us on [Our discord server](https://discord.gg/P2XRwVEJ) > Please follow the Bittensor Subnet [style guide](./STYLE.md) regardless of your contribution type. @@ -99,7 +99,7 @@ After you submit a pull request, it will be reviewed by the maintainers. They ma > 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 remote add upstream https://github.com/BTCopilot/btcopilot.git git fetch upstream git merge upstream/ git push origin From 3a34d7b21ccaf2bc9379cc9642f92c39aa9fe1ff Mon Sep 17 00:00:00 2001 From: sangar-1028 Date: Wed, 18 Sep 2024 06:11:35 -0700 Subject: [PATCH 004/129] feat: replaced template with btcopilot --- {template => btcopilot}/__init__.py | 0 {template => btcopilot}/api/__init__.py | 0 {template => btcopilot}/api/dummy.py | 2 +- {template => btcopilot}/api/get_query_axons.py | 0 {template => btcopilot}/base/__init__.py | 0 {template => btcopilot}/base/miner.py | 4 ++-- {template => btcopilot}/base/neuron.py | 8 ++++---- {template => btcopilot}/base/utils/__init__.py | 0 {template => btcopilot}/base/utils/weight_utils.py | 0 {template => btcopilot}/base/validator.py | 8 ++++---- {template => btcopilot}/mock.py | 0 {template => btcopilot}/protocol.py | 0 {template => btcopilot}/subnet_links.py | 0 {template => btcopilot}/utils/__init__.py | 0 {template => btcopilot}/utils/config.py | 0 {template => btcopilot}/utils/logging.py | 0 {template => btcopilot}/utils/misc.py | 0 {template => btcopilot}/utils/uids.py | 0 {template => btcopilot}/validator/__init__.py | 0 {template => btcopilot}/validator/forward.py | 6 +++--- {template => btcopilot}/validator/reward.py | 0 neurons/miner.py | 12 ++++++------ neurons/validator.py | 4 ++-- tests/test_template_validator.py | 8 ++++---- 24 files changed, 26 insertions(+), 26 deletions(-) rename {template => btcopilot}/__init__.py (100%) rename {template => btcopilot}/api/__init__.py (100%) rename {template => btcopilot}/api/dummy.py (98%) rename {template => btcopilot}/api/get_query_axons.py (100%) rename {template => btcopilot}/base/__init__.py (100%) rename {template => btcopilot}/base/miner.py (98%) rename {template => btcopilot}/base/neuron.py (96%) rename {template => btcopilot}/base/utils/__init__.py (100%) rename {template => btcopilot}/base/utils/weight_utils.py (100%) rename {template => btcopilot}/base/validator.py (98%) rename {template => btcopilot}/mock.py (100%) rename {template => btcopilot}/protocol.py (100%) rename {template => btcopilot}/subnet_links.py (100%) rename {template => btcopilot}/utils/__init__.py (100%) rename {template => btcopilot}/utils/config.py (100%) rename {template => btcopilot}/utils/logging.py (100%) rename {template => btcopilot}/utils/misc.py (100%) rename {template => btcopilot}/utils/uids.py (100%) rename {template => btcopilot}/validator/__init__.py (100%) rename {template => btcopilot}/validator/forward.py (95%) rename {template => btcopilot}/validator/reward.py (100%) diff --git a/template/__init__.py b/btcopilot/__init__.py similarity index 100% rename from template/__init__.py rename to btcopilot/__init__.py diff --git a/template/api/__init__.py b/btcopilot/api/__init__.py similarity index 100% rename from template/api/__init__.py rename to btcopilot/api/__init__.py diff --git a/template/api/dummy.py b/btcopilot/api/dummy.py similarity index 98% rename from template/api/dummy.py rename to btcopilot/api/dummy.py index f6a433f1..b1d0e13b 100644 --- a/template/api/dummy.py +++ b/btcopilot/api/dummy.py @@ -19,7 +19,7 @@ import bittensor as bt from typing import List, Optional, Union, Any, Dict -from template.protocol import Dummy +from btcopilot.protocol import Dummy from bittensor.subnets import SubnetsAPI diff --git a/template/api/get_query_axons.py b/btcopilot/api/get_query_axons.py similarity index 100% rename from template/api/get_query_axons.py rename to btcopilot/api/get_query_axons.py diff --git a/template/base/__init__.py b/btcopilot/base/__init__.py similarity index 100% rename from template/base/__init__.py rename to btcopilot/base/__init__.py diff --git a/template/base/miner.py b/btcopilot/base/miner.py similarity index 98% rename from template/base/miner.py rename to btcopilot/base/miner.py index 1788e24b..d81c9632 100644 --- a/template/base/miner.py +++ b/btcopilot/base/miner.py @@ -23,8 +23,8 @@ import bittensor as bt -from template.base.neuron import BaseNeuron -from template.utils.config import add_miner_args +from btcopilot.base.neuron import BaseNeuron +from btcopilot.utils.config import add_miner_args from typing import Union diff --git a/template/base/neuron.py b/btcopilot/base/neuron.py similarity index 96% rename from template/base/neuron.py rename to btcopilot/base/neuron.py index 9b2ce7b2..269cf0d4 100644 --- a/template/base/neuron.py +++ b/btcopilot/base/neuron.py @@ -23,10 +23,10 @@ from abc import ABC, abstractmethod # Sync calls set weights and also resyncs the metagraph. -from template.utils.config import check_config, add_args, config -from template.utils.misc import ttl_get_block -from template import __spec_version__ as spec_version -from template.mock import MockSubtensor, MockMetagraph +from btcopilot.utils.config import check_config, add_args, config +from btcopilot.utils.misc import ttl_get_block +from btcopilot import __spec_version__ as spec_version +from btcopilot.mock import MockSubtensor, MockMetagraph class BaseNeuron(ABC): diff --git a/template/base/utils/__init__.py b/btcopilot/base/utils/__init__.py similarity index 100% rename from template/base/utils/__init__.py rename to btcopilot/base/utils/__init__.py diff --git a/template/base/utils/weight_utils.py b/btcopilot/base/utils/weight_utils.py similarity index 100% rename from template/base/utils/weight_utils.py rename to btcopilot/base/utils/weight_utils.py diff --git a/template/base/validator.py b/btcopilot/base/validator.py similarity index 98% rename from template/base/validator.py rename to btcopilot/base/validator.py index c1ca07ed..cdc9afa3 100644 --- a/template/base/validator.py +++ b/btcopilot/base/validator.py @@ -28,13 +28,13 @@ from typing import List, Union from traceback import print_exception -from template.base.neuron import BaseNeuron -from template.base.utils.weight_utils import ( +from btcopilot.base.neuron import BaseNeuron +from btcopilot.base.utils.weight_utils import ( process_weights_for_netuid, convert_weights_and_uids_for_emit, ) # TODO: Replace when bittensor switches to numpy -from template.mock import MockDendrite -from template.utils.config import add_validator_args +from btcopilot.mock import MockDendrite +from btcopilot.utils.config import add_validator_args class BaseValidatorNeuron(BaseNeuron): diff --git a/template/mock.py b/btcopilot/mock.py similarity index 100% rename from template/mock.py rename to btcopilot/mock.py diff --git a/template/protocol.py b/btcopilot/protocol.py similarity index 100% rename from template/protocol.py rename to btcopilot/protocol.py diff --git a/template/subnet_links.py b/btcopilot/subnet_links.py similarity index 100% rename from template/subnet_links.py rename to btcopilot/subnet_links.py diff --git a/template/utils/__init__.py b/btcopilot/utils/__init__.py similarity index 100% rename from template/utils/__init__.py rename to btcopilot/utils/__init__.py diff --git a/template/utils/config.py b/btcopilot/utils/config.py similarity index 100% rename from template/utils/config.py rename to btcopilot/utils/config.py diff --git a/template/utils/logging.py b/btcopilot/utils/logging.py similarity index 100% rename from template/utils/logging.py rename to btcopilot/utils/logging.py diff --git a/template/utils/misc.py b/btcopilot/utils/misc.py similarity index 100% rename from template/utils/misc.py rename to btcopilot/utils/misc.py diff --git a/template/utils/uids.py b/btcopilot/utils/uids.py similarity index 100% rename from template/utils/uids.py rename to btcopilot/utils/uids.py diff --git a/template/validator/__init__.py b/btcopilot/validator/__init__.py similarity index 100% rename from template/validator/__init__.py rename to btcopilot/validator/__init__.py diff --git a/template/validator/forward.py b/btcopilot/validator/forward.py similarity index 95% rename from template/validator/forward.py rename to btcopilot/validator/forward.py index af5e7ee0..9ccf1bc1 100644 --- a/template/validator/forward.py +++ b/btcopilot/validator/forward.py @@ -20,9 +20,9 @@ import time import bittensor as bt -from template.protocol import Dummy -from template.validator.reward import get_rewards -from template.utils.uids import get_random_uids +from btcopilot.protocol import Dummy +from btcopilot.validator.reward import get_rewards +from btcopilot.utils.uids import get_random_uids async def forward(self): diff --git a/template/validator/reward.py b/btcopilot/validator/reward.py similarity index 100% rename from template/validator/reward.py rename to btcopilot/validator/reward.py diff --git a/neurons/miner.py b/neurons/miner.py index 5f7b9500..cd00004e 100644 --- a/neurons/miner.py +++ b/neurons/miner.py @@ -22,10 +22,10 @@ import bittensor as bt # Bittensor Miner Template: -import template +import btcopilot # import base miner class which takes care of most of the boilerplate -from template.base.miner import BaseMinerNeuron +from btcopilot.base.miner import BaseMinerNeuron class Miner(BaseMinerNeuron): @@ -43,8 +43,8 @@ def __init__(self, config=None): # TODO(developer): Anything specific to your use case you can do here async def forward( - self, synapse: template.protocol.Dummy - ) -> template.protocol.Dummy: + self, synapse: btcopilot.protocol.Dummy + ) -> btcopilot.protocol.Dummy: """ Processes the incoming 'Dummy' synapse by performing a predefined operation on the input data. This method should be replaced with actual logic relevant to the miner's purpose. @@ -63,7 +63,7 @@ async def forward( return synapse async def blacklist( - self, synapse: template.protocol.Dummy + self, synapse: btcopilot.protocol.Dummy ) -> typing.Tuple[bool, str]: """ Determines whether an incoming request should be blacklisted and thus ignored. Your implementation should @@ -124,7 +124,7 @@ async def blacklist( ) return False, "Hotkey recognized!" - async def priority(self, synapse: template.protocol.Dummy) -> float: + async def priority(self, synapse: btcopilot.protocol.Dummy) -> 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. diff --git a/neurons/validator.py b/neurons/validator.py index e28b972c..290e2a2c 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -24,9 +24,9 @@ import bittensor as bt # import base validator class which takes care of most of the boilerplate -from template.base.validator import BaseValidatorNeuron +from btcopilot.base.validator import BaseValidatorNeuron # Bittensor Validator Template: -from template.validator import forward +from btcopilot.validator import forward class Validator(BaseValidatorNeuron): diff --git a/tests/test_template_validator.py b/tests/test_template_validator.py index 48e015a9..92f82d36 100644 --- a/tests/test_template_validator.py +++ b/tests/test_template_validator.py @@ -23,10 +23,10 @@ import torch from neurons.validator import Validator -from template.base.validator import BaseValidatorNeuron -from template.protocol import Dummy -from template.utils.uids import get_random_uids -from template.validator.reward import get_rewards +from btcopilot.base.validator import BaseValidatorNeuron +from btcopilot.protocol import Dummy +from btcopilot.utils.uids import get_random_uids +from btcopilot.validator.reward import get_rewards class TemplateValidatorNeuronTestCase(unittest.TestCase): From 38ff64209d3a7a40b87b0a4a057b8d392519b788 Mon Sep 17 00:00:00 2001 From: sangar-1028 Date: Wed, 18 Sep 2024 06:14:50 -0700 Subject: [PATCH 005/129] feat: update btcopilot/__init__.py file --- btcopilot/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/btcopilot/__init__.py b/btcopilot/__init__.py index cb07b8c0..4115dac0 100644 --- a/btcopilot/__init__.py +++ b/btcopilot/__init__.py @@ -1,6 +1,6 @@ # The MIT License (MIT) # Copyright © 2023 Yuma Rao -# TODO(developer): Set your name +# Sangar # Copyright © 2023 # Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated @@ -17,9 +17,9 @@ # 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(developer): Change this value when updating your code base. -# Define the version of the template module. -__version__ = "0.0.0" +# Change this value when updating your code base. +# Define the version of the btcopilot. +__version__ = "0.0.1" version_split = __version__.split(".") __spec_version__ = ( (1000 * int(version_split[0])) From b63bf4d49ae904289475145b0050697827086ddb Mon Sep 17 00:00:00 2001 From: sangar-1028 Date: Wed, 18 Sep 2024 06:16:45 -0700 Subject: [PATCH 006/129] feat: update setup.py --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index 7c560404..18ca652c 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ # The MIT License (MIT) -# Copyright © 2023 Yuma Rao -# TODO(developer): Set your name -# Copyright © 2023 +# Copyright © 2023 BTCopilot +# Sangar +# Copyright © 2023 # 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 From c97712ea0e2af0b45f87869c70a32734ab9f5bad Mon Sep 17 00:00:00 2001 From: sangar-1028 Date: Wed, 18 Sep 2024 06:22:26 -0700 Subject: [PATCH 007/129] feat: update gitignore file --- .gitignore | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.gitignore b/.gitignore index 72a70c3a..6b79b60a 100644 --- a/.gitignore +++ b/.gitignore @@ -163,3 +163,6 @@ testing/ # Editors .vscode/settings.json + +.DS_Store +temp.txt \ No newline at end of file From 89a13c32688550cef4c6bde60c023a2d113f3901 Mon Sep 17 00:00:00 2001 From: sangar-1028 Date: Fri, 27 Sep 2024 15:22:32 -0700 Subject: [PATCH 008/129] feat: specified author's name --- btcopilot/base/validator.py | 3 +-- btcopilot/protocol.py | 3 +-- btcopilot/validator/forward.py | 3 +-- btcopilot/validator/reward.py | 3 +-- neurons/miner.py | 3 +-- neurons/validator.py | 3 +-- 6 files changed, 6 insertions(+), 12 deletions(-) diff --git a/btcopilot/base/validator.py b/btcopilot/base/validator.py index cdc9afa3..dc464428 100644 --- a/btcopilot/base/validator.py +++ b/btcopilot/base/validator.py @@ -1,7 +1,6 @@ # The MIT License (MIT) # Copyright © 2023 Yuma Rao -# TODO(developer): Set your name -# Copyright © 2023 +# Copyright © 2024 Sangar # 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 diff --git a/btcopilot/protocol.py b/btcopilot/protocol.py index c601e58a..b7105bc4 100644 --- a/btcopilot/protocol.py +++ b/btcopilot/protocol.py @@ -1,7 +1,6 @@ # The MIT License (MIT) # Copyright © 2023 Yuma Rao -# TODO(developer): Set your name -# Copyright © 2023 +# Copyright © 2024 Sangar # 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 diff --git a/btcopilot/validator/forward.py b/btcopilot/validator/forward.py index 9ccf1bc1..f679e907 100644 --- a/btcopilot/validator/forward.py +++ b/btcopilot/validator/forward.py @@ -1,7 +1,6 @@ # The MIT License (MIT) # Copyright © 2023 Yuma Rao -# TODO(developer): Set your name -# Copyright © 2023 +# Copyright © 2024 Sangar # 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 diff --git a/btcopilot/validator/reward.py b/btcopilot/validator/reward.py index 58492183..58f920a4 100644 --- a/btcopilot/validator/reward.py +++ b/btcopilot/validator/reward.py @@ -1,7 +1,6 @@ # The MIT License (MIT) # Copyright © 2023 Yuma Rao -# TODO(developer): Set your name -# Copyright © 2023 +# Copyright © 2024 Sangar # 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 diff --git a/neurons/miner.py b/neurons/miner.py index cd00004e..8e350ec9 100644 --- a/neurons/miner.py +++ b/neurons/miner.py @@ -1,7 +1,6 @@ # The MIT License (MIT) # Copyright © 2023 Yuma Rao -# TODO(developer): Set your name -# Copyright © 2023 +# Copyright © 2023 Sangar # 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 diff --git a/neurons/validator.py b/neurons/validator.py index 290e2a2c..ca71c02b 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -1,7 +1,6 @@ # The MIT License (MIT) # Copyright © 2023 Yuma Rao -# TODO(developer): Set your name -# Copyright © 2023 +# Copyright © 2024 Sangar # 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 From dd6a1256e05ad56e677d2e1e3f83e33d0ea78958 Mon Sep 17 00:00:00 2001 From: sangar-1028 Date: Thu, 31 Oct 2024 10:04:04 -0500 Subject: [PATCH 009/129] feat: complete miner and validator part for initial workflow --- btcopilot/base/miner.py | 3 +- btcopilot/base/neuron.py | 5 +- btcopilot/base/validator.py | 5 +- btcopilot/miners/dummy_miner.py | 116 +++++++++++++++++ btcopilot/protocol.py | 135 +++++++++++-------- btcopilot/rewards/__init__.py | 5 + btcopilot/rewards/gpt.py | 10 ++ btcopilot/rewards/penalty.py | 11 ++ btcopilot/rewards/reward.py | 2 + btcopilot/rewards/reward_manager.py | 67 ++++++++++ btcopilot/rewards/speed.py | 15 +++ btcopilot/solution/__init__.py | 1 + btcopilot/solution/solution.py | 7 + btcopilot/task_generator/__init__.py | 1 + btcopilot/task_generator/task_generator.py | 12 ++ btcopilot/tasks/__init__.py | 1 + btcopilot/tasks/task.py | 22 ++++ btcopilot/utils/config.py | 7 + btcopilot/validator/forward.py | 75 ++++++++--- docs/stream_tutorial/client.py | 2 +- lang_chain_test.py | 144 +++++++++++++++++++++ neurons/miner.py | 39 +++--- neurons/validator.py | 8 +- requirements.txt | 5 +- run_miner.sh | 3 + run_validator.sh | 2 + 26 files changed, 603 insertions(+), 100 deletions(-) create mode 100644 btcopilot/miners/dummy_miner.py create mode 100644 btcopilot/rewards/__init__.py create mode 100644 btcopilot/rewards/gpt.py create mode 100644 btcopilot/rewards/penalty.py create mode 100644 btcopilot/rewards/reward.py create mode 100644 btcopilot/rewards/reward_manager.py create mode 100644 btcopilot/rewards/speed.py create mode 100644 btcopilot/solution/__init__.py create mode 100644 btcopilot/solution/solution.py create mode 100644 btcopilot/task_generator/__init__.py create mode 100644 btcopilot/task_generator/task_generator.py create mode 100644 btcopilot/tasks/__init__.py create mode 100644 btcopilot/tasks/task.py create mode 100644 lang_chain_test.py create mode 100644 run_miner.sh create mode 100644 run_validator.sh diff --git a/btcopilot/base/miner.py b/btcopilot/base/miner.py index d81c9632..5f3e3402 100644 --- a/btcopilot/base/miner.py +++ b/btcopilot/base/miner.py @@ -186,7 +186,8 @@ def __exit__(self, exc_type, exc_value, traceback): def resync_metagraph(self): """Resyncs the metagraph and updates the hotkeys and moving averages based on the new metagraph.""" - bt.logging.info("resync_metagraph()") + #TODO: Implement this + #bt.logging.info("resync_metagraph()") # Sync the metagraph. self.metagraph.sync(subtensor=self.subtensor) diff --git a/btcopilot/base/neuron.py b/btcopilot/base/neuron.py index 269cf0d4..8c41320a 100644 --- a/btcopilot/base/neuron.py +++ b/btcopilot/base/neuron.py @@ -169,9 +169,8 @@ def should_set_weights(self) -> bool: ) # 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." - ) + #TODO: Implement this + pass def load_state(self): bt.logging.warning( diff --git a/btcopilot/base/validator.py b/btcopilot/base/validator.py index cdc9afa3..5e2ea255 100644 --- a/btcopilot/base/validator.py +++ b/btcopilot/base/validator.py @@ -54,6 +54,7 @@ def __init__(self, config=None): # Save a copy of the hotkeys to local memory. self.hotkeys = copy.deepcopy(self.metagraph.hotkeys) + print("=== self.hotkeys ===>", self.hotkeys) # Dendrite lets us send messages to other nodes (axons) in the network. if self.config.mock: @@ -280,7 +281,9 @@ def set_weights(self): def resync_metagraph(self): """Resyncs the metagraph and updates the hotkeys and moving averages based on the new metagraph.""" - bt.logging.info("resync_metagraph()") + #TODO: Implement this + + #bt.logging.info("resync_metagraph()") # Copies state of metagraph before syncing. previous_metagraph = copy.deepcopy(self.metagraph) diff --git a/btcopilot/miners/dummy_miner.py b/btcopilot/miners/dummy_miner.py new file mode 100644 index 00000000..13336d6c --- /dev/null +++ b/btcopilot/miners/dummy_miner.py @@ -0,0 +1,116 @@ +import json +from functools import partial +from starlette.types import Send +import time +from typing import Dict + +from typing import Dict, Awaitable +from langchain_openai import ChatOpenAI +from dotenv import load_dotenv, find_dotenv +from langchain.prompts import ChatPromptTemplate +from langchain_core.output_parsers import StrOutputParser +from langchain_core.runnables.base import RunnableSequence + +import bittensor as bt +import btcopilot +import os + +def miner_init(self): + bt.logging.debug(f"Dummy Miner initialized") + _ = load_dotenv(find_dotenv()) + api_key = os.getenv("OPENAI_API_KEY") + # Set openai key and other args + self.model = ChatOpenAI( + api_key=api_key, + model_name="gpt-4", + ) + +def miner_forward(self, synapse: btcopilot.protocol.BtCopilotSynapse)->Awaitable: + + async def _forward(self, chain: RunnableSequence, chain_formatter: Dict[str, str], timeout_threshold: float, init_time: float, send: Send): + try: + json_response = { + "css": "body { background-color: red; }", + "html": "

Hello World

", + } + await send( + { + "type": "http.response.body", + "body": json.dumps(json_response).encode("utf-8"), + "more_body": False, + } + ) + # buffer = [] + + # timeout_reached = False + # is_in_code_block = True + # is_first_line = True + + # for token in chain.stream(chain_formatter): + # if is_first_line and token.startswith("```"): + # is_in_code_block = False + + # if is_in_code_block and not token.startswith("```"): + # buffer.append(token) + + # if time.time() - init_time > timeout_threshold: + # bt.logging.debug(f"⏰ Timeout reached, stopping streaming") + # timeout_reached = True + # break + + # if len(buffer) == self.config.neuron.streaming_batch_size: + # joined_buffer = "".join(buffer) + # bt.logging.debug(f"Streamed tokens: {joined_buffer}") + + # await send( + # { + # "type": "http.response.body", + # "body": joined_buffer.encode("utf-8"), + # "more_body": True, + # } + # ) + # buffer = [] + + # if is_first_line and token.endswith("\n"): + # is_first_line = False + # is_in_code_block = True + + # if ( + # buffer and not timeout_reached + # ): # Don't send the last buffer of data if timeout. + # joined_buffer = "".join(buffer) + # await send( + # { + # "type": "http.response.body", + # "body": joined_buffer.encode("utf-8"), + # "more_body": False, + # } + # ) + except Exception as e: + bt.logging.error(f"Dummy Miner Error: {e}") + + bt.logging.debug(f"Dummy Miner Query received, forwarding synapse: {synapse}") + + prompt = ChatPromptTemplate.from_messages([ + ("system", "You are an expert programmer. Generate code based on the following request without explanations:\n\n{query}\n\nProvide only the code."), + ("user", "{query}"), + ]) + chain = prompt | self.model | StrOutputParser() + + query = synapse.task.query + bt.logging.debug(f"Dummy Miner Query received: {synapse}") + time.sleep(2) + chain_formatter = {"query": query} + + init_time = time.time() + timeout_threshold = float(synapse.timeout) + + token_streamer = partial( + _forward, + self, + chain, + chain_formatter, + timeout_threshold, + init_time, + ) + return synapse.create_streaming_response(token_streamer) \ No newline at end of file diff --git a/btcopilot/protocol.py b/btcopilot/protocol.py index c601e58a..db4e6ffd 100644 --- a/btcopilot/protocol.py +++ b/btcopilot/protocol.py @@ -1,7 +1,6 @@ # The MIT License (MIT) # Copyright © 2023 Yuma Rao -# TODO(developer): Set your name -# Copyright © 2023 +# Copyright © 2023 Dominique Hayes # 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 @@ -17,60 +16,94 @@ # 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 typing -import bittensor as bt - -# TODO(developer): Rewrite with your protocol definition. - -# This is the protocol for the dummy miner and validator. -# It is a simple request-response protocol where the validator sends a request -# to the miner, and the miner responds with a dummy response. - -# ---- miner ---- -# Example usage: -# def dummy( synapse: Dummy ) -> Dummy: -# synapse.dummy_output = synapse.dummy_input + 1 -# return synapse -# axon = bt.axon().attach( dummy ).serve(netuid=...).start() +import pydantic +import json +from typing import AsyncIterator, Union, Any +from starlette.responses import StreamingResponse -# ---- validator --- -# Example usage: -# dendrite = bt.dendrite() -# dummy_output = dendrite.query( Dummy( dummy_input = 1 ) ) -# assert dummy_output == 2 +import bittensor as bt +from btcopilot.tasks import Task +from btcopilot.solution import Solution -class Dummy(bt.Synapse): +class BtCopilotSynapse(bt.StreamingSynapse): """ - A simple dummy protocol representation which uses bt.Synapse as its base. - This protocol helps in handling dummy request and response communication between - the miner and the validator. - - Attributes: - - dummy_input: An integer value representing the input request sent by the validator. - - dummy_output: An optional integer value which, when filled, represents the response from the miner. + A protocol for the BtCopilot. """ - # Required request input, filled by sending dendrite caller. - dummy_input: int - - # Optional request output, filled by receiving axon. - dummy_output: typing.Optional[int] = None - - def deserialize(self) -> int: + task: Union[Task, None] = pydantic.Field( + None, + title="Task", + description="A task to be sent to miners." + ) + + solution: Union[Solution, None] = pydantic.Field( + None, + title="Solution", + description="A solution received from miners." + ) + + completion: str = pydantic.Field( + "", + title="Completion", + description="The completion response from miners." + ) + + async def process_streaming_response(self, response: StreamingResponse) -> AsyncIterator[str]: """ - Deserialize the dummy output. This method retrieves the response from - the miner in the form of dummy_output, deserializes it and returns it - as the output of the dendrite.query() call. - - Returns: - - int: The deserialized response, which in this case is the value of dummy_output. - - Example: - Assuming a Dummy instance has a dummy_output value of 5: - >>> dummy_instance = Dummy(dummy_input=4) - >>> dummy_instance.dummy_output = 5 - >>> dummy_instance.deserialize() - 5 + Processes a streaming response from a miner. + """ + if self.completion is None: + self.completion = "" + async for chunk in response.content.iter_any(): + tokens = chunk.decode("utf-8") + + for token in tokens: + if token: + self.completion += token + yield tokens + + def deserialize(self) -> Union[Any, None]: + """ + Deserializes the response. + """ + try: + bt.logging.debug(f"completion: {self.completion}") + json_response = json.loads(self.completion) + css = json_response.get("css", None) + html = json_response.get("html", None) + if css is None and html is None: + bt.logging.error(f"Invalid response: {json_response}") + return None + + css = str(css) + html = str(html) + + process_time = self.dendrite.process_time + bt.logging.debug(f"css: {css}, html: {html}, process_time: {process_time}") + self.solution = Solution(css=css, html=html, process_time=process_time, miner_uid=0) + return self + except Exception as e: + bt.logging.error(f"Failed to parse completion: {e}") + return None + + + def extract_response_json(self, response: StreamingResponse) -> dict: + """ + Extracts the response JSON. """ - return self.dummy_output + headers = { + k.decode("utf-8"): v.decode("utf-8") + for k, v in response.__dict__["_raw_headers"] + } + + def extract_info(prefix:str) -> dict: + return { + key.split("_")[-1]: value + for key, value in headers.items() + if key.startswith(prefix) + } + return { + "completion": self.completion + } + \ No newline at end of file diff --git a/btcopilot/rewards/__init__.py b/btcopilot/rewards/__init__.py new file mode 100644 index 00000000..9ff3b958 --- /dev/null +++ b/btcopilot/rewards/__init__.py @@ -0,0 +1,5 @@ +from .reward import Reward +from .gpt import GPTReward +from .speed import SpeedReward +from .penalty import PenaltyReward +from .reward_manager import RewardManager \ No newline at end of file diff --git a/btcopilot/rewards/gpt.py b/btcopilot/rewards/gpt.py new file mode 100644 index 00000000..2fe0e99b --- /dev/null +++ b/btcopilot/rewards/gpt.py @@ -0,0 +1,10 @@ +from btcopilot.rewards import Reward +from btcopilot.tasks import Task +from btcopilot.solution import Solution + +class GPTReward(Reward): + def __init__(self): + pass + + def reward(self, task: Task, solution: Solution) -> float: + return 0.0 \ No newline at end of file diff --git a/btcopilot/rewards/penalty.py b/btcopilot/rewards/penalty.py new file mode 100644 index 00000000..3dd02638 --- /dev/null +++ b/btcopilot/rewards/penalty.py @@ -0,0 +1,11 @@ +from btcopilot.rewards import Reward +from btcopilot.tasks import Task +from btcopilot.solution import Solution + +class PenaltyReward(Reward): + + def __init__(self): + pass + + def reward(self, task: Task, solution: Solution) -> float: + return 0.0 \ No newline at end of file diff --git a/btcopilot/rewards/reward.py b/btcopilot/rewards/reward.py new file mode 100644 index 00000000..39f95bd1 --- /dev/null +++ b/btcopilot/rewards/reward.py @@ -0,0 +1,2 @@ +class Reward: + pass \ No newline at end of file diff --git a/btcopilot/rewards/reward_manager.py b/btcopilot/rewards/reward_manager.py new file mode 100644 index 00000000..60f7d191 --- /dev/null +++ b/btcopilot/rewards/reward_manager.py @@ -0,0 +1,67 @@ +from typing import Dict, List, Tuple + +from btcopilot.rewards import Reward +from btcopilot.rewards import GPTReward +from btcopilot.rewards import SpeedReward +from btcopilot.rewards import PenaltyReward +from btcopilot.tasks import Task +from btcopilot.solution import Solution + +class RewardManager: + """ + A singleton manager for the reward models. + """ + reward_models: Dict[str, Reward] = {} + + def __init__(self): + self.reward_models = { + "gpt": GPTReward(), + "speed": SpeedReward(), + "penalty": PenaltyReward(), + } + + def _penalty(self, task: Task, solution: Solution) -> float: + """ + Penalize the solution based on the penalty models. + """ + penalty = 0 + for penalty_model in task.penalty_models: + model = self.reward_models[penalty_model[0]] + weight = penalty_model[1] + + penalty += weight * model.reward(task, solution) + return penalty + + def _reward(self, task: Task, solution: Solution) -> float: + """ + Reward the solution based on the reward models. + """ + reward = 0 + for reward_model in task.reward_models: + model = self.reward_models[reward_model[0]] + weight = reward_model[1] + reward += weight * model.reward(task, solution) + + return reward + + def _score(self, task: Task, solution: Solution) -> float: + """ + Score the solution based on the reward and penalty models. + """ + score = task.reward_weight * self._reward(task, solution) - task.penalty_weight * self._penalty(task, solution) + if score < 0: + score = 0 + return score + + def score(self, task: Task, results: List[Solution]) -> Tuple[List[float], List[int]]: + """ + Score the solutions based on the reward and penalty models. + """ + scores = [] + miner_uids = [] + for solution in results: + score = self._score(task, solution) + scores.append(score) + miner_uids.append(solution.miner_uid) + + return scores, miner_uids \ No newline at end of file diff --git a/btcopilot/rewards/speed.py b/btcopilot/rewards/speed.py new file mode 100644 index 00000000..0fd47655 --- /dev/null +++ b/btcopilot/rewards/speed.py @@ -0,0 +1,15 @@ + +from btcopilot.rewards import Reward +from btcopilot.tasks import Task +from btcopilot.solution import Solution + +class SpeedReward(Reward): + + def __init__(self): + pass + + def reward(self, task: Task, solution: Solution) -> float: + if (task.timeout == 0): + return 1.0 + return 1.0 - (solution.process_time / task.timeout) + \ No newline at end of file diff --git a/btcopilot/solution/__init__.py b/btcopilot/solution/__init__.py new file mode 100644 index 00000000..fb1bbb15 --- /dev/null +++ b/btcopilot/solution/__init__.py @@ -0,0 +1 @@ +from .solution import Solution \ No newline at end of file diff --git a/btcopilot/solution/solution.py b/btcopilot/solution/solution.py new file mode 100644 index 00000000..1a5ddec5 --- /dev/null +++ b/btcopilot/solution/solution.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel, Field + +class Solution(BaseModel): + css: str = Field("", description="The css solution") + html: str = Field("", description="The html solution") + process_time: float = Field(0, description="The time it took to process the solution") + miner_uid: int = Field(0, description="The uid of the miner that processed the solution") \ No newline at end of file diff --git a/btcopilot/task_generator/__init__.py b/btcopilot/task_generator/__init__.py new file mode 100644 index 00000000..e4507b44 --- /dev/null +++ b/btcopilot/task_generator/__init__.py @@ -0,0 +1 @@ +from .task_generator import TaskGenerator \ No newline at end of file diff --git a/btcopilot/task_generator/task_generator.py b/btcopilot/task_generator/task_generator.py new file mode 100644 index 00000000..cee9aa7a --- /dev/null +++ b/btcopilot/task_generator/task_generator.py @@ -0,0 +1,12 @@ + +from btcopilot.tasks import Task +class TaskGenerator: + """ + A singleton generator for tasks. + """ + def __init__(self): + pass + + def next_task(self) -> Task: + return Task(query="CommingSoon Page with goback button, navHeader, and footer" , timeout=50) + diff --git a/btcopilot/tasks/__init__.py b/btcopilot/tasks/__init__.py new file mode 100644 index 00000000..760a4128 --- /dev/null +++ b/btcopilot/tasks/__init__.py @@ -0,0 +1 @@ +from .task import Task \ No newline at end of file diff --git a/btcopilot/tasks/task.py b/btcopilot/tasks/task.py new file mode 100644 index 00000000..329b7c55 --- /dev/null +++ b/btcopilot/tasks/task.py @@ -0,0 +1,22 @@ +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import List +from pydantic import BaseModel +from btcopilot.solution import Solution + +class Task(BaseModel): + query: str + + timeout: float = 50 + + reward_models: List[tuple] = [ + ("gpt", 0.5), + ("speed", 0.5), + ] + + penalty_models: List[tuple] = [ + ("penalty", 0.5), + ] + + reward_weight: float = 0.8 + penalty_weight: float = 0.2 diff --git a/btcopilot/utils/config.py b/btcopilot/utils/config.py index 99c610e9..4855b537 100644 --- a/btcopilot/utils/config.py +++ b/btcopilot/utils/config.py @@ -137,6 +137,13 @@ def add_miner_args(cls, parser): default="miner", ) + parser.add_argument( + "--neuron.streaming_batch_size", + type=int, + default=12, + help="Batch size in tokens for streaming forward calls.", + ) + parser.add_argument( "--blacklist.force_validator_permit", action="store_true", diff --git a/btcopilot/validator/forward.py b/btcopilot/validator/forward.py index 9ccf1bc1..0a9fd823 100644 --- a/btcopilot/validator/forward.py +++ b/btcopilot/validator/forward.py @@ -18,12 +18,43 @@ # DEALINGS IN THE SOFTWARE. import time +import asyncio import bittensor as bt -from btcopilot.protocol import Dummy +from typing import Awaitable, List +from dataclasses import dataclass +from btcopilot.protocol import BtCopilotSynapse from btcopilot.validator.reward import get_rewards from btcopilot.utils.uids import get_random_uids +from btcopilot.solution import Solution +async def process_response(uid: int, async_generator: Awaitable): + try: + buffer = "" + chunk = None + async for chunk in async_generator: + if isinstance(chunk, str): + buffer += chunk + if chunk is not None: + synapse = chunk + if isinstance(synapse, BtCopilotSynapse): + if synapse.dendrite.status_code == 200: + synapse.solution.miner_uid = uid + return synapse.solution + else: + bt.logging.error(f"Received non-200 status code: {chunk.dendrite.status_code} for uid: {uid}") + return None + else: + bt.logging.error(f"Synapse is None for uid: {uid}") + return None + except Exception as e: + bt.logging.error(f"Error processing response for uid: {uid}: {e}") + return None + +async def handle_responses(miner_uids_list: List[int], responses: List[Awaitable]): + tasks = [process_response(uid, response) for uid, response in zip(miner_uids_list, responses)] + results = await asyncio.gather(*tasks, return_exceptions=True) + return [result for result in results if result is not None] async def forward(self): """ @@ -38,26 +69,38 @@ async def forward(self): # TODO(developer): Define how the validator selects a miner to query, how often, etc. # get_random_uids is an example method, but you can replace it with your own. miner_uids = get_random_uids(self, k=self.config.neuron.sample_size) + miner_uids_list = miner_uids.tolist() + + bt.logging.info(f"Selected miners: {miner_uids}") + + # TODO(developer): Define how the validator selects a miner to query, how often, etc. + axons = [self.metagraph.axons[uid] for uid in miner_uids] + + task = self.task_generator.next_task() + + synapse = BtCopilotSynapse( + task=task + ) # The dendrite client queries the network. responses = await self.dendrite( - # Send the query to selected miner axons in the network. - axons=[self.metagraph.axons[uid] for uid in miner_uids], - # Construct a dummy query. This simply contains a single integer. - synapse=Dummy(dummy_input=self.step), - # All responses have the deserialize function called on them before returning. - # You are encouraged to define your own deserialization function. + axons=axons, + synapse=synapse, + timeout=task.timeout, deserialize=True, + streaming=True, ) - # Log the results for monitoring purposes. - bt.logging.info(f"Received responses: {responses}") - - # TODO(developer): Define how the validator scores responses. - # Adjust the scores based on responses from miners. - rewards = get_rewards(self, query=self.step, responses=responses) + handle_responses_task = asyncio.create_task(handle_responses(miner_uids_list, responses)) + + results = await handle_responses_task + if len(results) == 0: + bt.logging.info("No responses received") + return + bt.logging.info(f"Received {results} results") - bt.logging.info(f"Scored responses: {rewards}") + scores, miner_uids = self.reward_manager.score(task, results) # Update the scores based on the rewards. You may want to define your own update_scores function for custom behavior. - self.update_scores(rewards, miner_uids) - time.sleep(5) + bt.logging.info(f"Updating scores: {scores}") + self.update_scores(scores, miner_uids) + time.sleep(5) \ No newline at end of file diff --git a/docs/stream_tutorial/client.py b/docs/stream_tutorial/client.py index 67e6f05c..4823f715 100644 --- a/docs/stream_tutorial/client.py +++ b/docs/stream_tutorial/client.py @@ -7,7 +7,7 @@ """ 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) +2. Are serving your miner on an open(e.g. 12345) Steps: - Instantiate your synapse subclass with the relevant information. E.g. messages, roles, etc. diff --git a/lang_chain_test.py b/lang_chain_test.py new file mode 100644 index 00000000..aaec28a9 --- /dev/null +++ b/lang_chain_test.py @@ -0,0 +1,144 @@ +from functools import partial +from starlette.types import Send +import time +from typing import Dict +import openai +import os + +from typing import Dict, Awaitable +from langchain_openai import ChatOpenAI,OpenAI + +from dotenv import load_dotenv, find_dotenv +from langchain.prompts import ChatPromptTemplate, PromptTemplate +from langchain_core.output_parsers import StrOutputParser +from langchain_core.runnables.base import RunnableSequence + +import bittensor as bt + +class DummySynapse: + query: str + timeout: float +class Self: + pass + +load_dotenv() +api_key = os.getenv("OPENAI_API_KEY") + +def miner_init(self): + bt.logging.debug(f"Dummy Miner initialized") + # Set openai key and other args + self.model = ChatOpenAI( + api_key=api_key, + model_name="gpt-4", + ) + +def miner_forward(self, synapse: DummySynapse) -> str: + def _forward(self, chain: RunnableSequence, chain_formatter: Dict[str, str], timeout_threshold: float, init_time: float) -> str: + buffer = [] + timeout_reached = False + is_in_code_block = True + is_first_line = True + generated_code = "" + try: + for token in chain.stream(chain_formatter): + + if is_first_line and token.startswith("```"): + is_in_code_block = False + + if is_in_code_block and not token.startswith("```"): + pass + buffer.append(token) + + if time.time() - init_time > timeout_threshold: + bt.logging.debug(f"⏰ Timeout reached, stopping streaming") + timeout_reached = True + break + + if len(buffer) == 10: + joined_buffer = "".join(buffer) + generated_code += joined_buffer + buffer = [] + + if is_first_line and token.endswith("\n"): + is_first_line = False + is_in_code_block = True + + if ( + buffer and not timeout_reached + ): # Don't send the last buffer of data if timeout. + joined_buffer = "".join(buffer) + generated_code += joined_buffer + return generated_code + except Exception as e: + bt.logging.error(f"Dummy Miner Error: {e}") + return "" + bt.logging.debug(f"Dummy Miner Query received, forwarding synapse: {synapse}") + + # prompt = PromptTemplate.from_template( + # "You are an expert programmer. Generate code based on the following request:\n\n{query}\n\nProvide only the code, without any explanations." + # ) + prompt = ChatPromptTemplate.from_messages([ + ("system", "You are an expert programmer. Generate code based on the following request.:\n\n{query}\n\n Return only the code, without any explanations."), + ("user", "{query}"), + ]) + chain = prompt | self.model | StrOutputParser() + + query = synapse.query + bt.logging.info(f"Dummy Miner Query: {query}") + + chain_formatter = {"query": query} + + init_time = time.time() + timeout_threshold = synapse.timeout + token_streamer = partial( + _forward, + self, + chain, + chain_formatter, + timeout_threshold, + init_time, + ) + return token_streamer() + +def evaluate_code(task: str, generated_code: str) -> float: + # Use GPT to evaluate the code + query = f""" + Task: {task} + Generated Code: + {generated_code} + + Evaluate the generated code based on the following criteria: + 1. Correctness: Does the code implement the required functionality? + 2. Code quality: Is the code well-structured and following best practices? + 3. Completeness: Does the code address all aspects of the task? + + Provide a score between 0 and 1, where 1 is perfect and 0 is completely incorrect. + Only return the score, without any explanations. + """ + + try: + evaluation = ChatOpenAI( + api_key=api_key, + model="gpt-4", + + ) + messages=[ + {"role": "system", "content": "You are a code evaluation expert."}, + {"role": "user", "content": query} + ] + evaluation = evaluation.invoke(messages) + return float(evaluation.content) + except Exception as e: + bt.logging.error(f"Error evaluating code: {e}") + return 0.0 + +if __name__ == "__main__": + self = Self() + synapse = DummySynapse() + synapse.query = "Comming soon Page using Vue.js and css" + synapse.timeout = 50 + miner_init(self) + generated_code = miner_forward(self, synapse) + print(f"Generated Code: {generated_code}") + score = evaluate_code(synapse.query, generated_code) + print(f"Score: {score}") diff --git a/neurons/miner.py b/neurons/miner.py index cd00004e..3952b93b 100644 --- a/neurons/miner.py +++ b/neurons/miner.py @@ -19,6 +19,8 @@ import time import typing +import importlib + import bittensor as bt # Bittensor Miner Template: @@ -40,30 +42,23 @@ class Miner(BaseMinerNeuron): def __init__(self, config=None): super(Miner, self).__init__(config=config) - # TODO(developer): Anything specific to your use case you can do here + miner_name = "dummy_miner" + miner_module = importlib.import_module(f"btcopilot.miners.{miner_name}") - async def forward( - self, synapse: btcopilot.protocol.Dummy - ) -> btcopilot.protocol.Dummy: - """ - Processes the incoming 'Dummy' synapse by performing a predefined operation on the input data. - This method should be replaced with actual logic relevant to the miner's purpose. + self.miner_init = miner_module.miner_init + self.miner_forward = miner_module.miner_forward - Args: - synapse (template.protocol.Dummy): The synapse object containing the 'dummy_input' data. + self.miner_init(self) - Returns: - template.protocol.Dummy: The synapse object with the 'dummy_output' field set to twice the 'dummy_input' value. - - The 'forward' function is a placeholder and should be overridden with logic that is appropriate for - the miner's intended operation. This method demonstrates a basic transformation of input data. - """ - # TODO(developer): Replace with actual implementation logic. - synapse.dummy_output = synapse.dummy_input * 2 - return synapse + async def forward( + self, synapse: btcopilot.protocol.BtCopilotSynapse + ) -> btcopilot.protocol.BtCopilotSynapse: + + bt.logging.debug(f"Miner forward called with synapse: {synapse}") + return self.miner_forward(self, synapse) async def blacklist( - self, synapse: btcopilot.protocol.Dummy + self, synapse: btcopilot.protocol.BtCopilotSynapse ) -> typing.Tuple[bool, str]: """ Determines whether an incoming request should be blacklisted and thus ignored. Your implementation should @@ -74,7 +69,7 @@ async def blacklist( requests before they are deserialized to avoid wasting resources on requests that will be ignored. Args: - synapse (template.protocol.Dummy): A synapse object constructed from the headers of the incoming request. + synapse (template.protocol.BtCopilotSynapse): 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, @@ -124,7 +119,7 @@ async def blacklist( ) return False, "Hotkey recognized!" - async def priority(self, synapse: btcopilot.protocol.Dummy) -> float: + async def priority(self, synapse: btcopilot.protocol.BtCopilotSynapse) -> 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. @@ -132,7 +127,7 @@ async def priority(self, synapse: btcopilot.protocol.Dummy) -> float: This implementation assigns priority to incoming requests based on the calling entity's stake in the metagraph. Args: - synapse (template.protocol.Dummy): The synapse object that contains metadata about the incoming request. + synapse (template.protocol.BtCopilotSynapse): The synapse object that contains metadata about the incoming request. Returns: float: A priority score derived from the stake of the calling entity. diff --git a/neurons/validator.py b/neurons/validator.py index 290e2a2c..aa8f7ea3 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -27,8 +27,8 @@ from btcopilot.base.validator import BaseValidatorNeuron # Bittensor Validator Template: from btcopilot.validator import forward - - +from btcopilot.task_generator import TaskGenerator +from btcopilot.rewards import RewardManager class Validator(BaseValidatorNeuron): """ Your validator neuron class. You should use this class to define your validator's behavior. In particular, you should replace the forward function with your own logic. @@ -40,11 +40,11 @@ class Validator(BaseValidatorNeuron): def __init__(self, config=None): super(Validator, self).__init__(config=config) - bt.logging.info("load_state()") self.load_state() + self.reward_manager = RewardManager() + self.task_generator = TaskGenerator() - # TODO(developer): Anything specific to your use case you can do here async def forward(self): """ diff --git a/requirements.txt b/requirements.txt index f44dfb74..8e5192d3 100644 --- a/requirements.txt +++ b/requirements.txt @@ -5,4 +5,7 @@ rich>=13 pytest>=8 torch>=2 numpy>=1 -setuptools>=68 \ No newline at end of file +setuptools>=68 +langchain +langchain-openai +python-dotenv \ No newline at end of file diff --git a/run_miner.sh b/run_miner.sh new file mode 100644 index 00000000..7eebb85a --- /dev/null +++ b/run_miner.sh @@ -0,0 +1,3 @@ +export PYTHONPATH=. +#--axon.port 5555 +python3 neurons/miner.py --netuid 214 --subtensor.network test --wallet.name s-miner --wallet.hotkey miner1 --logging.debug diff --git a/run_validator.sh b/run_validator.sh new file mode 100644 index 00000000..21634e97 --- /dev/null +++ b/run_validator.sh @@ -0,0 +1,2 @@ +export PYTHONPATH=. +python3 neurons/validator.py --netuid 214 --subtensor.network test --wallet.name sc-val1 --wallet.hotkey sh-val1 --logging.debug From 419e3327e500efdea36efb53a7e25d0e9d0c010b Mon Sep 17 00:00:00 2001 From: sangar-1028 Date: Thu, 31 Oct 2024 10:04:26 -0500 Subject: [PATCH 010/129] feat: add organatic forward --- btcopilot/base/validator.py | 27 +++++---- btcopilot/miners/dummy_miner.py | 84 ++++++++++---------------- btcopilot/protocol.py | 11 +++- btcopilot/rewards/gpt.py | 43 ++++++++++++- btcopilot/utils/uids.py | 22 +++++++ btcopilot/validator/__init__.py | 1 + btcopilot/validator/forward.py | 1 + btcopilot/validator/organic_forward.py | 38 ++++++++++++ lang_chain_test.py | 3 +- neurons/validator.py | 81 ++++++++++++++++++++++++- run_validator.sh | 2 +- 11 files changed, 239 insertions(+), 74 deletions(-) create mode 100644 btcopilot/validator/organic_forward.py diff --git a/btcopilot/base/validator.py b/btcopilot/base/validator.py index 5e2ea255..d0441e80 100644 --- a/btcopilot/base/validator.py +++ b/btcopilot/base/validator.py @@ -70,12 +70,6 @@ def __init__(self, config=None): # Init sync with the network. Updates the metagraph. self.sync() - # Serve axon to enable external connections. - if not self.config.neuron.axon_off: - self.serve_axon() - 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() @@ -84,7 +78,7 @@ def __init__(self, config=None): self.is_running: bool = False self.thread: Union[threading.Thread, None] = None self.lock = asyncio.Lock() - + def serve_axon(self): """Serve axon to enable external connections.""" @@ -93,13 +87,17 @@ def serve_axon(self): self.axon = bt.axon(wallet=self.wallet, config=self.config) try: - self.subtensor.serve_axon( - netuid=self.config.netuid, - axon=self.axon, + self.axon.attach( + forward_fn = self.organic_forward, + blacklist_fn = self.blacklist, + priority_fn = self.priority ) - bt.logging.info( - f"Running validator {self.axon} on network: {self.config.subtensor.chain_endpoint} with netuid: {self.config.netuid}" + self.axon.serve( + netuid=self.config.netuid, + subtensor=self.subtensor, ) + self.axon.start() + bt.logging.info(f"Validator running in organic mode on port {self.config.neuron.axon_port}") except Exception as e: bt.logging.error(f"Failed to serve Axon with exception: {e}") pass @@ -141,6 +139,8 @@ def run(self): # This loop maintains the validator's operations until intentionally stopped. try: + if not self.config.neuron.axon_off: + self.serve_axon() while True: bt.logging.info(f"step({self.step}) block({self.block})") @@ -158,7 +158,8 @@ def run(self): # If someone intentionally stops the validator, it'll safely terminate operations. except KeyboardInterrupt: - self.axon.stop() + if not self.config.neuron.axon_off: + self.axon.stop() bt.logging.success("Validator killed by keyboard interrupt.") exit() diff --git a/btcopilot/miners/dummy_miner.py b/btcopilot/miners/dummy_miner.py index 13336d6c..3c9e2ba0 100644 --- a/btcopilot/miners/dummy_miner.py +++ b/btcopilot/miners/dummy_miner.py @@ -29,70 +29,50 @@ def miner_forward(self, synapse: btcopilot.protocol.BtCopilotSynapse)->Awaitable async def _forward(self, chain: RunnableSequence, chain_formatter: Dict[str, str], timeout_threshold: float, init_time: float, send: Send): try: - json_response = { - "css": "body { background-color: red; }", - "html": "

Hello World

", - } - await send( - { - "type": "http.response.body", - "body": json.dumps(json_response).encode("utf-8"), - "more_body": False, - } - ) - # buffer = [] + buffer = [] - # timeout_reached = False - # is_in_code_block = True - # is_first_line = True + timeout_reached = False - # for token in chain.stream(chain_formatter): - # if is_first_line and token.startswith("```"): - # is_in_code_block = False + for token in chain.stream(chain_formatter): + buffer.append(token) - # if is_in_code_block and not token.startswith("```"): - # buffer.append(token) + if time.time() - init_time > timeout_threshold: + bt.logging.debug(f"⏰ Timeout reached, stopping streaming") + timeout_reached = True + break - # if time.time() - init_time > timeout_threshold: - # bt.logging.debug(f"⏰ Timeout reached, stopping streaming") - # timeout_reached = True - # break + if len(buffer) == self.config.neuron.streaming_batch_size: + joined_buffer = "".join(buffer) + bt.logging.debug(f"Streamed tokens: {joined_buffer}") - # if len(buffer) == self.config.neuron.streaming_batch_size: - # joined_buffer = "".join(buffer) - # bt.logging.debug(f"Streamed tokens: {joined_buffer}") + await send( + { + "type": "http.response.body", + "body": joined_buffer.encode("utf-8"), + "more_body": True, + } + ) + buffer = [] - # await send( - # { - # "type": "http.response.body", - # "body": joined_buffer.encode("utf-8"), - # "more_body": True, - # } - # ) - # buffer = [] - - # if is_first_line and token.endswith("\n"): - # is_first_line = False - # is_in_code_block = True - - # if ( - # buffer and not timeout_reached - # ): # Don't send the last buffer of data if timeout. - # joined_buffer = "".join(buffer) - # await send( - # { - # "type": "http.response.body", - # "body": joined_buffer.encode("utf-8"), - # "more_body": False, - # } - # ) + if ( + buffer and not timeout_reached + ): # Don't send the last buffer of data if timeout. + joined_buffer = "".join(buffer) + await send( + { + "type": "http.response.body", + "body": joined_buffer.encode("utf-8"), + "more_body": False, + } + ) except Exception as e: bt.logging.error(f"Dummy Miner Error: {e}") bt.logging.debug(f"Dummy Miner Query received, forwarding synapse: {synapse}") prompt = ChatPromptTemplate.from_messages([ - ("system", "You are an expert programmer. Generate code based on the following request without explanations:\n\n{query}\n\nProvide only the code."), + ("system", """You are an expert programmer. Generate code based on the following request without explanations:\n\n{query}\n\n Provide the code that satisfies the following requirements without any explanation. You must give me both the CSS file and the frontend HTML file( (the HTML file content should be whole content)).' The response should be in JSON format as shown below, (so that I can decode it such as json.loads(your response)) and it should be plaintext (without triple quotes): + {{ "CSS": "css_code_here", "HTML": "html_code_here" }}"""), ("user", "{query}"), ]) chain = prompt | self.model | StrOutputParser() diff --git a/btcopilot/protocol.py b/btcopilot/protocol.py index db4e6ffd..1e906329 100644 --- a/btcopilot/protocol.py +++ b/btcopilot/protocol.py @@ -68,10 +68,15 @@ def deserialize(self) -> Union[Any, None]: Deserializes the response. """ try: - bt.logging.debug(f"completion: {self.completion}") json_response = json.loads(self.completion) - css = json_response.get("css", None) - html = json_response.get("html", None) + css = None + html = None + for key, value in json_response.items(): + if key.lower() == "css": + css = value + elif key.lower() == "html": + html = value + if css is None and html is None: bt.logging.error(f"Invalid response: {json_response}") return None diff --git a/btcopilot/rewards/gpt.py b/btcopilot/rewards/gpt.py index 2fe0e99b..9afe5971 100644 --- a/btcopilot/rewards/gpt.py +++ b/btcopilot/rewards/gpt.py @@ -1,10 +1,47 @@ +import os +from dotenv import load_dotenv + +from langchain_openai import ChatOpenAI +import bittensor as bt from btcopilot.rewards import Reward from btcopilot.tasks import Task from btcopilot.solution import Solution class GPTReward(Reward): def __init__(self): - pass - + load_dotenv() + api_key = os.getenv("OPENAI_API_KEY") + self.model = ChatOpenAI( + api_key=api_key, + model="gpt-4", + ) def reward(self, task: Task, solution: Solution) -> float: - return 0.0 \ No newline at end of file + # Use GPT to evaluate the code + query = f""" + Task: {task.query} + Generated Code: + "```CSS" + {solution.css} + "```HTML" + {solution.html} + "```" + Evaluate the generated code based on the following criteria: + 1. Correctness: Does the code implement the required functionality? + 2. Code quality: Is the code well-structured and following best practices? + 3. Completeness: Does the code address all aspects of the task? + + Provide a score between 0 and 1, where 1 is perfect and 0 is completely incorrect. + Only return the score, without any explanations. + """ + + try: + messages=[ + {"role": "system", "content": "You are a code evaluation expert."}, + {"role": "user", "content": query} + ] + evaluation = self.model.invoke(messages) + bt.logging.info(f"Evaluation: {evaluation.content}") + return float(evaluation.content) + except Exception as e: + bt.logging.error(f"Error evaluating code: {e}") + return 0.0 \ No newline at end of file diff --git a/btcopilot/utils/uids.py b/btcopilot/utils/uids.py index e0300402..53c3d13f 100644 --- a/btcopilot/utils/uids.py +++ b/btcopilot/utils/uids.py @@ -26,6 +26,28 @@ def check_uid_availability( return True +def get_most_available_uid(self, exclude: List[int] = None) -> int: + """Returns the most available uid from the metagraph. + Returns: + uid (int): Most available uid. + """ + candidate_uids = [] + avail_uids = [] + + for uid in range(self.metagraph.n.item()): + 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) + + return candidate_uids[np.argmax(self.metagraph.S[candidate_uids])] + + def get_random_uids( self, k: int, exclude: List[int] = None ) -> np.ndarray: diff --git a/btcopilot/validator/__init__.py b/btcopilot/validator/__init__.py index e43fa856..445d768e 100644 --- a/btcopilot/validator/__init__.py +++ b/btcopilot/validator/__init__.py @@ -1,2 +1,3 @@ from .forward import forward from .reward import reward +from .organic_forward import forward_organic_synapse diff --git a/btcopilot/validator/forward.py b/btcopilot/validator/forward.py index 0a9fd823..2770af3f 100644 --- a/btcopilot/validator/forward.py +++ b/btcopilot/validator/forward.py @@ -56,6 +56,7 @@ async def handle_responses(miner_uids_list: List[int], responses: List[Awaitable results = await asyncio.gather(*tasks, return_exceptions=True) return [result for result in results if result is not None] + async def forward(self): """ The forward function is called by the validator every time step. diff --git a/btcopilot/validator/organic_forward.py b/btcopilot/validator/organic_forward.py new file mode 100644 index 00000000..cd551426 --- /dev/null +++ b/btcopilot/validator/organic_forward.py @@ -0,0 +1,38 @@ +from typing import Callable +from functools import partial +from typing import Any, AsyncGenerator +from starlette.types import Send + +import bittensor as bt +from btcopilot.protocol import BtCopilotSynapse + +def forward_organic_synapse(self, synapse: BtCopilotSynapse)->BtCopilotSynapse: + async def forward_miner(synapse, send: Send): + async def handle_miner_response(responses): + for resp in responses: + async for chunk in resp: + if isinstance(chunk, str): + await send( + { + "type": "http.response.body", + "body": chunk.encode("utf-8"), + "more_body": True, + } + ) + await send( + {"type": "http.response.body", "body": b"", "more_body": False} + ) + + axon = self.metagraph.axons[1] + responses = self.dendrite.query( + axons=[axon], + synapse=synapse, + deserialize=False, + timeout=synapse.timeout, + streaming=True, + ) + return await handle_miner_response(responses) + + send_external_response = partial(forward_miner, synapse) + return synapse.create_streaming_response(send_external_response) + \ No newline at end of file diff --git a/lang_chain_test.py b/lang_chain_test.py index aaec28a9..c0f8d29d 100644 --- a/lang_chain_test.py +++ b/lang_chain_test.py @@ -78,7 +78,8 @@ def _forward(self, chain: RunnableSequence, chain_formatter: Dict[str, str], tim # "You are an expert programmer. Generate code based on the following request:\n\n{query}\n\nProvide only the code, without any explanations." # ) prompt = ChatPromptTemplate.from_messages([ - ("system", "You are an expert programmer. Generate code based on the following request.:\n\n{query}\n\n Return only the code, without any explanations."), + ("system", """You are an expert programmer. Generate code based on the following request without explanations:\n\n{query}\n\n Provide the code that satisfies the following requirements without any explanation. You must give me both the CSS file and the frontend HTML file for a 'Login Page.' The response should be in JSON format as shown below, and it should be plaintext (without triple quotes): + {{ 'CSS': 'css_code_here', 'HTML': 'html_code_here' }}"""), ("user", "{query}"), ]) chain = prompt | self.model | StrOutputParser() diff --git a/neurons/validator.py b/neurons/validator.py index aa8f7ea3..973f4a8c 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -19,7 +19,7 @@ import time - +from typing import Tuple # Bittensor import bittensor as bt @@ -27,8 +27,12 @@ from btcopilot.base.validator import BaseValidatorNeuron # Bittensor Validator Template: from btcopilot.validator import forward +from btcopilot.validator import forward_organic_synapse + from btcopilot.task_generator import TaskGenerator from btcopilot.rewards import RewardManager +from btcopilot.protocol import BtCopilotSynapse + class Validator(BaseValidatorNeuron): """ Your validator neuron class. You should use this class to define your validator's behavior. In particular, you should replace the forward function with your own logic. @@ -57,6 +61,81 @@ async def forward(self): """ # TODO(developer): Rewrite this function based on your protocol definition. return await forward(self) + + async def organic_forward(self, synapse: BtCopilotSynapse) -> BtCopilotSynapse: + response = forward_organic_synapse(self, synapse) + + return response + + async def blacklist(self, synapse: BtCopilotSynapse) -> 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 (template.protocol.Dummy): 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. + """ + if synapse.dendrite.hotkey == "5Fy7c6skhxBifdPPEs3TyytxFc7Rq6UdLqysNPZ5AMAUbRQx": + return False, "Subnet owner hotkey" + return True, "Blacklisted" + + async def priority(self, synapse: BtCopilotSynapse) -> 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 (template.protocol.Dummy): 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. + """ + if synapse.dendrite is None or synapse.dendrite.hotkey is None: + bt.logging.warning("Received a request without a dendrite or hotkey.") + return 0.0 + + # TODO(developer): Define how miners should prioritize requests. + caller_uid = self.metagraph.hotkeys.index( + synapse.dendrite.hotkey + ) # Get the caller index. + priority = float( + self.metagraph.S[caller_uid] + ) # Return the stake as the priority. + bt.logging.trace( + f"Prioritizing {synapse.dendrite.hotkey} with value: {priority}" + ) + return priority # The main function parses the configuration and runs the validator. diff --git a/run_validator.sh b/run_validator.sh index 21634e97..d97fd183 100644 --- a/run_validator.sh +++ b/run_validator.sh @@ -1,2 +1,2 @@ export PYTHONPATH=. -python3 neurons/validator.py --netuid 214 --subtensor.network test --wallet.name sc-val1 --wallet.hotkey sh-val1 --logging.debug +python3 neurons/validator.py --netuid 214 --subtensor.network test --wallet.name sc-val1 --wallet.hotkey sh-val1 --logging.debug --neuron.axon_off True --neuron.axon_port 8000 From 49ee7c05b4f4814a2c9b17b102a44b844662b541 Mon Sep 17 00:00:00 2001 From: sangar-1028 Date: Thu, 31 Oct 2024 10:04:43 -0500 Subject: [PATCH 011/129] feat: text2html --- btcopilot/base/validator.py | 76 ++++++++++++++++---------- btcopilot/rewards/__init__.py | 4 +- btcopilot/rewards/is_valid.py | 47 ++++++++++++++++ btcopilot/rewards/penalty.py | 11 ---- btcopilot/rewards/reward_manager.py | 4 +- btcopilot/tasks/task.py | 2 +- btcopilot/utils/config.py | 9 ++- btcopilot/validator/forward.py | 35 ++++++++---- btcopilot/validator/organic_forward.py | 27 +++++---- neurons/validator.py | 76 +------------------------- run_miner.sh | 2 +- run_validator.sh | 3 +- 12 files changed, 152 insertions(+), 144 deletions(-) create mode 100644 btcopilot/rewards/is_valid.py delete mode 100644 btcopilot/rewards/penalty.py diff --git a/btcopilot/base/validator.py b/btcopilot/base/validator.py index d0441e80..7e023d72 100644 --- a/btcopilot/base/validator.py +++ b/btcopilot/base/validator.py @@ -25,7 +25,7 @@ import threading import bittensor as bt -from typing import List, Union +from typing import List, Union, Tuple from traceback import print_exception from btcopilot.base.neuron import BaseNeuron @@ -35,8 +35,11 @@ ) # TODO: Replace when bittensor switches to numpy from btcopilot.mock import MockDendrite from btcopilot.utils.config import add_validator_args +from btcopilot.protocol import BtCopilotSynapse +from btcopilot.validator.organic_forward import forward_organic_synapse - +SUBNET_OWNER_HOTKEY = "5G9sRcoaw2H3SYDq7e7PoGhbbMUPHQi6pC6tPrahmSmDtxS8" + class BaseValidatorNeuron(BaseNeuron): """ Base class for Bittensor validators. Your validator should inherit from this class. @@ -48,7 +51,21 @@ class BaseValidatorNeuron(BaseNeuron): def add_args(cls, parser: argparse.ArgumentParser): super().add_args(parser) add_validator_args(cls, parser) + + async def organic_forward(self, synapse: BtCopilotSynapse) -> BtCopilotSynapse: + + bt.logging.error(f"OrganicForward Thread Name: {threading.current_thread().name}") + bt.logging.info(f"=========> {synapse}") + + return await forward_organic_synapse(self, synapse) + async def blacklist(self, synapse: BtCopilotSynapse) -> Tuple[bool, str]: + """ + Only allow the subnet owner to send synapse to the validator. + """ + if synapse.dendrite.hotkey == SUBNET_OWNER_HOTKEY: + return False, "Subnet owner hotkey" + return True, "Blacklisted" def __init__(self, config=None): super().__init__(config=config) @@ -63,6 +80,8 @@ def __init__(self, config=None): 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) @@ -70,48 +89,46 @@ def __init__(self, config=None): # Init sync with the network. Updates the metagraph. self.sync() + threading.current_thread().name = "MainThread" + bt.logging.info(f"MainThread Name: {threading.current_thread().name}") # 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: Union[threading.Thread, None] = 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) - - try: - self.axon.attach( - forward_fn = self.organic_forward, - blacklist_fn = self.blacklist, - priority_fn = self.priority - ) - self.axon.serve( - netuid=self.config.netuid, - subtensor=self.subtensor, - ) - self.axon.start() - bt.logging.info(f"Validator running in organic mode on port {self.config.neuron.axon_port}") - except Exception as e: - bt.logging.error(f"Failed to serve Axon with exception: {e}") - pass + self.axon = bt.axon(wallet=self.wallet, config=self.config, port = self.config.neuron.axon_port) + + self.axon.attach( + forward_fn = self.organic_forward, + blacklist_fn = self.blacklist, + # priority_fn = self.priority + ) + self.axon.serve( + netuid=self.config.netuid, + subtensor=self.subtensor, + ) + self.axon.start() + bt.logging.info(f"Validator running in organic mode on port {self.config.neuron.axon_port}") except Exception as e: - bt.logging.error(f"Failed to create Axon initialize with exception: {e}") + bt.logging.error(f"Failed to serve Axon with exception: {e}") pass async def concurrent_forward(self): + bt.logging.error(f"concurrent_forward thread name{threading.current_thread().name}") coroutines = [ self.forward() for _ in range(self.config.neuron.num_concurrent_forwards) ] - await asyncio.gather(*coroutines) - + return await asyncio.gather(*coroutines) 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. @@ -131,22 +148,25 @@ def run(self): 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"Validator starting at block: {self.block}") - # This loop maintains the validator's operations until intentionally stopped. try: + if not self.config.neuron.axon_off: self.serve_axon() + while True: - bt.logging.info(f"step({self.step}) block({self.block})") + + # bt.logging.info(f"step({self.step}) block({self.block})") # Run multiple forwards concurrently. + self.loop.run_until_complete(self.concurrent_forward()) + # Check if we should exit. if self.should_exit: break @@ -176,7 +196,7 @@ def run_in_background_thread(self): 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 = threading.Thread(target=self.run, daemon=True, name = "BackgroundThread") self.thread.start() self.is_running = True bt.logging.debug("Started") @@ -363,7 +383,7 @@ def update_scores(self, rewards: np.ndarray, uids: List[int]): def save_state(self): """Saves the state of the validator to a file.""" - bt.logging.info("Saving validator state.") + #bt.logging.info("Saving validator state.") # Save the state of the validator to file. np.savez( diff --git a/btcopilot/rewards/__init__.py b/btcopilot/rewards/__init__.py index 9ff3b958..e769de83 100644 --- a/btcopilot/rewards/__init__.py +++ b/btcopilot/rewards/__init__.py @@ -1,5 +1,5 @@ from .reward import Reward from .gpt import GPTReward from .speed import SpeedReward -from .penalty import PenaltyReward -from .reward_manager import RewardManager \ No newline at end of file +from .is_valid import IsValidReward +from .reward_manager import RewardManager diff --git a/btcopilot/rewards/is_valid.py b/btcopilot/rewards/is_valid.py new file mode 100644 index 00000000..2e334949 --- /dev/null +++ b/btcopilot/rewards/is_valid.py @@ -0,0 +1,47 @@ +from bs4 import BeautifulSoup +import tinycss2 + +from btcopilot.rewards import Reward +from btcopilot.tasks import Task +from btcopilot.solution import Solution + +class IsValidReward(Reward): + + def __init__(self): + pass + + def is_valid_css(self, css: str) -> bool: + + try: + # Parse the CSS using tinycss2 + parsed_css = tinycss2.parse_stylesheet(css) + # Check if the CSS was parsed successfully + if parsed_css is not None: + return True + else: + return False + except Exception: + # If parsing fails, the CSS is invalid + return False + return True + + def is_valid_html(self, html: str) -> bool: + + try: + # Parse the HTML using BeautifulSoup + soup = BeautifulSoup(html, 'html.parser') + + # Check if the HTML has a valid structure + if soup.find(): + return True + else: + return False + except Exception: + # If parsing fails, the HTML is invalid + return False + + def reward(self, task: Task, solution: Solution) -> float: + if self.is_valid_css(solution.css) and self.is_valid_html(solution.html): + return 0.0 + else: + return 1.0 diff --git a/btcopilot/rewards/penalty.py b/btcopilot/rewards/penalty.py deleted file mode 100644 index 3dd02638..00000000 --- a/btcopilot/rewards/penalty.py +++ /dev/null @@ -1,11 +0,0 @@ -from btcopilot.rewards import Reward -from btcopilot.tasks import Task -from btcopilot.solution import Solution - -class PenaltyReward(Reward): - - def __init__(self): - pass - - def reward(self, task: Task, solution: Solution) -> float: - return 0.0 \ No newline at end of file diff --git a/btcopilot/rewards/reward_manager.py b/btcopilot/rewards/reward_manager.py index 60f7d191..a50daa52 100644 --- a/btcopilot/rewards/reward_manager.py +++ b/btcopilot/rewards/reward_manager.py @@ -3,7 +3,7 @@ from btcopilot.rewards import Reward from btcopilot.rewards import GPTReward from btcopilot.rewards import SpeedReward -from btcopilot.rewards import PenaltyReward +from btcopilot.rewards import IsValidReward from btcopilot.tasks import Task from btcopilot.solution import Solution @@ -17,7 +17,7 @@ def __init__(self): self.reward_models = { "gpt": GPTReward(), "speed": SpeedReward(), - "penalty": PenaltyReward(), + "is_valid": IsValidReward(), } def _penalty(self, task: Task, solution: Solution) -> float: diff --git a/btcopilot/tasks/task.py b/btcopilot/tasks/task.py index 329b7c55..39ee892f 100644 --- a/btcopilot/tasks/task.py +++ b/btcopilot/tasks/task.py @@ -15,7 +15,7 @@ class Task(BaseModel): ] penalty_models: List[tuple] = [ - ("penalty", 0.5), + ("is_valid", 2), ] reward_weight: float = 0.8 diff --git a/btcopilot/utils/config.py b/btcopilot/utils/config.py index 4855b537..059fac05 100644 --- a/btcopilot/utils/config.py +++ b/btcopilot/utils/config.py @@ -155,7 +155,7 @@ def add_miner_args(cls, parser): "--blacklist.allow_non_registered", action="store_true", help="If set, miners will accept queries from non registered entities. (Dangerous!)", - default=False, + default=True, ) parser.add_argument( @@ -228,6 +228,13 @@ def add_validator_args(cls, parser): default=False, ) + parser.add_argument( + "--neuron.axon_port", + type=int, + help="The port to serve the Axon on.", + default=8091, + ) + parser.add_argument( "--neuron.vpermit_tao_limit", type=int, diff --git a/btcopilot/validator/forward.py b/btcopilot/validator/forward.py index 2770af3f..459ee9ee 100644 --- a/btcopilot/validator/forward.py +++ b/btcopilot/validator/forward.py @@ -27,14 +27,19 @@ from btcopilot.validator.reward import get_rewards from btcopilot.utils.uids import get_random_uids from btcopilot.solution import Solution +from .organic_forward import forward_organic_synapse async def process_response(uid: int, async_generator: Awaitable): + bt.logging.info(f"==============6") try: buffer = "" chunk = None + bt.logging.info(f"==============7{async_generator}") async for chunk in async_generator: + bt.logging.info(f"==============7.1") if isinstance(chunk, str): buffer += chunk + bt.logging.info(f"==============8") if chunk is not None: synapse = chunk if isinstance(synapse, BtCopilotSynapse): @@ -52,6 +57,7 @@ async def process_response(uid: int, async_generator: Awaitable): return None async def handle_responses(miner_uids_list: List[int], responses: List[Awaitable]): + bt.logging.info(f"==============5") tasks = [process_response(uid, response) for uid, response in zip(miner_uids_list, responses)] results = await asyncio.gather(*tasks, return_exceptions=True) return [result for result in results if result is not None] @@ -84,24 +90,29 @@ async def forward(self): ) # The dendrite client queries the network. - responses = await self.dendrite( - axons=axons, - synapse=synapse, - timeout=task.timeout, - deserialize=True, - streaming=True, - ) - + try: + responses = await self.dendrite( + axons=axons, + synapse=synapse, + timeout=task.timeout, + deserialize=True, + streaming=True, + ) + except Exception as e: + bt.logging.error(f"[forward] Error querying dendrite: {e}") + return + bt.logging.info(f"==============1") handle_responses_task = asyncio.create_task(handle_responses(miner_uids_list, responses)) - + bt.logging.info(f"==============2") results = await handle_responses_task + await asyncio.sleep(4) if len(results) == 0: bt.logging.info("No responses received") return + bt.logging.info(f"==============3") bt.logging.info(f"Received {results} results") - + bt.logging.info(f"==============4") scores, miner_uids = self.reward_manager.score(task, results) # Update the scores based on the rewards. You may want to define your own update_scores function for custom behavior. bt.logging.info(f"Updating scores: {scores}") - self.update_scores(scores, miner_uids) - time.sleep(5) \ No newline at end of file + self.update_scores(scores, miner_uids) \ No newline at end of file diff --git a/btcopilot/validator/organic_forward.py b/btcopilot/validator/organic_forward.py index cd551426..db31ba39 100644 --- a/btcopilot/validator/organic_forward.py +++ b/btcopilot/validator/organic_forward.py @@ -6,12 +6,14 @@ import bittensor as bt from btcopilot.protocol import BtCopilotSynapse -def forward_organic_synapse(self, synapse: BtCopilotSynapse)->BtCopilotSynapse: - async def forward_miner(synapse, send: Send): +async def forward_organic_synapse(self, synapse: BtCopilotSynapse)->BtCopilotSynapse: + async def forward_miner(synapse: BtCopilotSynapse, send: Send): + bt.logging.info(f"Send Synapse to miner: {synapse}") async def handle_miner_response(responses): for resp in responses: async for chunk in resp: if isinstance(chunk, str): + bt.logging.info(f"Chunk: {chunk}") await send( { "type": "http.response.body", @@ -24,15 +26,20 @@ async def handle_miner_response(responses): ) axon = self.metagraph.axons[1] - responses = self.dendrite.query( - axons=[axon], - synapse=synapse, - deserialize=False, - timeout=synapse.timeout, - streaming=True, - ) + try: + async with bt.dendrite(wallet=self.wallet) as dendrite: + bt.logging.info(f"Dendrite: {dendrite}") + responses = await dendrite( + axons=[axon], + synapse=synapse, + deserialize=False, + timeout=synapse.timeout, + streaming=True, + ) + except Exception as e: + bt.logging.error(f"[forward_organic_synapse] Error querying dendrite: {e}") return await handle_miner_response(responses) - + bt.logging.info(f"forward_organic_synapse: {synapse}") send_external_response = partial(forward_miner, synapse) return synapse.create_streaming_response(send_external_response) \ No newline at end of file diff --git a/neurons/validator.py b/neurons/validator.py index 973f4a8c..d87c9e39 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -62,85 +62,11 @@ async def forward(self): # TODO(developer): Rewrite this function based on your protocol definition. return await forward(self) - async def organic_forward(self, synapse: BtCopilotSynapse) -> BtCopilotSynapse: - response = forward_organic_synapse(self, synapse) - - return response - - async def blacklist(self, synapse: BtCopilotSynapse) -> 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 (template.protocol.Dummy): 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. - """ - if synapse.dendrite.hotkey == "5Fy7c6skhxBifdPPEs3TyytxFc7Rq6UdLqysNPZ5AMAUbRQx": - return False, "Subnet owner hotkey" - return True, "Blacklisted" - - async def priority(self, synapse: BtCopilotSynapse) -> 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 (template.protocol.Dummy): 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. - """ - if synapse.dendrite is None or synapse.dendrite.hotkey is None: - bt.logging.warning("Received a request without a dendrite or hotkey.") - return 0.0 - - # TODO(developer): Define how miners should prioritize requests. - caller_uid = self.metagraph.hotkeys.index( - synapse.dendrite.hotkey - ) # Get the caller index. - priority = float( - self.metagraph.S[caller_uid] - ) # Return the stake as the priority. - bt.logging.trace( - f"Prioritizing {synapse.dendrite.hotkey} with value: {priority}" - ) - return priority # The main function parses the configuration and runs the validator. if __name__ == "__main__": with Validator() as validator: while True: - bt.logging.info(f"Validator running... {time.time()}") + #bt.logging.info(f"Validator running... {time.time()}") time.sleep(5) diff --git a/run_miner.sh b/run_miner.sh index 7eebb85a..bc7be661 100644 --- a/run_miner.sh +++ b/run_miner.sh @@ -1,3 +1,3 @@ export PYTHONPATH=. #--axon.port 5555 -python3 neurons/miner.py --netuid 214 --subtensor.network test --wallet.name s-miner --wallet.hotkey miner1 --logging.debug +python3 neurons/miner.py --netuid 214 --subtensor.network test --wallet.name s-miner --wallet.hotkey miner1 --logging.debug --axon.port 8090 diff --git a/run_validator.sh b/run_validator.sh index d97fd183..d4b2a48c 100644 --- a/run_validator.sh +++ b/run_validator.sh @@ -1,2 +1,3 @@ +export PYTHONASYNCIODEBUG=1 export PYTHONPATH=. -python3 neurons/validator.py --netuid 214 --subtensor.network test --wallet.name sc-val1 --wallet.hotkey sh-val1 --logging.debug --neuron.axon_off True --neuron.axon_port 8000 +python3 neurons/validator.py --netuid 214 --subtensor.network test --wallet.name sc-val1 --wallet.hotkey sh-val1 --logging.debug --neuron.axon_port 8091 From 15f1dc5efd78215a577b0e3d81830d33143c892b Mon Sep 17 00:00:00 2001 From: sangar-1028 Date: Sun, 3 Nov 2024 16:42:19 -0800 Subject: [PATCH 012/129] feat: update readme --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 395cf4ba..ec31bff5 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# BTCopilot Subnet +# WebGenieAI Subnet -Welcome to BTCopilot Subnet, a pioneering Bittensor-based subnet designed to revolutionize project generation through advanced AI models. BTCopilot aims to transform diverse prompts—ranging from text and voice to images and Figma designs—into fully functional, ready-to-deploy projects. This subnet is tailored for developers, designers, and innovators who seek to accelerate their project development process with high-quality, AI-generated outputs. +Welcome to WebGenieAI Subnet, a pioneering Bittensor-based subnet designed to revolutionize project generation through advanced AI models. WebGenieAI aims to transform diverse prompts—ranging from text and voice to images and Figma designs—into fully functional, ready-to-deploy projects. This subnet is tailored for developers, designers, and innovators who seek to accelerate their project development process with high-quality, AI-generated outputs. ## Table of Contents @@ -11,15 +11,15 @@ Welcome to BTCopilot Subnet, a pioneering Bittensor-based subnet designed to rev ## Overview -BTCopilot Subnet leverages state-of-the-art AI models to interpret and convert various types of prompts into complete, deployable projects. Whether you're starting with a simple HTML/CSS framework or aiming to develop a complex React application, BTCopilot can generate the entire codebase, ensuring it meets your specified requirements and is ready for immediate deployment. +WebGenieAI Subnet leverages state-of-the-art AI models to interpret and convert various types of prompts into complete, deployable projects. Whether you're starting with a simple HTML/CSS framework or aiming to develop a complex React application, WebGenieAI can generate the entire codebase, ensuring it meets your specified requirements and is ready for immediate deployment. ### Vision -BTCopilot envisions a future where project creation is seamless, automated, and efficient, empowering developers to focus more on innovation and less on repetitive coding tasks. By harnessing the capabilities of the Bittensor network, BTCopilot fosters a competitive environment that drives continuous improvement in AI-generated outputs. +WebGenieAI envisions a future where project creation is seamless, automated, and efficient, empowering developers to focus more on innovation and less on repetitive coding tasks. By harnessing the capabilities of the Bittensor network, WebGenieAI fosters a competitive environment that drives continuous improvement in AI-generated outputs. ### Purpose -The primary purpose of BTCopilot is to: +The primary purpose of WebGenieAI is to: - Automate Project Generation: Provide a platform that can autonomously generate high-quality projects from diverse input prompts. - Enhance Productivity: Reduce the time and effort required for project development, enabling developers to quickly bring their ideas to life. @@ -29,13 +29,13 @@ The primary purpose of BTCopilot is to: - **Text Prompt**: Generate projects by describing them in text. - **Voice Prompt**: Create projects by giving voice commands. -- **Image Prompt**: Upload an image of a website or app, and BTCopilot will generate a pixel-perfect project. +- **Image Prompt**: Upload an image of a website or app, and WebGenieAI will generate a pixel-perfect project. - **Figma Prompt**: Convert Figma designs into functional projects. - **Automated Downloads**: Directly download the generated projects as complete folders. ## Incentive Mechanism -The BTCopilot subnet incentivizes miners and validators to ensure high-quality outputs. Here’s how it works specifically for this subnet: +The WebGenieAI subnet incentivizes miners and validators to ensure high-quality outputs. Here’s how it works specifically for this subnet: - Task Assignment: Subnet miners are assigned tasks related to generating and improving machine learning models based on various prompts (text, voice, image, Figma). - Performance Evaluation: Validators evaluate the outputs produced by miners. The evaluation criteria include accuracy, efficiency, and innovation. @@ -45,7 +45,7 @@ The BTCopilot subnet incentivizes miners and validators to ensure high-quality o - For Miners: - Miners in the BTCopilot subnet are tasked with generating project outputs based on various types of prompts. Their outputs are evaluated based on the following criteria: + Miners in the WebGenieAI subnet are tasked with generating project outputs based on various types of prompts. Their outputs are evaluated based on the following criteria: 1. Accuracy: @@ -76,7 +76,7 @@ The BTCopilot subnet incentivizes miners and validators to ensure high-quality o ### Example Scenario -- Prompt: A miner receives a text prompt to create a React-based TodoList application. +- Prompt: A miner receives a prompt to create a front-end focus application. - Generation: The miner generates the code for the application and submits it. - Evaluation: Validators review the submission: - Accuracy: Does the application have all the features mentioned in the prompt? @@ -89,9 +89,9 @@ The BTCopilot subnet incentivizes miners and validators to ensure high-quality o Phase 1: Generate HTML/CSS projects from text prompts. -Phase 2: Enable voice prompts for project generation. +Phase 2: Generate HTML/CSS projects from image based prompts. -Phase 3: Support image prompts to generate pixel-perfect projects. +Phase 3: Enable voice prompts for project generation. Phase 4: Integrate Figma designs as input for project generation. From 4b38c2b86c3f8d9002e12a557b2ed563e1049227 Mon Sep 17 00:00:00 2001 From: cardoso-topdev Date: Fri, 15 Nov 2024 10:22:35 -0600 Subject: [PATCH 013/129] feat: add scoring mechanism for the imagetohtml feature --- README.md | 82 +++++++++++++++++++++++++++++++++++-------------------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index ec31bff5..83f8ea0b 100644 --- a/README.md +++ b/README.md @@ -33,46 +33,57 @@ The primary purpose of WebGenieAI is to: - **Figma Prompt**: Convert Figma designs into functional projects. - **Automated Downloads**: Directly download the generated projects as complete folders. -## Incentive Mechanism +## Incentive Mechanism v1 The WebGenieAI subnet incentivizes miners and validators to ensure high-quality outputs. Here’s how it works specifically for this subnet: -- Task Assignment: Subnet miners are assigned tasks related to generating and improving machine learning models based on various prompts (text, voice, image, Figma). +- Task Assignment: Subnet miners are assigned tasks related to generating and improving machine learning models based on various prompts (text and image). - Performance Evaluation: Validators evaluate the outputs produced by miners. The evaluation criteria include accuracy, efficiency, and innovation. - Ranking and Rewarding: Validators rank the miners according to their performance. The Bittensor blockchain’s Yuma Consensus mechanism determines the TAO rewards distribution based on these rankings. ## Evaluation Process -- For Miners: +### Automatic evaluation of ImageToHTML task for design-wise +We automatically evaluate generated webpages by calculating the similarity between the original input image and the rendered screenshot of generated webpage. +We break down the evaluation into both high-level visual similarity and low-level element matching. - Miners in the WebGenieAI subnet are tasked with generating project outputs based on various types of prompts. Their outputs are evaluated based on the following criteria: +#### High-level Visual Similarity - 1. Accuracy: +To evaluate the visual similarity of IR and IG, we use the similarity of their CLIP embedding, denoted as CLIP(IR, IG). Specifically, we extract features by CLIP-ViT-B/32 after resizing screenshots to squares. +To rule out the texts in the screenshots, we use the inpainting algorithm from [Telea](https://docs.opencv.org/4.3.0/df/d3d/tutorial_py_inpainting.html) to mask all detected text boxes using their bounding box coordinates. - Correctness: The generated code must accurately reflect the requirements stated in the prompt. +#### Low-level Element Matching - Functionality: The output should be fully functional with minimal to no errors. +Metrics like CLIP similarity only capture the similarity of the overall images rather than the matching of all the details like text. Moreover, the metric itself does not offer any fine-grained breakdown to help diagnose model weaknesses. - 2. Efficiency: +To complement that, we introduce a suite of element-matching metrics. Specifically, we consider whether the generated webpages manage to recall all visual elements, and whether the corresponding visual elements in the input image and generated webpages have aligned text content, position, and color. - Resource Utilization: The output should be produced using the least amount of computational resources without compromising on quality. +Given a reference webpage screenshot $I_R$ and a generated webpage screenshot $I_G$, we use a text detection module to output a set of detected visual element blocks for each: R = {$r_1$, $r_2$, ..., $r_m$} and G = {$g_1$, $g_2$, ..., $g_n$}, where each block contains its textual content and bounding box coordinates. - Speed: Faster generation times are favored, provided the output meets all other criteria. +Based on the two sets of detected blocks, we use the Jonker-Volgenant algorithm to get the optimal matching M between R and G based on text similarity, where (p, q) ∈ M indicates $r_p$ is matched with $g_q$. - 3. Innovation: +Given R, G, and matched pairs in M, we evaluate similarity along the following aspects: +- **Block-Match**: The first desideratum of the task is that all visual elements from the image should be reproduced in the generated webpage, and the generated webpage should not hallucinate non-existent new elements. We measure this by computing the total sizes of all matched blocks divided by the total sizes of all blocks, including unmatched ones (either because the generated webpages missed them or because the generated webpages contain hallucinated blocks): - Novelty: Unique and creative approaches to solving the prompt are rewarded. +$$ +\mathbf{match}_{\text{block}}(r_p, g_q) = \frac{S(r_p) + S(g_q)}{\sum_{(i,j) \in M} (S(r_i) + S(g_j)) + \left(\sum_{i \in U_R} S(r_i) + \sum_{j \in U_G} S(g_j)\right)} +$$ - Optimization: Innovative optimizations that improve the performance or usability of the generated project are highly valued. +$$ +\mathbf{match}_{\text{block}}(R, G) = \sum_{(p,q) \in M} \mathbf{match}_{\text{block}}(r_p, g_q) +$$ -- For Validators: +where S(·) returns the size of the blocks, $U_R$ and $U_G$ denotes the unmatched blocks in R +and G. The intuition here is that unmatched blocks will lower the score as they indicate +missing original blocks or generating hallucinated blocks, and the larger the unmatched +blocks are, the lower this score is. - Validators play a crucial role in ensuring the quality of outputs. They are responsible for evaluating and ranking the miners’ contributions. The evaluation process involves: +- **Text**: Given two strings from two matched blocks $r_p$ and $g_q$, the text similarity **sim**text($r_p$, $g_q$) is calculated as twice the number of overlapping characters divided by the total number of characters in the two strings (character-level Sørensen-Dice similarity). The overall score is averaged across all matched pairs. + +- Position: The positioning of the blocks largely impacts the overall layout. For each matched pair (p, q), we calculate the position similarity **sim**pos($r_p$, $g_q$) = 1 − max(abs($x_q$ − $x_p$), abs($y_q$ − $y_p$)), where ($x_p$, $y_p$) and ($x_q$, $y_q$) are normalized coordinates (in [0, 1]) of $r_p$ and $g_q$’s centors. The overall score is averaged across all matched pairs. + +- Color: We use the [CIEDE2000](https://en.wikipedia.org/wiki/Color_difference) color difference formula to assess the perceptual difference between the colors of the generated text in block $g_q$ and the reference text in block $r_p$, denoted as **sim**color(rp, gq), where the formula considers the complexities of human color vision. The overall score is averaged across all matched pairs. - 1. Initial Review: Validators perform an initial check to ensure that the submitted outputs meet basic functional requirements. - 2. Detailed Assessment: Each output is thoroughly reviewed against the criteria of accuracy, efficiency, and innovation. - 3. Feedback Provision: Validators provide detailed feedback to miners, highlighting strengths and areas for improvement. - 4. Ranking Submission: Validators rank the outputs from different miners. These rankings are submitted to the Bittensor blockchain. ### Example Scenario @@ -87,14 +98,25 @@ The WebGenieAI subnet incentivizes miners and validators to ensure high-quality ## Roadmap -Phase 1: Generate HTML/CSS projects from text prompts. - -Phase 2: Generate HTML/CSS projects from image based prompts. - -Phase 3: Enable voice prompts for project generation. - -Phase 4: Integrate Figma designs as input for project generation. - -Phase 5: Automate the downloading of fully functional project folders. - -Phase 6: Expand to generate full framework-based projects like React, Angular, etc. +### Phase 1: Foundation (Q4 2024) +- [x] Launch on testnet (214) +- [] Launch front-end application v1 (webgenieai.co) + - Enable Text & image inputs +- [] Incentive mechanism v1 + - Generate pure HTML/CSS web pages from text & image based prompts +- [] Begin marketing for brand awareness and interest +- [] Launch on mainnet + +### Phase 2: Upgrade (Q1 2025) +- [] Build dashboard to track miner performance and progress +- [] Upgrade front-end application to v2 + - Enable figma design inputs +- [] Upgrade incentive mechanism to v2 + - Generate full framework based on React, Vue, and Next.js projects from text, image, and figma prompts + +### Phase 3: Expand (Q2 2025) +- [] Add features to monetize the application + - Add payment gateways + - Automate the downloading of fully functional projects +- [] Market and B2B sales expansion +- [] Grow the team \ No newline at end of file From b76142480f4cae7d40de33d81ea19f98c3ce13cb Mon Sep 17 00:00:00 2001 From: cardoso-topdev Date: Fri, 15 Nov 2024 10:45:44 -0600 Subject: [PATCH 014/129] chore: readme update --- README.md | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 83f8ea0b..9dcc6144 100644 --- a/README.md +++ b/README.md @@ -100,23 +100,23 @@ blocks are, the lower this score is. ### Phase 1: Foundation (Q4 2024) - [x] Launch on testnet (214) -- [] Launch front-end application v1 (webgenieai.co) +- [ ] Launch front-end application v1 (webgenieai.co) - Enable Text & image inputs -- [] Incentive mechanism v1 +- [ ] Incentive mechanism v1 - Generate pure HTML/CSS web pages from text & image based prompts -- [] Begin marketing for brand awareness and interest -- [] Launch on mainnet +- [ ] Begin marketing for brand awareness and interest +- [ ] Launch on mainnet ### Phase 2: Upgrade (Q1 2025) -- [] Build dashboard to track miner performance and progress -- [] Upgrade front-end application to v2 +- [ ] Build dashboard to track miner performance and progress +- [ ] Upgrade front-end application to v2 - Enable figma design inputs -- [] Upgrade incentive mechanism to v2 +- [ ] Upgrade incentive mechanism to v2 - Generate full framework based on React, Vue, and Next.js projects from text, image, and figma prompts ### Phase 3: Expand (Q2 2025) -- [] Add features to monetize the application +- [ ] Add features to monetize the application - Add payment gateways - Automate the downloading of fully functional projects -- [] Market and B2B sales expansion -- [] Grow the team \ No newline at end of file +- [ ] Market and B2B sales expansion +- [ ] Grow the team \ No newline at end of file From 9967324a958d1b9c8df80484847f020749f12833 Mon Sep 17 00:00:00 2001 From: cardoso-topdev Date: Fri, 15 Nov 2024 10:47:51 -0600 Subject: [PATCH 015/129] chore: readme update --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9dcc6144..aedb193b 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ We break down the evaluation into both high-level visual similarity and low-leve #### High-level Visual Similarity -To evaluate the visual similarity of IR and IG, we use the similarity of their CLIP embedding, denoted as CLIP(IR, IG). Specifically, we extract features by CLIP-ViT-B/32 after resizing screenshots to squares. +To evaluate the visual similarity of IR and IG, we use the similarity of their CLIP embedding, denoted as CLIP(IR, IG). Specifically, we extract features by CLIP-ViT-B/32 after resizing screenshots to squares. To rule out the texts in the screenshots, we use the inpainting algorithm from [Telea](https://docs.opencv.org/4.3.0/df/d3d/tutorial_py_inpainting.html) to mask all detected text boxes using their bounding box coordinates. #### Low-level Element Matching @@ -58,7 +58,7 @@ Metrics like CLIP similarity only capture the similarity of the overall images r To complement that, we introduce a suite of element-matching metrics. Specifically, we consider whether the generated webpages manage to recall all visual elements, and whether the corresponding visual elements in the input image and generated webpages have aligned text content, position, and color. -Given a reference webpage screenshot $I_R$ and a generated webpage screenshot $I_G$, we use a text detection module to output a set of detected visual element blocks for each: R = {$r_1$, $r_2$, ..., $r_m$} and G = {$g_1$, $g_2$, ..., $g_n$}, where each block contains its textual content and bounding box coordinates. +Given a reference webpage screenshot $I_R$ and a generated webpage screenshot $I_G$, we use a text detection module to output a set of detected visual element blocks for each: R = { $r_1$, $r_2$, ..., $r_m$ } and G = { $g_1$, $g_2$, ..., $g_n$ }, where each block contains its textual content and bounding box coordinates. Based on the two sets of detected blocks, we use the Jonker-Volgenant algorithm to get the optimal matching M between R and G based on text similarity, where (p, q) ∈ M indicates $r_p$ is matched with $g_q$. From 355f557bf248ccd613dc32675bf3bcaef4066fa0 Mon Sep 17 00:00:00 2001 From: cardoso-topdev Date: Fri, 15 Nov 2024 10:49:07 -0600 Subject: [PATCH 016/129] chore: readme update --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index aedb193b..e6d99564 100644 --- a/README.md +++ b/README.md @@ -49,7 +49,7 @@ We break down the evaluation into both high-level visual similarity and low-leve #### High-level Visual Similarity -To evaluate the visual similarity of IR and IG, we use the similarity of their CLIP embedding, denoted as CLIP(IR, IG). Specifically, we extract features by CLIP-ViT-B/32 after resizing screenshots to squares. +To evaluate the visual similarity of $I_R$ and $I_G$, we use the similarity of their CLIP embedding, denoted as CLIP($I_R$, $I_G$). Specifically, we extract features by CLIP-ViT-B/32 after resizing screenshots to squares. To rule out the texts in the screenshots, we use the inpainting algorithm from [Telea](https://docs.opencv.org/4.3.0/df/d3d/tutorial_py_inpainting.html) to mask all detected text boxes using their bounding box coordinates. #### Low-level Element Matching From e28172e23bc32ee24f6dec6590efbb64178185ed Mon Sep 17 00:00:00 2001 From: sangar-1028 Date: Wed, 11 Dec 2024 06:39:34 -0700 Subject: [PATCH 017/129] chore: update documents --- README.md | 16 ++++++++++++++-- docs/running_on_mainnet.md | 18 +++++++++--------- docs/running_on_staging.md | 12 ++++++------ docs/running_on_testnet.md | 24 ++++++++++++------------ min_compute.yml | 8 ++++---- 5 files changed, 45 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index e6d99564..bc0f773b 100644 --- a/README.md +++ b/README.md @@ -96,13 +96,25 @@ blocks are, the lower this score is. - Ranking: Validators rank this submission against others. - Rewarding: Based on the ranking, the miner receives TAO rewards. +## Installation + +- See [Running on Staging](docs/running_on_staging.md) for instructions on how to run the subnet on staging. +- See [Running on Testnet](docs/running_on_testnet.md) for instructions on how to run the subnet on testnet. +- See [Running on Mainnet](docs/running_on_mainnet.md) for instructions on how to run the subnet on mainnet. + +## Requirements + +- Miners can use any port. +- Miners can use OpenAI API key or can use their own model. +- Validators need to use OpenAI API key to generate a task for miners. + ## Roadmap ### Phase 1: Foundation (Q4 2024) - [x] Launch on testnet (214) -- [ ] Launch front-end application v1 (webgenieai.co) +- [x] Launch front-end application v1 (webgenieai.co) - Enable Text & image inputs -- [ ] Incentive mechanism v1 +- [x] Incentive mechanism v1 - Generate pure HTML/CSS web pages from text & image based prompts - [ ] Begin marketing for brand awareness and interest - [ ] Launch on mainnet diff --git a/docs/running_on_mainnet.md b/docs/running_on_mainnet.md index 38be00a6..f95f363c 100644 --- a/docs/running_on_mainnet.md +++ b/docs/running_on_mainnet.md @@ -24,20 +24,20 @@ After installing `bittensor`, proceed as below: ## Steps -## 1. Install your subnet template +## 1. Install web-genie-ai **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 +git clone https://github.com/web-genie-ai/web-genie-ai.git ``` -Next, `cd` into `bittensor-subnet-template` repo directory: +Next, `cd` into `web-genie-ai` repo directory: ```bash -cd bittensor-subnet-template +cd web-genie-ai ``` Install the Bittensor subnet template package: @@ -132,7 +132,7 @@ This step registers your subnet validator and subnet miner keys to the subnet gi Register your miner key to the subnet: ```bash -btcli subnet recycle_register --netuid 1 --subtensor.network finney --wallet.name miner --wallet.hotkey default +btcli subnet recycle_register --netuid [mainnet_netuid] --subtensor.network finney --wallet.name miner --wallet.hotkey default ``` Follow the below prompts: @@ -149,13 +149,13 @@ Follow the below prompts: Next, register your validator key to the subnet: ```bash -btcli subnet recycle_register --netuid 1 --subtensor.network finney --wallet.name validator --wallet.hotkey default +btcli subnet recycle_register --netuid [mainnet_netuid] --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. +>> Enter netuid [1] (1): # Enter netuid [mainnet_netuid] to specify the subnet you just created. >> Continue Registration? hotkey: ... coldkey: ... @@ -202,7 +202,7 @@ miner default 1 True 0.00000 0.00000 0.00000 0.00000 0.00000 Run the subnet miner: ```bash -python neurons/miner.py --netuid 1 --wallet.name miner --wallet.hotkey default --logging.debug +python neurons/miner.py --netuid [mainnet_netuid] --wallet.name miner --wallet.hotkey default --logging.debug ``` You will see the below terminal output: @@ -214,7 +214,7 @@ You will see the below terminal output: Run the subnet validator: ```bash -python neurons/validator.py --netuid 1 --wallet.name validator --wallet.hotkey default --logging.debug +python neurons/validator.py --netuid [mainnet_netuid] --wallet.name validator --wallet.hotkey default --logging.debug ``` You will see the below terminal output: diff --git a/docs/running_on_staging.md b/docs/running_on_staging.md index e282dcfc..d93c9986 100644 --- a/docs/running_on_staging.md +++ b/docs/running_on_staging.md @@ -1,6 +1,6 @@ # Running Subnet Locally -This tutorial will guide you through: +This document will guide you through: - Setting up a local blockchain that is not connected to either Bittensor testchain or mainchain - Creating a subnet @@ -92,21 +92,21 @@ 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 +## 6. Install web-genie-ai subnet -`cd` to your project directory and clone the bittensor subnet template repository: +`cd` to your project directory and clone the web-genie-ai repository: ```bash -git clone https://github.com/opentensor/bittensor-subnet-template.git +git clone https://github.com/web-genie-ai/web-genie-ai.git ``` Navigate to the cloned repository: ```bash -cd bittensor-subnet-template +cd web-genie-ai ``` -Install the bittensor-subnet-template Python package: +Install the web-genie-ai Python package: ```bash python -m pip install -e . diff --git a/docs/running_on_testnet.md b/docs/running_on_testnet.md index 9203d3a5..be4e8553 100644 --- a/docs/running_on_testnet.md +++ b/docs/running_on_testnet.md @@ -18,23 +18,23 @@ Before proceeding further, make sure that you have installed Bittensor. See the After installing `bittensor`, proceed as below: -## 1. Install Bittensor subnet template +## 1. Install web-genie-ai **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: +`cd` into your project directory and clone the web-genie-ai repo: ```bash -git clone https://github.com/opentensor/bittensor-subnet-template.git +git clone https://github.com/web-genie-ai/web-genie-ai.git ``` -Next, `cd` into bittensor-subnet-template repo directory: +Next, `cd` into web-genie-ai repo directory: ```bash -cd bittensor-subnet-template # Enter the +cd web-genie-ai # Enter the ``` -Install the bittensor-subnet-template package: +Install the web-genie-ai package: ```bash python -m pip install -e . @@ -129,13 +129,13 @@ This step registers your subnet validator and subnet miner keys to the subnet, g Register your miner key to the subnet: ```bash -btcli subnet register --netuid 13 --subtensor.network test --wallet.name miner --wallet.hotkey default +btcli subnet register --netuid 214 --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. +>> Enter netuid [1] (1): # Enter netuid 214 to specify web-genie-ai test subnet. >> Continue Registration? hotkey: ... coldkey: ... @@ -146,13 +146,13 @@ Follow the below prompts: Next, register your validator key to the subnet: ```bash -btcli subnet register --netuid 13 --subtensor.network test --wallet.name validator --wallet.hotkey default +btcli subnet register --netuid 214 --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. +>> Enter netuid [1] (1): # Enter netuid 214 to specify web-genie-ai test subnet. >> Continue Registration? hotkey: ... coldkey: ... @@ -201,7 +201,7 @@ miner default 1 True 0.00000 0.00000 0.00000 0.00000 0.00000 Run the subnet miner: ```bash -python neurons/miner.py --netuid 1 --subtensor.network test --wallet.name miner --wallet.hotkey default --logging.debug +python neurons/miner.py --netuid 214 --subtensor.network test --wallet.name miner --wallet.hotkey default --logging.debug ``` You will see the below terminal output: @@ -213,7 +213,7 @@ You will see the below terminal output: Next, run the subnet validator: ```bash -python neurons/validator.py --netuid 1 --subtensor.network test --wallet.name validator --wallet.hotkey default --logging.debug +python neurons/validator.py --netuid 214 --subtensor.network test --wallet.name validator --wallet.hotkey default --logging.debug ``` You will see the below terminal output: diff --git a/min_compute.yml b/min_compute.yml index 1da3bb04..3d8e0153 100644 --- a/min_compute.yml +++ b/min_compute.yml @@ -57,8 +57,8 @@ compute_spec: gpu: required: True # Does the application require a GPU? - min_vram: 8 # Minimum GPU VRAM (GB) - recommended_vram: 24 # Recommended GPU VRAM (GB) + min_vram: 80 # Minimum GPU VRAM (GB) + recommended_vram: 100 # Recommended GPU VRAM (GB) cuda_cores: 1024 # Minimum number of CUDA cores (if applicable) min_compute_capability: 6.0 # Minimum CUDA compute capability recommended_compute_capability: 7.0 # Recommended CUDA compute capability @@ -71,8 +71,8 @@ compute_spec: ram_type: "DDR4" # RAM type (e.g., DDR4, DDR3, etc.) storage: - min_space: 10 # Minimum free storage space (GB) - recommended_space: 100 # Recommended free storage space (GB) + min_space: 500 # Minimum free storage space (GB) + recommended_space: 1000 # Recommended free storage space (GB) type: "SSD" # Preferred storage type (e.g., SSD, HDD) min_iops: 1000 # Minimum I/O operations per second (if applicable) recommended_iops: 5000 # Recommended I/O operations per second From 287a02a6696b3e602826dcf519e6c4040b8aed8d Mon Sep 17 00:00:00 2001 From: sangar-1028 Date: Wed, 11 Dec 2024 06:43:59 -0700 Subject: [PATCH 018/129] chore: replaced incentive formula code with an image --- README.md | 5 +++-- docs/incentive-fomula.png | Bin 0 -> 90280 bytes 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 docs/incentive-fomula.png diff --git a/README.md b/README.md index bc0f773b..4aac8a23 100644 --- a/README.md +++ b/README.md @@ -65,13 +65,14 @@ Based on the two sets of detected blocks, we use the Jonker-Volgenant algorithm Given R, G, and matched pairs in M, we evaluate similarity along the following aspects: - **Block-Match**: The first desideratum of the task is that all visual elements from the image should be reproduced in the generated webpage, and the generated webpage should not hallucinate non-existent new elements. We measure this by computing the total sizes of all matched blocks divided by the total sizes of all blocks, including unmatched ones (either because the generated webpages missed them or because the generated webpages contain hallucinated blocks): -$$ +![Incentive Mechanism Fomula](docs/incentive-fomula.png) + where S(·) returns the size of the blocks, $U_R$ and $U_G$ denotes the unmatched blocks in R and G. The intuition here is that unmatched blocks will lower the score as they indicate diff --git a/docs/incentive-fomula.png b/docs/incentive-fomula.png new file mode 100644 index 0000000000000000000000000000000000000000..848b6fa99735521e91b0e79f057429b43a0c6c81 GIT binary patch literal 90280 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%0A7N@e3r)La^Vdk2%q@_ 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

J&TrSBq) z%QqGkaW^9NmcI7HF#`1o9vGj-+GerBm^uaI`mEM6Cg@9&T`WG*t9O6A2VS7;JHxrd>&jF~OY=`R`sH?)Y>^B7**}vq@zpo2;0tD94gA3P$ z){9wnN1_h`m?F`8jxG2iJiUFxy7kiF8tBBD<-ZSnwK zBuV4Z$2VJb0{jo>+Pc!QA@}zUX2XH{?>kgVwH|^uj8(J$WQnHB2a9fNt&hckwWY%B ztK(#jbQVaL&(<`~uZrV>2y#kUP2rjixa6~d$TyiP*o@uq^NppR@wLV3xxK)DvqXIw z@>UvGKQ2B!UCws#yH6!kPhBY&o9TpxJAETw0Vzq%#LxZ>kaEgzqvsPA&g?BbyCM|4 zyF4skDk!x!2e;r`y2nh(VFy`eD{xyQY~kv&7|Xn{eK|uhy=(jW|FcA+r5P}Umk*YK zwAjMsM;?qGPAk;jW7}u~czwW@Jmt1T@%h{?-Cg(T_P;AHasjAScco587k_TfpxuHI zL16kIa0kK;z(t-Q$`hy5ZVMdz=6P7iy`?SO;+=t__3ws~c{s2rf8`w51LLipoOtT& zL`1;r#x86k3wR9e{x6J9!0J<={X3p4cIgKEyMlr~Re;DGOh2Itzc_kjSiU{d@qL-j z)Lk{a9Bs%q_+Q)Lri170ITs1aN@E~E)K?k6e}}jCRlc3R1-DVoz&X^(bR-FexhJ{B zqwn*h!_PkD+X9inM7osqg$9KMxE1?R8eUYKm{IdmPKN)-^8?lnQK)_`+_3&PV_=mH zkNI5Kp#p%bVG~tjV)vK3o;o=F$VEb+!=S%p%_~*=x_n5eoB-1T8~`~W2Au!6H5B;@ z9Yn%#?H!8-BLm78gm4QtZH{IU^bv9B(V=t&xj-3!TWEvhnf<5NVi75{;Y>J{*|HkY z==Mp}*d)M4x)0~|)%xtxihi-=W${m-(Go3YD%sa@RPJ|d<7v|ZJq#1OX0mL#4lXM& z#32BSX8I%G$sE~25*>S*&DTrdjQ<%7{x^Dp{3BhzMbtB!FHey#7B%9kHGW!xpf8_0 zlJS*nC5#x^7Y2{l$73Hq&)|7p1RDoeG)|3?g4;(qz%|Cv(to^A#i(y+(9};ouV~{q8AigU|73 zQ%9y8P%adJAT<;Blq!63H9gJLw~u!(gmmU5fW~I?iN3fTu=Rlct0|MAeX{y|jtzFV zj3nR^S*Q&%Uqb3VT)2`0Dw@I4U)2K4si4(h;5!rPQiG$^V(N!Y-U0g@sW`2g-YW7+ z0T#DQ2jF}wURxZ&{Pb#nCJ3*ghph$I>$Uk*0Y&LrdATB+zgXu>$MvnZR0um4i2zHeHM?e_DNppL(@wunc`K9n?$weYZlm311)7M}d}zYra;f$Qbsu0p zrv;X@E^Mm=qFkc;MBRPL$gm#0B+Yd$av7ww^=_$I$8;X&qu{&t#MwRZ)bkD%5F((0 zw!QO*mk*FVEpI6t`2Ba#wPwI#nSj{8`h9`v%_$lvD99TaF?z{iCHa)d8RI3o3F{qO?2MxqNY$$~{+!Lc)NSpn=Zw-+<1A;pwl4;OKH z?{Mth`O9PrKN#WU-Lj83?@lB)HAfsN>W4D)_#XUPOMMY$cH2d-Iag2xB2{`Hy1R|< zLeMm6_8Pmsdp!xJjtN#fIPys1!p0Pc7BE8@zs@9AJe-BQ z_5W6NTvS``?OKD0*}EztXaHYlde_E5u3Ka(Pk4H^Z^m^6+$_q$$gdO=xdLC*+7fyI zspDsOjx6SrM&PHkKN66Vc?cM`2zav-0%7FNUpkH&j-#9r3B+x^V*CFd1Mjxfi) zA92{4iz9mN#;MgDDg>q*N$7vr$tQD$$+2rV%W>E@lORVx93z~&P@c&yg1;%KTbQOrx#K(+SDWrCaM0FTrNNF*K9kMok)elo+T0JtjqyMBfG!blZOjdvx>@t?4AHZ0=W|{rWI>*PzvT@T z6lez+G~SG(*Th_LgC)zars(b#dU`2~K!D~)p4^K;w*bNp4l9iy;C8?-PWFN>Lu6^h z#B&KtV7DY9{HcBcbdrWojQkx15)FZ8mn9SnvzCO75VOOv$ zS87~>9Wl@8&}{sY;2UEsuwaAeStCYOQPI~YnUuC}I?tX?6~n^DxOcsOzFIvCq9<&v z`bXh*c1iNwD{TXVtU4U?K}Yl7I|1bYaY@KUEPV0+%?v+0Y>$^UsS-fzhE}cyqD&HJ z1mAsGwA0H_i9W?w(tseUm5Pg0V^bdks!+=Hj@e62o9}3yO*>ECg=`}n-DVTaJ&mRCE$A~nB5yKKF%%O-e21^{Eat#RXtFuUMpJXFjSH-3sr15m{U+> zIBQ6(9(TevN%0sC+E+%tDgxt=grK6mAg9?6S#E$@xsra9+h}kMcatky&6X*hbEtXs zkDxsM-H~unXS4R!@nBXh=3f)y9~&bL)WMe9(PF^f9i5x=D$c_H6a(PkRjE4Sn#gN^ zhL20w&EK8PN&!9$xVhgoyIv?ZAz;&D4l@gxzjZzekHtc5QSRdybiLRU6Af!2+rJTH zQIDJ@v%IH5x3-urf*ANh`aI$lkA&lBYHJz>qQYit{1Y2*wGtUk7CByNwe&KG1l9Rx z(22*b!#5={fH%IPeobVzL?5o6Yt3aOxYm7$!1qGyMPI*`y@v!&P3nV@5evbLYC_<4 z*3j;aOcs4#u&&1TaDRqn210t&`z#hQPZ2UAg`EYiQN6YrOWpIs+Lf`nO5?04^e}Yy z;q#DDe)mt&7$fVt)n=oMT&FHo{0O&9Nya$VI1DMuQx<~u8$HNtdsa1e<9+&Uh}2Uj zD@cf*7FeAjgqM|rJapxCZPw&=6&PQtJZZ1Xer0Bljm2~%m_#Z?_kQ;;*X?{KpDj6B zWc*3`KyQS4s(3+Q@$SLkMVO$wr3%s7kEaBr{C0d>zo9(=gS|UFjk5bY-GlgFJ$mpU zefFv2DTsesNrWi#*jYYudqr5}de%JP8%q-++6Ndv{J<`<7L`xeR;E6^Rw4k(`nRh^ z2JmAipOdh81*$atm;iI5$!LnEn%pa~@arUMg);0mSkxmN^TKlA_+?YMKY<@UD~Hh^ zP4S@9x6o|$j7Z?H&YI3Qoc(IOJ254BJ>f4@oE$}4P+3=!4WhH?y-W6F@d3SMWy9Jg_zVi4i*4xXA_fc;qhx5i> z>`wC5Ti?|mG63~1vM80{%Ma+Vzo6VJ$B5~kX>5=Hrd}ed;1N;&rgg159z@qmK!)x9 z@!q_8GcQ$hq22GfNmno>vF}}Z+EqV=;X$BC;WqEtGn-0$QIFp9-N{K%jA9u;32%phxInd0W4k=UoeyAIo+^trRf1g- zb0lic2UlH-xe7Fpzk&yncy^PEcFeTjy_1nhm(l}T?kVm2cOtk;$yJsM9TP%hRviN| znZ2|6g3t}1u0vg;mLe|UyY6G`~eU&Yh z@ySMEQSfo0E7rJ#{J~>T|EXAqcH}>L9GD=zu^>?d(Qt8#M3_9OBDJ-`B5L6ZZ{(i; z>+C*yxB76!p9S3JKkW6IFr#gh;X1y5v)GWw-H>OuKBCch?qnx%52j{9>FZgs;5=;B5BiurOfwQv`jT>Pq3eymFstCTC;o<*6WG&)t% z2vpLDH`=Q4st=TRC-JFvw@%hBY`S~^4reUe>6S0Hiy)21P;@+>f3&sVi)=I~u*IU^ zqrmoV@E7VQ>=$h5@`c-(F*?Q8QB#bt9d6oTm0XOkKSu{X!td>I$9kp|n*j5@LM-MH_wU|lx85U?MWIOzB7Z*-ZDPjg$&zgcs#3M=CH-HB}uoNSc9-8=p| z#a{cd3?yl|6Q+V)Ufaiy4ihdK4XXunD$iNa)y%H@!<54yZo%)D7`ri#X2VMisp=o> zyfkg%Qkn-&`si-lsHvXB(e#Kl($XbbaypgOCPtv~z6UWj564hYv<5%E`JLMQZVjQ5 zacg@1eF5zSp7%uT~fCUu($3X z#Aab1KzO}-<;um?pXi*=q%AA)9CeE%l&=os(l>*UEMh9o~EWC zp3+fa-K-Ryffc5lbhOk+LEDqk{bA$uGjY$gw9-2C)#L=|V~PgPUoqvzmy<=;t0X0C zKQnK?a8=yNFz&Zt`Vj82UDW#~8OPq!r#BOC5x*&^CpE~iOetVNRqO5&+)h0j<1*wi zH%!{-_S2wN%md>6k#emS@=1@`(3Kr1OzKy0f4jBXzC$ll&&T)e(S5$j2|Mc6!+r$} z@S-oeq+)JG;LhZ$^0QV4Itpn*=ytA;y<>+u%9W!2V~qmr*-!#oYA9A%sN3*K|dTQZwwz+PI=`7Osg5dDU^SR zPuCOPp|U-qnJ@8;GOYZ&2Da5gjk@Zy6GG7%1B1fO7LUxLe9!xns&Ld?>{Wwz4B=o} zmv4y&*NLe4){Kv>6I6!;2XH9AP=5`o0Ela%e|+WXieLZaXCYcZ7TMW-QgQCi##l>PkTIj5`bXE`xhNsM(G~H*`t7{I!Ljw9VNKvCOk8%! z%{rLK2;i_Cvr&E#76KLz8?&N0?y%9R=0@!*4#`g~K)fo<=6s=j04$bYAV;T&D40Qc zU5;c-2JJJyKdY>gaYN|;$LlNd zr79PD9?XI5HG|)wa$nx{zCYZXrUR4&@U4X&$CE|JEHpemJpUFq!$O0HJp{6`IVRE%AuFtuywO*3l_*=+0;3#?S>iI{oiF;OXmF<^EQj zG22Z}A&tAybTd2*@?2>AA5&EYZ5t~?x~qX%ow_#QZRCs%2umg7s+P{x_rpeZB20eETQAsyH zfYnLC9fi7|6Wvbe{=a)71k`T~IOL@_+5?l`v(0tc)f@kmR6#)XCquN(wg5}_g+=6* z`Q{;Yy}fwwgCMRHT)}r1RkSfgeG9V9L(LiH9L(oJKfQz10Pjr;16@2M*383Q$=GuU zni;Wdds9?DmH%4bn4Be2u_cuGtH_Oc^a0pPC6!D0Dg~%D=W#m+kAmLxdZ^biiu8CC z%4KF?%KY?SPX)3#rO(Kp!2mJy3>ZYAJe_t9#W}Oof^oqd#lo5n$s)f!TcC)pf0rMn z+1;?PJ*WWkH*W05YBS`QZ$H%)AfjjyttWEfY^1qeE&oUp)aQyVK31`CAd3g*=FA?q z@)PS#J60wcovdR$rKj=dQ3$SuRNkPl{V;`TP6Ond8Nl(e({G{<-c^FkS3%_H;I=b+ zwdI1G%J*uyCH+nTqqcAkoP9XA?`Hl`3ph zzjNyDoA%!7D#x7#6%v5Kq4cj$*n7FRz*&l;b`cxeq+OFWxgzDBT%h1WK9I0oPE+YN z3Od>bj9Rq8oid=ho{tdY{g-6%7C5DNs%KRLmcuo-<3EGcd$nSxx!$k-6-vF?AU$IT zmU||$rEwuObhM~a@1(Il*XPfPP zw$q_|42%*mKXjosF$VShW$~@}aC^R+ZPI;&!BviCdpK{sPS?M{+!Ed5`EWaCGR$7?U`tMV(4}U!ACH3~(Od2bowRH0O|avf~%gCg#h77~&tnjsMgI{h^<|7;7!+ z-UQNBFwVA_9rk~+6aoThc`D`1$b^ryNhr_Owx^^Mr*rW+th_(a0!liJj1x)f5t^05 z>8~8_u1u+9%$_9NBwQKH>_m3^_~L#!>MYnf2Kg24dnHq6sl$FRl#>eG229O=%y}>I zirXOv`1`2BsEZ@Eb=m`AV!yAegIXT!60rmX-K}_-1or1)WMYx-qMd=rKMMTW)FGa_ zk!to0(({0T+BGeeG~}j>9vo1M@=7hAE4e@XFW;` zYo50sJE+;!;~frt(oEpoYgK|o4hC_!);R^jmYx;QJ7mx?6jT6^4jm{Y6j5}FUb7|W zm5~D8{v2RnGZX!{SnP?>7rISLfBz!PQ7CPB(8lo! zYt8L8kD0jy9R%WDGMr!9Kmja`HqClG;dbmH0P;Ccgm5f2JG{?kva=+@RiJ2=z~vwV zKVtfN-P>H6ZOKITVzg9h$^G0+v8b`#NeWH8lvR(55<=B*U1z_MV6KyDMvrj1Load| z1k9|D*?h!K!hKC6DZCq}yCC-_3IdZ9qFpk{^P#-;TS9U?x`1c@QJi>oGV^%5&#=y^ zC7&z9TzCIt%iaN${o#N*RKZ1SQWQ7{l zd2n#Wd* zlnT7+_Qghg&;R&|n_gZH=s8acBp#wEoFKjerxogOeR!z4~wwk9d)zB!(Vy z!=c0LIVv8rP#0z)y_Ch_mW6xy=g(N{hcPpobwC)GPjIY+!T%dQ-}AezK$44&J8Fiu z`jX%>Lh3=FV=+;LzX_2VvF+!g7qlDU+D4KZ(S=9M3syyg6!rBn5RwxR?7F(!|Djer z#mmdG#LL^rQdyhwyrH0BeRnyfpy4V-3Jwj*AA&fI^fRd#3g|&mtFGzh)q6RHp20~h z@aO+~`jQXlU2Q%XtGR9=yLGfs8%Or0Dn_D#TfiGyAEhlL1N?+lUMQOyO?j6hm~X$cR-MUn@r#cs@AaK z?$tS*@DTDwBZ7v5{PT;?tZ>`FjBRJ2?EJ{$uSsOtXdS+Zs0rOJUAM$tGeksc0u*(Jb3O77GPT zq_g74J63`M-|5nof-UI5T69v_!m3vBj}s`wt4*#K1*@FPwZ|-8qExB>`B;!bM9)Ye z0t2$Ll93WdnY{~MAAr+Y{`$;+eknr#bUC+ugSVv8IgH9T+v$3FVAhu@VgoJCh)qCB zEeNitsdt++$5Pj-^5Mapw#Lmx1&fG96F#fS&`~`F1@IO6XIT2^Sb*Oh>4!fe&d5c0 zp|s?CE|i1~lD`9Up!`+2tg zY1*2r;pw0I_p*I1N6-71%?%80&5-_BG_iJu^&x?8CI9;^0&uwKvrSy_ATcCr`1o*z zrPSbn9+MNy3qHRQDh0JD_<>B`0&5+mWx!tlJV`=hF_D(^wu_nPR~6K5ZmOpNAePD- zL|sgS1*VHQDi7u=5oJtw?NNM(%}eT);M_o4U&8Nq-|S-ec7LJ`o5mksV5zMD5$n7` z)r{d83541VILxN}DW4_3tQG7EefqOLN?qLe#Q-c2OG0;U+A=Z)e8lEfytGjgCnQCl zefEKMez=&a3?lUOPv-wE2KZW+n-a)W5&Fw&|j=>(J}~)}3At-QOKxcYAR-mb-gheiSglcxtktpyo&$y3e#@8C2q-e-x4Ww(`uUeasDE1 z1(4H9S>jBchp%}X%0O^r<+E5w8{B6dT1GyHIPhfY!9iM(H#i-(qh3E6fuE;nL0dN4 zbkq(z%&he|DjPj?*W~-!k%PaX+1*#VaW-b|DZFpIEIe~khz5>KH@xrM?8XepHk zMxUndoxOx!eao&i#2MSqX!b$AHG~O+S4@Pz;4^F-c$V1hx1iD;d;%q7-Gx=ggMOHB zD_ov>TvQZ0c$2I*4t*sM5S_zeeWOH|H+XXQH(z=;or=)B9~cmydIHI~P?EfHUs+0QsZO1*Es z#?opehn090*T-am@Fr2`;{^SY+D})f%>k^~&ttJ`)^Cy^2SjC=Cjb}VCc5JKy89Wa zfNy!*^_b@0s|isCTAScR1jjP2oc<{X?u5!|5Ypy?AVe$}lt*QQo>dx2>xYCO9)jc} zGk`*?2ynw)vuP(wO)kPt2eaeB1cd}YlAV{ksRHAeR7Q#1Py5C2lzc?=FwB+VC}SD4 zDXJ!!YM(uC(E(^ae8AN8k4I0z7jH{&rbIK9%~bELY%=f&?|0g(;RoFK$erzmw)w+P zs0Wl6@^t*^3aKF7*B1K6HPg{6`;ZQqP|a;W))Ws{+~;uhbAt*(*tU5t+`&CP_$Q|;$z)^gADp*WQM$Zpw*gz z^eCv#B%ia-Y3yj^v|w3_xMH8?=_i14br1;5OJz=nYO{1Qn{bc;0$}%;1x;3U)0#$K zwyTw>P3Q;WO1pL`LhTZI78kiM)pVsn(*b`ufDJZlZJw7SJUm>*i!N+AL4@>epoWhA z5jGUdO>}c1q)qVj*P8y1l{Mh8nqBWcJ5-RqVm5lYu2>jr@3VRBK&V`Vvf7P<7-sDb z9x5AMMWqwE-y2aYTy%r*ms83?{x)_X(H6M7vE=Kxd3mTl3uYVSD_=6BQLVmJQp`0_ zl&lA$k@a#WoI&}=wWkgf7@{Wwdp zko;i36t%F1lxw2?Og)L0LcQp7W9;I0y9h zVxNlrXDkPAUzcya$@n_b^T4tlpK700fdv1@&*}F9a5|Hb(%rxgKpMJM)*>0hSzZsP zFgn6(@fd{isv8u&Q<~Vqf!bWFQIyyBLIkTL@s;8#GHYdpA|W%r({V6%KkNJu+m$E_ zY2Y~2YGuj^z8R7{nDIHhW-;Q-6;vnO?Tjoqd5SE78d%9@3LjY1709wa(Vg*3R2r?O ztl(1Z)<8gkOLiUJ&lIaO$Rw($VRjSaFAsJGwN}ZqWek4$<9!qHiw*RvtO(|GK1wR{ zg*U{lVn=}CEx41&Ux&8USV zoAC%z3tuQo_De2w%|5>iP{MSPIUSXqBwtsmQn>C0+aANEu{lk|X%d7OYy&e^xz-%e zfehX56HT%N=hn$)Wj4bLRmE=?m0VALPeZR>qg1@@b$6(I!f^*0gT9Bkehoa`3`W;S zwiMmanq>50l>s|I1Z3{f-`K%N79FDd1gm+8u9 z;~AC!g+rmGUftl2H4z8$8q*C#?z=q*rfW?nHB<(!i*?T49nN2LHJV*GpIxmeUqeFE zl0{&D+3^}liZ23L!9`zYQMh}6z&Y$K@M`jMWX{Ep^MILz=h;zB^v}P;%<<7 z8;nq=#~t5USy1>KCUkBl@L$?+TcOC(UvH<<4A^~a^#VSN8s}qUTKW>-fus3q(mPN< zs>m(-hQ5Skf0jwKF3=9R|F(W_K^I^sUNyOEH(SsY2p~=B6e<jyCn17h!brA_OFXnrdlG|G=gfF1O!fQz&2_^7koG$*tMsl)>( zA(1!>LKX&Jyp`|C+cK0(q|&NDo7kkcBgsk5&1c;GG_`7KeA-h|+r?d8@`F-K+& zDE4IsVF?U;CGlHq0mkZ}8?dS=QU&!lmjD!~EN2e&iFsd&E%TF7qM^QI)nIc!-Elg)0_+l{nvQmmZ* z;E{Qi1iSm;OEMt9J$2+ijA8ftfs=%^lEq>g@JmG3{%6vGd5J;AKM~tdr&Q!$?2^2Q{pQ+H@LB zOcutZP63XptkSsCSaoj~dZL~QBRu=;S=tf&-x$3X4hfWu@czCJi+yS(>3<-`8{VTs zbo~zRnq|8|UiqAG$lL@A+afI{T>uVR2*NWGXl`lji)F6R0eX4qAiL=MC~v);?Ciw! zt5mp7AIqkV&2paLXk(W=b)nhSQ6EhM8j#G>Ha|il0)+4hP z@|d7{0V9ylDtbWr8Tf4J&!rU-b^TmSqsc3xUWB zn8@_dW--(;Arglk3@geE3ka9tdJipFo9Qpidv&hI<|%Q799B@Eu9e8iOAN|Hq9GN$ zf8|39mbA*&{h~pFLaww!ZmG6zxoN=2x+#ou*UX3A1rhkNpFfL(-&wXjrpcyLOkIQG z_D-k6`Sc0Q6H$n1Um9hrUs5Y`*;3Ch=ZsUYm5}sN?hhkhvbC4-1!kz$+mUt5(y+Zk zfj05NL{W&a+=HT_ENPD_J!+j-nfEa333uVMf(pJ|=1X~lTN#BF^X8vd1p)H=NU2)2 zAg5H#OO@V8OMgG3LbtUc;21=4+er^n#F3D|=W-m9jmd!{)(L>v`I+K*tLkVz5DkAX zpc60I81lV3db|gK+qyCHk7HY)!H{e%>OI5GPaQDW&`B=fTXsQBA)z1u>t}jVkGEJ1 z^=8MOf?6+s?0Q;f^L9#-nPl1$h#O?kA-=WOWUa6_X3n+TEu25Y0W$)%IcTYFc(~bi zQw3&sMebM=<>6!xF zFse0Daj%=KRO2ycv^{U)!MxW0**K;Tcy_dNi{xwdM0X%oN;sQh+G#c^=Cq2mh9&iH z- zbJPXqd1qZOre^BN6blH^b02OK<=_0Ic;ldNI2fxe>~ld-v_nF+ie@?xJ$Ju!6USz$ zmYU3EPyR75r$?^UQy80YMI@V8$KNaa+4IZ7CINI4ix0`%3$jmqj>VN#W)!5b&u@%h zF3!ELoD0;Y6WMioqX^}9Rl@$<-#Kj}1Dy#wbl{vW&?4EOiX$BSsg5vx?r#XOS5%hv zXqar%%djgav8UV1FvoVgbzi~I88BB;r$&^@`7oDnFVdsUpuQ8?KMOVZ_m-9I8C+lRYQLlSeaIQ&8luO^uOS6Z%k+iE|^1pq_{Wntq0S!(@ z8Tzfts3d{@@9y^I!e%UCKuYADqIySB$W@ZGSSbVeP z#k+#4#Ne1;kO~Yk(A{as&;bcZPQ`Xx!|Fp|B-U2ahljVUkG;_XzZD#%Jh4sm^T64k z;CW-x`*a``L){5xN-CnrDq)VB#cyAoY+Ci=4Zqf>XDv2dSc-NA8|wsR?3ec<*o7|r zcs3ngX8OlKM|bgyH(aE%G3xkj%ayCQ4mOlp>9gX9rgy9xRU9s-NSA%2QTgz{Z}(NL z#?=M04@HJT+0s5PR~kFF6#%1|o`eKXj3`+ie)^kp0DGW|aOgOQfBCI`2t3>NGRC5L zNw@#A`MeJ0^;_TO^NGQX{R~n0l3ga0LO#&$*$wKByY{dXS&U(o#gnXE7?R>kTeYG= za7Na_$GKV|pQI*FsFRD_jED1S@sIoxM+V{#Lt1W!$WuQlQr){i5Hf2*OT^}=!f4n? zSpIs6QuP#B_%&G(I#K&~Hu~tl=q>>`5iBUB#R)qo8xz7lLSNeLi)!^+Eq6gBHCFsc z(X^S3?8^yMpW(!BF@`uOH#7>A-Bg?OfviMh;mcsOFNgpft)j_pJ`(;Hy>7{}6yCf1 zuTCg6cAEpkX;^fAiO<+nIX;#e%@2y--qFjGM>-xQtq)r6jQLpHUDBb$WX34v;%n1@ zs!QYAC8x&kiJDS9pv11+7lu&zQ{AqzVq`{e+;L%?r338WQ-%s`uVO9lJ0X8>!xXq^2m=zWAnmMFq_)h1yNGU!0O4~JtW%9i+ zJi>tN7qmdrmPAr%Mpz@5`=fnHHVl>~ZBRAzb9zP0P$@yvG4%^*-r1gJ+Mg`4-s^ky zgk*yf7u2}y7Tb4i%U*xM2iOFk0mmstd-*WIL756u1GzVx@nhm&qrDzno zfVT}TVdrwe$7jy+YTEMueyJlH#iR5PTL(Cfav*l;~~ zT?k5OCRh;Sap&o9nVt<8vT#oUT6{iRC_YlN!K~ta0E^puqEbl)=DB8}tbcC}^I*ok zrS4g8$KI;;xN8h_VK>JfjeI5LW@#8MifU_%rP>>*DWdcSp=Y{esS9|8MF}g=poNJJ z-q5O)MYs1p7Xs1K*&xCe!ZPln9UZwh!mi^71CNupdz*KAhU!}DXuHRJTV~ng8u=gb z0@)<%z&hU%U|~^Hh$!|G35!YY*CK#ln%Hf>7`jkBS_}>jK$d7e2BC$TSE|{P zii+FdjgF92Q^I>K0*gY>1M>HZ!csiW2D1pn|RX6R$Ph{NAz zD~&m}FF&N?vaDpX5`TUT_p|Sd-=tI#ivuu$N;~cSkPuFp6ai^mRd5U((v%Jte2L?= zU>Uvxb$3=)gdm%)`H#F|pj;LwwAyWk{;E%7)}w@K=+mgTE5s{St^BG!6QQ_!9twg8 z0oCSl9ZQ5EzD$*}Fr0H-tZW-p`8L0_He}If1RzIRpAJw6rB!2%x+9CvpD2HQ^2BS< zG1x|(JFo1yp-L_4b$_I@XzmiW4x4_*{Sp(^zwQ1;YPR`miwW~)HI`e)ji}_i4B=Hw>!1k^ux9HW-y>M&vN!=!-kHA z`$CP|IqPsMly$*q_fz&L*D5nb)K*AuoaPf*o@vyc z$C@C=ffP*vKoJ6`mTc^@``Iwkk54!Zfm?CI$2zoDPsf)xR1jhz{qLfh@;h>-h@0(R&3=G1n3eIs+{O`4ZV9 zP!svT?w2&m5YTqEE`!=zf0d*b0gUfq>ftlGIP~CEo!Y-X{IIN2OCRz(*7%!532T+VJobl=rW9YEsCB~V zYt5dO@2wC8;~=EZ>|R*@C|Rh}gUi(YrMtuDD-`azXe6qPu&JVLjPg0}`Rv#`z1zsX!oU6n==^deQ`d$naSgy$+yjBvy4Ld~%D==@+5Wj4(n( z&FEvvllrt;rZw@eX7|thA)X9xNvAfM`mHjqJW95l?^+LQ7N+QOpPS|dKP#5YBrj`n zIm>qfr{E%hGfBz#-$-p{1}PCihc%!Pc&#)#ks%J&2n6XCtf7$rF#*=k<{hjGEf#}S z*00Z36Q?hCJDxUg)vclYSaojpG(m8@T*7vAbi*d;ZD+e`2nJOwlI!pEF2Sg&G(pnK zt`{Hh-8DOo`$v-dLj#doMvhwUUc(YQs=@v-Xq{sCiF;iQ9}{n-#I_unSpM8xz;3hz zUfc*Q+UvSa==40m8jD`!Djro+3>48{)Q zKKdiSaQ+SelW<$!KPn&^GF&u`atg=;4cq1M-`l)1d7-wP^EX^QvAv-bAlX6zlU8^`o0F#+nX(l|%qW%oXhA$5H4WYV;hqUVL{Lms*V-_k5 zS+0heA`$U&DxT(UMUkI}OLtTtD%iYor?u$K&vuPCAzW*fJPTg3EaPmKCSV6hils6< z0#_H4HNccF7~)le$DS${49qsi3W5G{b8Vu26PJN#V2P%io#ZHUK_M8iD;pdO3mquY zP1-LHd4hp6P=}BSbe_IgdVA6(zmDLex;d~(AeR`(Ec54VZ78$V@a6Gnf8(1zztC!~w0phz0n3YwrDwD4k?HL%aYrnQfWWc-2 zj3maHq)|p6t^x11wV*sNP_JQW^t^N@vTUEknnx?Qui8zYB#!Vaqs$P`s)gZ;sQsxh zq@cuAl0ip~(KdfN5h`2fyV?e` zjw#o%@=e+5X$PJ&m{99KJB7AOVk8sr-DBOI4*DSlH5+O3nxVa&Xm&Hc1eTc{Row`; zCr&t4`CH_5-_pqf?uwiv39jdIsq}vnnu!h(Qc>_Uq3LH;rS(=#1F1AxoZaD4ncj^j~5zl}oj-Jexl&=Z$?USWNYL z>@TKYR@k)>Sv|(^WVjfsvu$?BGkKVi?J6t^vj8}{SM*^44Rpu5}CBm7UG&mpYqfM+cW`HAdI;aI} z;9a``tRKu;14le>vR?SD|A7Z#Wrf5=ZW~uX_kicB);MG5`7dd4bJ9-IG=CTVNDEupRKU+Gq7HD;gKh(vJu8MJ>huyr6_qzVXAbM za%BbXA8;lE?Ol2(rYF1{S9q4D7~!G|FEURs{?_DC(E3t;>mH%%)}$g&8VVZ1Jenf- z3#gGoNLH8YQ{Xkgpfk}p53HM^d=i-x}rI*SVo15@Ie-@ea*`pRK+R2RAGqY zr%i+hPz=>HA9M2S854l!bAh|d-mqjV_H*_O$Fv<|G#(S2$mC-m0&C%ge~a_P{@Y6x zL3Z1WQfAP=BUNWJT!%Wf@A9e@gh=KV>%)+TV`*<-J6&hPypO3`3b#OuCY7?qi!CQT zE@|I#70(7%j=x_nZ&C|n1!1wL!h%mN)W{KipR+i z6q&FPtmAVdf3{i2ookZt`(0r+MGbAT^a+H)Ba&`+l)iu=gP?Owak&Y#9K`zZ5Kp6J z(BZuFqxiM@a^I_|YzR!=_fyCdOzA^_@h9O-`$a7WG-ikkIy&4O@OBRq|DAIh|9 z_VkkWXRHCyh(oWTgGC*UaiLaaLQ2_l{w_`7N#hs9k8r2iH&l6yrIObu#g46uAF&+I za7h~Aj)~-^O@=jPRfEz0jvZ}@fXVpLdT@6nm0$f@`KxII29yAi+NIc5E(`~Hn#i

MR4eBEGyd1rVfr1t1)(2tUQ3xHDp9rWzFFL`|wW+xNYH-N-?1mS6-C=e2ql;Ey z*sC)f2)Hc|m-w(=uO6ZNnL%z7=j9%dfk(d>H7o<;hVX1*X-i4PYGyHj*3`+bzlh_Y zd}s1e;-`Il?RH=*b{?u;OKzJ`B)GNZK2he*cxUI+J=gB1eH~r>O%5C~G@bY5i|act zW55f_=R$fBMmXM^UaCz6=^(e{+vG9nc9_)IZnzfe37-9_rtP#@Vh5i)d(BtX(|SGL z2hM?MK>a$F)gVXLclw39Jg)ql6^zDX(9qySZXf&Zzn7tC zG_v;_H!t!8NA3NE%`vxR+^v0Xz1qBW57L8PV+hew;?yAz|M9^y?U zhLeKl*Fuz<4Z)H@G+#j{GprXucW5&eZL#kwhiZ$t?;Z~qYiXTKgtxB&n5gY}f5C;F z=kqappbTKgP?FpBn7Ly(gzr=s<@w+}Cs`@u9kRKGoKFG?PG(uITSOx>9+euwg$Yx} zNwLS1Uc#b}*~~JDCspoQh|i&v7SF!oNtBrkXU{8mFOMo5E<)&*0c8|Zw+MLvKiBHg) z{Yx@^2SnCuj9cYjO$#GIoaW^M@Y%1E=f5STdICqYj{_6-QeXesBn|IA_-ZM)z}`*c zBPgJvE<%pi9Ca`3F9U5lg7a8C4tzApM`1!4vOfiO{Q>e^OfGN&Syr}7aM<#%8KY~t z;aEiAox#^U7Px_rNT;R?v~y@czfosUQ_q{;&RfW>6LyxzyS~gbl{Gy@i^(ata@hK+ z?awXsWlCy$Jhh6^z76DE9EE_^(95g{0|_pxb|9wZYMaw5G@n|Jw)!|eJmihK_95+4 zJY+1JF=jLa{^m+V-u)YoJ4aJ^bXTVmG+pVx@45oD4QtzNWptQZDHW)j8U`WO3NQ=- z8Q2=jYtS|FlC-w?aw)U-t+IOx&R|3yK11xP7H$daJh!Fj`Na60M+3#>h<7?mUo1|8 z%f^om>|piMQFAXwBD?DNYqY&4aF;9m4fZlMz%El%n-r;&{-el9JHXF`O|!PJg=c7p zZ@Dzoe}=dT6*3?>dt@{moJiGCTAP!w8iIl&y1Sm^`Q`}@>g^LsT1X5%{r;KSFu2Ik zYqyh?5c7O5+x|uy8?4%|45_mTsV3@Kb^{&p2Iy%+@;D&0U>rj<)bIjB&%AWoS?Zj{ zu4J8wT{^gmDIk#B_rIgK9Tm=?`D_KfxjI#|^4(*(HS3`o1MLbA97r(npnL(SXl<=J z8?TC%szt*%vO0lDMcb?scSLocwV(@~Ae{WH2zIO=i_D z7lz*i%C{x8{}r^P9`y8e`$gxe^5?(`N#Y4n?&!|D_eXqD^j81YCm>-3o>sa#<`376 zlIqlCw?UF74wM2`qfSz|4Fqahd#u_2=$1K{b|hOFaVzB{XpI65-dlcff`t#Zi;b zW^4cv0?Sk|NCwm-h~ChQ)=r9y&P^AoXBj|!(?Quvl0;6gJ;$xqneN|KU*AtDiyT0y zv3QK30-e-E8aRSwg_-_UgTzt33&U-~`zl@mlWSvE=ChsrRVzfn?Pg;g98B0V{GB{X zGR!MTT$NPB@W-iw_sxXG-6VG;9lLw;jtMT$uSAOt7;#H5!xrPR(it$g$+PF3$V+(x zf%&5m@h47$lDFZ=Z}_2dLiNg^_SeNKS+F1Tck_*9{0^kP5OfkC#BS|Q2Ke?P3|w6< zm*}TWjPPhiFNtpBwz%yvBj|a$M_>tfoKX^MzY}_On9IkqRyllodpIC^?Tu9Vk>5Gv z^RIr>!TuABxOj5DXd!$fGb2HWo-n-xzKtjcPD1M{$)H?=z zFrj3Ysyr-a-8w{LG4ZWA$3eUQuoSQDT!YQccs3r?0~!OLlwYZLjExDZiYyF#6sYmW z3P8&Kf;BOm6<|p4@NEH7f^0M0Cj!9$+yB>7j@1W3hrdk$)9U@JW(ynyQ}ci~7Mtzs zH8mdGHAJ4>(ACXzPQBP!bNZ_J>1MZAzc({$NTa)+i4mB5&Z#0%0?s5^YF4F~|7_1k z5#nF~;JhMv4~txa6Wuh~`C;GFt4zNsBRjRlX%$AtCB^pWk_mA?-5Uz5&JFsAe=J(MGIZ>rS${FY0PVxcDm1IV?0bRla@a){X*a zfrCbh9R!)BPDj-Dd=CZ@UVsfPSdKvZ#eLN?3BSDyI98>Wc{;)bbNWIdwWjVUlW?=j zZ9fge5zKx*AXwl1=iUyKLICiVE-LSB6E*bAUm)sL1(LFYcJ#j8ZoBLipT@$?CioTM zCrMcs#s=`{C{EElGV|n9R*5u=GD%yI@3q#)u#qo3aYyr-}yrwWm17W=D*NV7KPD`64P6IF4XmnL$Cw$<) zp)~XXV!@`IzA((o+gRBzoHz(FN1e`W8MFXw zWnp<0Vgz9H`LVX1N#anjW$3pxED~O|k{RKcrQKooF5Dd-Z|=CY*V51pH)$WdEDY2v zO40wlES0j^FB3#RI+TBH7H~4AuD6}WN>M(P8~!NJ#50d)a*eM<1QDE^E!7!sa-R!6 z3m+Q|ct_VA%Faab-pFN$C)@`o9{j8l$~gv~-BUcgX3|iJYBa32RmWoMaZmT-HNyBb~o>B!a}?N3qN%>=R1J#3IP|O@;PC2A`=C& zp63Pd3cmq87kJCk4yL2A*gV@2a#k!FCdP&968CcWSaM1Ru^{z=hH)4+4uiZsnmqZ( z{rK*;E^iu-#-iPKfQI@0*>jD#Ym{~)q!Q5cFMWUJ5{A#a^SEV!^^U`}PKq8^);fW& z!Yn|-VeU7+`ez)?Y=TTi)p{%o%f7H^%=(P1adSoIXa6;Uhi(0YkE(4;)v&{n?t%8l zIRaDx@)7`M6aOq3X>@@YHhM@d{%%Jxfp*VBLO^dhp{?*l7H9$)z0yt@qs?Vu53TkO z;XrF!4d0ERQ7%u}qyMI;nRHa9}nyMYhvCP&CbHXOHb=rSZE zyO1}2_vF9aMV`&eSFk*kABiryO)r|FT+N5PpGpvWNHmm+p7GBqb-vwih;mGtyj1dE z2mdicf^J;pZsLgE^Y+XjYM!9oEnLyco0Pi_8j_>R?xpx)qf_qR5xT%Ru^2&MY$)$~o|(`70W{5v={BWSo#Tx%F$5XU=qvdrj) zt{vrR6jk3DZyHUtczCGX!M%#zsB_=H34V|85W}EIG6Yug?2biyiL_5REk=0e$YdqIjjUJcZ#Tudq|s?O zrZ|2)#Mi3>jI#Ec4ikmUC3<{yMlD1lT5&#pmFF{Nh_h*&NhC`~q|c3@PZ2&;R=SW| z45keLv}{E&wZKVTrMM>P4o=k5)xh#1bS99B-jQ!tZ*doSpsMG{MpZCcv1d%=VA^PBJc?+A3aq@;#wU}4)HB3V1Cnr!h1aE2*az5;3$| zWEOW1fvGCmgYC_p5`_IfUQxuhL=eP3;n7l7t0%#&kWa9G$i9&Ftoe~f?)li zfPc5(dIEL^mBXKwO|$)$s?3CpsLwpcW7h?z4Lm4`-Dlw8g2Cp1`fCV5@p3=sR%!^Z0h zj$t16%|TKrLnhRkHu+jYMNbm>fU4IY5X`Dho?27=cff$$TjencuU+ljjJNX-l58RE z1_s5n9yB6f{fWAgMU6^h+UJGxBHuj^TRn7uS<&G9umW$}9+b>}tvr4CDg^QD=3Eca z4fU=bwlcNFc&~jqz|YgBEvJ`m=6XyUoFCNvS^|kVKH`iz81*0s4t-m!Ww% z)QqS#ski?IUOHakfBq2n_Zf_dY8JWDtA3L*_8=>yT!uRoG2elpyWK^hTW>eOwq!$A z9f3{#{ZDujhL^YL#qY4+LC91SHE=h?10N1)SND*YUsFiaGd$lwt{$3LEuv&IlCsPX z{d?h058n|aeRT+(d}{2cQE%tfRd!|=Bi%K}#5N@m9+68wdU=1jJW}@TEe8|IPCwzJ z8-f2v&!I@E+R77o?CVo8R@Tbn`DZ&V&eY~6_BUW(kJ@1sO`mS~Vt>uz7h*#{CBGb- zPIWCAKb8@&D^ZTVmmu7Hhx??b8t5p7=9D5gJPqx(U&oZ`497Ttpu-qSv`syA<#oDg zp8moTw<`z+H(0u%!>&)*V$ew;??uCYOHl!OO72h7Qv3P%k4ufk88?m2U#_5U;^beN zN*CaURpxpgYmeKdWyAG0U{1MNkhwwZK_%gbHOH%?Rd!OShj$>|(}&iZ-901)wdC$O zvfH2HJ~XXmPCaJU0bfTD(p+REfYIqX$(hglomP6o~(C#(qvJa)x#V^DX->retf5Mas0I3u_ftenM@*T*Ug57lcP`LOfJl7CzO_5BOz3fb zy@|ze_uQ*6p;(g~%|=F{J1$*7*m?5yWaD_jarUjmPmd810U7lih>z)zdHqA7VJn@G z&jm4G*IB7DH+qjNEd$JKr3odukb9`*Oz0QdyMwMy86;JrsWLqShVkDvOE1Nna50G7 zgUcIntb^rFzj0()Em5c6;~~wpSPW&PpdV3x;e=M+@Y}eTLYIB9FXu*Zdjewj3ZtJ!g4p#`fDDyRC`v^WzTWtJH} zOh?S8L}0hBH|}^{zIuV3Wf@(8GjR(7Q#v4B>7Jo#J{U3Jzj^C|TJdbRd=h-5a-Rhy)^@-aWC9C#< z(yFHc@q2OjdS|nBi8|2-ac34XvRd=0V$2Cy{sWo!PVK&lvDf|ZvO+G%hzzXOZfGUq znce~RSirB=!6h3G0@rA|FVpK?hEmxw`JEc9}iJYvOOb!k$m zuJeIb7p`4n-GUC(Uo=yHy)PIvp=ps{J(g{rfo^)^GF{RgKt}=|I?R$alcAW@I$UwN z7&m2(N|PZ0vmRkh3|qO;&{RAq6!^XHCzGgk%_97KrAa$>EYpQ;LR<$bjR* z?h((Z$FPwU@Sq_Y&#ba1U1E$u@ZESMv26sj(yWXO%eDPxR;A)a z8qD%NVMa1Jsx%213%mGfhkJ-1HW~{O&sh}}6;39)xY+MdTZgDRX$X+R+-Ns9|2p2!-DG1@%jZ}(4;uLv@5-t z4(n@Z*<0av-azsEDoRR5{GXo~hYoI(0(1zXdQfCaW`=k9TSd!^cHJ8bnrVAF5)KCTv|4sppMX*S~J291#(tfJ6(n{nmu zyT%Ijlle+9ff};>6B)u@`DqDD)1jzlGk}eB#;WK12@i=7{MXX*Lh7IW(f(1Y7Wo^P zSnWWUymAJvc_#O(s?o$L)6p{|#E%$2dfm#{M+cwGI;U0bnQVz7N!$ixuXw>aHU8J3 z@LB}VNkktmR;1ve+;E5#$@Pv|*;uq{L~zUfDeK9jOsanOR8aa#f@Ih>M0dp1YOHdL z5+sOryI3b+RLXFo#0t9q?r&o;Keu^?rAs!-u8U}$K3bw(Jjvu64=xj0gwxn88u>&2 z_-c#b8dtQ;j-C2VRt&RVfzV+D9Tx1>tUj1 zoKXtp-;`eq(iFO88%URX%CJF-EK*L#$-CV~qimL;y1f=_~y zU~Ju(Gw=hRtiPpa6{k8hZ`6{P4Zzn@iB^gU>I`bwS$wa4)CUq5 z){YBFe=)`gJ_TGRrv6nBG0=gaNLu0S!D>W0ep5^Cu)$w$wk>~kj+!@e51Rf=d2BRr z`%S!|v6<=IfWYt_u}xuPKTuosd{yrVq$NRP`w(2J(q3j&y;$(tN(N8ncdwxDV}omO z^e2kQ_uchqJ$i<3t}?O-vKwzjObB5$*i|XB9K_9QG06 zQ2te~(}=MD4>l}b?9Y<_fe3tOg1bQ?jp+c0*`^aU0tZh}4$zOSy)MJ&fPlJ6l{J2G z4)0vxyWzAV`gVh#evtbzZYwg|$oU5p%+D@r1GU20!4?d)emP1^A(YLNKyWMwo`+*ol z!o*J-+PoS*SnU(UVpW{V-q1j+MffKMgCfo5IucZZ$G;Wqx%zW3*MdR9p>bvYQ-Sem zZ5`X)#Gc5bn9h-n1@cDqmF^Sw*)Zg)vQ5A056AK%Bw*H3Z(dPX5Fg9oVx{pU4!MNj zwS$xw|9&5D$PrPh!Ico3>I$wDA%%O8=>;$*hx-blep|Pgdsg5oxQ)EuZbZs$YaW;g zP{b9X`GQbCRo5fpBl5F8MrxfU5U^uxc=Mp(`|?eYZr$#QKtuAY=im2#Sbf;w9x6Cw$_v#StG&YAaRos4kKhYe!&S$jGCu=0) z)a$TrkR~9a0Zb44J=i;F9}MCbptW$K{Gff40(b1Z8zIY${v(#YiU}d zUM};15MoL_z7CM{JCVVSaJF3|#4#Z@aJ zOQf$Ojc>V5P4&^w^d(4mO#no;rq)BU-1JZW3&~M+_qZID)xlkcbtv2#^Zw?318?b{ub3X1od+_k$>I#E1}LAzUxbAa+3jXCW5ji{LuW?(?|HiE zo~Zf>U4oxv=4O%TMT+61fct?nD38|pRG4KDo&2yPB%&sQ${V6(3Ha|^6h|fGuyiw# z!E+;hu>Cg^BHs7*Jq*bQWJ7?Y6a+ zthBV^D+Kc|s&`Qty#k(2ZP{8AH=dn@3A`O?wY%_CS8-tbjH|6LME2ZstknRL2k_$( zSALzwN0(Qq+Vw%X%g5`z6o(R{NQD|EI`kn)bv+`NU#mH!gA;@WkoC=+1D8gN{j4PA&mkaj zSXP?mIe{TaD&cz0q2iEVD+jK!jUV{SV*@B**btd~(p>BttcRlZC zs-ASVg^>>=8lR@Pngjf%2{N|7$x&3DNtf`Au!g4*E4K7>moonpc z)EiCAB9`Pj*pL&7!NshbD>ZdTT<**`c zdso9r9_VzKL@ca38VadHDRq-X%?=B3QVa)bp>xT>8kaSYw&%%q55N5;AZpE%&r|(u zT1FLJ*EC!lc(KTY)938vm$Ii{o0$sd!*|Ltu7*W1)644xKlRgn ziskBgU>)qO)YUUY!j@0j>OVU?mI~FR?3t;MZ1k32W`D#l$^O@sH#oFxlg;VntdgZ& z`a8i@9~kFJJIWIoM_L!&az1HPlBfVj^k(PgRIxsj zEn!XG{Le2&=o0ld3n7u|bqGofjE=u<<23@}sa)}s9#=g7mugM(g)R`2(aQE|>i}rN zeT2y6p;HLTtbwS;h+mBBF)6Z@t~g~ROmQU{&$H`fPRl@{J765;dvmyuK6p=g&a=^^ z)E?z_xODIL@!}>kL{sc0{2XZS)gb4pM~Ls$+}>G67_r<_M}88BhCEZb5Ez!MyodAS zWg5koGgwPerpNF&=^dmIRZy!iV5QyvK(8pJEdS<$lBv`Q`1?AVR`}4dRG~imsuIM)^g($>WNseXWNy5>!|^Vcq|O#oQR=P zb#M8AxCCl38GAhEK<#GN;>7PplmXWkN*a8U*a7wt8Pv?3K_GIEqEHD7f5YJmf2InA z8vkvrkB_#&+wR68;~W?XHYm>vfv{A3lW$8<&2LU7UsM`F!(vsn`6tR|-8$P8%L|zh zE(-XG2KAiL`Qy5Yu5>G4$R2VBz3H^Av{;IjX@c zS`8z43x!q?K=GyKnCM~ZGRgE&FcmovfPP`cPG9B z>x2Gn52yJC9T;Iuz>pvUy{f89JuIW|^6wbs$O(f8uJK0qi2AL{8Mv3$f~;7m0-zu>_}x#So&&T-v&>-%%wfB;?E48}KmH52qQ zOI-81ZnqmNwLsai3&qdjpGk*;N=atqZjcZ$1=h18YwFIY{6f%lm+P)7ZY&vbGuRkb zQcyeHT{bfHVkp!}vKFb0;hX zjKmM~Es>tnzR&kW4~7rVcSGUD?OTo-!1;<+Y$`yegRaQ+&C7hwgM2KVZsN~iVRL$A`9Ii$ud6s%8h z{R^e(+5@sZVX#Hv5Rw6{rh#=4>IYXqQ%X@8Yc4~a{<=o3Koe$>loto(p?39gB1P0B z45h(Sp1~bNHV1dajfDr|ZV|pe>2(Xn^kH2MRv4ZMybg-I5ZF3kUj}Rin#Fwe5-kIy zpidYx6sAc`%cyqSRaFi6N0+S*i$wHq(9bUI3q&dgq(hwA_XOtcM_5O(p4Rd;ph_6! zD^AbiDZe6nHH(BbxMKVB)sejV^xx6pqp?YY9rw4}N(<}p{16lQ90&zDS+$(>{M?Uy zLB@}^sHYuO_yM^mD>FsEYP-J-;^qXjnhaImMytO@*%=KBE(|_&4XFn%? zZjyd1yq6G4*iuuU9wmmS?#Eo7Q-Let)P<5+cECH5xgRQ#@MDxb`eO3 zA+-CL<)^m`uO#2}r?iD(bM8Eah%pI){7Sh=`kGJYFn?+P1{ea3X%BZI*tys<%8#6Z zJoAl=%jyFF@)nO-Q@%SSy#eH2VyH}YWMYjyts8g#rW*HHv6md(ppk}m|5{u}-yz-O?k&9WeW^zWXq zh$#QzX6)2WX%Sdcj^S&0Z|vut|0Zs#MPQK;zpO5fq`zP^dQ!?U`0S%r-Oqa7w=Ki( zmOU$`@osdHu!o|=)LM{xJsF8uAr}0j<3>14Qme}#GSbI3)wW?9X04C2?q62WKpAxm zvcDX@9_DFc0rlBE$vd^s-DoVww$_j!sz&{$lZCl&nPwfa1WrE$zBY^{Qbc?Mb%HWi zY&Z9pE;|fP@69-ka?)TKu=H^&wTnOuldZ(nMK33xR<)DwCEE7r5&%jeo`z}8BMR{B zfCj4WWljXIw+8ubdqD_hCzJ^Di2m%~&@9YzMnrF=RTpNh+mt^c1*?5WRbpk00MUp2 zbu5_sAQR)E48l87_RWApg;2KursOU6%|t5mh2IH*IzV>nc0{;fy~2c4nMVCOa!bW; z4aA2P-JL8p*I;rT&<$+Et>pR9Mi|@Xx|8caJ$jS&>HE;F)kODlx;l#hqhHT!9lY4? zvGK1>|C0UQ$RP-N_{Z2L_Y^fD&wVVEG>LrEpZv;-tCKN=3Qo<+Rtb5Qvr}MN5C0vEQB#?`5 z!6~E>yF;TfI3aD z%HO2+`6$k-H~~Q}bFHB_2RQf$?bSuSMHuc6F;dfk9P@@hv0b*GIWGMqZ;uWS z1l@c9K%|p8yWa`tpLi^?dSp}XNrS#J>!{;b*7Z%YINK*tapP{vjZ=8d6%gSN@n!VW z7w?gKgOWp3kt4c6E(oXX13MG~;SO0Qg5XJ#ZZSMZ`2h}ScTn#Y8KSlT*aZxh3~7!B zTnuBIJm8-Ah94&J@aY_|$uNeRK-5<}U`1nB2WFHxM$l8GJV%%jUc%VQ8-HfeM(cuy zrXL^IHJ-=%p$)9G&aedCe_v>dk~hJrGbS{w-XNFH8C`g*gFan*6gKav2aI0-x+=CBSzBU^R8fOwi=7MGp@&8~@SM zt-nE;j4^IH2}g^_CAJo5C^LV-GD(Q~?7 zBSf)!zqq;Br{#?e z>YoOh<+uv(>9L1-{Xt+Ldhqj5KxvDz^xpV^>wooAl?U4!dxcJl>7n}#d1D68_?P?9 z@88nVr!C}qM~(CaS9|Wksw29M`pV6zdG+EqR(( zZ^)pvhR>*MnINL2s#Fm0X9>eL@(d*#zva_-G_A28$_*cr#P8+9!S}>xYuTh9Oa{|d z^@FTh6~}mB-~eRb;&Ic;6y(unI~^}6N0}YH)nI&#N4*@X-Zg!s=8t+Igq@WW#_vXB zg=0b*7U&^gP<(o6AGF4pOi`I7>~~XJ;w90DkyEY%t7h3I-lcru`MQue))ZUvISP*i zd?ZXyd=UJtZZ)?!Evq5&oYW=Qfg>#TvxP&K8yx)%PMkcjHXC7_~4t zn31DjUJ~bsFycu(bTO28ooX;P9u*K_4fO#6RLl@XIDAQ3V?`0wY1NOSy`g!SIHEeyUS|`Vk{2AVsQbDpdRO zzC)W5tK2D&UaAm0XF`qfB%D*?oOHckK1Ag=9-xqt=WY&>hf81XxE~x@R(c+zK?Mf# zGhi@PK-Vc@UR?7kWALS0cl5cu0!eT*;93<*VTnyL3BloVpko51VZ?aW7V8Qsbvs4j zZS7jxh@5i)&Lh8%{!&M1#RV}$l6)?i-nwE0?rWz~j$MCmduN;$6Ia<1T{^3svgW`d zfaD_cZs@Z(9=h!l7!!}bGlBzs6sIOIkllk!b0iX`J9fOelu|~mG3dL)@B98e40MoL zpQJVqe{M|_0kB!vfx=ETioO&UzJd?>#Z#A%0Ij0e%8=2#^V*}!&^l7AD|3GDGu!gy z>COw!(eSgD7+$#|Svt}?8!g#WMsc*I_bnii9c15Bk;^5?WIx$In)6^yP}cvX!uwoT zf9H7$-E7v|P#xxOBcF~F30nV)-|WRCcXg|dib#ynrZ{6#9q_ay-JcJ*XwoVbj51#t zwZHYPa<6L<3#KD34L^qwR2sF}?ND@MJ4AFDry1DMexZj!U;&hbBH}7S$JuH&ZLYGS ze!(g^jTAfCr(H3I-*kB_p@(P9q-@GyD82Y@4gF6q4r&*<+<3FH8?njSi7oaK*WYMx z&dE@PNRm}`(KzY0pQYkiF@L~4Lav%Lr5`OaV^^hgB|aX$HrJ>Z4TLRmzt*H6ww&_< zprx8r5PitZD*NS>m)M#E#lQEPDyG8b$$oeajTmw7LGH)2vOz+B&rt1g*gz7R!e!hQ zBhXL7cDT`Tc6aU4n7?l4rKe9nErw^Pez&rMIC2p$bU@$-E5yGOuN6SYzYv_j+sBq5 z^-!$2J|>ffU>8FikTk9)@`DED8X(yyoEOZfvLz)OA0fwO5pB$9KcfO2WFko+s57fu zNXeO-Fk&z6RMbaT)+j$rS$Ydx5k4AcSTdBnvg0qr3y}6b3p)Op9fqF}4Ro!wD_+AZ z5I|TX$V|=c5h#9_yL8s2a_G70Lw%#h-H98{BhRF^jFEe*B^CwQM4`Mw&TBqi3I@}q zvxC-zH8E$^=23PxXSJ4csv)Q@U*GhN-8trd*zGO~OD*1(Ku8NAg`2)yUS9tZQKeG_ zV|xMii#L!%PmaMf+gwN$Ua94Xzm4I)Oh64mecaM>W6p=c7b8&tS3OtgGZD$GJ6W&I_N|CU4fi5&ii+hoEx}#gl znP*T>oOd%`H@T|8X)SHix|F2~%5AL$@6b1dDxM*-W0{U#<93dcUzP?qw0-g~D zhUD2RX$cLpc9_)ktCp|Ej~levt|dt@yjELnmrE_dNH`&Pm&<=p zcVU!iXv%S(-yH_r5hN9qb@9;M0(`@2OtIn}ju`$lMyoOe(*!?OX&+Fhe_)F_z7FQ! zbB}GxZbjMThj@Gw1}GhWoZ9x8NE!K?f%gqySJYkiu-X6BQu{P#bfRd=->p<|Vh>68 zqdcuTKM(xs3_y<(t{DVYys$Gp&%RCIciqlD`V}H|`iMNUE!7^2K~u7~d;P}A$BJ!F z&%mUt{!O2a+e|#BJ%AgG%cQ;G5iRht*b7r-68LzruXzo2c}@yJHpNXKdVt=M(Vs1+ zL6=P}U4W9a-fa>hn%)uevhsuBNuB59!1giN#jd1=4d~zg2{MMCisOo<68VUJ`01zp zU+Qz}M|IgYQzcj<*J=Hv>I)buLnfW?V{<=t1Q{u8qu>y!%+>P;H7fQ%-a9lmx*r9H zRK%N3JKH-$?a-FBvDu)^TVKtcmy2jNuTHtpn>hKq&_iIBwQw%s(Znb730 z`>ujvHA?_@@ODpqB!WCxKBc(0^t-ezi>9V{j2sKY)o)53n>2Suo*Tfia1MYbNh4hZ znGX;V%bOR``jx$WBJ8CyAYee7BO4w8pX=##6d{e+t*TGoBl z!9Kv6Kg`TKpi6j~X{m)2_#Qb~q?54JYGm=jAlx?g-O_W^9=dzfpI84S@L=)bnlGAn zw4~`x(h<<}#7$-@HI_g-pte&MLD8J%!>)u24p}G)1V$=^RI(8itf4DBY0)xxnEP;T zLH49Ycck#oQ5WKONvdO@p+;fiu5Hq6VGnzaCWSrWwwXw#R_xY8YaXS>VxsS+y7LaI zrgPb3y@O$0inyqdiJAKN@YKzj`nfKORmpHhow(Yi^cRj-0jaYY*xQ%|dzVG^$;1 z2kfd84>w1DZS4W;LxtBzO>cpJ0%P1ma@?};%7`v?N>+JVU=^a!-b+~8YhJm>h%YIE z(r_HNJ(d^3W+c}fMrw5fP(V*x2(^7nj(qd7KF6L3y?I~&jemiD18fYrwA-D?rPBcN z-UmooIG9o@)0~pbxEg*q7cad@1SbUIa?6DyuXS`v_f8E#51%U1V;uyb+`Vf1CFqd9 zRI}Hs@>GSM#*-G0T5vH}zcqtY8hI@K!Qa9Lfd*XBrK1K$#4#LM z5IJhc{o2e0Q;G)gM%&RM9S#M+l4U6qLsr)GADFly3d1NK!Ho2QkOt=8=P4I8h)U!J zMKH;V#AUvI=o;zDxIHJB0m+pUrLS-PYON-q{iLzTmK20F=?mW2JmaOF35Dob20c{0 zO4^j1#~=%`5X20ijBj{#YNFW&TqFo#NC)8CT+zAKSNw6}M-Qx1BbJcGpxiO)=Ao*> z^0z+D8y_MId33%ryy^tw>8;N`L93N2RPD&~{K&TKt%qIO>VyY1sIVg~GBL8E!M$fx zUz!5$_KlN3U;A{7UY6fQ6#q8&hcB*YbWXtT6=}(ez4B4DC!EW6G2qGE&P)}iSY%_k z?X|MTP%y(#-)iDtakdCmO`28}sCW;V)35Vzoo~LK_g4X>qj?y$R~Ihg9e{0Uw{`Hl z{$3E^*ni>AA`XjcatowS#9!qPAMd*J;II!If!H!`H}XsJikuW{wCXeH^%>I-+Oja= z;W8Gdo`0~qKPR@ozL`Tu!vJsNi4P4mv&;B&2Z_4wZBH}-#X>AR@+r=HY41SQ+^^_l zGBu#Il?Ix)aYIV9!xYnMSO`0jeYKd_5uZz4ss)sAmW!k-Wl%UPsl})7MrMN&3M{7k zY0)F2#<7eWr`_D+HUUp(HJ;XjAH{pl?3y!`wb1Z{!2RPlN3bpU1vieCt22lQ%>&C9 zh8ENTaGiBqYk1A;54hweZgCIHGg2TmcHxx!`rlhtsJJu@P2DD)DF-YPZmframi=fp zE`Tvr$vwt&iQn?PYhbMqOu<4s+nY{$L~u(6p4^?Xb6NR3a-t{0dbEMcE(06YZQmW8 z7e@Hu*#=`Swc`GAn+nCk4fAdiKyv{aWfs>5z&8d6#eLuY9~i2kfzRkDbj@`H%qM{b zCN!+KJ;-kcOtxM2<$%y|JQ+vMoqSgR&%qgy0xn=W`WbRMx&*~6;*OU&}j)@|d?;S5bJ07o9g!{q0 z=T&k?@K#oB?9?Ufz)bn)`REfE;~|7aFu*fwa06l|Xq>`|bq zgUu#75vzISrevUu09M{TVrhf%B%*>ilR9K2)XzqF=ofklH9QM-Br;- z8-(^9NuHBbt1hoM7h}XU3Wl6lZEH+t?)#MOQ;o%f?|5h}7 zZ?-Rq5dzvP?#d&;<2-!HaxQ}p5DyHj$3y#SbCKe@hY7Cc7_kV0Pc26t2IZ6GW~U{y zqs}8K#g=Xqzp6N*6abXt_xXFpwtX*(0Q_xrZ0o$0{7ce(XMoSR#s7MY{|`Rn4HH7P z2s)c2>xKk<`wS0~E(_}!?uJQ{VvJ(upS}=BRy5o=3fqC|Ec8 zQ!*X7Q=?3Sn2^nd5YdTD@fj2eU!BFlFAy8C4z3_?^``_hrfQ*vh!>>wIrBW5%OQT` zx9UNCKmATUy;H$~HfYEuMmpGoB{Igrx-E2>3^D;i4q6oQQsmVxTNku?cqZ_dyCoGh zvT+%xHqx$Gd(Fq|FKYn&p5(LjK&t(^2W-I1S>mWBWiNnUu}$?<@WEE0o*pQ1;k z!_RbuhFr493EB7Z_?{2>u70K6qdjmbzR2=#hds%j|>GQW{k6h%H7V>5E%w5DmwF}f8ZL=VmI6-sN61ElNwAzW76Ix~aL2czL8gis@TeRQayC)IX-ZyrE z@0S*eH3z0|mTagGl^m<)(EwBTa&~O+zdKJ*p}4q>wn=5>CfbG~rEU$sc!vc}Cdnv# zXEDkS_)0s+b<&3mWESB$jbj-#dgbTz_G z6j7&#%kT#WbCkyhdHQB}22Q7{Qi1tQ$@`T|%Ot>j1$?Qf^ zZrh`?9PSX`imlXTQUQL0t!;=`Q)=S=Zu0bxkoWyBsr_h57n~vYqf*zi1|ExS(SR8o zb^d!+(`tF(rlb6*3trwb^vTP3r<@FDwx0zfFc&@K-QVx?mDXSVpi9jDgA%kY-CpXs z9cqdn?2WQgwH1Qn2Y4X9%MF7(WlYI0bH0Y0qrhDlfpZh}7O>yL&IO^4I0up|2{4;z zC?mo9$ZpgbT0ZKvDT$+Tw?E=vFnyGlU<~D^7m%S3JEBts(&RxeukyV=Kr_och_Yc8 z4w5b53OG_ubRX?8homT=dL@)UJfdC$?6$G~bIXdOe1(Mr@3$Euy6AInGSE73Xp=R84|vZIZ3?~5qa{5zD=?rE*(rbrfK0NzEirxtB6N-CGye5} zqZI;|+s6CG|Mmja*Dyr}4^Y4;nH;b6b4cL+3^l&5OTZRR#)Sz2Nrcn3pts?8apcox zk5M7o1iTLL*h8FR3laYNJ;hN%abZW1>=zmhAg)2DP`g>Ya%)aFOm(Vuknj9>I+1Xx zN03YwLtLSH`HAYgoG$zzVyZ>u_qXJaTl`eUk$ViXIGp#ZSq?>rN`2iiTS9Y{O_b=9 z|E|$#5WY#nbf!+0@R72(hn(*O3IGzh`8s-Q?}3KU97 zRRMZ(WZMfV270?kE{-0D8twn}`!m9U%h{TiS>M9Q)Hy3fgz4)e_lLW~1r=Ex&XmWF z1Ie(@8socQLYdZW3s*l^)bIjNI8c`I_`|f*Vd(^APz0i|VF>=W-GU#kGSUzre2jTy zpD3vI;Q6x(WDRXgM_tlr%3!C_P+Wp%I$quKu|zHr9nbb zkOpaxkPtx{q)R%an~@R`B}JsWyE}#urF+N$29#!iQDP{8v&Oym=X~}#=Q@AE_ZRi@ zdS~8w*IMsd&vQTbbKke5J4mpBe3NE-+C$f$OIV=QgHSS$Rg4N#p?`OC;XnPe*Ms<< zWP0xS&fj(G&n6z8bm3W(^Ii(N)`W+^e)6Qyo`w0r&h0nCv+_r~X?lxoIB`zA!cvZ7 z*)gqj2dBr&s+EB8@ZR-rZD71I8QlLw1`$zGpjIUkX-tjV1&Y$&0BZ#pDSV*2T5UZt zY}$$9z9U?jQRlR%xG+UBi?M5}ia!HpDy6_?c6+{QQA;EYvqxJn@;JZ^<0S+91*ndt z-_>CxdT;rt1(s?R8S2{XzbLqMvoHD3O3?eKv*W_)+f|#NVnRTFbhn0=MF(Cg-R=|r zP(7a->(~-^Ig2tRj{nU&K^%J3 zm50a%0?Fj+gNdTpW7O8|q+ENvr%RwVXAM|mdO%gby2d)-))dS7NXAIV`_T#fpvw`& zIh3FL9oC%yF2NgKe7r5SwD{k+LWS<(8x0n6Fz0?Lzgi^q;7P2-ZML^JY#$DWKj~{# z$Dw?ALitwc?)}1>7c=~KN%y8Kgmy$K6S#z;gS&>0480N4HT&Cs5-OpP5g+4qf*Ztb zgXe7$k8mcc5A==I8**}Fs2QT4iTasFB|u~@{c*9C$7j67j_B88f`i4!#MT6b+6^9@ z4TuNqwde<}u10HUx^-RUJZj-q&VKNUhgVzLP?{<53ifrzK&fBY`cBY#bp8vCs+{Gl z)Qk*DhLv`{T4FDQIlm9GPw%G;B{TVkAVx>oiz=KdDPB#PCn>?3VaVg-0w2S>k+NpS zTU(AFjL)ZMPa2o->!i*0sjl9Xx`K7J-zc-;sL9Mep{ufu70b*6*uScd?1dZETxjQ*`~!pBBUpfB#7;B64Ec zCZFSt#oQexw0z^Q@4xvlOG@sWfS$hMhxobK+1XERZ3Pcr3!Y`w{mzLGnh5w^d+_7p zBK^a`)5?O{GZ1He;6{2j*dS3 z<^5#^;_^#JI_p|}LJAL-5yI4LaFzSY_6}~(k?1xyb}kbuzlhp_|HXutsHQV(#G$M@ia@;5}4*k(GiD|JZ1XWZjr*T=7tSVtg8^^x|M@k4Ed$K& zdWH>z(uU(_Ybc)qzf&a#Ak|!1Ycr;XMqNI@x05ITd!ViaMUdLe0-L~%(LRw*U@gn# zcRG*c;3K6Cx^uPL45v^1p9}wU3r0a~fW)N_Ch|_>NA1iV#R549wJ16%N%(`&*x4)7$}+&-9hyw7EOKb{ZBx zpY1NMsYQ5;-1vJ^?#D>=eaf={c9Eahb+|Oy;o{V1Yd5I|bWNh7{`WHa`(L(XKvyn8 z2{@SGM=7U2elk_67uTIrMf=YLe@w*&;;*r0-@;P*q5H#g<2BX|E2TCRMdU$M9D7e) zssCOr|5*qYav1w4Q2Qk5S71rI4=a%50{+Q^VF`T#cmKLbN|0i-US+m=bp@Q04RCRs znkw1XTxlJ;uTw<`|GDpfzVENa`1dE@;9ie-a7?sx@PFR*zpuK2{p%JZ4+Vqu@SFeZ zasT^sA5F2a&GG$lgvI~=H+_Bedc?P$q?&)<XVY)k*o>%Yo_oBq$R{b%g{XW0I|`2J_L{d*tzpO^IC zTb}~%VkpDRn{*Pnk|9O(?DJ=mmTU-zh!^b-!Ur!fLdy=e*mvLc`6{VReAvH=IU9J`#zKk?6>xV7WKXqJq%Eud1`D^I11`~PkP<)xP2 zK)zV-Ou9+*zpw2b2Z2%b!Q8;Nzk`WC`<>KNe14lzT|h}rQV&(_UoJO>N`rRR#Lpf% z8vlAADSq(rZ1wDaLZ!bBjwL4G(Gu%=x@mp<3g7S$0YUYPAGzZjQhyr1D=!ApBh$uW1a#J}v84htmwUFWpxAAK(&@RbFsEv`%{=tr*W?xuw_Y?E-XJ8dHJ1 zLUxbJPA>GD-DL46_`<_~M+WyJuLM=)`mh&X|L-MeA&5m3#%bKD0j!LutF~bI;#?qz zi`3;Z##Kzq| zIxg_9cnbdKjQum9eH>Dz97&6GTkrn+o=MWC2OGaMs?74&g;QQ6vMp8Hy0Ln)b9YZfpOU5fqDr9wH_WvC;6=7m2yGNe?JLt#!>kVc+ z+4-!8;{dDrs?$L!;5j7%2zsbSP=rzd`UAL;kvbbCQ&el074ID_20XCND-g3TYeOD&$ANA~@Sn2a`6y=J-~MN%OXc4lb%}Z!NyC&3-&yL!(P;NC zQ+jc^tuf1KkRAtQPJ6$1h30vo==T6G(lX)s$lkPw->G5K&o6{b#_L{-e(a#k8l}sn zM?l7=HIo}Y7|Rk}G@d0BnrjT*>eCS43c7+l`waY$DeoFvLd(dbm=er@KFR^>ZSc5U z-TI#wmho+e9Hr4cKvQzpNFPcUH;U)bqsrzlA!X%pUb-gyw6TM4uj(Of@qOB5mT&dQp zHps8fMVCip3?42=ir`@8C$RlW5N`nG)St0o{Fryub$ysqE}SCnOGx;8iC-qU&u-bb zRg!V&_Ht0NX+7IpAK~V1YaIBUk;f%LYZTE|F_2JE<$E`8K4O~w8R$kHeu`byK9MTo z7A6trzdzIfEMklt{iA^T8+WrY3z3cA;iU0w{SNDFqf6RvFrrW58c)^?EH_4PWwUYu zLJ4&xs2-6sujZ9cR3!sq%)@KMcdkF|l3pV#RLjbapgYsmibG9TB(#DBtRAQQPMYK~ zkA`E@s-PyhGA_K_XuzV7!lQQ$yVj$+=Ne|zAsJxQp+}$Dr7@!(j2ZQxT#jiG(n}25&t}RtF{*^ELkt$b^sT&VV0U?#nJw=9y2)TiWG@FHR3z z7Pmiv_vn4g1(52=k7&yuq)%Axxddi<(Y-*x*8*4HOCKxea1VS^_ObRO@`uK)jqW?X z(ev&wPUZ)e{2nmISzvC*1{4^)!tB+Qg0I_cCQR+E#H!Z6NwSx7+Sob(wk0V`Gcio^ zspdSfKDfSzTP(naC)^v0h76$l>Ew^kIn8D*9%cr82mZ1KBzmc@ieb$jnkT^WJvPg3 z;w~-W=v3dSlYc2(RL_LX&FHQ}*5IMPx&e4@#**tGzhy(+p9V`4vo5Hx94SIN`|hQ< zD^&qgl1{@&ydrQqZ~%2=|KbYA-^YEflP`1%eA}Oi*qngHl-`4T#^yeo(B?OOP^D^qsA@Ok)S4Hyt#@O#;QClf#NCbe_Ej5gUgu*{ zm=vB}o5Q2O(Hcye{UJv&subSp6a4se?t#VpzjNb0r@)j+*UwTZoF8G}RC_UW+O0%n%XKw|x%JFVu=n`&`4XwI?Z)gh11 zXj3-u9(?+aL>2$D2s`Ox&1QFMc5wuw$l-*UMyc+vk9BwB%$_CZE;wJDQ1))m5~f}m zyf|-9Bnzu5b{Z!=ADHNwB_x`HI%3Bz}CYA`N9cqOjC_ZAqszu|-xu7*I$a z?ozpK@%f5FTEwT-#L$ec-GV`l9S{a(jMxV_(f_if_i5kDD|hU9t;B&wl12p zLcP(Kpbw4%l++wXeUHMxj;}D1?w6}yN3%O=Wej`liJW4!@oUwwiLUrSS|Dovx@n}s zoSBGze(`|Jt%5`daL!!Eq*A<2!CsK3UB-r(#2SE`m-*~#4>udN7=HxKkfH;F7LHiT z2|nkCLQh+-D=W`bzTBw{nt6XM>k2k6MP?bPxf*c-TTp}}GPn2<*{~WGT$;Lnpz{{y z2@OFIW=J*o_XbT71QdXteYpJ~%_vr!M>|Vej#>=OThO*kjcx82xsowG;fI_Oe;`T) zRROYv^}e<*D0UpkN`UoRlSckv2v#UiyKc{Y8rhG9{KDCoap94G9LuR`1)?LnFI>{N zP1@c7NyR6aaLUXIu6NQ2wbAnaH`xRg_3AVJ18Y`~9Rn(v(oW`US)1N#7eaf{!K*CS4f?xUIk zr&NCvO(RA=U-`03;bgt^edl02=ZGR8a$-k)h9iuDka`T*$nGvtHCMhqxlI~K0KXYZ zPR_3V2^8-q@Y&!66Q)`t=?kS&Qyhnw_p1G*|$1Uh&kpbBN!z0fuJfw`SrP*?^D2oHLAPai^e=yx@L8vDR{7d zpt6t3;Jvks6-pkE1(SKRmkNun1xP=iU|my1cgz483af5ug2ID#!mHiCwLW<*tPRaQ zFlqap|AuC?NT;G1w81;^mi$bzPJaLldI&@jsUm!qW7!=eerk88!@vn2L+jYsSI2CL zzPvzjZcYwJmRYNPrz*!BIUL5V{X%{}aut>>ci@woq6QVl=ea6rzF0jrSQo__t{N8E> z@eIo>jya{t>msG3M|_#XXzA+dg>se(v+kX(#ehngELMC0ftMTw@tkvtp~RibfdckZ zQvRSURj4WuaG@b`w(a(&zx^DY;=1T3{$|R0g+N)XsIv{GD#^6LZ&3fLdOSjctX86q zy_PaV;=7~jSOm9K0&uQ&qa0{jPhT%pv=|XBxIOq)ODfc1}!x6SDEJXj~MpiCWVka=I4;0yMP}Ogi4M{h4)mjde6oEGU2`mQS-+a5&}FzMdC(mv7k$ zc>igu&-j4z!6&g@n-ww)m8R^9rM-1h(jfOoP&c%1ts+v19&y$Io>!XM@R3{J!zzh_ zZjgfKdRco%m>|fz)+4(RPwqCOl)*!+jnvzi#b|}e4`*qISO4rTU!O`r+0gQ~>=5&)DVywuf*bo@yJ4H&tgjoNbzjW$uMGX>k`^q{Bg$li9MXUez` zF4L7BMK8_<=qKUOUVDmLKk?MMjHZC6)#^7&uMqYxf>+JBKt!;XdxwV{`VKX;2%sNXU03kuX)8m9l5Y>3&a<%_fWYx%he;A<7s#}>!#%1`mmExhA8Ufiu zQ^zRP)Z@a}XAVocEbueXa*bSx;2Zt~VEYeIA3z=XiO<=Nki{9gV5s`Y5bc^J9a5G8 zom@?I8?Hlha{277@Gfs`c(Vb%SxhU~8$8Ff+CeL^f@AA1E0L2G&0L$Y?Ckaa1n!KY z=&1dDFao+g0WTzum!Op%2kE-7N>C0~0K`8okRtCn3JysA{s|pS7hmw2qjhR!1_AHS z1q!=WJ?prSAo)~CdPFHYooyL#c`jsrKEU1HTW&Nm0RDamoYF}=sI2`!(O+3@MDKg@ zEanggH0-*kvXZo?5=E9^LtF(p)Ti^afEZv8H#c86$?u}{i@eaJJ!{@=#BW$3`i_WM zo_6b`UevdU3#D>HYO2?;ev~ zO59@DDh8sGug({Wto_$R*}6`9LD+MT))pTw3Kr;DuEnb&-3E_g$xCb)-n8JWLWMcB zTzL8`IP80W2wak{f?Kcp^N9-C$hG({K4TOF1zm!FUPxUK(}suK>-m#gH3j0;)B6j% zit@FveD+r#OYs5DszOg{)DkM5BxBmM@osF8I%_schlGs-M%yCo#ICt*zaZVd_1+5p z?&RHdRgqP#66<~@jrvzjAD^(JhT)pBI6lB%<(4uNmEE|#G1NDQoQLC;BUK0+rR{kl zZ_Y|lfuKB_RlSfCLJFs&JSib_Gdb^0j+AWdWCdGmnLZZPZLoKnSAbd1FX0i-eP2+;@VD zB0&c(?ecQ%J|+^VCkK&$!=f+O9k4?Q-&M;~6rkp}+4w+yca5K?-fq&>XCtc$%Oq2p z%uAa=)V=rg$K>GSydu`g^7f0T)&~51{?NqMGb8u02inf|iZhH+UJR%`2nuSC8*+Lg z5rbhF!GgP5RP7RS@FQ%uvK30Ecr+pQnF`LDQrOGniuQEQ5B!BKgfQvq1;dyh~1gJ7ACt}M2!Yc&o-)h z^52sS#nXw^=Wjx(d4Jq$`u;8@)pvt*KH2vZD-wT0hHfBUd^06^8d3+E%RaKCCw?~? zawP-rkj6(F7=%MgL&tZcwRn(Fe!kz+{57XXqXqlyiN zVlSwff!MI#l#t{6yd83pZsMuiv?}a5YUWoh^}ZIQ(w?>*WNT|#@?K35sRf*$!TCsw zIno0=s4_5rU%~o-wi*MJoi^Nqt@T+joZy{Q>fb890{e&vx1w;4;(LSOz**Aj@vV#tu4u`&Oy9>k3hZnHP)Fe zK?NA(6s?q#J=`dvRjy~z6b(tl#_ zfE-ujcu&AHy}90Nl+mZbkIEVfmXKC>k^5WevjG|x7m>5&Awt#?=>Ffp5 z6-;i=m@RVTF<%N(5i=SVL%rsk(z#E0ey=F#qe5)An5tSlm(f$u1Vp{h<1BT1=!Tvcpc+e7`>yYcr?d#Cuqc=8$#d_TV+RYI1>bK89Q?nI^Q z>cnBahbJ*_j?noow85*`_e!X+R z=8#540v8R+2)ED+lQWQq{Md4F2POvz3K>*(rRwLjCFX?y+3h_zM&m`L2VCgI7C+W| zJ+49G_spjMvmO>J{IFRdFSmYdTcO8n0;S-;SOVHIISDBS>%y&$?H_~0(EnEIUy-#f z2L0VJkoks*NWWgUakg4$=EPFf^tE>dKq32!jQsKAi53N*-D)N7;4O#AZb>9xFFyXY zYh17o*9092q$Y#&b6pE$Z3p86r7peF1@R!_NQ@(}NI7%PKZ3=oI_{m17vKt9IouoI z#@k*t2BgRg@~e9!k7v!Rng-JDTTHtdjiZqQR0`^7+P5tTD;KZDq)t&MWSB2KLVSzN<8hAg71pvhvj!GugHe= zlAeOd1!w#e!a1JH>QvXjqzMNQX>Q}#R9|9!)YjI>FCeA6HW#J1Q=lzb0ZJD)cP3*? zH96+Id?Pm0>e3pWqh^E-GoS$WrNX*98 zkGUI~we!Y94mK<`9>Ohmrl0M+ZZRx1K1W3BHiVeH(22qT){GxdFv()uQ3MSp=b+{S zHoLdY1sp zoD88fw(z~!jJ$W2e2(a<)mjrQ(a959g$GFcAh5XeHTQ=GNy|T^meVHIC!oPd@cHKP|&L9HWK!26Jo2=EoaVnl|m%kd-Xw zuL)oE#;ybLjltTG!R<-NqnBTF$BcWGY(Z8eW5A;AK({jq^gDuiZ8I*in&_Au7|=In*yDG^+PCr=%9iP2-XKEEZ)i!kMoz82dBkU9Y6EgUWdVRx zKcExCD~Ry_@gYfDjq|sRrT=sc2a;c)#(&yb^}BB;IjhE3y83|G#uT5y;1~^-vB?cm7r)W?8VHiGsB*Vm4r&zt|dIyN@T)j;1 zjz6?vKBTTe|N9rY`I1y0Wjc?g4lL_gaZ)yc{?6cb#q-k(96PVgab=KYe3yves|eyJ zDhx?BqoXpYwFwcp&{J51Z7wbO?stpXDn3&t%@MkQ!b&RIIB@U|&|ou;!8SkrS}Gwz zYnLqQ5kFb8b9-?JQMKfL2ZwH?Qw$U*WI%CZ$%xKVM2nGU7TY{aqAMBxhyt4LzRg7> zH5ZFsia3lUXg3am99^ga`*}N+l1_;o{DRKmH@Jk!Rcz(WTU(h+u~7^&wIF(a%L_E0Kd?3l&VwVPSPwyL^z|{ntU=FZO>A<# zN?~XUBH&V-{k--PN9n?v9F`*wL3@e? I3)S;wcOsb$2^0dc(+$BX?xr#XAM7+~3;|$~|1~`_{)s#5KfTm`5 zm`!6Lu9w=Q*KR#RUm`!Za90y4M-U-kt1;qAoQB^0)nncJAi;1gbwr%66Be9h*c6@% za^=z|gHEx8Tn~51l%Ja_EiMBYy%%g+jZe#=XvyKKPn%R- zR}Fm8+iMNa4W~}TJ<3^Bc9iQIwWi!|J4I7CEzN4Ce&e{GDXuCIr<<#wSpu*kMo_ys zyFNN~i%;-@16>A5XdRn?1KoLx53($7Xk?p6DD|0nT6{;MgtJDC6FB5 znz5Wb6a(nAMX`_R^-=XQ2dDy-T~n15K`(uWvleWX^@Mv>ChccsF0V@xx36M?+Q^`J z?iH^Ofx!LhinjLAv1PNwF;4naB4kvKJm+B7{|38#=# zIm(Cj99)8g_x=ykG7w;mUFG(8EJQaw+>TNY?h@DK+If#rmNjg#x3%f8=I-Q5(wZLYATFB8o*X-31W@@v_ zmQU0gDPZGMV+#w^H^y&MxxIMuG{ReUTgssHa;jw{HvL4m0kz$n+fe3)f6zc0%NtRLmP!Pd&f>He;yR%2o;c+igFq1foNlkrX*#ITMF~o`n{NO_FleFZaGB zj0mAk87$9SjKqb^0S3LrWN`<-09x=!x%U9C8|<_X;K51-7DN#~n#XddvsG#Lpu{u> zfm%hbEt=b4eFz4zSj&l8|8{(bVSzADs^!X@MJi;S{R2|l9^IJT_1l`4=zGq+Bd8iQ zHxOgIzc$1I1T)`j4SKcYa};Z=%5q~bN=vZsXsMy|!O8a?4Y%y4$A+8BLdtA11?I_y zaEQL6N6P0w30a6I-Uu$L`nXCB1JmrA#~}c^VOVS|{(#QA8n636+I+UHCV;^bY`H1S z`j>(LE8x@<%|n6IcojhTwR%Iy#ehpoOPazvmBTGdbo+V?7h|;xb`+=&^=hr(L>MrU zkRK)z$~a19mjff7O>y0;rqFT0PFuBfQJTKQ$L;b-iASia5{&z+a)H;G;_!w+L{%>BxK;ccDK@8V7~BHC~tvwO5YqL}ID zLGGJumB_Y7z7b6V*%@A!%6q$V1}t*9I^3X$GV+cq)E;RwVkYL{?hBsu^ahVD`C)3+ zg+WD6#N_8&K~(@t3p=r}zAo_2sT6eg3HzI~f)Zv4^W^Ko)00SUbQj6u^ZhjqGp{5NtMwDP!ZEZW_GiMQZ&QaLk-ixvycUe(6(Lp z;hP(DTaunhEve#*f76+uR0lLjgdq4*&;eKNJNJU5I2a<=*f9f46HED}!{@)H`TR8- zbdRAHeHv>sU`qByd%)>Z{KCL?u`PC)W`MW5mNFgPy{|IXX7Yznp4Kwc(n6i0MIgujoU2nW(_6Z-KQow9Ump9cK>s2^#%y&C19$o96mwY}} zPuVKzhFAr||9hMb$W5ttNBPY_wm53;*bUCyVtUtKGxBa13stA{aY90g?71vlx@uxY z_s?PBhy_Y{L~DP7!ic~#!tw-Jn9UtA&p}W}C8nf;8KRS667ytSJivWMtYrNg%afK+ zT1F#6)MERhBC26$q)WcB^37DuM`G{Rhew7OJO7 zQyBxgN)B`N4c$}7o9=~HLo~g>Z}Da=qUD~}N3ZS(R_13boD9*Hs58@y{a_MAED!~n z3%B(*s;I&I=hBuojlu%Bx}!W+muK|4!U#4*ct8m*n~7Do;7`%GThDf zisxi8QTB3F6U(_+9_u?^_Yzr2)J4k5LU_GO7(g~JJ9HcChfDSL%LGlRaS{>je6u1v z6v+Bv^JwL&?mKO4OY%^s#dKymua16>+M9N-d6jbtH_2p_0X2tAc@>$CN3_Kb+D)?| zFXq>lCn>__a9!^G0ikmNlRlo`377l{c!xg8GtLiX146{RN^&AFZGmdh^xvIi3j}#Q zW!@tgJs))CIVq`{au}X=fzq@dfTSICI^WJmM6YB8#8|F>jLnHzFr{0Y*CTIz) z^^OFOX9q#VBmrbw_iA^o&=5E?R{;eoiI)5{SHPPR6eKoBogxd)T!;H*#}!YAt$Lnp~5uErSCz4B9IzV?syR_$KBp)0P6W}qjy`e zl9-jZ`r7=?ltgxR@`Y*+@qdEh*Wd5C52UEINpinfEr8UR-u*E|4#0M0EU64NltcEp z83Tb|UQz&K0ZvLIcNeJ-Hr?5kUWpQZ<%j9UHr%2W)a0WsDmPztqrBjNg8=Ar9{0AX z{oR1DW7*NgZ01A^a9MIm)?jHK(XKRoGF$oG_K1x=%~FTGY*h>kIbM^QF$RQ$aVI6k zdz5zCKsRaq!S?2&n^0X(fAQ*JO(pOvL3(}t&@ltL=W}gg?_N4NTD^$YHA+}S;hxRJ z_!o@-2_Y_TeryRXIG0L9cekBx*#o=F@Um;HDskYEh97fOJ1wfB<%f?1>VkM3o==uR zLa8;`bjl6)Rue8`FVET=FqNUz^$bloLmP{08ORmny(Zg(J(m#YOEA`gRJIU8%6OoN z_06|%x6r3jWlj|!2mIDIbPB+3(7LQ;OUuD&9st)`9WZf-gck2KXIKC?9;_n7h-k@M ze*3BZ*6C)Wxt8zclM+^?CO}EUF)tBC@2u-!GF9XT=@3P|5%PFhyBxkL6F6;bpnw7P z*!g-wwP_(GasHPdMSeRpyoh%j7U7=U%sfPyJZ%VRogVp%HiO12P(OryIT^??-|I8x zI8w;*si@oGn1dQE#kuR2kga|P9+6$h_V@Vo4qX80Z!y&f&vN4f1bqSb1e7r#%+I##YWZC4M-kjG#OiWoA6Z&zpf8yxe) zAtJJcHqRYq_PF&v&#}O15Xx$adcLBq9EN5-5m0`xnSM|F9LYuNhyaK109*+DWyjiy z@UW$e_D20c57MJMLIhL7$%F~my8aNyhf{qE10F*98L*d69#C>7TjQo$;g+cm5un!P zk0wWEK(3vV-1U|d0x-ie{@M=;F19*KkTF3!t!d+xNv6yozjGh2%kqhXqA~*^Pwbs^ zBi#rM#t9Vrg1C4PFI<$>Ny-zzLOAA-BY#_5S%2bLmB}4U>r*XJE5m{fjyYUqEHqv^ z&v4Q+EIGgPf{`9z<}j zCB(a&q09C*a-w?^d4bkK{^kdvSGxA+P&!(*&z1mPWRY#uvy5l6W{%zF_1EET8pAT<_G=^Rx^h?o48EMfDeNrT?5idKv(T zh<|O+^bl-njDFZjn^t3ctM;#Zj?{y^7SAP{KrLFv>}_*VnV3yjv2zjGTm#BxA=hyo z^2$%NL8pRL)rRL_6|RV1^aF2=EUd8~Av_PD|r6Qj=agA|p|0LPBF<^&`}F~oH6V`DFVfb-kwJ6+$1 zv=lIG_m6yhexRw%v+acp_u)iZz2;+~qGtP;rJY>N2WkQNDj4Z11^bg>u}hYMRM)VG zJ0kG5&zXURM+2>5bl>~qkhzHhH(M9UHuH<|G!CsN#5b*9NY-p7wI0?*ueSABR_sHF zj+({`Vi%ssw4W~^-G58^jW=2JebOc=@)|m`lJwZ-qTFdk&$t(4)E>byVtCD;&W0EH z17)^|&B%>~Do}IT8xk^s=oHvBy-^+Mc(zQRw{`WBVro&<7sjIw1InH&AT?2(*o?hs zMTANo-~o2cqUHJUZ{ba3`Zjqk2Bey|<}ZUTF9h+P9Vh3XPU@J%fK>|1)hpF3XtRmt zO5@jcK3br|_PsNECPzxeuCsfOTJpkWtRr3p-Ui59MXy1K>$)5%Gyo90_}3>I^&xg9 zxdml8>+aXO|3;Q4C$*dRVzlRmC2wYvFi(~nO@7`#V~KZ212Q)H-A7kbL5sL@o5M<= zv;-mC6NaLP*Utk+Tby81e&4-JxkKngI#fQIzOMTxoGSpxnP^oQ4{Rzon0?duz>lyC z#)~Xt9ADL@{lQ*R*6_f{0iWpe*7}&Z4-DGUu=yG^PnV)~OxaGHm&1J+9?b*rw4~q% zrM6RZ^AiWcb0vg_4lRnci>s%EldS&stG`Gz)fnEww!uIM20F2&>{Sv9Pxm&`bcf(T5l@sWp}Vl*95Smd1(sZptAQGeiz0ZZ#hLcF>-UXI?@OgsdLix3u% zQ4uRH5@-;3Cd;E@8%5)dKjNNB?7ICXjCjbFPoI5CcjY_#)< zd2YK#xW`4=76^%r^M!2TZkBsBF6)$N%bXrDY#V%?JCk-=Qcf?$>SAGr0VFvv;0QmI)7sjj6cNmBuZST_Ra&P`fJ?)BKMErbuv|gY`CKkV^^PV zN7q~s0nfv_Nw?NQy}X@pc%2i;)&tZ9U1m{29np{U1Juarp{fW8>iAG`y!}Nu1*g|= zp83?)OdUEGEB&C><3Jeffe=H(vcD^>JuNIUV5=-gEQQ|e(}{$MGfpj%4zraj4Hw>}$hke|w?fhi4u zcw_ad7|z_}shCL*S`pdBbc%3Xq(46T0m{265jTbv4Ef-KX~HgcQfC@Mp)@8+bMVuj ziRtnMFS~XVD2=^`K|MnUrUPdc^jM8mEwAmrmX9qI()AlKsc#Uc{GlX{a{`E9;q4s| z`rYjH#q65u4UR|po$s03oZk3jIZPBr-FfWIW7CrV8&fiTg4`h&y-v)rDpc0s=!>f`}%_?RT^&1xbqi|uWCqWCz0^Fl(ZDX<}RQ~QA#Fmo(&h$%JSK#NcqJuKc~spmMF1W zYL4<@Ip@$We{J5e1t5=X5Lod%ftP+IykP|?|Vf`enu8YDy%Z!;kr#4MMaU_M_JDFOVPqwkb2qyq;K7S6C!HK15tu0W`d3REOGLOaI zQrecEPq&5`NjCw>jKGv0FJ!D0r7w@lb!X8<2-SE{P zMHD{4g8=X9s?CrblX~tp;nna8|C4ckrYvU2gm}E zP#@0$Iu(l?TYjTRt z+5@%W6fJ##U1k_g9u10bo`l|@UMd}5`o7apkn1+F4_H=B!-7WrR?w8a^e}J|3q)A}Dkv2cGZX(q*V*+^;WiS$=BJh8)T!MuTcm|CL`^j!GLBq8-it zg6`YIbbMXutwFqW3~XMzkEbFlSRyjeSH@GWn!Nti1bseHK_bqteX*Z`OxwehH{f;l zn;Zq6PowiBLp@c+A(s<(jxdB(Rhqf8W|IZ+?t_{dmu1-SNRtpz?!^t83SLyMQ1wB6 z#)V5+385+ahV7aLym=fCZpU~28_*~;RZUayWsQJNc7*ZrHLM_%s{7zUh2 zJ~I2tge4j6FaG4y5y7V#tEoK!+ACK?b1@3SwI070DdD1KzN24@%W^tH9$EI&yi&$r zD2}`AGSKg!tNev@xWmPR(HMa-^<1>ju{{qwFSj4L+hQG!{ya{vDGDlFIX%wTLMgaX zS;8qt5-@H4MH<#A>E&?5T(xBmh4Lg>czu*8*nGs7T5ah}lrAquQX0PEYsYa=3$GzT zAa~8E0G+780MT!DkD*2l8P}VH68ot+LMJbSpg~`wyK;Sj)t1ou8vc%Wc%L8rLPEWo z9WLu1>iAvhQTw#ME~r*Uq}6 zQtW5W69(vaI>&Kt3(tx&=T&KkxjH0n?3aG15f1*ekykL}#f%Tsis-F3>|wLlxYXch z)*5NN@_9>{A!H=g4pz?Q`nG~oHNv?|8ErOE;hPF_l8IciDyKr(mVE%Auq3}5NuL)| z`GVVB+uE`X8i7g=p0Jvf?wE%VMl`j*w?8>Ow7>coYR@2JBA^S9$`4Bmd?T-fip~!d zbk83-sv_*Grb8iz07rfI0kKluLvA}=9)f9UXq0(7pD#pnAiBdk&WQFD@f%7J%HS|; z9K34kwj$g&!7>ji$(RH6UVZ3Xlzvcqj8bDKE*;mj_rQ}ZiTnwg0!1;#D;6m`Mh z{DV7M77ZW)XF%PZ7;BSj=dj2JNcMydHM0Ak*)SRf7y9C;F1j7L&pE^mHCl_Nhx;*& zLl@GW#&g^73+D!G{SNhs=~FMB)*@iOHi2=HhFDUilDpUG)Q(Gq= zey6{213p&wrv4*gu-+f=ClQqM!bMoGVItH=8B$j5dbvNq=}@h4%~SEkPbApO;=t)? zi~CeCWE;z}J!Mxa6{>>c0AO^+hv%u#h4W%nO>ovvZr!S%fQqiX2FZ7sshUiQv5$28 zuenU|zSBb_`RBd+HMrgLs_G?1acn_P#Jzrgg}}T4oqR34Z6{~bmA`UX$To9rvmnDu z&^A*MNBi`k6p>rWvUAH51@)Q>SBC1i%(L_YKid1uMcR_G;i`|~ zGnE3$YaNjL5a}3c3CFuC&{#Tk+U_-O+JbmLL7Rbw%_PUG_mC){{$VescG(z7=b3sC z@WyW9)>8h+mSe~9N6odzK05#Qpc8j=Dyxii%baQ;G*E+-YCp=O1k2e0HWMqRn{9hK zZyvEJ+;Dm{p3ba9Wq+8jXU~lXwTs6QyOJHyUVEg%0T=bX=VdwwF*|SZ?x7RF@tuGS zywz7xHRzGZx2hY}`n22URBCdBwpsV!Fc5o@E1{b+y_wd!&cJI{=!A0jQ&WS?HqCjU ztA1loq3dmo>&i-sx+*})`s25{w5-^p-=}HP5T@pWB28<9X}_wPe(s?P31_g(2#0@- zk0X=nKgtaL>6w+n!v~yXMqT1?3^9ut>&z;~AH2J7+u$Z2=cltw;tvAOt8*$e^OZh> zl8Y{@rUpRykBI8pcS|mjo^)QlyS!EzFo}0(t)eLLhDAyaWu0uH7W>I!4Ha=nOa~-UnsExJ|j7Lzn{6Y}(`ZF|SIPc+J>hG+NhwEISgk+4c=n zN=;kshiJfReKaE)2abo3O{m74n7HDKybaA_PiU*2~<+1iaajume068nRW)?>SAYdawulvEy`1iZ_XR5g;C zXs%@AXPo1Y%6{hFNfn1Q1DYkzk1Vj@a^m1c4r)zm8P_-qQAH|f%{8mLUnPBiPnFGu z%p_Oq)47kHf(>}2!7!R0T6#qjni)8;(nmTd{Gd%N&o!@2@^q!a4+y+0)a_!>+sS>0 zQU$)^Gh0!Z|M_kLrd8(}Jr&tbmUR4n{_6>r7BMf0Us}Gsbh96Aacfx$E_Pin5ir^^ z&;~^^E5ipfe)Wb;#UQjk>U^wbj@7f}0Z5o9u6DN;VY4XHIEk0m!OQclBdR(O`@wj) zQHv{-sm{a}L*8>Ql9P2or67XP))u)TGQT5cgs%5620h>kIz@9DVtI9C<{1Sb8`E~m zzd)PKd|p$@dT0#<(7r#`rQD(sIQ$`^P4E;_Y|zj=$t-Tcjn}$`dZ=uxTWQ*PMaD@? z$h1IHYsbi9MQ!y_3C6x-zN1dfaX(ywYK$8;&o=ycl&4!N3=(Mh3&{`Y zZPFLx9vFYr;mI0>koTfJ3 z6xHJuel{AFAzLD3;e_KXk34RW(g|^b{uS+?s7CTi->wLCituG*!VE^`2C#$$gT7s! zqX_vVUKDcAR0&&iM1OB{xy{P6M!zqOT%ylWOz6VxKKNiDP+j26N_fazi{Pkcp;HAG z(s;~`pg{3~mK7AG+Z2Lr6T;UfX{lhm;!pL0%R!|Lfq%TQ4m81ip9qm#N zw9e!Vo&~64dgRNS*7Ql$CNs>Ba)sX2t|#WdnTMX?@^xq6&G5PI`M-lguRq$Gzaz$r zjYugtr3PK zoKIs)qEp3qxE|x7j_-)+sD>PExt1j|{BG{tLeVj)O3Li=&eh6iny8*!oL@@`eRrUn zh5ZWcBMlv=Ab8N_`{#hu1)EuT-2EO*aRb9Rti;}>WdTefU#3oCwe7`r6VL!a=e!+4 znD89*2o5%ba#>e%m0NVpf>@wWW9FAQo%jTNQhG0RT}J<;@!RC#v0YZnYlEq8)CXsv zcXzTqohz!XTf>1#k1wR zzB``k{_j7nD@l|PMYLpRCF4jT*|N8?H<{TqZL&AX=9tIsP*)*4j!l$(?2JR^?{!>V z*Twg~AHTov$Njkb!{hWh$LD;!$LswX&*$@17%TzhiiDF>#`#iFr|De}2^@>Ld&oPL z?19XqW&|$mcn<3I=7P%fV^^qiR^f@sy#)^)A9CHAP%~%2$cKGf)-4;jTdD>Li>I1p zLX(1vT)(TxxvxLVa_LK(E~;eUxsrj@U8l>1is}En)6n1DURY}%z~P$x@LiEZpf^d& zprfWCGu?W!_GepXl~*K}W?5dxwLj8<*h%J$tMsJJIHsyaKqr>g8tNk|7o4R(y!9t% zwnBU_X;r6eS=Za3Q-UlTQzmuk^R=5WdhIpogO zv{V;3AZ@0E{*lU?ShT?niiI=93bhr@kd{_s-&C{rz`82`vsRUf^>$Qof$<~ck&4faSPl4i@*fbvH z9E9a?wKA>Cjcbp>09;Lr*}fSDC(h~+suPdymUx#tpulxaU$Y)jy;l(O*w(lkHAcIv za!IEn{P`aww5OOR%kEw3?|5Bif3jz1WuQkVgK>fOD)D4wh1{vEk$1jDY0~_Y$~S z>a?(%3eDE5#V$D;#A~_Ec{bldOAJ;zka4laS1op=<%|ROh&u-YPHn8LRH^4Kd&=nR zDH*~Al*{UIE|0jpwV~efosA2IV_(`^+1@7lb*)4e8?|s@S$V`-fgs*26suy_7#`EQ z;~31L{dSZPsa@4T6?bnYo$*o)u$431xVeDNP}qtXW{mB6CO+RO#J>*A?W72XIrTE< z<spZT_f3 zMJbKeIk?w{XR8+n@d!;Hn&f1_yJBA@Bod7C?K9pjAFo3N1c@u>>l{(k8aTtM!EH<1 zS{VE`|H^&A%g`_|OfgR;E?u^Y32jM(m@2hozfDm2S5bWa=46NG3IV;l{FlV7Jj@Wq zG!NAP|NN8EtvX$BO)F19WuYSVFSHOtzj^B`QZC2oCJ?gJSa=dhnzp5CF86_uEL{!T z(-eI;c3%Dx-6rH;-JHJu(EIntIE#2J>@EqPxxG`b@GOW%FiZRe(uGj-@v%W~0DJKo-NUD^;~uockFFD6 z_ne3^j$zOw0)|=R_Tr~CmHtB95|rQhH%$x7HdYwoFYsJ)U-5Q0Qu(e}+xR#!KZgza z_;-E>GvhS2P447?HOBGZmEgJXc8*4F@{X(wisIw!cR0%5dt^cARS|Ky&|eXj$mUoe zua4be`}uPc8V{`R0k{0hQX@0_{cX|0`HYXe#DDf-fc1$?9x!v|P^_su5~XTKEOT-rRIj#u+GA< zi0xmV5E~}Y@_3M}o7m_>L-nx~f3{l;f-WnW_VaETRGQs+-laVDg9?`ZQ>h<|BUvvT zL(v16h8a45*m?9wC+(E;ICd#)l1}g-0d#vtDQ~b=McDI|otsPzuVJHp%BU&U9%@pW zXk12|^S1baW>X|v%D0_Z zwT_}OaadF8Ow{WSYfWenK2oZjoqE(T;fQE7%shcl%ihd$056$>OY-#JYB4{K&H~uU ztntn0)%J7jE@Z~_K^ZMxi+HaaLnIx%+{`tAq}3$3{TeutFC7#! z;E3ZUP8quR1&}`K-ErJ|z&&?6tycaS7Dlb|0`=Ve!xac2Uosg1lup;}d@|(GWPG!$7#N#TD^3ur$?X5A++tQ|uc2(|3l1!|tb1k_le+=oY z`^qxp&Zp}r-?19Z{Pf#{=eL7_=hTQE{VihSb*A^dbJbW=)bQZe_WCG^;nsS6OUn`V z;L|7b2{_OXXA@Vj>h69j*0->`^AfJwQ@bwuedwh0k*dkW&9TZEE1Sf@k~PeSNFg4C z(|oQE0VQ#VNq_NWF@?m&(%`qe$vPgt#JFz7pj&0zG6&v=32AQw2BFyHy$ml}Y%5Q! zDb+D~l|%pf7oHxinRr=i1$J%f+Y`@=ImnvBS=1#b+H|Lno<2?@#DUYip6L;rJvGM~ z6XCLxMtIeKKD8!{f=%m30CQ^8uUg6^sn@5GT;Is8dlyktGR_H5gr7|{YO<%i_wtO- zTd!}L^|Y6iF<9j+1=bt#Z%H~>)NV4|Kqm$fme`J07FZ_yhNvWHn0wuG&Fle+s-Vf3 zMS&42;VnU2q5h?lQxX$w`fVBMNc}Q_t$vPvT8wZz$Z_f+wopwMhX$-Lr9nal_C4B` zvlwx=k~ULHX(dhZOBMyLOQvzkuE)#@FbJ%OWWRuQeURmR*W<~qu?7ZSHOrPT89sL( z$>Fk2=?wvOPhQuIxq+n8x`+$&xvh{Lla9a5^N2!i}+CW-=lw|hG!Dfh0x6Yh?9eTBP>$2T*(fT(X93S-%SebX``P;$q0CkvJ4g4U3+N|uAspO+sNFci6v&|Bq#!kaN|j( z_iv^h$t@EW7Xn-ZSS_WdMb!BdRnsJUd~u~6rH?gSzM3^Q@jFb%gyC^!0dkXTAL5KT>8eVKdsx8s!k2LbK;2&$**cNN(95%BY zDF-;CVnR}(m9J_UCJRGrqNj@bUYZj1oqhio6I}|@y6;o@{t@E}@jfi(cdpQ?@(xyz zHP1AFnkRcE)wuD4V*S=vN4axo2~6@`q_i_}d&)UbVG1}C#%oPmEM=x3CFXj%@tTgX z>tU1Wtm0OtA4(JO*M{SZt@h@vjnX&YAv7XcT|2NZH$Rhc8U7sk6_Y%nbOthbRl+F) z{2O0`gx>4R;hTilNoiWMr_VSHmNx*!wkv&7*@f214Ehs636yKP(&YFtnkI|Eat;Fh zA7bu@R6pK*E*mhMa~Ew=4+doHp21(Sv=ywSk^3#azdb|{$|e8$5o6q@^L?W|k889#3@R+&mp1D>_9mQwdJa6SF7 zchec!96E%fyVWrWq3sn4X7|xXAI4h6AnISbGDosWrxO`%1B-_?Tvo>Tp1= z2mGfKpC2ac)k-b_MK%5$AAjA;ptQPhx5M)&3F=2vgX$Pvk61VIwENhz=eegoldZVW z$vE&o5?=87Ns4iJc6-hZ2nLOdKS=nzoe*>Q4kk{+-#BD>zH`HoE$k497r2bV0fqUhRQ?84_nsj)Kmpn{fMQ~O@5IO7BmNjj zO4WdBnoawPWtOuZZ);aGE&~qQ*Z5ZIpi^L0cf*&v*S++QrWc3$mkTWia0EP_vxVZw zxYy=~yRU6N(|NAr?*!y;ABm6S+I#4$kaoOmfr+N1=dLN!Ai$9LNoY+USwM`J{zvzp zFYWZZXPDl7({pyCxswmoHfYnVm!kh70bdRKu{OV*G&}g~8PBW5jHTIPf%w%SuuwW@ zBKJU|BiA7I)9Hx+V=pd;A3pEAhCcA^ZV}~PIGfY}IWfw5F1u`#V=LS3Yo=|9!3M+C z?(OsUE2J;WBQ=Pbzw_NrF70!uD8)Y=O@J{Kki2r`J-$Y&Aw5$5Ad5lid0as|%jGkrhLoT0ZHh*{^T?yENyc z$886D)%u>XMqM=0Dh4=AQ@YGd%Wb19J~qR>p(FKq4RinWhTUm=O5^YG`$BOTc&(uT zHIx8oV#3V!Y_VLLetl`HEoZAqWkD>DxzuP~%lK?@o3C1J5Y2$P{)qcoyvEJHw}AYJ z0bbI^V&TNWC=U5n@9&knH}dL7LBEF)ntYs- z5t&K^X*;~8M1g_~O__=&UwSTf{b#spF=_3c*hnTrN$0L)m7t4^%Nu6wGdbN?0O#0k z&W0DxILEA_k{l~!k&lxvTBJVI+ zehJ`7(fW&A8#^C^-XXj@ntI@Qy@Elc@zOaJe3kp}H-x)MZzyGkG{^Gj;z@u&l0req zDTL8h1JzH1xqepL^~J~7zT!BymG(bk=BYDp{`0GQ3ycN7z{V4jY5%?<{wk@R_c5YK z2`KtEL5YM#CbqK;g|35T&yE+*!UOO@lw_=Gp*paFe6P^?@5U>_ODoPUve^ahyP<)I zH`G)Z%LZ-Z2w#D^lv4Hft>xokc=H5ZTf!7&`NW{B>5Z0Ho)G+gHXZ7MOUi&BzXrVh zP`uP==S-64&KpREx3BaP^^(@!16Cnu2v$9poaP9o5x9=8oh1k)k@UBoJZM0|;c?_S zbNBaf@Rnx&TJV?*+dZeFxZ50iuiZZ%0eCab%lt#VaPF{L4&0K9rL`fZ=8go3F_)ZK z7I1a(S?xI%aV|vwq(dT1CRD&_KBTs|(e=X8;l6$I0YYw+kSefKIPLTABmuAU+zmWw z2-+H~Xl+1F2zf|7LsW$!ebQ>LKSGEtVtMioJv7|%#s0*&6m0!kZh{VbH^AAA0D;*r zfxJT(ZEnaAs3ZH41T&Xed$Cu(ji097Qtr;z)laM267$$`PJ+rT8^*3#`<3&-A&7?| zu;I55`R81NLrI$9CiH>C!>wh|RraRrt~j@NY#zN5u!(yCdUsVWQ4sW7U#B_FF-cIK zpYBMj;z`t8PL?X)#%Yd$_Uhf4Z50z6kf4;f8;56YN-|b(n;@9~T?g>$ztt^%;NpE* z%WE{U}(c&q24)$cwjaKByZV|N9i*J9r)- zVGug~9@6f=8mf{E_&7WZRIvuH8~}i(Sd@c|dF@GZCYG6QOOB5)7!C!@jY9u8WX8v+P{yrcx0oEl}D@wnA62F`}>?nU< z5obDGoy*^nP^H$2SQE5!-riUZgAT6mRp*0XL+;=F!YB$9b2q<`P2V9y|vP>1^tCfX2y`3|^*+$p2Y4nxpt5hRs ztp`doQch}V1&8!j>@V7m(=)Jo=Ov5%8KO97HL>vBlJULw;XT`0cz-7aIhY& z_6{({Qa{Xq#$PfUxH;1ak?rC~A358qob$eF&=2H;0)0emMVcwGu7`7(1WhL5o4w{t_ng7P5c_zavD13u zeZ^BjR9qh|il56@AKTSd2V_dc?#^Q)?AN!?Q_#&RR13_59NVQbF`SBcf0jY5?C~dz z0+<%Xm7IK>#@*rxkE$34ut23QzN_KC`HhTRM$K=E44e4IXS$JWHQSC~{>v)&hgFWs zEc_>4#v7)q2_Hskna6XBC8J%4sW^ol7lx=xgY@f#u7f7_sT*8+pXK|2%13p_qbT07 zA4@O*;?viI1v&3I&h}Ci2$c}^7bC@kH}L@^K^3jw(+`s5y^3`~2{%9(q2{IS?_K<6fwl*jTh(6juGcWqs#3A0$8CI(Y!<%}1Mtlq4wPj!kOu7EQ3<3RRE zx#Q5y_^>-bN~1!rNyVi6s5`+7XPI(+U?`|F+g8f(T$7)WzNcpA~N8&*T=)RaFA@?Ayr;quVwMW)EiVXD&^V=Co-^FmX z2}%Kn*RYNf^Ya^b$Ze}ZgB;_*u8C7FLrlt6ONfE;Ct=bgmDi*w8fW`3E$gQIL9qzz zd9(hezyrP|>4SWjrRQ0eng zi3g{iAHDhc5Iy}1iIDV97fDY@YQz}alqRI3^Eobm@4+Fdqhb`bnay1a8po6f?uowg z`Ra6OT4yL^EtImTaw*%xnALqPv8v6TVR*C#jq7umj_J2>B*CC*XZAtVRWTyK9o?#4vCAG6$XjCPmXFW2!1K(iVLuxw!d;WCc0N# zPha*f1nG-$SULs&02z=q|(A?bG|S^UQILqC2$KfkMjTlez|M;uL5 zZ<&gPsX+5XRo$YZNdC7P&S~Uod7tJz4ckn|73m^WSv7^&O-)DV#%1rd#uTtcvhT{9 z^;GvrV}~@#Ji1p4drHhn^7@L0-+&2puD!~z7RIKnhdjxg#S-Tt^@n^68ZkI$zC~=muM+97n`x`i!&_qO7?lh8C z*3G~m8i4!_6+QmAIo(k2Bf54Ipu%vyKG<_+aI@+wtu7zmjA)oJziiS@j{W@Lkh!n9 z-fXa^AREHvvrBEcY&orBx$U@Z48rx*jJ_ShvfqM`&{R*^YBdVs>gbGMZzF8m(c74Z z7K#nwjL@~M^Bw)F&jZ&}RH0@;g@Z-oPAVfdFA#Ixy1505@5hI`o$HS6U+#1^%!jYm zS&0Rg?DI0|uWk`r`yk-EAm5esGS@zG=^q`{brpxh88#CDPquXB%7>h`h(#^i>k|{=HU>)Ev`$P%+os?yy>+ z#2n)>>kKI|=e=#-ldsDV#vsSNwl=)$1)_VOvlpxMXbR8Iue`uGZ}{^WXdO7K_=rNz zGaJk69O!1YyL{`(}CPh{qsd2sq-bWM-c6ZQL9ErT2UPER_^As;e zv&`yE`@7f9ucA}KuV+{p&VH-X1gh)g8u`zk2;|8!(Xt&q+_PuTSY=BFA)7wlrK;Hc zE=}qVsZ6F!{O#`PpOMzDOBPs{7U@#_+3gNU+$qeJvvW^=j)?jCnOP3= zC*?`7$&^QQ>Pl0lXBaYd~;0@;;J9vs7{(y*Avu z1xE|R&!5_8JwXPe=BKKl&SjB~KZT&H#>%NT6?VmGcb~uF^z=x7zot{#v%C~rkTZgk zi^dVz(d2bGZVY^`a}14O?ujOyD*>v%vGymV{)IacOv>5O6_-3&!X4+VXdh!Kz7QYS zWAeud&gvTq>l5z3s>DKooao4Aqk$pganF-17J;Rw@2(tMfm`Y*|2bO!1)6}a7JY6< zYC2)Na>~dib84dJwKoH>YE`$y)1@11y4NeeG`h$^1(RTT{hO0-a*3Da1b>ECbVWmvKQ_@yU41#%foSgLa+ZXk z%tW95WGf9wIMHoJ%I#ZNq{81Lk39(Z#!1iq&tk&|CVq4UPevzg2Q15Cj>?!!Vbc0T z%{>dDE~4;?_`*o3Y?yLIMO^&Jbnrr?q@@MQqN^5mRP9)juTO$h9t++G3R-tDbxv=u zR@&*t?&v-LV-0WD_{`n3r_KbrD7`za+ra;AcVjiKg(GC)J(sf3*5ZBTgnXw+Lm@`S zT7FtUV-~ls5ud-Q2FcDboSClscLjyxYSp|scnus)TU(V1)dXdI59BnSh<|$`gsV6T8GZ>-GS;DiKn^?c=#D+| z8HUf(GYb9c;FQpj!XM&oat05E{m0sot2}CrKL&+>T`L@~)4YXxsfF(_+670+%)}I$ zr!DZM!fvvezwJe90F~m{yI>(3S7Q?sb1FXD+=0Q*zWRovb#BI1UD+im%7*;*=xXu$ zd=H0U7OnNf&di6O24{A6Rx%dGBUCGIuT`_HxNqptK!THEE^xvcn)oTkayv_V{2HeO zx!(pA^1=F@Kc(E#U9E=RzwBaX(V=x)q$kVY{-ERO9xyvlCf?aWOMF3yB_>84ha8&( zML*C`TyuGtrEtyMQ|8(g{aCxyu?S`{l~pxV+l!S^oHelnl7CFVO&I{)U9t zXElj?f(KGC$)sOT=8`kpHtP@(wP}h|m#$VV3=Xy^AT^Zt%wZO^k{)!POt3PCDvQN6 z>kfYeuI)4s=Q1NuzV;g-P|a5g4XqpqZplzl4ZILF|9NT*Tr#nmwRZ6fe1);im{c8! zPdsmxVyAC!y1Q?z6lWLcIELECp4ghN8nwmEs#m!ED#}{v7^M@-8;~++iM?1>Gc3ga zFj&-{rhi!Gbt?$~Oe8DkoM`j4tL{SAVm4FLD+U9lZ3)hZ`$Znxo6YELp(?rfZ6Wuk z8!Nhd{Ve#|{yo$1W~S&Q9N#$?XZBxB{*%yc>q&Lk;B+rX2OmYd-Md6|4NB5{dqZxl zu%0%mE>L6)w10|R*Vy=+mtIUNB(1?|pW9f#k$ySXdN@^Y)MGhrxRFdw(3Hkg?P`u{ zJ}0_NqwF+qq4x@9Q-iwkMyRPR>*W#WjKev-?5oir>Ho*oYldEYl?5)V0)mMy!_bb! z1EblpNyzo@_Jdc#i-aNPiK>SKX!ciXyhsRS)<1u?=aKWDF*IbCdhj5$yqp$?gj^F* zN|$08Cq57a>Pvjrruj?YbRd=THLABnva~mK$XrK0C#7kYMW5}#wm;Y4NY`JU2!xUk z{D1xA*BxDYdKsBer0sZP%5H^<8Maa;ZfSh+he)-~N5OrD0g{nQ;Oae_p42Pe|GeMj zgvkUncvQfzX}>}+P)f{N8q36G_m@J*$FmPBDlSRguS*S$Y}oqcKmPHLOPo&tj8hy$C8#|@eR%(T!CJOD5psT1_wfFQ z+7cmLnU1iymNC*YjCyo8okcx#Zgp*Z+W7_88O$&9%Bepuz<^o5A>>1 z$w}&!P%pDWa_ROHA%{Sx@J-ehHNqv@;#+0yqypj=2NP3&HcOQzux*{QAj}}kA=fu< zjgJJSmRKRf)+15=&CM)19dnP{hxEV`IWp}%u|&;wFzWEs0L#9Mx`Xh%z3*9f%MU{$ zBOIz|KGz#&8?(<`>A1==do0Nlg*%VY(f2+rC z>&C{%H4vY#Kdrrb9rvcUSG9R-@yG79filk@p)NAfCeNCyw zz?!>|9%}w`gr&mf$1{{E8NHa8>KO3#jH(sV8f`b{%Hobf+9^qMyK0DdJ>Rm6N$Jl| zR}4Edl`uWc!$cz^l}%8VGF=X98s zCW}fy3l|knRtZh)K|%kugvsHsiX{of&hLNhqe;Tj%}t@99mA6Mw9ABC8=%_yY(6+s zqa-_5CxN*uTbUQ}1w~19z*{C2PPTx1HBQvw_3r=qh7)JK7Z*#O3%jcT+*hv#wF4>a{TprX4blMo9{j1Q-}YbL zI`E4G5U>zM2ORtVf9fLy+brijd2j&-pXI=<{_oNLV-Wv0jM|4?0?ink)m0y_jzjQI NLQGaP@7|+l{|`XQYU}_2 literal 0 HcmV?d00001 From 3008c6c2062da414eaad2795872b1a111df2e590 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Wed, 11 Dec 2024 13:18:31 -0600 Subject: [PATCH 019/129] chore: rename folder name --- requirements.txt | 2 +- {btcopilot => webgenie}/__init__.py | 0 {btcopilot => webgenie}/api/__init__.py | 0 {btcopilot => webgenie}/api/dummy.py | 0 {btcopilot => webgenie}/api/get_query_axons.py | 0 {btcopilot => webgenie}/base/__init__.py | 0 {btcopilot => webgenie}/base/miner.py | 0 {btcopilot => webgenie}/base/neuron.py | 0 {btcopilot => webgenie}/base/utils/__init__.py | 0 {btcopilot => webgenie}/base/utils/weight_utils.py | 0 {btcopilot => webgenie}/base/validator.py | 0 {btcopilot => webgenie}/miners/dummy_miner.py | 0 {btcopilot => webgenie}/mock.py | 0 {btcopilot => webgenie}/protocol.py | 0 {btcopilot => webgenie}/rewards/__init__.py | 0 {btcopilot => webgenie}/rewards/gpt.py | 0 {btcopilot => webgenie}/rewards/is_valid.py | 0 {btcopilot => webgenie}/rewards/reward.py | 0 {btcopilot => webgenie}/rewards/reward_manager.py | 0 {btcopilot => webgenie}/rewards/speed.py | 0 {btcopilot => webgenie}/solution/__init__.py | 0 {btcopilot => webgenie}/solution/solution.py | 0 {btcopilot => webgenie}/subnet_links.py | 0 {btcopilot => webgenie}/task_generator/__init__.py | 0 {btcopilot => webgenie}/task_generator/task_generator.py | 0 {btcopilot => webgenie}/tasks/__init__.py | 0 {btcopilot => webgenie}/tasks/task.py | 0 {btcopilot => webgenie}/utils/__init__.py | 0 {btcopilot => webgenie}/utils/config.py | 0 {btcopilot => webgenie}/utils/logging.py | 0 {btcopilot => webgenie}/utils/misc.py | 0 {btcopilot => webgenie}/utils/uids.py | 0 {btcopilot => webgenie}/validator/__init__.py | 0 {btcopilot => webgenie}/validator/forward.py | 0 {btcopilot => webgenie}/validator/organic_forward.py | 0 {btcopilot => webgenie}/validator/reward.py | 0 36 files changed, 1 insertion(+), 1 deletion(-) rename {btcopilot => webgenie}/__init__.py (100%) rename {btcopilot => webgenie}/api/__init__.py (100%) rename {btcopilot => webgenie}/api/dummy.py (100%) rename {btcopilot => webgenie}/api/get_query_axons.py (100%) rename {btcopilot => webgenie}/base/__init__.py (100%) rename {btcopilot => webgenie}/base/miner.py (100%) rename {btcopilot => webgenie}/base/neuron.py (100%) rename {btcopilot => webgenie}/base/utils/__init__.py (100%) rename {btcopilot => webgenie}/base/utils/weight_utils.py (100%) rename {btcopilot => webgenie}/base/validator.py (100%) rename {btcopilot => webgenie}/miners/dummy_miner.py (100%) rename {btcopilot => webgenie}/mock.py (100%) rename {btcopilot => webgenie}/protocol.py (100%) rename {btcopilot => webgenie}/rewards/__init__.py (100%) rename {btcopilot => webgenie}/rewards/gpt.py (100%) rename {btcopilot => webgenie}/rewards/is_valid.py (100%) rename {btcopilot => webgenie}/rewards/reward.py (100%) rename {btcopilot => webgenie}/rewards/reward_manager.py (100%) rename {btcopilot => webgenie}/rewards/speed.py (100%) rename {btcopilot => webgenie}/solution/__init__.py (100%) rename {btcopilot => webgenie}/solution/solution.py (100%) rename {btcopilot => webgenie}/subnet_links.py (100%) rename {btcopilot => webgenie}/task_generator/__init__.py (100%) rename {btcopilot => webgenie}/task_generator/task_generator.py (100%) rename {btcopilot => webgenie}/tasks/__init__.py (100%) rename {btcopilot => webgenie}/tasks/task.py (100%) rename {btcopilot => webgenie}/utils/__init__.py (100%) rename {btcopilot => webgenie}/utils/config.py (100%) rename {btcopilot => webgenie}/utils/logging.py (100%) rename {btcopilot => webgenie}/utils/misc.py (100%) rename {btcopilot => webgenie}/utils/uids.py (100%) rename {btcopilot => webgenie}/validator/__init__.py (100%) rename {btcopilot => webgenie}/validator/forward.py (100%) rename {btcopilot => webgenie}/validator/organic_forward.py (100%) rename {btcopilot => webgenie}/validator/reward.py (100%) diff --git a/requirements.txt b/requirements.txt index 8e5192d3..ea719aa4 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -bittensor>=7 +bittensor==6.9.3 starlette>=0.30.0 pydantic>=2 rich>=13 diff --git a/btcopilot/__init__.py b/webgenie/__init__.py similarity index 100% rename from btcopilot/__init__.py rename to webgenie/__init__.py diff --git a/btcopilot/api/__init__.py b/webgenie/api/__init__.py similarity index 100% rename from btcopilot/api/__init__.py rename to webgenie/api/__init__.py diff --git a/btcopilot/api/dummy.py b/webgenie/api/dummy.py similarity index 100% rename from btcopilot/api/dummy.py rename to webgenie/api/dummy.py diff --git a/btcopilot/api/get_query_axons.py b/webgenie/api/get_query_axons.py similarity index 100% rename from btcopilot/api/get_query_axons.py rename to webgenie/api/get_query_axons.py diff --git a/btcopilot/base/__init__.py b/webgenie/base/__init__.py similarity index 100% rename from btcopilot/base/__init__.py rename to webgenie/base/__init__.py diff --git a/btcopilot/base/miner.py b/webgenie/base/miner.py similarity index 100% rename from btcopilot/base/miner.py rename to webgenie/base/miner.py diff --git a/btcopilot/base/neuron.py b/webgenie/base/neuron.py similarity index 100% rename from btcopilot/base/neuron.py rename to webgenie/base/neuron.py diff --git a/btcopilot/base/utils/__init__.py b/webgenie/base/utils/__init__.py similarity index 100% rename from btcopilot/base/utils/__init__.py rename to webgenie/base/utils/__init__.py diff --git a/btcopilot/base/utils/weight_utils.py b/webgenie/base/utils/weight_utils.py similarity index 100% rename from btcopilot/base/utils/weight_utils.py rename to webgenie/base/utils/weight_utils.py diff --git a/btcopilot/base/validator.py b/webgenie/base/validator.py similarity index 100% rename from btcopilot/base/validator.py rename to webgenie/base/validator.py diff --git a/btcopilot/miners/dummy_miner.py b/webgenie/miners/dummy_miner.py similarity index 100% rename from btcopilot/miners/dummy_miner.py rename to webgenie/miners/dummy_miner.py diff --git a/btcopilot/mock.py b/webgenie/mock.py similarity index 100% rename from btcopilot/mock.py rename to webgenie/mock.py diff --git a/btcopilot/protocol.py b/webgenie/protocol.py similarity index 100% rename from btcopilot/protocol.py rename to webgenie/protocol.py diff --git a/btcopilot/rewards/__init__.py b/webgenie/rewards/__init__.py similarity index 100% rename from btcopilot/rewards/__init__.py rename to webgenie/rewards/__init__.py diff --git a/btcopilot/rewards/gpt.py b/webgenie/rewards/gpt.py similarity index 100% rename from btcopilot/rewards/gpt.py rename to webgenie/rewards/gpt.py diff --git a/btcopilot/rewards/is_valid.py b/webgenie/rewards/is_valid.py similarity index 100% rename from btcopilot/rewards/is_valid.py rename to webgenie/rewards/is_valid.py diff --git a/btcopilot/rewards/reward.py b/webgenie/rewards/reward.py similarity index 100% rename from btcopilot/rewards/reward.py rename to webgenie/rewards/reward.py diff --git a/btcopilot/rewards/reward_manager.py b/webgenie/rewards/reward_manager.py similarity index 100% rename from btcopilot/rewards/reward_manager.py rename to webgenie/rewards/reward_manager.py diff --git a/btcopilot/rewards/speed.py b/webgenie/rewards/speed.py similarity index 100% rename from btcopilot/rewards/speed.py rename to webgenie/rewards/speed.py diff --git a/btcopilot/solution/__init__.py b/webgenie/solution/__init__.py similarity index 100% rename from btcopilot/solution/__init__.py rename to webgenie/solution/__init__.py diff --git a/btcopilot/solution/solution.py b/webgenie/solution/solution.py similarity index 100% rename from btcopilot/solution/solution.py rename to webgenie/solution/solution.py diff --git a/btcopilot/subnet_links.py b/webgenie/subnet_links.py similarity index 100% rename from btcopilot/subnet_links.py rename to webgenie/subnet_links.py diff --git a/btcopilot/task_generator/__init__.py b/webgenie/task_generator/__init__.py similarity index 100% rename from btcopilot/task_generator/__init__.py rename to webgenie/task_generator/__init__.py diff --git a/btcopilot/task_generator/task_generator.py b/webgenie/task_generator/task_generator.py similarity index 100% rename from btcopilot/task_generator/task_generator.py rename to webgenie/task_generator/task_generator.py diff --git a/btcopilot/tasks/__init__.py b/webgenie/tasks/__init__.py similarity index 100% rename from btcopilot/tasks/__init__.py rename to webgenie/tasks/__init__.py diff --git a/btcopilot/tasks/task.py b/webgenie/tasks/task.py similarity index 100% rename from btcopilot/tasks/task.py rename to webgenie/tasks/task.py diff --git a/btcopilot/utils/__init__.py b/webgenie/utils/__init__.py similarity index 100% rename from btcopilot/utils/__init__.py rename to webgenie/utils/__init__.py diff --git a/btcopilot/utils/config.py b/webgenie/utils/config.py similarity index 100% rename from btcopilot/utils/config.py rename to webgenie/utils/config.py diff --git a/btcopilot/utils/logging.py b/webgenie/utils/logging.py similarity index 100% rename from btcopilot/utils/logging.py rename to webgenie/utils/logging.py diff --git a/btcopilot/utils/misc.py b/webgenie/utils/misc.py similarity index 100% rename from btcopilot/utils/misc.py rename to webgenie/utils/misc.py diff --git a/btcopilot/utils/uids.py b/webgenie/utils/uids.py similarity index 100% rename from btcopilot/utils/uids.py rename to webgenie/utils/uids.py diff --git a/btcopilot/validator/__init__.py b/webgenie/validator/__init__.py similarity index 100% rename from btcopilot/validator/__init__.py rename to webgenie/validator/__init__.py diff --git a/btcopilot/validator/forward.py b/webgenie/validator/forward.py similarity index 100% rename from btcopilot/validator/forward.py rename to webgenie/validator/forward.py diff --git a/btcopilot/validator/organic_forward.py b/webgenie/validator/organic_forward.py similarity index 100% rename from btcopilot/validator/organic_forward.py rename to webgenie/validator/organic_forward.py diff --git a/btcopilot/validator/reward.py b/webgenie/validator/reward.py similarity index 100% rename from btcopilot/validator/reward.py rename to webgenie/validator/reward.py From d541dc97502b3b5265f637c8a1669fb8f1554c84 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Wed, 11 Dec 2024 14:24:57 -0600 Subject: [PATCH 020/129] chore: refactor packagename --- contrib/CONTRIBUTING.md | 6 +++--- neurons/miner.py | 18 +++++++++--------- neurons/validator.py | 12 ++++++------ requirements.txt | 2 +- run_miner.sh | 3 ++- run_validator.sh | 2 +- setup.py | 10 +++++----- tests/test_template_validator.py | 8 ++++---- webgenie/__init__.py | 2 +- webgenie/api/dummy.py | 2 +- webgenie/base/miner.py | 4 ++-- webgenie/base/neuron.py | 8 ++++---- webgenie/base/validator.py | 16 ++++++++-------- webgenie/miners/dummy_miner.py | 4 ++-- webgenie/protocol.py | 8 ++++---- webgenie/rewards/gpt.py | 6 +++--- webgenie/rewards/is_valid.py | 6 +++--- webgenie/rewards/reward_manager.py | 12 ++++++------ webgenie/rewards/speed.py | 6 +++--- webgenie/task_generator/task_generator.py | 2 +- webgenie/tasks/task.py | 2 +- webgenie/validator/forward.py | 12 ++++++------ webgenie/validator/organic_forward.py | 6 +++--- 23 files changed, 79 insertions(+), 78 deletions(-) diff --git a/contrib/CONTRIBUTING.md b/contrib/CONTRIBUTING.md index 2393c621..9e98a31f 100644 --- a/contrib/CONTRIBUTING.md +++ b/contrib/CONTRIBUTING.md @@ -1,6 +1,6 @@ -# Contributing to BTCopilot +# Contributing to webgenie -The following is a set of guidelines for contributing to the BTCopilot Subnet. 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. +The following is a set of guidelines for contributing to the webgenie Subnet. 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) @@ -99,7 +99,7 @@ After you submit a pull request, it will be reviewed by the maintainers. They ma > Note: Be sure to merge the latest from "upstream" before making a pull request: ```bash -git remote add upstream https://github.com/BTCopilot/btcopilot.git +git remote add upstream https://github.com/webgenie/webgenie.git git fetch upstream git merge upstream/ git push origin diff --git a/neurons/miner.py b/neurons/miner.py index 363df6e9..9098a42f 100644 --- a/neurons/miner.py +++ b/neurons/miner.py @@ -23,10 +23,10 @@ import bittensor as bt # Bittensor Miner Template: -import btcopilot +import webgenie # import base miner class which takes care of most of the boilerplate -from btcopilot.base.miner import BaseMinerNeuron +from webgenie.base.miner import BaseMinerNeuron class Miner(BaseMinerNeuron): @@ -42,7 +42,7 @@ def __init__(self, config=None): super(Miner, self).__init__(config=config) miner_name = "dummy_miner" - miner_module = importlib.import_module(f"btcopilot.miners.{miner_name}") + miner_module = importlib.import_module(f"webgenie.miners.{miner_name}") self.miner_init = miner_module.miner_init self.miner_forward = miner_module.miner_forward @@ -50,14 +50,14 @@ def __init__(self, config=None): self.miner_init(self) async def forward( - self, synapse: btcopilot.protocol.BtCopilotSynapse - ) -> btcopilot.protocol.BtCopilotSynapse: + self, synapse: webgenie.protocol.webgenieSynapse + ) -> webgenie.protocol.webgenieSynapse: bt.logging.debug(f"Miner forward called with synapse: {synapse}") return self.miner_forward(self, synapse) async def blacklist( - self, synapse: btcopilot.protocol.BtCopilotSynapse + self, synapse: webgenie.protocol.webgenieSynapse ) -> typing.Tuple[bool, str]: """ Determines whether an incoming request should be blacklisted and thus ignored. Your implementation should @@ -68,7 +68,7 @@ async def blacklist( requests before they are deserialized to avoid wasting resources on requests that will be ignored. Args: - synapse (template.protocol.BtCopilotSynapse): A synapse object constructed from the headers of the incoming request. + synapse (template.protocol.webgenieSynapse): 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, @@ -118,7 +118,7 @@ async def blacklist( ) return False, "Hotkey recognized!" - async def priority(self, synapse: btcopilot.protocol.BtCopilotSynapse) -> float: + async def priority(self, synapse: webgenie.protocol.webgenieSynapse) -> 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. @@ -126,7 +126,7 @@ async def priority(self, synapse: btcopilot.protocol.BtCopilotSynapse) -> float: This implementation assigns priority to incoming requests based on the calling entity's stake in the metagraph. Args: - synapse (template.protocol.BtCopilotSynapse): The synapse object that contains metadata about the incoming request. + synapse (template.protocol.webgenieSynapse): The synapse object that contains metadata about the incoming request. Returns: float: A priority score derived from the stake of the calling entity. diff --git a/neurons/validator.py b/neurons/validator.py index cdb97877..8e375b3b 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -23,14 +23,14 @@ import bittensor as bt # import base validator class which takes care of most of the boilerplate -from btcopilot.base.validator import BaseValidatorNeuron +from webgenie.base.validator import BaseValidatorNeuron # Bittensor Validator Template: -from btcopilot.validator import forward -from btcopilot.validator import forward_organic_synapse +from webgenie.validator import forward +from webgenie.validator import forward_organic_synapse -from btcopilot.task_generator import TaskGenerator -from btcopilot.rewards import RewardManager -from btcopilot.protocol import BtCopilotSynapse +from webgenie.task_generator import TaskGenerator +from webgenie.rewards import RewardManager +from webgenie.protocol import webgenieSynapse class Validator(BaseValidatorNeuron): """ diff --git a/requirements.txt b/requirements.txt index ea719aa4..c8d4ef70 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -bittensor==6.9.3 +bittensor==7.4.0 starlette>=0.30.0 pydantic>=2 rich>=13 diff --git a/run_miner.sh b/run_miner.sh index bc7be661..eb48a80a 100644 --- a/run_miner.sh +++ b/run_miner.sh @@ -1,3 +1,4 @@ export PYTHONPATH=. #--axon.port 5555 -python3 neurons/miner.py --netuid 214 --subtensor.network test --wallet.name s-miner --wallet.hotkey miner1 --logging.debug --axon.port 8090 +python3.12 neurons/miner.py --netuid 214 --subtensor.network test --wallet.name s-miner --wallet.hotkey miner1 --logging.debug --axon.port 8090 + diff --git a/run_validator.sh b/run_validator.sh index d4b2a48c..723b580d 100644 --- a/run_validator.sh +++ b/run_validator.sh @@ -1,3 +1,3 @@ export PYTHONASYNCIODEBUG=1 export PYTHONPATH=. -python3 neurons/validator.py --netuid 214 --subtensor.network test --wallet.name sc-val1 --wallet.hotkey sh-val1 --logging.debug --neuron.axon_port 8091 +python3.12 neurons/validator.py --netuid 214 --subtensor.network test --wallet.name sc-val1 --wallet.hotkey sh-val1 --logging.debug --neuron.axon_port 8091 diff --git a/setup.py b/setup.py index 18ca652c..440a4ffa 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ # The MIT License (MIT) -# Copyright © 2023 BTCopilot +# Copyright © 2023 webgenie # Sangar -# Copyright © 2023 +# Copyright © 2023 # 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 @@ -63,12 +63,12 @@ def read_requirements(path): version_string = version_match.group(1) setup( - name="btcopilot", + name="webgenie", version=version_string, - description="BTCopilot aims to transform diverse prompts—ranging from text and voice to images and Figma designs—into fully functional, ready-to-deploy projects.", + description="webgenie aims to transform diverse prompts—ranging from text and voice to images and Figma designs—into fully functional, ready-to-deploy projects.", long_description=long_description, long_description_content_type="text/markdown", - url="https://github.com/BTCopilot/btcopilot", + url="https://github.com/webgenie/webgenie", author="Sangar", packages=find_packages(), include_package_data=True, diff --git a/tests/test_template_validator.py b/tests/test_template_validator.py index 92f82d36..021318e7 100644 --- a/tests/test_template_validator.py +++ b/tests/test_template_validator.py @@ -23,10 +23,10 @@ import torch from neurons.validator import Validator -from btcopilot.base.validator import BaseValidatorNeuron -from btcopilot.protocol import Dummy -from btcopilot.utils.uids import get_random_uids -from btcopilot.validator.reward import get_rewards +from webgenie.base.validator import BaseValidatorNeuron +from webgenie.protocol import Dummy +from webgenie.utils.uids import get_random_uids +from webgenie.validator.reward import get_rewards class TemplateValidatorNeuronTestCase(unittest.TestCase): diff --git a/webgenie/__init__.py b/webgenie/__init__.py index 4115dac0..41bb1b50 100644 --- a/webgenie/__init__.py +++ b/webgenie/__init__.py @@ -18,7 +18,7 @@ # DEALINGS IN THE SOFTWARE. # Change this value when updating your code base. -# Define the version of the btcopilot. +# Define the version of the webgenie. __version__ = "0.0.1" version_split = __version__.split(".") __spec_version__ = ( diff --git a/webgenie/api/dummy.py b/webgenie/api/dummy.py index b1d0e13b..2845f6b5 100644 --- a/webgenie/api/dummy.py +++ b/webgenie/api/dummy.py @@ -19,7 +19,7 @@ import bittensor as bt from typing import List, Optional, Union, Any, Dict -from btcopilot.protocol import Dummy +from webgenie.protocol import Dummy from bittensor.subnets import SubnetsAPI diff --git a/webgenie/base/miner.py b/webgenie/base/miner.py index 5f3e3402..999b489e 100644 --- a/webgenie/base/miner.py +++ b/webgenie/base/miner.py @@ -23,8 +23,8 @@ import bittensor as bt -from btcopilot.base.neuron import BaseNeuron -from btcopilot.utils.config import add_miner_args +from webgenie.base.neuron import BaseNeuron +from webgenie.utils.config import add_miner_args from typing import Union diff --git a/webgenie/base/neuron.py b/webgenie/base/neuron.py index 8c41320a..1eed9a77 100644 --- a/webgenie/base/neuron.py +++ b/webgenie/base/neuron.py @@ -23,10 +23,10 @@ from abc import ABC, abstractmethod # Sync calls set weights and also resyncs the metagraph. -from btcopilot.utils.config import check_config, add_args, config -from btcopilot.utils.misc import ttl_get_block -from btcopilot import __spec_version__ as spec_version -from btcopilot.mock import MockSubtensor, MockMetagraph +from webgenie.utils.config import check_config, add_args, config +from webgenie.utils.misc import ttl_get_block +from webgenie import __spec_version__ as spec_version +from webgenie.mock import MockSubtensor, MockMetagraph class BaseNeuron(ABC): diff --git a/webgenie/base/validator.py b/webgenie/base/validator.py index 70872c54..56f4cf6a 100644 --- a/webgenie/base/validator.py +++ b/webgenie/base/validator.py @@ -27,15 +27,15 @@ from typing import List, Union, Tuple from traceback import print_exception -from btcopilot.base.neuron import BaseNeuron -from btcopilot.base.utils.weight_utils import ( +from webgenie.base.neuron import BaseNeuron +from webgenie.base.utils.weight_utils import ( process_weights_for_netuid, convert_weights_and_uids_for_emit, ) # TODO: Replace when bittensor switches to numpy -from btcopilot.mock import MockDendrite -from btcopilot.utils.config import add_validator_args -from btcopilot.protocol import BtCopilotSynapse -from btcopilot.validator.organic_forward import forward_organic_synapse +from webgenie.mock import MockDendrite +from webgenie.utils.config import add_validator_args +from webgenie.protocol import webgenieSynapse +from webgenie.validator.organic_forward import forward_organic_synapse SUBNET_OWNER_HOTKEY = "5G9sRcoaw2H3SYDq7e7PoGhbbMUPHQi6pC6tPrahmSmDtxS8" @@ -51,14 +51,14 @@ def add_args(cls, parser: argparse.ArgumentParser): super().add_args(parser) add_validator_args(cls, parser) - async def organic_forward(self, synapse: BtCopilotSynapse) -> BtCopilotSynapse: + async def organic_forward(self, synapse: webgenieSynapse) -> webgenieSynapse: bt.logging.error(f"OrganicForward Thread Name: {threading.current_thread().name}") bt.logging.info(f"=========> {synapse}") return await forward_organic_synapse(self, synapse) - async def blacklist(self, synapse: BtCopilotSynapse) -> Tuple[bool, str]: + async def blacklist(self, synapse: webgenieSynapse) -> Tuple[bool, str]: """ Only allow the subnet owner to send synapse to the validator. """ diff --git a/webgenie/miners/dummy_miner.py b/webgenie/miners/dummy_miner.py index 3c9e2ba0..05d41bba 100644 --- a/webgenie/miners/dummy_miner.py +++ b/webgenie/miners/dummy_miner.py @@ -12,7 +12,7 @@ from langchain_core.runnables.base import RunnableSequence import bittensor as bt -import btcopilot +import webgenie import os def miner_init(self): @@ -25,7 +25,7 @@ def miner_init(self): model_name="gpt-4", ) -def miner_forward(self, synapse: btcopilot.protocol.BtCopilotSynapse)->Awaitable: +def miner_forward(self, synapse: webgenie.protocol.webgenieSynapse)->Awaitable: async def _forward(self, chain: RunnableSequence, chain_formatter: Dict[str, str], timeout_threshold: float, init_time: float, send: Send): try: diff --git a/webgenie/protocol.py b/webgenie/protocol.py index 8b55edf2..cac1a840 100644 --- a/webgenie/protocol.py +++ b/webgenie/protocol.py @@ -22,12 +22,12 @@ import bittensor as bt -from btcopilot.tasks import Task -from btcopilot.solution import Solution +from webgenie.tasks import Task +from webgenie.solution import Solution -class BtCopilotSynapse(bt.StreamingSynapse): +class webgenieSynapse(bt.StreamingSynapse): """ - A protocol for the BtCopilot. + A protocol for the webgenie. """ task: Union[Task, None] = pydantic.Field( diff --git a/webgenie/rewards/gpt.py b/webgenie/rewards/gpt.py index 9afe5971..056fc1c8 100644 --- a/webgenie/rewards/gpt.py +++ b/webgenie/rewards/gpt.py @@ -3,9 +3,9 @@ from langchain_openai import ChatOpenAI import bittensor as bt -from btcopilot.rewards import Reward -from btcopilot.tasks import Task -from btcopilot.solution import Solution +from webgenie.rewards import Reward +from webgenie.tasks import Task +from webgenie.solution import Solution class GPTReward(Reward): def __init__(self): diff --git a/webgenie/rewards/is_valid.py b/webgenie/rewards/is_valid.py index 2e334949..4a065508 100644 --- a/webgenie/rewards/is_valid.py +++ b/webgenie/rewards/is_valid.py @@ -1,9 +1,9 @@ from bs4 import BeautifulSoup import tinycss2 -from btcopilot.rewards import Reward -from btcopilot.tasks import Task -from btcopilot.solution import Solution +from webgenie.rewards import Reward +from webgenie.tasks import Task +from webgenie.solution import Solution class IsValidReward(Reward): diff --git a/webgenie/rewards/reward_manager.py b/webgenie/rewards/reward_manager.py index a50daa52..f68d255a 100644 --- a/webgenie/rewards/reward_manager.py +++ b/webgenie/rewards/reward_manager.py @@ -1,11 +1,11 @@ from typing import Dict, List, Tuple -from btcopilot.rewards import Reward -from btcopilot.rewards import GPTReward -from btcopilot.rewards import SpeedReward -from btcopilot.rewards import IsValidReward -from btcopilot.tasks import Task -from btcopilot.solution import Solution +from webgenie.rewards import Reward +from webgenie.rewards import GPTReward +from webgenie.rewards import SpeedReward +from webgenie.rewards import IsValidReward +from webgenie.tasks import Task +from webgenie.solution import Solution class RewardManager: """ diff --git a/webgenie/rewards/speed.py b/webgenie/rewards/speed.py index 0fd47655..d9e905ff 100644 --- a/webgenie/rewards/speed.py +++ b/webgenie/rewards/speed.py @@ -1,7 +1,7 @@ -from btcopilot.rewards import Reward -from btcopilot.tasks import Task -from btcopilot.solution import Solution +from webgenie.rewards import Reward +from webgenie.tasks import Task +from webgenie.solution import Solution class SpeedReward(Reward): diff --git a/webgenie/task_generator/task_generator.py b/webgenie/task_generator/task_generator.py index cee9aa7a..9f0c15a3 100644 --- a/webgenie/task_generator/task_generator.py +++ b/webgenie/task_generator/task_generator.py @@ -1,5 +1,5 @@ -from btcopilot.tasks import Task +from webgenie.tasks import Task class TaskGenerator: """ A singleton generator for tasks. diff --git a/webgenie/tasks/task.py b/webgenie/tasks/task.py index 39ee892f..3caa3274 100644 --- a/webgenie/tasks/task.py +++ b/webgenie/tasks/task.py @@ -2,7 +2,7 @@ from dataclasses import dataclass from typing import List from pydantic import BaseModel -from btcopilot.solution import Solution +from webgenie.solution import Solution class Task(BaseModel): query: str diff --git a/webgenie/validator/forward.py b/webgenie/validator/forward.py index c8fd63eb..d73e9873 100644 --- a/webgenie/validator/forward.py +++ b/webgenie/validator/forward.py @@ -22,10 +22,10 @@ from typing import Awaitable, List from dataclasses import dataclass -from btcopilot.protocol import BtCopilotSynapse -from btcopilot.validator.reward import get_rewards -from btcopilot.utils.uids import get_random_uids -from btcopilot.solution import Solution +from webgenie.protocol import webgenieSynapse +from webgenie.validator.reward import get_rewards +from webgenie.utils.uids import get_random_uids +from webgenie.solution import Solution from .organic_forward import forward_organic_synapse async def process_response(uid: int, async_generator: Awaitable): @@ -41,7 +41,7 @@ async def process_response(uid: int, async_generator: Awaitable): bt.logging.info(f"==============8") if chunk is not None: synapse = chunk - if isinstance(synapse, BtCopilotSynapse): + if isinstance(synapse, webgenieSynapse): if synapse.dendrite.status_code == 200: synapse.solution.miner_uid = uid return synapse.solution @@ -84,7 +84,7 @@ async def forward(self): task = self.task_generator.next_task() - synapse = BtCopilotSynapse( + synapse = webgenieSynapse( task=task ) diff --git a/webgenie/validator/organic_forward.py b/webgenie/validator/organic_forward.py index db31ba39..c7965f81 100644 --- a/webgenie/validator/organic_forward.py +++ b/webgenie/validator/organic_forward.py @@ -4,10 +4,10 @@ from starlette.types import Send import bittensor as bt -from btcopilot.protocol import BtCopilotSynapse +from webgenie.protocol import webgenieSynapse -async def forward_organic_synapse(self, synapse: BtCopilotSynapse)->BtCopilotSynapse: - async def forward_miner(synapse: BtCopilotSynapse, send: Send): +async def forward_organic_synapse(self, synapse: webgenieSynapse)->webgenieSynapse: + async def forward_miner(synapse: webgenieSynapse, send: Send): bt.logging.info(f"Send Synapse to miner: {synapse}") async def handle_miner_response(responses): for resp in responses: From abd64fb3f53ae80b2177eadee9c81c90e4d9a3d3 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Wed, 11 Dec 2024 14:27:48 -0600 Subject: [PATCH 021/129] chore: refactor folder structure --- neurons/{ => miners}/miner.py | 0 neurons/{ => validators}/validator.py | 0 run_miner.sh | 2 +- run_validator.sh | 2 +- 4 files changed, 2 insertions(+), 2 deletions(-) rename neurons/{ => miners}/miner.py (100%) rename neurons/{ => validators}/validator.py (100%) diff --git a/neurons/miner.py b/neurons/miners/miner.py similarity index 100% rename from neurons/miner.py rename to neurons/miners/miner.py diff --git a/neurons/validator.py b/neurons/validators/validator.py similarity index 100% rename from neurons/validator.py rename to neurons/validators/validator.py diff --git a/run_miner.sh b/run_miner.sh index eb48a80a..0d2d9538 100644 --- a/run_miner.sh +++ b/run_miner.sh @@ -1,4 +1,4 @@ export PYTHONPATH=. #--axon.port 5555 -python3.12 neurons/miner.py --netuid 214 --subtensor.network test --wallet.name s-miner --wallet.hotkey miner1 --logging.debug --axon.port 8090 +python3.12 neurons/miners/miner.py --netuid 214 --subtensor.network test --wallet.name s-miner --wallet.hotkey miner1 --logging.debug --axon.port 8090 diff --git a/run_validator.sh b/run_validator.sh index 723b580d..508d7d76 100644 --- a/run_validator.sh +++ b/run_validator.sh @@ -1,3 +1,3 @@ export PYTHONASYNCIODEBUG=1 export PYTHONPATH=. -python3.12 neurons/validator.py --netuid 214 --subtensor.network test --wallet.name sc-val1 --wallet.hotkey sh-val1 --logging.debug --neuron.axon_port 8091 +python3.12 neurons/validators/validator.py --netuid 214 --subtensor.network test --wallet.name sc-val1 --wallet.hotkey sh-val1 --logging.debug --neuron.axon_port 8091 From 170271599a3d0c23358460171e4b87b9e2a5aae7 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Wed, 11 Dec 2024 20:27:15 -0600 Subject: [PATCH 022/129] feat: restructure architecture --- neurons/miners/genie_miner.py | 0 neurons/miners/miner.py | 8 +- neurons/validators/genie_validator.py | 95 +++++++++++ neurons/validators/validator.py | 105 +++++++------ webgenie/base/validator.py | 148 ++---------------- webgenie/miners/dummy_miner.py | 2 +- webgenie/protocol.py | 58 ++++--- webgenie/solution/solution.py | 1 - .../task_generator/image_task_generator.py | 21 +++ webgenie/task_generator/task_generator.py | 11 +- .../task_generator/text_task_generator.py | 21 +++ webgenie/tasks/task.py | 26 +-- webgenie/validator/__init__.py | 3 - webgenie/validator/forward.py | 117 -------------- webgenie/validator/organic_forward.py | 45 ------ webgenie/validator/reward.py | 54 ------- 16 files changed, 274 insertions(+), 441 deletions(-) create mode 100644 neurons/miners/genie_miner.py create mode 100644 neurons/validators/genie_validator.py create mode 100644 webgenie/task_generator/image_task_generator.py create mode 100644 webgenie/task_generator/text_task_generator.py delete mode 100644 webgenie/validator/__init__.py delete mode 100644 webgenie/validator/forward.py delete mode 100644 webgenie/validator/organic_forward.py delete mode 100644 webgenie/validator/reward.py diff --git a/neurons/miners/genie_miner.py b/neurons/miners/genie_miner.py new file mode 100644 index 00000000..e69de29b diff --git a/neurons/miners/miner.py b/neurons/miners/miner.py index 9098a42f..ae3d52ee 100644 --- a/neurons/miners/miner.py +++ b/neurons/miners/miner.py @@ -50,14 +50,14 @@ def __init__(self, config=None): self.miner_init(self) async def forward( - self, synapse: webgenie.protocol.webgenieSynapse - ) -> webgenie.protocol.webgenieSynapse: + self, synapse: webgenie.protocol.WebgenieStreamingSynapse + ) -> webgenie.protocol.WebgenieStreamingSynapse: bt.logging.debug(f"Miner forward called with synapse: {synapse}") return self.miner_forward(self, synapse) async def blacklist( - self, synapse: webgenie.protocol.webgenieSynapse + self, synapse: webgenie.protocol.WebgenieStreamingSynapse ) -> typing.Tuple[bool, str]: """ Determines whether an incoming request should be blacklisted and thus ignored. Your implementation should @@ -118,7 +118,7 @@ async def blacklist( ) return False, "Hotkey recognized!" - async def priority(self, synapse: webgenie.protocol.webgenieSynapse) -> float: + async def priority(self, synapse: webgenie.protocol.WebgenieStreamingSynapse) -> 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. diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py new file mode 100644 index 00000000..f069b42b --- /dev/null +++ b/neurons/validators/genie_validator.py @@ -0,0 +1,95 @@ +import bittensor as bt +import random +import torch +from typing import List + +from webgenie.base.neuron import BaseNeuron +from webgenie.protocol import WebgenieStreamingSynapse, WebgenieTextSynapse +from webgenie.rewards import RewardManager +from webgenie.solution import Solution +from webgenie.task_generator.image_task_generator import ImageTaskGenerator +from webgenie.task_generator.text_task_generator import TextTaskGenerator +from webgenie.utils.uids import get_random_uids + + +MAX_SYNTHETIC_HISTORY_SIZE = 10 + +class GenieValidator: + def __init__(self, neuron: BaseNeuron): + self.neuron = neuron + self.config = neuron.config + self.synthetic_history = [] + + self.reward_manager = RewardManager() + self.task_generators = [ + TextTaskGenerator(), + ImageTaskGenerator(), + ] + + async def forward(self): + try: + if len(self.synthetic_history) > MAX_SYNTHETIC_HISTORY_SIZE: + return + + miner_uids = get_random_uids(self, k=self.config.neuron.sample_size) + + task, synapse = await random.choice(self.task_generators).generate_task() + + all_synapse_results = await self.neuron.dendrite( + axons = [self.metagraph.axons[uid] for uid in miner_uids], + synapse=synapse, + timeout=task.timeout + ) + + processed_synapses, processed_miner_uids = await self.process_synapses(all_synapse_results, miner_uids) + + if len(processed_synapses) == 0: + bt.logging.error(f"No valid synapses received") + return + bt.logging.debug(f"Processed synapses: {processed_synapses}") + + self.synthetic_history.append((task, processed_synapses, processed_miner_uids)) + except Exception as e: + bt.logging.error(f"Error in forward: {e}") + raise e + + async def process_synapses(self, synapses: List[WebgenieStreamingSynapse], miner_uids: List[int]): + processed_synapses = [] + processed_miner_uids = [] + + for synapse, miner_uid in zip(synapses, miner_uids): + if synapse.dendrite.status_code == 200: + processed_synapses.append(synapse) + processed_miner_uids.append(miner_uid) + + return processed_synapses, processed_miner_uids + + async def score(self): + if len(self.synthetic_history) == 0: + bt.logging.warning(f"No synthetic history to score") + return + + task, synapses, miner_uids = self.synthetic_history.pop(0) + + task_generator = task.generator + scores = await task_generator.reward(task, synapses) + self.neuron.update_scores(scores, miner_uids) + self.neuron.sync() + + async def organic_forward(self, synapse: WebgenieTextSynapse): + axon = self.metagraph.axons[1] + try: + async with bt.dendrite(wallet=self.wallet) as dendrite: + bt.logging.info(f"Dendrite: {dendrite}") + responses = await dendrite( + axons=[axon], + synapse=synapse, + timeout=synapse.timeout, + ) + return responses[0] + except Exception as e: + bt.logging.error(f"[forward_organic_synapse] Error querying dendrite: {e}") + synapse.solution = Solution( + html = "", + ) + return synapse \ No newline at end of file diff --git a/neurons/validators/validator.py b/neurons/validators/validator.py index 8e375b3b..743a6c51 100644 --- a/neurons/validators/validator.py +++ b/neurons/validators/validator.py @@ -1,36 +1,16 @@ # The MIT License (MIT) # Copyright © 2023 Yuma Rao -# Copyright © 2024 Sangar - -# 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. - - +# Copyright © 2024 pycorn +import asyncio +from dotenv import load_dotenv +load_dotenv() import time -from typing import Tuple -# Bittensor + import bittensor as bt -# import base validator class which takes care of most of the boilerplate from webgenie.base.validator import BaseValidatorNeuron -# Bittensor Validator Template: -from webgenie.validator import forward -from webgenie.validator import forward_organic_synapse - -from webgenie.task_generator import TaskGenerator -from webgenie.rewards import RewardManager -from webgenie.protocol import webgenieSynapse +from webgenie.protocol import WebgenieStreamingSynapse +from neurons.validators.genie_validator import GenieValidator class Validator(BaseValidatorNeuron): """ @@ -45,27 +25,64 @@ def __init__(self, config=None): super(Validator, self).__init__(config=config) bt.logging.info("load_state()") self.load_state() - self.reward_manager = RewardManager() - self.task_generator = TaskGenerator() + self.genie_validator = GenieValidator() + async def organic_forward(self, synapse: WebgenieStreamingSynapse): + return await self.genie_validator.organic_forward(synapse) async def forward(self): - """ - Validator forward pass. Consists of: - - Generating the query - - Querying the miners - - Getting the responses - - Rewarding the miners - - Updating the scores - """ - # TODO(developer): Rewrite this function based on your protocol definition. - return await forward(self) - + return await self.genie_validator.forward(self) + async def concurrent_forward(self): + coroutines = [ + self.forward() + for _ in range(self.config.neuron.num_concurrent_forwards) + ] + await asyncio.gather(*coroutines) + async def forward_loop(self): + self.sync() + bt.logging.info(f"Validator starting at block: {self.block}") + + while True: + try: + bt.logging.info(f"step({self.step}) block({self.block})") + self.loop.run_until_complete(self.concurrent_forward()) + self.sync() + self.step += 1 + except Exception as e: + bt.logging.error(f"Error during forward loop: {str(e)}") + + await asyncio.sleep(5) + + async def scoring_loop(self): + bt.logging.info(f"Scoring loop starting") + while True: + try: + await self.genie_validator.score() + except Exception as e: + bt.logging.error(f"Error during scoring: {str(e)}") + + await asyncio.sleep(5) + + async def __aenter__(self): + self.loop.create_task(self.forward_loop()) + self.loop.create_task(self.scoring_loop()) + self.is_running = True + bt.logging.debug("Starting validator in background thread") + return self + + async def __aexit__(self, exc_type, exc_value, traceback): + if self.is_running: + self.should_exit = True + self.is_running = False + bt.logging.debug("Stopping validator in background thread") + +async def main(): + async with Validator() as validator: + while validator.is_running and not validator.should_exit: + await asyncio.sleep(15) + # The main function parses the configuration and runs the validator. if __name__ == "__main__": - with Validator() as validator: - while True: - #bt.logging.info(f"Validator running... {time.time()}") - time.sleep(5) + asyncio.run(main()) diff --git a/webgenie/base/validator.py b/webgenie/base/validator.py index 56f4cf6a..437ac1a5 100644 --- a/webgenie/base/validator.py +++ b/webgenie/base/validator.py @@ -34,7 +34,7 @@ ) # TODO: Replace when bittensor switches to numpy from webgenie.mock import MockDendrite from webgenie.utils.config import add_validator_args -from webgenie.protocol import webgenieSynapse +from webgenie.protocol import WebgenieStreamingSynapse from webgenie.validator.organic_forward import forward_organic_synapse SUBNET_OWNER_HOTKEY = "5G9sRcoaw2H3SYDq7e7PoGhbbMUPHQi6pC6tPrahmSmDtxS8" @@ -50,27 +50,12 @@ class BaseValidatorNeuron(BaseNeuron): def add_args(cls, parser: argparse.ArgumentParser): super().add_args(parser) add_validator_args(cls, parser) - - async def organic_forward(self, synapse: webgenieSynapse) -> webgenieSynapse: - - bt.logging.error(f"OrganicForward Thread Name: {threading.current_thread().name}") - bt.logging.info(f"=========> {synapse}") - - return await forward_organic_synapse(self, synapse) - async def blacklist(self, synapse: webgenieSynapse) -> Tuple[bool, str]: - """ - Only allow the subnet owner to send synapse to the validator. - """ - if synapse.dendrite.hotkey == SUBNET_OWNER_HOTKEY: - return False, "Subnet owner hotkey" - return True, "Blacklisted" 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) - print("=== self.hotkeys ===>", self.hotkeys) # Dendrite lets us send messages to other nodes (axons) in the network. if self.config.mock: @@ -79,8 +64,6 @@ def __init__(self, config=None): 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) @@ -88,29 +71,26 @@ def __init__(self, config=None): # Init sync with the network. Updates the metagraph. self.sync() - threading.current_thread().name = "MainThread" - bt.logging.info(f"MainThread Name: {threading.current_thread().name}") # 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: Union[threading.Thread, None] = 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, port = self.config.neuron.axon_port) + self.axon = bt.axon(wallet=self.wallet, config=self.config) self.axon.attach( forward_fn = self.organic_forward, - blacklist_fn = self.blacklist, - # priority_fn = self.priority + blacklist_fn = self.blacklist ) self.axon.serve( netuid=self.config.netuid, @@ -122,118 +102,22 @@ def serve_axon(self): bt.logging.error(f"Failed to serve Axon with exception: {e}") pass - async def concurrent_forward(self): - bt.logging.error(f"concurrent_forward thread name{threading.current_thread().name}") - coroutines = [ - self.forward() for _ in range(self.config.neuron.num_concurrent_forwards) - ] - return await asyncio.gather(*coroutines) 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"Validator starting at block: {self.block}") - # This loop maintains the validator's operations until intentionally stopped. - try: - - if not self.config.neuron.axon_off: - self.serve_axon() - - while True: - - # bt.logging.info(f"step({self.step}) block({self.block})") - - # Run multiple forwards concurrently. - - self.loop.run_until_complete(self.concurrent_forward()) - + pass - # Check if we should exit. - if self.should_exit: - break - - # Sync metagraph and potentially set weights. - self.sync() - - self.step += 1 - - # If someone intentionally stops the validator, it'll safely terminate operations. - except KeyboardInterrupt: - if not self.config.neuron.axon_off: - 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(f"Error during validation: {str(err)}") - bt.logging.debug(str(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. + async def blacklist(self, synapse: WebgenieStreamingSynapse) -> Tuple[bool, str]: """ - 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, name = "BackgroundThread") - 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. + Only allow the subnet owner to send synapse to the validator. """ - 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") + if synapse.dendrite.hotkey == SUBNET_OWNER_HOTKEY: + return False, "Subnet owner hotkey" + return True, "Blacklisted" + async def organic_forward(self, synapse: WebgenieStreamingSynapse) -> WebgenieStreamingSynapse: + + bt.logging.error(f"OrganicForward Thread Name: {threading.current_thread().name}") + bt.logging.info(f"=========> {synapse}") + + return await forward_organic_synapse(self, synapse) def set_weights(self): """ diff --git a/webgenie/miners/dummy_miner.py b/webgenie/miners/dummy_miner.py index 05d41bba..5b616403 100644 --- a/webgenie/miners/dummy_miner.py +++ b/webgenie/miners/dummy_miner.py @@ -25,7 +25,7 @@ def miner_init(self): model_name="gpt-4", ) -def miner_forward(self, synapse: webgenie.protocol.webgenieSynapse)->Awaitable: +def miner_forward(self, synapse: webgenie.protocol.WebgenieStreamingSynapse)->Awaitable: async def _forward(self, chain: RunnableSequence, chain_formatter: Dict[str, str], timeout_threshold: float, init_time: float, send: Send): try: diff --git a/webgenie/protocol.py b/webgenie/protocol.py index cac1a840..d6d7824e 100644 --- a/webgenie/protocol.py +++ b/webgenie/protocol.py @@ -1,33 +1,51 @@ # The MIT License (MIT) -# Copyright © 2024 Sangar - -# 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. +# Copyright © 2024 pycorn +import bittensor as bt import pydantic import json from typing import AsyncIterator, Union, Any from starlette.responses import StreamingResponse -import bittensor as bt - -from webgenie.tasks import Task from webgenie.solution import Solution +from webgenie.tasks import Task + +class WebgenieTextSynapse(bt.Synapse): + """ + A protocol for the webgenie text task. + """ + prompt: str = pydantic.Field( + "", + title="Prompt", + description="The prompt to be sent to miners." + ) + + solution: Union[Solution, None] = pydantic.Field( + None, + title="Solution", + description="A solution received from miners." + ) + +class WebgenieImageSynapse(bt.Synapse): + """ + A protocol for the webgenie image task. + """ + base64_image: str = pydantic.Field( + "", + title="Base64 Image", + description="The base64 image to be sent to miners." + ) + + solution: Union[Solution, None] = pydantic.Field( + None, + title="Solution", + description="A solution received from miners." + ) + -class webgenieSynapse(bt.StreamingSynapse): +class WebgenieStreamingSynapse(bt.StreamingSynapse): """ - A protocol for the webgenie. + A protocol for the webgenie streaming task. """ task: Union[Task, None] = pydantic.Field( diff --git a/webgenie/solution/solution.py b/webgenie/solution/solution.py index 1a5ddec5..7a898cc6 100644 --- a/webgenie/solution/solution.py +++ b/webgenie/solution/solution.py @@ -1,7 +1,6 @@ from pydantic import BaseModel, Field class Solution(BaseModel): - css: str = Field("", description="The css solution") html: str = Field("", description="The html solution") process_time: float = Field(0, description="The time it took to process the solution") miner_uid: int = Field(0, description="The uid of the miner that processed the solution") \ No newline at end of file diff --git a/webgenie/task_generator/image_task_generator.py b/webgenie/task_generator/image_task_generator.py new file mode 100644 index 00000000..d8c4bac7 --- /dev/null +++ b/webgenie/task_generator/image_task_generator.py @@ -0,0 +1,21 @@ +import bittensor as bt +from typing import List, Tuple + +from webgenie.protocol import WebgenieImageSynapse +from webgenie.solution import Solution +from webgenie.tasks.task import Task, ImageTask +from webgenie.task_generator.task_generator import TaskGenerator + +class ImageTaskGenerator(TaskGenerator): + def __init__(self): + super().__init__() + + async def generate_task(self) -> Tuple[Task, bt.Synapse]: + return ImageTask( + base64_image="base64_image" , + timeout=50, + generator=self + ), WebgenieImageSynapse(base64_image="base64_image") + + async def reward(self, task: Task, responses: List[Solution]) -> List[float]: + pass \ No newline at end of file diff --git a/webgenie/task_generator/task_generator.py b/webgenie/task_generator/task_generator.py index 9f0c15a3..9bff3b40 100644 --- a/webgenie/task_generator/task_generator.py +++ b/webgenie/task_generator/task_generator.py @@ -1,5 +1,9 @@ +import bittensor as bt +from typing import List, Tuple from webgenie.tasks import Task +from webgenie.solution import Solution + class TaskGenerator: """ A singleton generator for tasks. @@ -7,6 +11,9 @@ class TaskGenerator: def __init__(self): pass - def next_task(self) -> Task: - return Task(query="CommingSoon Page with goback button, navHeader, and footer" , timeout=50) + async def generate_task(self) -> Tuple[Task, bt.Synapse]: + pass + + async def reward(self, task: Task, responses: List[Solution]) -> List[float]: + pass diff --git a/webgenie/task_generator/text_task_generator.py b/webgenie/task_generator/text_task_generator.py new file mode 100644 index 00000000..c300a175 --- /dev/null +++ b/webgenie/task_generator/text_task_generator.py @@ -0,0 +1,21 @@ +import bittensor as bt +from typing import List, Tuple + +from webgenie.protocol import WebgenieTextSynapse +from webgenie.solution import Solution +from webgenie.tasks.task import Task, TextTask +from webgenie.task_generator.task_generator import TaskGenerator + +class TextTaskGenerator(TaskGenerator): + def __init__(self): + super().__init__() + + async def generate_task(self) -> Tuple[Task, bt.Synapse]: + return TextTask( + prompt="CommingSoon Page with goback button, navHeader, and footer" , + timeout=50, + generator=self + ), WebgenieTextSynapse(prompt="CommingSoon Page with goback button, navHeader, and footer") + + async def reward(self, task: Task, responses: List[Solution]) -> List[float]: + pass \ No newline at end of file diff --git a/webgenie/tasks/task.py b/webgenie/tasks/task.py index 3caa3274..8d633538 100644 --- a/webgenie/tasks/task.py +++ b/webgenie/tasks/task.py @@ -1,22 +1,12 @@ -from abc import ABC, abstractmethod -from dataclasses import dataclass -from typing import List -from pydantic import BaseModel -from webgenie.solution import Solution +from typing import Any +from pydantic import BaseModel, Field class Task(BaseModel): - query: str - - timeout: float = 50 + timeout: float = Field(default=50) + generator: Any = Field(default=None) - reward_models: List[tuple] = [ - ("gpt", 0.5), - ("speed", 0.5), - ] +class ImageTask(Task): + base64_image: str = Field(default="") - penalty_models: List[tuple] = [ - ("is_valid", 2), - ] - - reward_weight: float = 0.8 - penalty_weight: float = 0.2 +class TextTask(Task): + prompt: str = Field(default="") diff --git a/webgenie/validator/__init__.py b/webgenie/validator/__init__.py deleted file mode 100644 index 445d768e..00000000 --- a/webgenie/validator/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from .forward import forward -from .reward import reward -from .organic_forward import forward_organic_synapse diff --git a/webgenie/validator/forward.py b/webgenie/validator/forward.py deleted file mode 100644 index d73e9873..00000000 --- a/webgenie/validator/forward.py +++ /dev/null @@ -1,117 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2023 Yuma Rao -# Copyright © 2024 Sangar - -# 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 bittensor as bt - -from typing import Awaitable, List -from dataclasses import dataclass -from webgenie.protocol import webgenieSynapse -from webgenie.validator.reward import get_rewards -from webgenie.utils.uids import get_random_uids -from webgenie.solution import Solution -from .organic_forward import forward_organic_synapse - -async def process_response(uid: int, async_generator: Awaitable): - bt.logging.info(f"==============6") - try: - buffer = "" - chunk = None - bt.logging.info(f"==============7{async_generator}") - async for chunk in async_generator: - bt.logging.info(f"==============7.1") - if isinstance(chunk, str): - buffer += chunk - bt.logging.info(f"==============8") - if chunk is not None: - synapse = chunk - if isinstance(synapse, webgenieSynapse): - if synapse.dendrite.status_code == 200: - synapse.solution.miner_uid = uid - return synapse.solution - else: - bt.logging.error(f"Received non-200 status code: {chunk.dendrite.status_code} for uid: {uid}") - return None - else: - bt.logging.error(f"Synapse is None for uid: {uid}") - return None - except Exception as e: - bt.logging.error(f"Error processing response for uid: {uid}: {e}") - return None - -async def handle_responses(miner_uids_list: List[int], responses: List[Awaitable]): - bt.logging.info(f"==============5") - tasks = [process_response(uid, response) for uid, response in zip(miner_uids_list, responses)] - results = await asyncio.gather(*tasks, return_exceptions=True) - return [result for result in results if result is not None] - - -async def forward(self): - """ - 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. - - """ - # TODO(developer): Define how the validator selects a miner to query, how often, etc. - # get_random_uids is an example method, but you can replace it with your own. - miner_uids = get_random_uids(self, k=self.config.neuron.sample_size) - miner_uids_list = miner_uids.tolist() - - bt.logging.info(f"Selected miners: {miner_uids}") - - # TODO(developer): Define how the validator selects a miner to query, how often, etc. - axons = [self.metagraph.axons[uid] for uid in miner_uids] - - task = self.task_generator.next_task() - - synapse = webgenieSynapse( - task=task - ) - - # The dendrite client queries the network. - try: - responses = await self.dendrite( - axons=axons, - synapse=synapse, - timeout=task.timeout, - deserialize=True, - streaming=True, - ) - except Exception as e: - bt.logging.error(f"[forward] Error querying dendrite: {e}") - return - bt.logging.info(f"==============1") - handle_responses_task = asyncio.create_task(handle_responses(miner_uids_list, responses)) - bt.logging.info(f"==============2") - results = await handle_responses_task - await asyncio.sleep(4) - if len(results) == 0: - bt.logging.info("No responses received") - return - bt.logging.info(f"==============3") - bt.logging.info(f"Received {results} results") - bt.logging.info(f"==============4") - scores, miner_uids = self.reward_manager.score(task, results) - # Update the scores based on the rewards. You may want to define your own update_scores function for custom behavior. - bt.logging.info(f"Updating scores: {scores}") - self.update_scores(scores, miner_uids) \ No newline at end of file diff --git a/webgenie/validator/organic_forward.py b/webgenie/validator/organic_forward.py deleted file mode 100644 index c7965f81..00000000 --- a/webgenie/validator/organic_forward.py +++ /dev/null @@ -1,45 +0,0 @@ -from typing import Callable -from functools import partial -from typing import Any, AsyncGenerator -from starlette.types import Send - -import bittensor as bt -from webgenie.protocol import webgenieSynapse - -async def forward_organic_synapse(self, synapse: webgenieSynapse)->webgenieSynapse: - async def forward_miner(synapse: webgenieSynapse, send: Send): - bt.logging.info(f"Send Synapse to miner: {synapse}") - async def handle_miner_response(responses): - for resp in responses: - async for chunk in resp: - if isinstance(chunk, str): - bt.logging.info(f"Chunk: {chunk}") - await send( - { - "type": "http.response.body", - "body": chunk.encode("utf-8"), - "more_body": True, - } - ) - await send( - {"type": "http.response.body", "body": b"", "more_body": False} - ) - - axon = self.metagraph.axons[1] - try: - async with bt.dendrite(wallet=self.wallet) as dendrite: - bt.logging.info(f"Dendrite: {dendrite}") - responses = await dendrite( - axons=[axon], - synapse=synapse, - deserialize=False, - timeout=synapse.timeout, - streaming=True, - ) - except Exception as e: - bt.logging.error(f"[forward_organic_synapse] Error querying dendrite: {e}") - return await handle_miner_response(responses) - bt.logging.info(f"forward_organic_synapse: {synapse}") - send_external_response = partial(forward_miner, synapse) - return synapse.create_streaming_response(send_external_response) - \ No newline at end of file diff --git a/webgenie/validator/reward.py b/webgenie/validator/reward.py deleted file mode 100644 index 58f920a4..00000000 --- a/webgenie/validator/reward.py +++ /dev/null @@ -1,54 +0,0 @@ -# The MIT License (MIT) -# Copyright © 2023 Yuma Rao -# Copyright © 2024 Sangar - -# 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 numpy as np -from typing import List -import bittensor as bt - - -def reward(query: int, response: int) -> float: - """ - Reward the miner response to the dummy request. This method returns a reward - value for the miner, which is used to update the miner's score. - - Returns: - - float: The reward value for the miner. - """ - bt.logging.info(f"In rewards, query val: {query}, response val: {response}, rewards val: {1.0 if response == query * 2 else 0}") - return 1.0 if response == query * 2 else 0 - - -def get_rewards( - self, - query: int, - responses: List[float], -) -> np.ndarray: - """ - Returns an array of rewards for the given query and responses. - - Args: - - query (int): The query sent to the miner. - - responses (List[float]): A list of responses from the miner. - - Returns: - - np.ndarray: An array of rewards for the given query and responses. - """ - # Get all the reward results by iteratively calling your reward() function. - - return np.array( - [reward(query, response) for response in responses] - ) From 8945131980e281ae9df6e182232df348fcef804e Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Wed, 11 Dec 2024 20:48:16 -0600 Subject: [PATCH 023/129] chore: refactor rewarding structure --- neurons/validators/genie_validator.py | 56 +++++++++++-------- .../task_generator/image_task_generator.py | 4 +- webgenie/task_generator/task_generator.py | 2 +- .../task_generator/text_task_generator.py | 4 +- 4 files changed, 38 insertions(+), 28 deletions(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index f069b42b..6ee7279c 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -41,44 +41,41 @@ async def forward(self): timeout=task.timeout ) - processed_synapses, processed_miner_uids = await self.process_synapses(all_synapse_results, miner_uids) + solutions = [] - if len(processed_synapses) == 0: - bt.logging.error(f"No valid synapses received") + for synapse, miner_uid in zip(all_synapse_results, miner_uids): + processed_synapse = await self.process_synapse(synapse, miner_uid) + if processed_synapse is not None: + solutions.append(processed_synapse.solution) + + if len(solutions) == 0: + bt.logging.error(f"No valid solutions received") return - bt.logging.debug(f"Processed synapses: {processed_synapses}") + + bt.logging.debug(f"Processed solutions: {solutions}") - self.synthetic_history.append((task, processed_synapses, processed_miner_uids)) + self.synthetic_history.append((task, solutions)) except Exception as e: bt.logging.error(f"Error in forward: {e}") raise e - async def process_synapses(self, synapses: List[WebgenieStreamingSynapse], miner_uids: List[int]): - processed_synapses = [] - processed_miner_uids = [] - - for synapse, miner_uid in zip(synapses, miner_uids): - if synapse.dendrite.status_code == 200: - processed_synapses.append(synapse) - processed_miner_uids.append(miner_uid) - - return processed_synapses, processed_miner_uids - async def score(self): if len(self.synthetic_history) == 0: bt.logging.warning(f"No synthetic history to score") return - task, synapses, miner_uids = self.synthetic_history.pop(0) + task, solutions = self.synthetic_history.pop(0) task_generator = task.generator - scores = await task_generator.reward(task, synapses) - self.neuron.update_scores(scores, miner_uids) + scores = await task_generator.reward(task, solutions) + self.neuron.update_scores(scores, [solution.miner_uid for solution in solutions]) self.neuron.sync() async def organic_forward(self, synapse: WebgenieTextSynapse): - axon = self.metagraph.axons[1] + best_miner_uid = 1 try: + axon = self.metagraph.axons[best_miner_uid] + async with bt.dendrite(wallet=self.wallet) as dendrite: bt.logging.info(f"Dendrite: {dendrite}") responses = await dendrite( @@ -86,10 +83,23 @@ async def organic_forward(self, synapse: WebgenieTextSynapse): synapse=synapse, timeout=synapse.timeout, ) - return responses[0] + + processed_synapse = await self.process_synapse(responses[0], best_miner_uid) + if processed_synapse is None: + raise Exception(f"No valid solution received") + + return processed_synapse except Exception as e: bt.logging.error(f"[forward_organic_synapse] Error querying dendrite: {e}") synapse.solution = Solution( - html = "", + html = f"Error: {e}", + process_time = 0, + miner_uid = best_miner_uid, ) - return synapse \ No newline at end of file + return synapse + + async def process_synapse(self, synapse: bt.Synapse, miner_uid: int) -> bt.Synapse: + if synapse.dendrite.status_code == 200: + synapse.solution.miner_uid = miner_uid + return synapse + return None \ No newline at end of file diff --git a/webgenie/task_generator/image_task_generator.py b/webgenie/task_generator/image_task_generator.py index d8c4bac7..5a7218e2 100644 --- a/webgenie/task_generator/image_task_generator.py +++ b/webgenie/task_generator/image_task_generator.py @@ -17,5 +17,5 @@ async def generate_task(self) -> Tuple[Task, bt.Synapse]: generator=self ), WebgenieImageSynapse(base64_image="base64_image") - async def reward(self, task: Task, responses: List[Solution]) -> List[float]: - pass \ No newline at end of file + async def reward(self, task: Task, solutions: List[Solution]) -> List[float]: + pass diff --git a/webgenie/task_generator/task_generator.py b/webgenie/task_generator/task_generator.py index 9bff3b40..a41b3a82 100644 --- a/webgenie/task_generator/task_generator.py +++ b/webgenie/task_generator/task_generator.py @@ -14,6 +14,6 @@ def __init__(self): async def generate_task(self) -> Tuple[Task, bt.Synapse]: pass - async def reward(self, task: Task, responses: List[Solution]) -> List[float]: + async def reward(self, task: Task, solutions: List[Solution]) -> List[float]: pass diff --git a/webgenie/task_generator/text_task_generator.py b/webgenie/task_generator/text_task_generator.py index c300a175..44fa9aa3 100644 --- a/webgenie/task_generator/text_task_generator.py +++ b/webgenie/task_generator/text_task_generator.py @@ -17,5 +17,5 @@ async def generate_task(self) -> Tuple[Task, bt.Synapse]: generator=self ), WebgenieTextSynapse(prompt="CommingSoon Page with goback button, navHeader, and footer") - async def reward(self, task: Task, responses: List[Solution]) -> List[float]: - pass \ No newline at end of file + async def reward(self, task: Task, solutions: List[Solution]) -> List[float]: + pass From e799192709de8a6575a8de6467f598812870f832 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Thu, 12 Dec 2024 06:48:38 -0600 Subject: [PATCH 024/129] fix: fixed bugs for launch --- .gitignore | 8 +- lang_chain_test.py | 145 -------------------------- neurons/miners/miner.py | 2 +- neurons/validators/genie_validator.py | 6 +- neurons/validators/validator.py | 4 +- webgenie/__init__.py | 1 - webgenie/base/validator.py | 1 - 7 files changed, 13 insertions(+), 154 deletions(-) delete mode 100644 lang_chain_test.py diff --git a/.gitignore b/.gitignore index 6b79b60a..3c6f1d53 100644 --- a/.gitignore +++ b/.gitignore @@ -165,4 +165,10 @@ testing/ .vscode/settings.json .DS_Store -temp.txt \ No newline at end of file +temp.txt + +# env +.env + +# test +test.py \ No newline at end of file diff --git a/lang_chain_test.py b/lang_chain_test.py deleted file mode 100644 index c0f8d29d..00000000 --- a/lang_chain_test.py +++ /dev/null @@ -1,145 +0,0 @@ -from functools import partial -from starlette.types import Send -import time -from typing import Dict -import openai -import os - -from typing import Dict, Awaitable -from langchain_openai import ChatOpenAI,OpenAI - -from dotenv import load_dotenv, find_dotenv -from langchain.prompts import ChatPromptTemplate, PromptTemplate -from langchain_core.output_parsers import StrOutputParser -from langchain_core.runnables.base import RunnableSequence - -import bittensor as bt - -class DummySynapse: - query: str - timeout: float -class Self: - pass - -load_dotenv() -api_key = os.getenv("OPENAI_API_KEY") - -def miner_init(self): - bt.logging.debug(f"Dummy Miner initialized") - # Set openai key and other args - self.model = ChatOpenAI( - api_key=api_key, - model_name="gpt-4", - ) - -def miner_forward(self, synapse: DummySynapse) -> str: - def _forward(self, chain: RunnableSequence, chain_formatter: Dict[str, str], timeout_threshold: float, init_time: float) -> str: - buffer = [] - timeout_reached = False - is_in_code_block = True - is_first_line = True - generated_code = "" - try: - for token in chain.stream(chain_formatter): - - if is_first_line and token.startswith("```"): - is_in_code_block = False - - if is_in_code_block and not token.startswith("```"): - pass - buffer.append(token) - - if time.time() - init_time > timeout_threshold: - bt.logging.debug(f"⏰ Timeout reached, stopping streaming") - timeout_reached = True - break - - if len(buffer) == 10: - joined_buffer = "".join(buffer) - generated_code += joined_buffer - buffer = [] - - if is_first_line and token.endswith("\n"): - is_first_line = False - is_in_code_block = True - - if ( - buffer and not timeout_reached - ): # Don't send the last buffer of data if timeout. - joined_buffer = "".join(buffer) - generated_code += joined_buffer - return generated_code - except Exception as e: - bt.logging.error(f"Dummy Miner Error: {e}") - return "" - bt.logging.debug(f"Dummy Miner Query received, forwarding synapse: {synapse}") - - # prompt = PromptTemplate.from_template( - # "You are an expert programmer. Generate code based on the following request:\n\n{query}\n\nProvide only the code, without any explanations." - # ) - prompt = ChatPromptTemplate.from_messages([ - ("system", """You are an expert programmer. Generate code based on the following request without explanations:\n\n{query}\n\n Provide the code that satisfies the following requirements without any explanation. You must give me both the CSS file and the frontend HTML file for a 'Login Page.' The response should be in JSON format as shown below, and it should be plaintext (without triple quotes): - {{ 'CSS': 'css_code_here', 'HTML': 'html_code_here' }}"""), - ("user", "{query}"), - ]) - chain = prompt | self.model | StrOutputParser() - - query = synapse.query - bt.logging.info(f"Dummy Miner Query: {query}") - - chain_formatter = {"query": query} - - init_time = time.time() - timeout_threshold = synapse.timeout - token_streamer = partial( - _forward, - self, - chain, - chain_formatter, - timeout_threshold, - init_time, - ) - return token_streamer() - -def evaluate_code(task: str, generated_code: str) -> float: - # Use GPT to evaluate the code - query = f""" - Task: {task} - Generated Code: - {generated_code} - - Evaluate the generated code based on the following criteria: - 1. Correctness: Does the code implement the required functionality? - 2. Code quality: Is the code well-structured and following best practices? - 3. Completeness: Does the code address all aspects of the task? - - Provide a score between 0 and 1, where 1 is perfect and 0 is completely incorrect. - Only return the score, without any explanations. - """ - - try: - evaluation = ChatOpenAI( - api_key=api_key, - model="gpt-4", - - ) - messages=[ - {"role": "system", "content": "You are a code evaluation expert."}, - {"role": "user", "content": query} - ] - evaluation = evaluation.invoke(messages) - return float(evaluation.content) - except Exception as e: - bt.logging.error(f"Error evaluating code: {e}") - return 0.0 - -if __name__ == "__main__": - self = Self() - synapse = DummySynapse() - synapse.query = "Comming soon Page using Vue.js and css" - synapse.timeout = 50 - miner_init(self) - generated_code = miner_forward(self, synapse) - print(f"Generated Code: {generated_code}") - score = evaluate_code(synapse.query, generated_code) - print(f"Score: {score}") diff --git a/neurons/miners/miner.py b/neurons/miners/miner.py index ae3d52ee..d60606cf 100644 --- a/neurons/miners/miner.py +++ b/neurons/miners/miner.py @@ -1,6 +1,6 @@ # The MIT License (MIT) # Copyright © 2023 Yuma Rao -# Copyright © 2023 Sangar +# Copyright © 2023 Sangar, pycorn0729 # 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 diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 6ee7279c..420d403c 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -31,12 +31,12 @@ async def forward(self): if len(self.synthetic_history) > MAX_SYNTHETIC_HISTORY_SIZE: return - miner_uids = get_random_uids(self, k=self.config.neuron.sample_size) + miner_uids = get_random_uids(self.neuron, k=self.config.neuron.sample_size) task, synapse = await random.choice(self.task_generators).generate_task() all_synapse_results = await self.neuron.dendrite( - axons = [self.metagraph.axons[uid] for uid in miner_uids], + axons = [self.neuron.metagraph.axons[uid] for uid in miner_uids], synapse=synapse, timeout=task.timeout ) @@ -49,7 +49,7 @@ async def forward(self): solutions.append(processed_synapse.solution) if len(solutions) == 0: - bt.logging.error(f"No valid solutions received") + bt.logging.warning(f"No valid solutions received") return bt.logging.debug(f"Processed solutions: {solutions}") diff --git a/neurons/validators/validator.py b/neurons/validators/validator.py index 743a6c51..5620c940 100644 --- a/neurons/validators/validator.py +++ b/neurons/validators/validator.py @@ -25,13 +25,13 @@ def __init__(self, config=None): super(Validator, self).__init__(config=config) bt.logging.info("load_state()") self.load_state() - self.genie_validator = GenieValidator() + self.genie_validator = GenieValidator(neuron=self) async def organic_forward(self, synapse: WebgenieStreamingSynapse): return await self.genie_validator.organic_forward(synapse) async def forward(self): - return await self.genie_validator.forward(self) + return await self.genie_validator.forward() async def concurrent_forward(self): coroutines = [ diff --git a/webgenie/__init__.py b/webgenie/__init__.py index 41bb1b50..993ab636 100644 --- a/webgenie/__init__.py +++ b/webgenie/__init__.py @@ -30,6 +30,5 @@ # Import all submodules. from . import protocol from . import base -from . import validator from . import api from .subnet_links import SUBNET_LINKS diff --git a/webgenie/base/validator.py b/webgenie/base/validator.py index 437ac1a5..d777187d 100644 --- a/webgenie/base/validator.py +++ b/webgenie/base/validator.py @@ -35,7 +35,6 @@ from webgenie.mock import MockDendrite from webgenie.utils.config import add_validator_args from webgenie.protocol import WebgenieStreamingSynapse -from webgenie.validator.organic_forward import forward_organic_synapse SUBNET_OWNER_HOTKEY = "5G9sRcoaw2H3SYDq7e7PoGhbbMUPHQi6pC6tPrahmSmDtxS8" From 5cf0c922e756248e98d8a3885581aed3944ca900 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Thu, 12 Dec 2024 07:16:03 -0600 Subject: [PATCH 025/129] fix: fixed bugs in miner --- neurons/miners/miner.py | 65 ++++++++++++++++++--------- neurons/validators/genie_validator.py | 2 + webgenie/base/miner.py | 12 ++--- 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/neurons/miners/miner.py b/neurons/miners/miner.py index d60606cf..c2e5118d 100644 --- a/neurons/miners/miner.py +++ b/neurons/miners/miner.py @@ -18,16 +18,11 @@ import time import typing -import importlib import bittensor as bt - -# Bittensor Miner Template: -import webgenie - -# import base miner class which takes care of most of the boilerplate from webgenie.base.miner import BaseMinerNeuron - +from webgenie.protocol import WebgenieTextSynapse, WebgenieImageSynapse +from webgenie.solution import Solution class Miner(BaseMinerNeuron): """ @@ -41,23 +36,51 @@ class Miner(BaseMinerNeuron): def __init__(self, config=None): super(Miner, self).__init__(config=config) - miner_name = "dummy_miner" - miner_module = importlib.import_module(f"webgenie.miners.{miner_name}") - - self.miner_init = miner_module.miner_init - self.miner_forward = miner_module.miner_forward - - self.miner_init(self) + # Attach determiners which functions are called when servicing a request. + bt.logging.info(f"Attaching forward function to miner axon.") + self.axon.attach( + forward_fn=self.forward_text, + blacklist_fn=self.blacklist_text, + priority_fn=self.priority_text, + ).attach( + forward_fn = self.forward_image, + blacklist_fn=self.blacklist_image, + priority_fn=self.priority_image, + ) - async def forward( - self, synapse: webgenie.protocol.WebgenieStreamingSynapse - ) -> webgenie.protocol.WebgenieStreamingSynapse: + async def forward_text( + self, synapse: WebgenieTextSynapse + ) -> WebgenieTextSynapse: - bt.logging.debug(f"Miner forward called with synapse: {synapse}") - return self.miner_forward(self, synapse) + bt.logging.debug(f"Miner text forward called with synapse: {synapse}") + synapse.solution = Solution( + html = "

Hello, world!

" + ) + return synapse + + async def forward_image( + self, synapse: WebgenieImageSynapse + ) -> WebgenieImageSynapse: + bt.logging.debug(f"Miner image forward called with synapse: {synapse}") + synapse.solution = Solution( + html = "

Hello, Image!

" + ) + return synapse + + async def blacklist_text(self, synapse: WebgenieTextSynapse) -> typing.Tuple[bool, str]: + return await self.blacklist(synapse) + + async def blacklist_image(self, synapse: WebgenieImageSynapse) -> typing.Tuple[bool, str]: + return await self.blacklist(synapse) + + async def priority_text(self, synapse: WebgenieTextSynapse) -> float: + return await self.priority(synapse) + + async def priority_image(self, synapse: WebgenieImageSynapse) -> float: + return await self.priority(synapse) async def blacklist( - self, synapse: webgenie.protocol.WebgenieStreamingSynapse + self, synapse: bt.Synapse ) -> typing.Tuple[bool, str]: """ Determines whether an incoming request should be blacklisted and thus ignored. Your implementation should @@ -118,7 +141,7 @@ async def blacklist( ) return False, "Hotkey recognized!" - async def priority(self, synapse: webgenie.protocol.WebgenieStreamingSynapse) -> float: + 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. diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 420d403c..2ab46ceb 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -100,6 +100,8 @@ async def organic_forward(self, synapse: WebgenieTextSynapse): async def process_synapse(self, synapse: bt.Synapse, miner_uid: int) -> bt.Synapse: if synapse.dendrite.status_code == 200: + if synapse.solution is None: + return None synapse.solution.miner_uid = miner_uid return synapse return None \ No newline at end of file diff --git a/webgenie/base/miner.py b/webgenie/base/miner.py index 999b489e..0f567652 100644 --- a/webgenie/base/miner.py +++ b/webgenie/base/miner.py @@ -55,21 +55,15 @@ def __init__(self, config=None): # The axon handles request processing, allowing validators to send this miner requests. self.axon = bt.axon(wallet=self.wallet, config=self.config() if callable(self.config) else self.config) - # Attach determiners which functions are called when servicing a request. - bt.logging.info(f"Attaching forward function to miner axon.") - self.axon.attach( - forward_fn=self.forward, - blacklist_fn=self.blacklist, - priority_fn=self.priority, - ) - bt.logging.info(f"Axon created: {self.axon}") - # Instantiate runners self.should_exit: bool = False self.is_running: bool = False self.thread: Union[threading.Thread, None] = None self.lock = asyncio.Lock() + async def forward(self): + pass + 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. From afcaf02e384375e0137a265996e98a4b2c193533 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Thu, 12 Dec 2024 08:04:33 -0600 Subject: [PATCH 026/129] feat: added wandb logging --- neurons/miners/miner.py | 5 +- neurons/validators/validator.py | 3 + wandb/latest-run | 1 + .../files/requirements.txt | 139 ++++++++++++++++++ .../files/wandb-metadata.json | 45 ++++++ .../run-fmmfb2d8.wandb | Bin 0 -> 32768 bytes .../files/requirements.txt | 139 ++++++++++++++++++ .../files/wandb-metadata.json | 45 ++++++ .../run-p5q8p631.wandb | 0 webgenie/__init__.py | 4 +- webgenie/helpers/weights.py | 45 ++++++ 11 files changed, 424 insertions(+), 2 deletions(-) create mode 120000 wandb/latest-run create mode 100644 wandb/run-20241212_080228-fmmfb2d8/files/requirements.txt create mode 100644 wandb/run-20241212_080228-fmmfb2d8/files/wandb-metadata.json create mode 100644 wandb/run-20241212_080228-fmmfb2d8/run-fmmfb2d8.wandb create mode 100644 wandb/run-20241212_080410-p5q8p631/files/requirements.txt create mode 100644 wandb/run-20241212_080410-p5q8p631/files/wandb-metadata.json create mode 100644 wandb/run-20241212_080410-p5q8p631/run-p5q8p631.wandb create mode 100644 webgenie/helpers/weights.py diff --git a/neurons/miners/miner.py b/neurons/miners/miner.py index c2e5118d..7c7ab172 100644 --- a/neurons/miners/miner.py +++ b/neurons/miners/miner.py @@ -1,6 +1,6 @@ # The MIT License (MIT) # Copyright © 2023 Yuma Rao -# Copyright © 2023 Sangar, pycorn0729 +# Copyright © 2024 pycorn0729 # 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 @@ -21,6 +21,7 @@ import bittensor as bt from webgenie.base.miner import BaseMinerNeuron +from webgenie.helpers.weights import init_wandb from webgenie.protocol import WebgenieTextSynapse, WebgenieImageSynapse from webgenie.solution import Solution @@ -48,6 +49,8 @@ def __init__(self, config=None): priority_fn=self.priority_image, ) + init_wandb(self) + async def forward_text( self, synapse: WebgenieTextSynapse ) -> WebgenieTextSynapse: diff --git a/neurons/validators/validator.py b/neurons/validators/validator.py index 5620c940..fbf3245f 100644 --- a/neurons/validators/validator.py +++ b/neurons/validators/validator.py @@ -9,6 +9,7 @@ import bittensor as bt from webgenie.base.validator import BaseValidatorNeuron +from webgenie.helpers.weights import init_wandb from webgenie.protocol import WebgenieStreamingSynapse from neurons.validators.genie_validator import GenieValidator @@ -27,6 +28,8 @@ def __init__(self, config=None): self.load_state() self.genie_validator = GenieValidator(neuron=self) + init_wandb(self) + async def organic_forward(self, synapse: WebgenieStreamingSynapse): return await self.genie_validator.organic_forward(synapse) diff --git a/wandb/latest-run b/wandb/latest-run new file mode 120000 index 00000000..3e4f5871 --- /dev/null +++ b/wandb/latest-run @@ -0,0 +1 @@ +run-20241212_080410-p5q8p631 \ No newline at end of file diff --git a/wandb/run-20241212_080228-fmmfb2d8/files/requirements.txt b/wandb/run-20241212_080228-fmmfb2d8/files/requirements.txt new file mode 100644 index 00000000..d0cdb471 --- /dev/null +++ b/wandb/run-20241212_080228-fmmfb2d8/files/requirements.txt @@ -0,0 +1,139 @@ +wandb==0.19.0 +charset-normalizer==3.4.0 +triton==3.1.0 +cryptography==42.0.8 +fsspec==2024.10.0 +backoff==2.2.1 +wheel==0.45.1 +GitPython==3.1.43 +Levenshtein==0.26.1 +urllib3==2.2.3 +munch==2.5.0 +typing_extensions==4.12.2 +distro==1.9.0 +rich==13.9.4 +langchain-openai==0.2.12 +scalecodec==1.2.11 +eth-utils==2.2.2 +attrs==24.2.0 +nvidia-nccl-cu12==2.21.5 +py==1.11.0 +sentry-sdk==2.19.2 +gitdb==4.0.11 +colorama==0.4.6 +multidict==6.1.0 +nvidia-nvjitlink-cu12==12.4.127 +nvidia-curand-cu12==10.3.5.147 +setproctitle==1.3.4 +setuptools==70.0.0 +httpx==0.28.1 +aiosignal==1.3.1 +python-statemachine==2.5.0 +packaging==24.2 +langchain-core==0.3.24 +substrate-interface==1.7.11 +eth-keys==0.6.0 +webencodings==0.5.1 +propcache==0.2.1 +pydantic_core==2.27.1 +xxhash==3.5.0 +platformdirs==4.3.6 +pip==24.2 +annotated-types==0.7.0 +sympy==1.13.1 +ddt==1.6.0 +openai==1.57.2 +tenacity==9.0.0 +nvidia-cusolver-cu12==11.6.1.9 +nvidia-nvtx-cu12==12.4.127 +Pygments==2.18.0 +fastapi==0.110.3 +eth-hash==0.7.0 +smmap==5.0.1 +regex==2024.11.6 +retry==0.9.2 +pluggy==1.5.0 +ansible-vault==2.1.0 +click==8.1.7 +RapidFuzz==3.10.1 +nvidia-cusparse-cu12==12.3.1.170 +jsonpointer==3.0.0 +shtab==1.6.5 +SQLAlchemy==2.0.36 +password-strength==0.0.3.post2 +py-sr25519-bindings==0.2.1 +ansible==8.5.0 +more-itertools==10.5.0 +pycparser==2.22 +msgpack==1.1.0 +PyYAML==6.0.2 +filelock==3.16.1 +soupsieve==2.6 +beautifulsoup4==4.12.3 +py-bip39-bindings==0.2.0 +jsonpatch==1.33 +nvidia-cuda-nvrtc-cu12==12.4.127 +fuzzywuzzy==0.18.0 +h11==0.14.0 +jiter==0.8.2 +protobuf==5.29.1 +eth-typing==5.0.1 +termcolor==2.5.0 +yarl==1.18.3 +aiohappyeyeballs==2.4.4 +nvidia-cuda-cupti-cu12==12.4.127 +docker-pycreds==0.4.0 +starlette==0.37.2 +ecdsa==0.19.0 +networkx==3.4.2 +sniffio==1.3.1 +ansible-core==2.15.13 +Jinja2==3.1.4 +certifi==2024.7.4 +torch==2.5.1 +toolz==1.0.0 +langchain==0.3.11 +netaddr==1.3.0 +MarkupSafe==3.0.2 +websocket-client==1.8.0 +langsmith==0.2.3 +python-dotenv==1.0.1 +greenlet==3.1.1 +orjson==3.10.12 +idna==3.10 +frozenlist==1.5.0 +decorator==5.1.1 +anyio==4.7.0 +nest-asyncio==1.6.0 +markdown-it-py==3.0.0 +psutil==6.1.0 +nvidia-cufft-cu12==11.2.1.3 +bittensor==7.4.0 +py-ed25519-zebra-bindings==1.2.0 +nvidia-cudnn-cu12==9.1.0.70 +requests-toolbelt==1.0.0 +uvicorn==0.32.1 +msgpack-numpy-opentensor==0.5.0 +python-Levenshtein==0.26.1 +tinycss2==1.4.0 +mdurl==0.1.2 +mpmath==1.3.0 +cffi==1.17.1 +numpy==1.26.4 +six==1.17.0 +pydantic==2.10.3 +tiktoken==0.8.0 +iniconfig==2.0.0 +langchain-text-splitters==0.3.2 +pycryptodome==3.21.0 +tqdm==4.67.1 +nvidia-cublas-cu12==12.4.5.8 +requests==2.32.3 +base58==2.1.1 +httpcore==1.0.7 +aiohttp==3.11.10 +PyNaCl==1.5.0 +cytoolz==1.0.0 +pytest==8.3.4 +resolvelib==1.0.1 +nvidia-cuda-runtime-cu12==12.4.127 diff --git a/wandb/run-20241212_080228-fmmfb2d8/files/wandb-metadata.json b/wandb/run-20241212_080228-fmmfb2d8/files/wandb-metadata.json new file mode 100644 index 00000000..4b201625 --- /dev/null +++ b/wandb/run-20241212_080228-fmmfb2d8/files/wandb-metadata.json @@ -0,0 +1,45 @@ +{ + "os": "Linux-5.4.0-105-generic-x86_64-with-glibc2.31", + "python": "CPython 3.12.7", + "startedAt": "2024-12-12T14:02:28.547811Z", + "args": [ + "--netuid", + "214", + "--subtensor.network", + "test", + "--wallet.name", + "sc-val1", + "--wallet.hotkey", + "sh-val1", + "--logging.debug", + "--neuron.axon_port", + "8091" + ], + "program": "/home/dev2user/workspace/sangar/web-genie-ai/neurons/validators/validator.py", + "codePath": "neurons/validators/validator.py", + "git": { + "remote": "https://github.com/web-genie-ai/web-genie-ai.git", + "commit": "5cf0c922e756248e98d8a3885581aed3944ca900" + }, + "email": "hayesdominique0729@gmail.com", + "root": "/home/dev2user/workspace/sangar/web-genie-ai", + "host": "vmi1162577.contaboserver.net", + "username": "root", + "executable": "/home/dev2user/workspace/sangar/web-genie-ai/venv/bin/python3.12", + "codePathLocal": "neurons/validators/validator.py", + "cpu_count": 6, + "cpu_count_logical": 6, + "disk": { + "/": { + "total": "630869753856", + "used": "33456181248" + } + }, + "memory": { + "total": "16786370560" + }, + "cpu": { + "count": 6, + "countLogical": 6 + } +} \ No newline at end of file diff --git a/wandb/run-20241212_080228-fmmfb2d8/run-fmmfb2d8.wandb b/wandb/run-20241212_080228-fmmfb2d8/run-fmmfb2d8.wandb new file mode 100644 index 0000000000000000000000000000000000000000..f20c3edc1d597796ebc65fe83ab9c295a3ca1097 GIT binary patch literal 32768 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 ReNs# Date: Thu, 12 Dec 2024 08:13:34 -0600 Subject: [PATCH 027/129] chore : added wandb to gitignore --- .gitignore | 5 ++++- wandb/latest-run | 2 +- .../run-fmmfb2d8.wandb | Bin 32768 -> 82089 bytes .../run-p5q8p631.wandb | Bin 0 -> 13718 bytes 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index 3c6f1d53..b9e1a14d 100644 --- a/.gitignore +++ b/.gitignore @@ -171,4 +171,7 @@ temp.txt .env # test -test.py \ No newline at end of file +test.py + +# wandb +wandb/ \ No newline at end of file diff --git a/wandb/latest-run b/wandb/latest-run index 3e4f5871..6f2f1da7 120000 --- a/wandb/latest-run +++ b/wandb/latest-run @@ -1 +1 @@ -run-20241212_080410-p5q8p631 \ No newline at end of file +run-20241212_080507-u6u37fmk \ No newline at end of file diff --git a/wandb/run-20241212_080228-fmmfb2d8/run-fmmfb2d8.wandb b/wandb/run-20241212_080228-fmmfb2d8/run-fmmfb2d8.wandb index f20c3edc1d597796ebc65fe83ab9c295a3ca1097..8a19372f048c24edb47da814bcba5f07509c6e8f 100644 GIT binary patch literal 82089 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 delta 8 PcmZ44$lB1rw4eb14?6== diff --git a/wandb/run-20241212_080410-p5q8p631/run-p5q8p631.wandb b/wandb/run-20241212_080410-p5q8p631/run-p5q8p631.wandb index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..f768135d09c2656671e5383f57d5822d7eaacab6 100644 GIT binary patch literal 13718 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 Eze2+$8~^|S literal 0 HcmV?d00001 From 1cc9a2620f900119c18e1fccb90e3d164434a560 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Thu, 12 Dec 2024 08:24:38 -0600 Subject: [PATCH 028/129] chre: added pseudo rewards --- wandb/latest-run | 2 +- webgenie/task_generator/image_task_generator.py | 3 ++- webgenie/task_generator/text_task_generator.py | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/wandb/latest-run b/wandb/latest-run index 6f2f1da7..8f30dc87 120000 --- a/wandb/latest-run +++ b/wandb/latest-run @@ -1 +1 @@ -run-20241212_080507-u6u37fmk \ No newline at end of file +run-20241212_082229-ryemav9r \ No newline at end of file diff --git a/webgenie/task_generator/image_task_generator.py b/webgenie/task_generator/image_task_generator.py index 5a7218e2..cb7b5124 100644 --- a/webgenie/task_generator/image_task_generator.py +++ b/webgenie/task_generator/image_task_generator.py @@ -18,4 +18,5 @@ async def generate_task(self) -> Tuple[Task, bt.Synapse]: ), WebgenieImageSynapse(base64_image="base64_image") async def reward(self, task: Task, solutions: List[Solution]) -> List[float]: - pass + bt.logging.debug(f"Rewarding image task {task} with solutions {solutions}") + return [1.0] * len(solutions) diff --git a/webgenie/task_generator/text_task_generator.py b/webgenie/task_generator/text_task_generator.py index 44fa9aa3..2629e3db 100644 --- a/webgenie/task_generator/text_task_generator.py +++ b/webgenie/task_generator/text_task_generator.py @@ -18,4 +18,5 @@ async def generate_task(self) -> Tuple[Task, bt.Synapse]: ), WebgenieTextSynapse(prompt="CommingSoon Page with goback button, navHeader, and footer") async def reward(self, task: Task, solutions: List[Solution]) -> List[float]: - pass + bt.logging.debug(f"Rewarding text task {task} with solutions {solutions}") + return [1.0] * len(solutions) From 562b8c14265ba875814e600fe36110f1c74acac2 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Thu, 12 Dec 2024 08:30:00 -0600 Subject: [PATCH 029/129] chore: added sanity check --- webgenie/task_generator/image_task_generator.py | 3 +++ webgenie/task_generator/text_task_generator.py | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/webgenie/task_generator/image_task_generator.py b/webgenie/task_generator/image_task_generator.py index cb7b5124..6d86cc99 100644 --- a/webgenie/task_generator/image_task_generator.py +++ b/webgenie/task_generator/image_task_generator.py @@ -18,5 +18,8 @@ async def generate_task(self) -> Tuple[Task, bt.Synapse]: ), WebgenieImageSynapse(base64_image="base64_image") async def reward(self, task: Task, solutions: List[Solution]) -> List[float]: + if not isinstance(task, ImageTask): + raise ValueError(f"Task is not a ImageTask: {type(task)}") + bt.logging.debug(task.base64_image) bt.logging.debug(f"Rewarding image task {task} with solutions {solutions}") return [1.0] * len(solutions) diff --git a/webgenie/task_generator/text_task_generator.py b/webgenie/task_generator/text_task_generator.py index 2629e3db..e46e88a2 100644 --- a/webgenie/task_generator/text_task_generator.py +++ b/webgenie/task_generator/text_task_generator.py @@ -18,5 +18,9 @@ async def generate_task(self) -> Tuple[Task, bt.Synapse]: ), WebgenieTextSynapse(prompt="CommingSoon Page with goback button, navHeader, and footer") async def reward(self, task: Task, solutions: List[Solution]) -> List[float]: + if not isinstance(task, TextTask): + raise ValueError(f"Task is not a TextTask: {type(task)}") + bt.logging.debug(task.prompt) bt.logging.debug(f"Rewarding text task {task} with solutions {solutions}") + return [1.0] * len(solutions) From 4739278d5ae089ad14ea875d56d661cb5977a9f1 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Thu, 12 Dec 2024 09:15:50 -0600 Subject: [PATCH 030/129] feat: added openai text miner --- .gitignore | 2 +- neurons/miners/genie_miner.py | 0 neurons/miners/miner.py | 21 ++++++---- neurons/miners/openai_miner.py | 59 +++++++++++++++++++++++++++ neurons/validators/genie_validator.py | 1 + wandb/latest-run | 2 +- 6 files changed, 74 insertions(+), 11 deletions(-) delete mode 100644 neurons/miners/genie_miner.py create mode 100644 neurons/miners/openai_miner.py diff --git a/.gitignore b/.gitignore index b9e1a14d..26851170 100644 --- a/.gitignore +++ b/.gitignore @@ -173,5 +173,5 @@ temp.txt # test test.py -# wandb +# Wandb wandb/ \ No newline at end of file diff --git a/neurons/miners/genie_miner.py b/neurons/miners/genie_miner.py deleted file mode 100644 index e69de29b..00000000 diff --git a/neurons/miners/miner.py b/neurons/miners/miner.py index 7c7ab172..3b0dafa6 100644 --- a/neurons/miners/miner.py +++ b/neurons/miners/miner.py @@ -15,8 +15,12 @@ # 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 dotenv import load_dotenv + +load_dotenv() import time + import typing import bittensor as bt @@ -24,6 +28,7 @@ from webgenie.helpers.weights import init_wandb from webgenie.protocol import WebgenieTextSynapse, WebgenieImageSynapse from webgenie.solution import Solution +from neurons.miners.openai_miner import OpenaiMiner class Miner(BaseMinerNeuron): """ @@ -49,26 +54,24 @@ def __init__(self, config=None): priority_fn=self.priority_image, ) + self.genie_miner = OpenaiMiner(self) + init_wandb(self) + async def forward_text( self, synapse: WebgenieTextSynapse ) -> WebgenieTextSynapse: - bt.logging.debug(f"Miner text forward called with synapse: {synapse}") - synapse.solution = Solution( - html = "

Hello, world!

" - ) - return synapse + + return await self.genie_miner.forward_text(synapse) async def forward_image( self, synapse: WebgenieImageSynapse ) -> WebgenieImageSynapse: bt.logging.debug(f"Miner image forward called with synapse: {synapse}") - synapse.solution = Solution( - html = "

Hello, Image!

" - ) - return synapse + + return await self.genie_miner.forward_image(synapse) async def blacklist_text(self, synapse: WebgenieTextSynapse) -> typing.Tuple[bool, str]: return await self.blacklist(synapse) diff --git a/neurons/miners/openai_miner.py b/neurons/miners/openai_miner.py new file mode 100644 index 00000000..5e475dee --- /dev/null +++ b/neurons/miners/openai_miner.py @@ -0,0 +1,59 @@ +import bittensor as bt +import os + +from langchain_openai import ChatOpenAI +from langchain.prompts import ChatPromptTemplate +from langchain_core.output_parsers import StrOutputParser, JsonOutputParser +from langchain_core.pydantic_v1 import BaseModel, Field +from langchain_core.runnables.base import RunnableSequence + +from webgenie.base.neuron import BaseNeuron +from webgenie.protocol import WebgenieTextSynapse, WebgenieImageSynapse +from webgenie.solution.solution import Solution + +class HTMLResponse(BaseModel): + html: str = Field(default="", description="The HTML code for the webpage") + +class OpenaiMiner: + def __init__(self, neuron: BaseNeuron): + self.neuron = neuron + + self.model = ChatOpenAI( + api_key= os.getenv("OPENAI_API_KEY"), + model_name="gpt-4o", + ) + + self.html_response_parser = JsonOutputParser(pydantic_object=HTMLResponse) + + async def forward_text(self, synapse: WebgenieTextSynapse) -> WebgenieTextSynapse: + try: + prompt = ChatPromptTemplate.from_messages([ + ("system", """You are an expert web developer who specializes in HTML and CSS. A user will provide you with the webpage requirements. You need to return a single html file that uses HTML and CSS to satisfy the requirements. + Include all CSS code in the HTML file itself. + If it involves any images, use "rick.jpg" as the placeholder. + Do not hallucinate any dependencies to external files. You do not need to include JavaScript scripts for dynamic interactions. + Pay attention to things like size, text, position, and color of all the elements, as well as the overall layout. + Respond with the content of the HTML+CSS file: + {instructions}"""), + ("user", "{query}"), + ]) + + chain = prompt | self.model | self.html_response_parser + html_response = chain.invoke({ + "query": synapse.prompt, + "instructions": self.html_response_parser.get_format_instructions() + }) + + synapse.solution = Solution(html=html_response["html"]) + return synapse + except: + bt.logging.error(f"Error in OpenaiMiner forward_text: {e}") + return synapse + + async def forward_image(self, synapse: WebgenieImageSynapse) -> WebgenieImageSynapse: + try: + synapse.solution = Solution(html = "Not implemented Yet") + return synapse + except: + bt.logging.error(f"Error in OpenaiMiner forward_image: {e}") + return synapse \ No newline at end of file diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 2ab46ceb..430ec11e 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -103,5 +103,6 @@ async def process_synapse(self, synapse: bt.Synapse, miner_uid: int) -> bt.Synap if synapse.solution is None: return None synapse.solution.miner_uid = miner_uid + synapse.solution.process_time = synapse.dendrite.process_time return synapse return None \ No newline at end of file diff --git a/wandb/latest-run b/wandb/latest-run index 8f30dc87..8f84c6d6 120000 --- a/wandb/latest-run +++ b/wandb/latest-run @@ -1 +1 @@ -run-20241212_082229-ryemav9r \ No newline at end of file +run-20241212_091353-r7n43ygp \ No newline at end of file From 95591081fd8b8f2339b5265dd30b32a496dc464a Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Thu, 12 Dec 2024 10:03:16 -0600 Subject: [PATCH 031/129] feat: added image task process --- .gitignore | 5 ++- neurons/miners/openai_miner.py | 38 ++++++++++++++++--- wandb/latest-run | 2 +- webgenie/helpers/images.py | 15 ++++++++ .../task_generator/image_task_generator.py | 6 ++- 5 files changed, 57 insertions(+), 9 deletions(-) create mode 100644 webgenie/helpers/images.py diff --git a/.gitignore b/.gitignore index 26851170..73c0f3a3 100644 --- a/.gitignore +++ b/.gitignore @@ -174,4 +174,7 @@ temp.txt test.py # Wandb -wandb/ \ No newline at end of file +wandb/ + +# test images +original.jpg \ No newline at end of file diff --git a/neurons/miners/openai_miner.py b/neurons/miners/openai_miner.py index 5e475dee..6e663dd6 100644 --- a/neurons/miners/openai_miner.py +++ b/neurons/miners/openai_miner.py @@ -2,10 +2,9 @@ import os from langchain_openai import ChatOpenAI -from langchain.prompts import ChatPromptTemplate -from langchain_core.output_parsers import StrOutputParser, JsonOutputParser +from langchain.prompts import ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate +from langchain_core.output_parsers import JsonOutputParser from langchain_core.pydantic_v1 import BaseModel, Field -from langchain_core.runnables.base import RunnableSequence from webgenie.base.neuron import BaseNeuron from webgenie.protocol import WebgenieTextSynapse, WebgenieImageSynapse @@ -52,8 +51,37 @@ async def forward_text(self, synapse: WebgenieTextSynapse) -> WebgenieTextSynaps async def forward_image(self, synapse: WebgenieImageSynapse) -> WebgenieImageSynapse: try: - synapse.solution = Solution(html = "Not implemented Yet") + + prompt_messages = [ + SystemMessagePromptTemplate.from_template(""" + You are an expert web developer who specializes in HTML and CSS. + A user will provide you with a screenshot of a webpage, along with all texts that they want to put on the webpage. + You need to return a single html file that uses HTML and CSS to reproduce the given website. + Include all CSS code in the HTML file itself. + If it involves any images, use "rick.jpg" as the placeholder. + Some images on the webpage are replaced with a blue rectangle as the placeholder, use "rick.jpg" for those as well. + Do not hallucinate any dependencies to external files. You do not need to include JavaScript scripts for dynamic interactions. + Pay attention to things like size, text, position, and color of all the elements, as well as the overall layout. + Respond with the content of the HTML+CSS file: + {instructions}"""), + HumanMessagePromptTemplate.from_template( + template=[ + {"type": "image_url", "image_url": {"url": "{image_url}"}}, + ] + ) + ] + + prompt = ChatPromptTemplate(messages=prompt_messages) + + chain = prompt | self.model | self.html_response_parser + + html_response = chain.invoke({ + "instructions": self.html_response_parser.get_format_instructions(), + "image_url": f"data:image/jpeg;base64,{synapse.base64_image}", + }) + + synapse.solution = Solution(html = html_response["html"]) return synapse - except: + except Exception as e: bt.logging.error(f"Error in OpenaiMiner forward_image: {e}") return synapse \ No newline at end of file diff --git a/wandb/latest-run b/wandb/latest-run index 8f84c6d6..95dde8c5 120000 --- a/wandb/latest-run +++ b/wandb/latest-run @@ -1 +1 @@ -run-20241212_091353-r7n43ygp \ No newline at end of file +run-20241212_095851-fzlykewr \ No newline at end of file diff --git a/webgenie/helpers/images.py b/webgenie/helpers/images.py new file mode 100644 index 00000000..c0938be3 --- /dev/null +++ b/webgenie/helpers/images.py @@ -0,0 +1,15 @@ +from PIL import Image +import io +import base64 + +def pil_image_to_base64(img: Image.Image) -> str: + buffered = io.BytesIO() + img.save(buffered, format="jpeg") + img_bytes = buffered.getvalue() + base64_str = base64.b64encode(img_bytes).decode("utf-8") + + return base64_str + +def image_to_base64(image_path: str) -> str: + img = Image.open(image_path) + return pil_image_to_base64(img) diff --git a/webgenie/task_generator/image_task_generator.py b/webgenie/task_generator/image_task_generator.py index 6d86cc99..96053562 100644 --- a/webgenie/task_generator/image_task_generator.py +++ b/webgenie/task_generator/image_task_generator.py @@ -1,6 +1,7 @@ import bittensor as bt from typing import List, Tuple +from webgenie.helpers.images import image_to_base64 from webgenie.protocol import WebgenieImageSynapse from webgenie.solution import Solution from webgenie.tasks.task import Task, ImageTask @@ -11,11 +12,12 @@ def __init__(self): super().__init__() async def generate_task(self) -> Tuple[Task, bt.Synapse]: + base64_image = image_to_base64("original.jpg") return ImageTask( - base64_image="base64_image" , + base64_image=base64_image, timeout=50, generator=self - ), WebgenieImageSynapse(base64_image="base64_image") + ), WebgenieImageSynapse(base64_image=base64_image) async def reward(self, task: Task, solutions: List[Solution]) -> List[float]: if not isinstance(task, ImageTask): From f0305fe9987a09932c63aee32ee6fb4da999cadf Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Thu, 12 Dec 2024 10:08:16 -0600 Subject: [PATCH 032/129] feat: remove wandb --- .gitignore | 2 +- wandb/latest-run | 1 - .../files/requirements.txt | 139 ------------------ .../files/wandb-metadata.json | 45 ------ .../run-fmmfb2d8.wandb | Bin 82089 -> 0 bytes .../files/requirements.txt | 139 ------------------ .../files/wandb-metadata.json | 45 ------ .../run-p5q8p631.wandb | Bin 13718 -> 0 bytes 8 files changed, 1 insertion(+), 370 deletions(-) delete mode 120000 wandb/latest-run delete mode 100644 wandb/run-20241212_080228-fmmfb2d8/files/requirements.txt delete mode 100644 wandb/run-20241212_080228-fmmfb2d8/files/wandb-metadata.json delete mode 100644 wandb/run-20241212_080228-fmmfb2d8/run-fmmfb2d8.wandb delete mode 100644 wandb/run-20241212_080410-p5q8p631/files/requirements.txt delete mode 100644 wandb/run-20241212_080410-p5q8p631/files/wandb-metadata.json delete mode 100644 wandb/run-20241212_080410-p5q8p631/run-p5q8p631.wandb diff --git a/.gitignore b/.gitignore index 73c0f3a3..f50d3e89 100644 --- a/.gitignore +++ b/.gitignore @@ -174,7 +174,7 @@ temp.txt test.py # Wandb -wandb/ +# wandb/ # test images original.jpg \ No newline at end of file diff --git a/wandb/latest-run b/wandb/latest-run deleted file mode 120000 index 95dde8c5..00000000 --- a/wandb/latest-run +++ /dev/null @@ -1 +0,0 @@ -run-20241212_095851-fzlykewr \ No newline at end of file diff --git a/wandb/run-20241212_080228-fmmfb2d8/files/requirements.txt b/wandb/run-20241212_080228-fmmfb2d8/files/requirements.txt deleted file mode 100644 index d0cdb471..00000000 --- a/wandb/run-20241212_080228-fmmfb2d8/files/requirements.txt +++ /dev/null @@ -1,139 +0,0 @@ -wandb==0.19.0 -charset-normalizer==3.4.0 -triton==3.1.0 -cryptography==42.0.8 -fsspec==2024.10.0 -backoff==2.2.1 -wheel==0.45.1 -GitPython==3.1.43 -Levenshtein==0.26.1 -urllib3==2.2.3 -munch==2.5.0 -typing_extensions==4.12.2 -distro==1.9.0 -rich==13.9.4 -langchain-openai==0.2.12 -scalecodec==1.2.11 -eth-utils==2.2.2 -attrs==24.2.0 -nvidia-nccl-cu12==2.21.5 -py==1.11.0 -sentry-sdk==2.19.2 -gitdb==4.0.11 -colorama==0.4.6 -multidict==6.1.0 -nvidia-nvjitlink-cu12==12.4.127 -nvidia-curand-cu12==10.3.5.147 -setproctitle==1.3.4 -setuptools==70.0.0 -httpx==0.28.1 -aiosignal==1.3.1 -python-statemachine==2.5.0 -packaging==24.2 -langchain-core==0.3.24 -substrate-interface==1.7.11 -eth-keys==0.6.0 -webencodings==0.5.1 -propcache==0.2.1 -pydantic_core==2.27.1 -xxhash==3.5.0 -platformdirs==4.3.6 -pip==24.2 -annotated-types==0.7.0 -sympy==1.13.1 -ddt==1.6.0 -openai==1.57.2 -tenacity==9.0.0 -nvidia-cusolver-cu12==11.6.1.9 -nvidia-nvtx-cu12==12.4.127 -Pygments==2.18.0 -fastapi==0.110.3 -eth-hash==0.7.0 -smmap==5.0.1 -regex==2024.11.6 -retry==0.9.2 -pluggy==1.5.0 -ansible-vault==2.1.0 -click==8.1.7 -RapidFuzz==3.10.1 -nvidia-cusparse-cu12==12.3.1.170 -jsonpointer==3.0.0 -shtab==1.6.5 -SQLAlchemy==2.0.36 -password-strength==0.0.3.post2 -py-sr25519-bindings==0.2.1 -ansible==8.5.0 -more-itertools==10.5.0 -pycparser==2.22 -msgpack==1.1.0 -PyYAML==6.0.2 -filelock==3.16.1 -soupsieve==2.6 -beautifulsoup4==4.12.3 -py-bip39-bindings==0.2.0 -jsonpatch==1.33 -nvidia-cuda-nvrtc-cu12==12.4.127 -fuzzywuzzy==0.18.0 -h11==0.14.0 -jiter==0.8.2 -protobuf==5.29.1 -eth-typing==5.0.1 -termcolor==2.5.0 -yarl==1.18.3 -aiohappyeyeballs==2.4.4 -nvidia-cuda-cupti-cu12==12.4.127 -docker-pycreds==0.4.0 -starlette==0.37.2 -ecdsa==0.19.0 -networkx==3.4.2 -sniffio==1.3.1 -ansible-core==2.15.13 -Jinja2==3.1.4 -certifi==2024.7.4 -torch==2.5.1 -toolz==1.0.0 -langchain==0.3.11 -netaddr==1.3.0 -MarkupSafe==3.0.2 -websocket-client==1.8.0 -langsmith==0.2.3 -python-dotenv==1.0.1 -greenlet==3.1.1 -orjson==3.10.12 -idna==3.10 -frozenlist==1.5.0 -decorator==5.1.1 -anyio==4.7.0 -nest-asyncio==1.6.0 -markdown-it-py==3.0.0 -psutil==6.1.0 -nvidia-cufft-cu12==11.2.1.3 -bittensor==7.4.0 -py-ed25519-zebra-bindings==1.2.0 -nvidia-cudnn-cu12==9.1.0.70 -requests-toolbelt==1.0.0 -uvicorn==0.32.1 -msgpack-numpy-opentensor==0.5.0 -python-Levenshtein==0.26.1 -tinycss2==1.4.0 -mdurl==0.1.2 -mpmath==1.3.0 -cffi==1.17.1 -numpy==1.26.4 -six==1.17.0 -pydantic==2.10.3 -tiktoken==0.8.0 -iniconfig==2.0.0 -langchain-text-splitters==0.3.2 -pycryptodome==3.21.0 -tqdm==4.67.1 -nvidia-cublas-cu12==12.4.5.8 -requests==2.32.3 -base58==2.1.1 -httpcore==1.0.7 -aiohttp==3.11.10 -PyNaCl==1.5.0 -cytoolz==1.0.0 -pytest==8.3.4 -resolvelib==1.0.1 -nvidia-cuda-runtime-cu12==12.4.127 diff --git a/wandb/run-20241212_080228-fmmfb2d8/files/wandb-metadata.json b/wandb/run-20241212_080228-fmmfb2d8/files/wandb-metadata.json deleted file mode 100644 index 4b201625..00000000 --- a/wandb/run-20241212_080228-fmmfb2d8/files/wandb-metadata.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "os": "Linux-5.4.0-105-generic-x86_64-with-glibc2.31", - "python": "CPython 3.12.7", - "startedAt": "2024-12-12T14:02:28.547811Z", - "args": [ - "--netuid", - "214", - "--subtensor.network", - "test", - "--wallet.name", - "sc-val1", - "--wallet.hotkey", - "sh-val1", - "--logging.debug", - "--neuron.axon_port", - "8091" - ], - "program": "/home/dev2user/workspace/sangar/web-genie-ai/neurons/validators/validator.py", - "codePath": "neurons/validators/validator.py", - "git": { - "remote": "https://github.com/web-genie-ai/web-genie-ai.git", - "commit": "5cf0c922e756248e98d8a3885581aed3944ca900" - }, - "email": "hayesdominique0729@gmail.com", - "root": "/home/dev2user/workspace/sangar/web-genie-ai", - "host": "vmi1162577.contaboserver.net", - "username": "root", - "executable": "/home/dev2user/workspace/sangar/web-genie-ai/venv/bin/python3.12", - "codePathLocal": "neurons/validators/validator.py", - "cpu_count": 6, - "cpu_count_logical": 6, - "disk": { - "/": { - "total": "630869753856", - "used": "33456181248" - } - }, - "memory": { - "total": "16786370560" - }, - "cpu": { - "count": 6, - "countLogical": 6 - } -} \ No newline at end of file diff --git a/wandb/run-20241212_080228-fmmfb2d8/run-fmmfb2d8.wandb b/wandb/run-20241212_080228-fmmfb2d8/run-fmmfb2d8.wandb deleted file mode 100644 index 8a19372f048c24edb47da814bcba5f07509c6e8f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 82089 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 diff --git a/wandb/run-20241212_080410-p5q8p631/files/requirements.txt b/wandb/run-20241212_080410-p5q8p631/files/requirements.txt deleted file mode 100644 index d0cdb471..00000000 --- a/wandb/run-20241212_080410-p5q8p631/files/requirements.txt +++ /dev/null @@ -1,139 +0,0 @@ -wandb==0.19.0 -charset-normalizer==3.4.0 -triton==3.1.0 -cryptography==42.0.8 -fsspec==2024.10.0 -backoff==2.2.1 -wheel==0.45.1 -GitPython==3.1.43 -Levenshtein==0.26.1 -urllib3==2.2.3 -munch==2.5.0 -typing_extensions==4.12.2 -distro==1.9.0 -rich==13.9.4 -langchain-openai==0.2.12 -scalecodec==1.2.11 -eth-utils==2.2.2 -attrs==24.2.0 -nvidia-nccl-cu12==2.21.5 -py==1.11.0 -sentry-sdk==2.19.2 -gitdb==4.0.11 -colorama==0.4.6 -multidict==6.1.0 -nvidia-nvjitlink-cu12==12.4.127 -nvidia-curand-cu12==10.3.5.147 -setproctitle==1.3.4 -setuptools==70.0.0 -httpx==0.28.1 -aiosignal==1.3.1 -python-statemachine==2.5.0 -packaging==24.2 -langchain-core==0.3.24 -substrate-interface==1.7.11 -eth-keys==0.6.0 -webencodings==0.5.1 -propcache==0.2.1 -pydantic_core==2.27.1 -xxhash==3.5.0 -platformdirs==4.3.6 -pip==24.2 -annotated-types==0.7.0 -sympy==1.13.1 -ddt==1.6.0 -openai==1.57.2 -tenacity==9.0.0 -nvidia-cusolver-cu12==11.6.1.9 -nvidia-nvtx-cu12==12.4.127 -Pygments==2.18.0 -fastapi==0.110.3 -eth-hash==0.7.0 -smmap==5.0.1 -regex==2024.11.6 -retry==0.9.2 -pluggy==1.5.0 -ansible-vault==2.1.0 -click==8.1.7 -RapidFuzz==3.10.1 -nvidia-cusparse-cu12==12.3.1.170 -jsonpointer==3.0.0 -shtab==1.6.5 -SQLAlchemy==2.0.36 -password-strength==0.0.3.post2 -py-sr25519-bindings==0.2.1 -ansible==8.5.0 -more-itertools==10.5.0 -pycparser==2.22 -msgpack==1.1.0 -PyYAML==6.0.2 -filelock==3.16.1 -soupsieve==2.6 -beautifulsoup4==4.12.3 -py-bip39-bindings==0.2.0 -jsonpatch==1.33 -nvidia-cuda-nvrtc-cu12==12.4.127 -fuzzywuzzy==0.18.0 -h11==0.14.0 -jiter==0.8.2 -protobuf==5.29.1 -eth-typing==5.0.1 -termcolor==2.5.0 -yarl==1.18.3 -aiohappyeyeballs==2.4.4 -nvidia-cuda-cupti-cu12==12.4.127 -docker-pycreds==0.4.0 -starlette==0.37.2 -ecdsa==0.19.0 -networkx==3.4.2 -sniffio==1.3.1 -ansible-core==2.15.13 -Jinja2==3.1.4 -certifi==2024.7.4 -torch==2.5.1 -toolz==1.0.0 -langchain==0.3.11 -netaddr==1.3.0 -MarkupSafe==3.0.2 -websocket-client==1.8.0 -langsmith==0.2.3 -python-dotenv==1.0.1 -greenlet==3.1.1 -orjson==3.10.12 -idna==3.10 -frozenlist==1.5.0 -decorator==5.1.1 -anyio==4.7.0 -nest-asyncio==1.6.0 -markdown-it-py==3.0.0 -psutil==6.1.0 -nvidia-cufft-cu12==11.2.1.3 -bittensor==7.4.0 -py-ed25519-zebra-bindings==1.2.0 -nvidia-cudnn-cu12==9.1.0.70 -requests-toolbelt==1.0.0 -uvicorn==0.32.1 -msgpack-numpy-opentensor==0.5.0 -python-Levenshtein==0.26.1 -tinycss2==1.4.0 -mdurl==0.1.2 -mpmath==1.3.0 -cffi==1.17.1 -numpy==1.26.4 -six==1.17.0 -pydantic==2.10.3 -tiktoken==0.8.0 -iniconfig==2.0.0 -langchain-text-splitters==0.3.2 -pycryptodome==3.21.0 -tqdm==4.67.1 -nvidia-cublas-cu12==12.4.5.8 -requests==2.32.3 -base58==2.1.1 -httpcore==1.0.7 -aiohttp==3.11.10 -PyNaCl==1.5.0 -cytoolz==1.0.0 -pytest==8.3.4 -resolvelib==1.0.1 -nvidia-cuda-runtime-cu12==12.4.127 diff --git a/wandb/run-20241212_080410-p5q8p631/files/wandb-metadata.json b/wandb/run-20241212_080410-p5q8p631/files/wandb-metadata.json deleted file mode 100644 index c8e61208..00000000 --- a/wandb/run-20241212_080410-p5q8p631/files/wandb-metadata.json +++ /dev/null @@ -1,45 +0,0 @@ -{ - "os": "Linux-5.4.0-105-generic-x86_64-with-glibc2.31", - "python": "CPython 3.12.7", - "startedAt": "2024-12-12T14:04:10.946323Z", - "args": [ - "--netuid", - "214", - "--subtensor.network", - "test", - "--wallet.name", - "s-miner", - "--wallet.hotkey", - "miner1", - "--logging.debug", - "--axon.port", - "8090" - ], - "program": "/home/dev2user/workspace/sangar/web-genie-ai/neurons/miners/miner.py", - "codePath": "neurons/miners/miner.py", - "git": { - "remote": "https://github.com/web-genie-ai/web-genie-ai.git", - "commit": "5cf0c922e756248e98d8a3885581aed3944ca900" - }, - "email": "hayesdominique0729@gmail.com", - "root": "/home/dev2user/workspace/sangar/web-genie-ai", - "host": "vmi1162577.contaboserver.net", - "username": "root", - "executable": "/home/dev2user/workspace/sangar/web-genie-ai/venv/bin/python3.12", - "codePathLocal": "neurons/miners/miner.py", - "cpu_count": 6, - "cpu_count_logical": 6, - "disk": { - "/": { - "total": "630869753856", - "used": "33456324608" - } - }, - "memory": { - "total": "16786370560" - }, - "cpu": { - "count": 6, - "countLogical": 6 - } -} \ No newline at end of file diff --git a/wandb/run-20241212_080410-p5q8p631/run-p5q8p631.wandb b/wandb/run-20241212_080410-p5q8p631/run-p5q8p631.wandb deleted file mode 100644 index f768135d09c2656671e5383f57d5822d7eaacab6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 13718 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 Eze2+$8~^|S From 7036a55d20e66006776bb0f446ede0387aafd677 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Thu, 12 Dec 2024 10:08:58 -0600 Subject: [PATCH 033/129] feat: add wandb to the gitignore list --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index f50d3e89..73c0f3a3 100644 --- a/.gitignore +++ b/.gitignore @@ -174,7 +174,7 @@ temp.txt test.py # Wandb -# wandb/ +wandb/ # test images original.jpg \ No newline at end of file From ad8f1a59b25fb5a83c8f217b9f9742d45e82120d Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Thu, 12 Dec 2024 14:18:20 -0600 Subject: [PATCH 034/129] feat: implemented organic task --- neurons/validators/genie_validator.py | 13 +++--- neurons/validators/validator.py | 62 +++++++++++++++++++++++---- webgenie/base/validator.py | 38 +--------------- 3 files changed, 61 insertions(+), 52 deletions(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 430ec11e..0f8747c7 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -1,17 +1,15 @@ import bittensor as bt import random -import torch -from typing import List +from typing import Union from webgenie.base.neuron import BaseNeuron -from webgenie.protocol import WebgenieStreamingSynapse, WebgenieTextSynapse +from webgenie.protocol import WebgenieImageSynapse, WebgenieTextSynapse from webgenie.rewards import RewardManager from webgenie.solution import Solution from webgenie.task_generator.image_task_generator import ImageTaskGenerator from webgenie.task_generator.text_task_generator import TextTaskGenerator from webgenie.utils.uids import get_random_uids - MAX_SYNTHETIC_HISTORY_SIZE = 10 class GenieValidator: @@ -71,12 +69,13 @@ async def score(self): self.neuron.update_scores(scores, [solution.miner_uid for solution in solutions]) self.neuron.sync() - async def organic_forward(self, synapse: WebgenieTextSynapse): + async def organic_forward(self, synapse: Union[WebgenieTextSynapse, WebgenieImageSynapse]): + bt.logging.debug(f"Organic forward: {synapse}") best_miner_uid = 1 try: - axon = self.metagraph.axons[best_miner_uid] + axon = self.neuron.metagraph.axons[best_miner_uid] - async with bt.dendrite(wallet=self.wallet) as dendrite: + async with bt.dendrite(wallet=self.neuron.wallet) as dendrite: bt.logging.info(f"Dendrite: {dendrite}") responses = await dendrite( axons=[axon], diff --git a/neurons/validators/validator.py b/neurons/validators/validator.py index fbf3245f..e5763f21 100644 --- a/neurons/validators/validator.py +++ b/neurons/validators/validator.py @@ -1,18 +1,19 @@ # The MIT License (MIT) # Copyright © 2023 Yuma Rao # Copyright © 2024 pycorn +import bittensor as bt import asyncio from dotenv import load_dotenv load_dotenv() -import time - -import bittensor as bt +from typing import Tuple, Union from webgenie.base.validator import BaseValidatorNeuron from webgenie.helpers.weights import init_wandb -from webgenie.protocol import WebgenieStreamingSynapse +from webgenie.protocol import WebgenieTextSynapse, WebgenieImageSynapse from neurons.validators.genie_validator import GenieValidator +API_HOTKEY = "5D72esHuc1DxD6PD8S6VyU24bTHGQjHHyodzTGsem1sejUYj" + class Validator(BaseValidatorNeuron): """ Your validator neuron class. You should use this class to define your validator's behavior. In particular, you should replace the forward function with your own logic. @@ -26,13 +27,58 @@ def __init__(self, config=None): super(Validator, self).__init__(config=config) bt.logging.info("load_state()") self.load_state() + init_wandb(self) + + if not self.config.axon_off: + self.serve_axon() + self.genie_validator = GenieValidator(neuron=self) - init_wandb(self) + async def blacklist_text(self, synapse: WebgenieTextSynapse) -> Tuple[bool, str]: + """ + Only allow the subnet owner to send synapse to the validator. + """ + if synapse.dendrite.hotkey == API_HOTKEY: + return False, "Subnet owner hotkey" + return True, "Blacklisted" + async def blacklist_image(self, synapse: WebgenieImageSynapse) -> Tuple[bool, str]: + """ + Only allow the subnet owner to send synapse to the validator. + """ + if synapse.dendrite.hotkey == API_HOTKEY: + return False, "Subnet owner hotkey" + return True, "Blacklisted" + + async def organic_forward_text(self, synapse: WebgenieTextSynapse): + return await self.genie_validator.organic_forward(synapse) - async def organic_forward(self, synapse: WebgenieStreamingSynapse): + async def organic_forward_image(self, synapse: WebgenieImageSynapse): return await self.genie_validator.organic_forward(synapse) + 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.organic_forward_text, + blacklist_fn = self.blacklist_text + ).attach( + forward_fn = self.organic_forward_image, + blacklist_fn = self.blacklist_image + ) + + self.axon.serve( + netuid=self.config.netuid, + subtensor=self.subtensor, + ) + self.axon.start() + bt.logging.info(f"Validator running in organic mode on port {self.config.neuron.axon_port}") + except Exception as e: + bt.logging.error(f"Failed to serve Axon with exception: {e}") + pass + async def forward(self): return await self.genie_validator.forward() @@ -69,8 +115,8 @@ async def scoring_loop(self): await asyncio.sleep(5) async def __aenter__(self): - self.loop.create_task(self.forward_loop()) - self.loop.create_task(self.scoring_loop()) + #self.loop.create_task(self.forward_loop()) + #self.loop.create_task(self.scoring_loop()) self.is_running = True bt.logging.debug("Starting validator in background thread") return self diff --git a/webgenie/base/validator.py b/webgenie/base/validator.py index d777187d..e7202051 100644 --- a/webgenie/base/validator.py +++ b/webgenie/base/validator.py @@ -36,7 +36,6 @@ from webgenie.utils.config import add_validator_args from webgenie.protocol import WebgenieStreamingSynapse -SUBNET_OWNER_HOTKEY = "5G9sRcoaw2H3SYDq7e7PoGhbbMUPHQi6pC6tPrahmSmDtxS8" class BaseValidatorNeuron(BaseNeuron): """ @@ -78,46 +77,11 @@ def __init__(self, config=None): self.is_running: bool = False self.thread: Union[threading.Thread, None] = 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.organic_forward, - blacklist_fn = self.blacklist - ) - self.axon.serve( - netuid=self.config.netuid, - subtensor=self.subtensor, - ) - self.axon.start() - bt.logging.info(f"Validator running in organic mode on port {self.config.neuron.axon_port}") - except Exception as e: - bt.logging.error(f"Failed to serve Axon with exception: {e}") - pass + def run(self): pass - async def blacklist(self, synapse: WebgenieStreamingSynapse) -> Tuple[bool, str]: - """ - Only allow the subnet owner to send synapse to the validator. - """ - if synapse.dendrite.hotkey == SUBNET_OWNER_HOTKEY: - return False, "Subnet owner hotkey" - return True, "Blacklisted" - async def organic_forward(self, synapse: WebgenieStreamingSynapse) -> WebgenieStreamingSynapse: - - bt.logging.error(f"OrganicForward Thread Name: {threading.current_thread().name}") - bt.logging.info(f"=========> {synapse}") - - return await forward_organic_synapse(self, synapse) - 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. From 5d061cc1a4904dbab04011d91721f5b9d2094226 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Thu, 12 Dec 2024 14:27:36 -0600 Subject: [PATCH 035/129] chore: removed version in wandb run name --- webgenie/helpers/weights.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webgenie/helpers/weights.py b/webgenie/helpers/weights.py index 835c9449..0ea254b6 100644 --- a/webgenie/helpers/weights.py +++ b/webgenie/helpers/weights.py @@ -16,7 +16,7 @@ def init_wandb(self): wandb_on = True wandb.login(key=os.getenv("WANDB_API_KEY")) - run_name = f"{self.config.neuron.name}-{self.uid}--{webgenie.__version__}" + run_name = f"{self.config.neuron.name}-{self.uid}" run = wandb.init( project=webgenie.PROJECT_NAME, entity=os.getenv("WANDB_ENTITY_NAME"), From b8c8807b022ba84091c57dc90eead9ba4d339145 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 05:59:58 -0600 Subject: [PATCH 036/129] feat: implemented visual reward --- .gitignore | 5 +- neurons/miners/miner.py | 3 +- neurons/miners/openai_miner.py | 6 +- neurons/validators/genie_validator.py | 33 +- neurons/validators/validator.py | 4 +- requirements.txt | 169 ++++- run_miner.sh | 2 +- run_validator.sh | 2 +- webgenie/base/validator.py | 1 - webgenie/constants.py | 4 + webgenie/datasets/dataset.py | 152 +++++ webgenie/helpers/htmls.py | 14 + webgenie/protocol.py | 108 +--- webgenie/rewards/__init__.py | 5 +- webgenie/rewards/gpt.py | 47 -- webgenie/rewards/is_valid.py | 47 -- webgenie/rewards/metrics/__init__.py | 0 webgenie/rewards/metrics/dedup_post_gen.py | 69 +++ .../rewards/metrics/multi_processing_eval.py | 123 ++++ webgenie/rewards/metrics/ocr_free_utils.py | 259 ++++++++ webgenie/rewards/metrics/screenshot_single.py | 48 ++ webgenie/rewards/metrics/visual_score.py | 581 ++++++++++++++++++ webgenie/rewards/reward.py | 13 +- webgenie/rewards/reward_manager.py | 67 -- webgenie/rewards/speed.py | 15 - webgenie/rewards/visual_reward.py | 33 + webgenie/solution/__init__.py | 1 - webgenie/task_generator/__init__.py | 1 - .../task_generator/image_task_generator.py | 27 - .../task_generator/text_task_generator.py | 26 - webgenie/tasks/__init__.py | 6 +- webgenie/tasks/image_task_generator.py | 33 + webgenie/{solution => tasks}/solution.py | 0 webgenie/tasks/task.py | 6 +- .../task_generator.py | 13 +- webgenie/tasks/text_task_generator.py | 26 + 36 files changed, 1560 insertions(+), 389 deletions(-) create mode 100644 webgenie/constants.py create mode 100644 webgenie/datasets/dataset.py create mode 100644 webgenie/helpers/htmls.py delete mode 100644 webgenie/rewards/gpt.py delete mode 100644 webgenie/rewards/is_valid.py create mode 100644 webgenie/rewards/metrics/__init__.py create mode 100644 webgenie/rewards/metrics/dedup_post_gen.py create mode 100644 webgenie/rewards/metrics/multi_processing_eval.py create mode 100644 webgenie/rewards/metrics/ocr_free_utils.py create mode 100644 webgenie/rewards/metrics/screenshot_single.py create mode 100644 webgenie/rewards/metrics/visual_score.py delete mode 100644 webgenie/rewards/reward_manager.py delete mode 100644 webgenie/rewards/speed.py create mode 100644 webgenie/rewards/visual_reward.py delete mode 100644 webgenie/solution/__init__.py delete mode 100644 webgenie/task_generator/__init__.py delete mode 100644 webgenie/task_generator/image_task_generator.py delete mode 100644 webgenie/task_generator/text_task_generator.py create mode 100644 webgenie/tasks/image_task_generator.py rename webgenie/{solution => tasks}/solution.py (100%) rename webgenie/{task_generator => tasks}/task_generator.py (52%) create mode 100644 webgenie/tasks/text_task_generator.py diff --git a/.gitignore b/.gitignore index 73c0f3a3..a169e7c9 100644 --- a/.gitignore +++ b/.gitignore @@ -177,4 +177,7 @@ test.py wandb/ # test images -original.jpg \ No newline at end of file +original.jpg + +# work dir +work/ \ No newline at end of file diff --git a/neurons/miners/miner.py b/neurons/miners/miner.py index 3b0dafa6..5b09a02f 100644 --- a/neurons/miners/miner.py +++ b/neurons/miners/miner.py @@ -27,7 +27,7 @@ from webgenie.base.miner import BaseMinerNeuron from webgenie.helpers.weights import init_wandb from webgenie.protocol import WebgenieTextSynapse, WebgenieImageSynapse -from webgenie.solution import Solution +from webgenie.tasks import Solution from neurons.miners.openai_miner import OpenaiMiner class Miner(BaseMinerNeuron): @@ -188,5 +188,4 @@ async def priority(self, synapse: bt.Synapse) -> float: if __name__ == "__main__": with Miner() as miner: while True: - bt.logging.info(f"Miner running... {time.time()}") time.sleep(5) diff --git a/neurons/miners/openai_miner.py b/neurons/miners/openai_miner.py index 6e663dd6..c7d2f1f5 100644 --- a/neurons/miners/openai_miner.py +++ b/neurons/miners/openai_miner.py @@ -8,7 +8,7 @@ from webgenie.base.neuron import BaseNeuron from webgenie.protocol import WebgenieTextSynapse, WebgenieImageSynapse -from webgenie.solution.solution import Solution +from webgenie.tasks.solution import Solution class HTMLResponse(BaseModel): html: str = Field(default="", description="The HTML code for the webpage") @@ -43,7 +43,7 @@ async def forward_text(self, synapse: WebgenieTextSynapse) -> WebgenieTextSynaps "instructions": self.html_response_parser.get_format_instructions() }) - synapse.solution = Solution(html=html_response["html"]) + synapse.html = html_response["html"] return synapse except: bt.logging.error(f"Error in OpenaiMiner forward_text: {e}") @@ -80,7 +80,7 @@ async def forward_image(self, synapse: WebgenieImageSynapse) -> WebgenieImageSyn "image_url": f"data:image/jpeg;base64,{synapse.base64_image}", }) - synapse.solution = Solution(html = html_response["html"]) + synapse.html = html_response["html"] return synapse except Exception as e: bt.logging.error(f"Error in OpenaiMiner forward_image: {e}") diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 0f8747c7..0a6ba728 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -3,22 +3,19 @@ from typing import Union from webgenie.base.neuron import BaseNeuron +from webgenie.constants import MAX_SYNTHETIC_HISTORY_SIZE from webgenie.protocol import WebgenieImageSynapse, WebgenieTextSynapse -from webgenie.rewards import RewardManager -from webgenie.solution import Solution -from webgenie.task_generator.image_task_generator import ImageTaskGenerator -from webgenie.task_generator.text_task_generator import TextTaskGenerator +from webgenie.tasks.solution import Solution +from webgenie.tasks.image_task_generator import ImageTaskGenerator +from webgenie.tasks.text_task_generator import TextTaskGenerator from webgenie.utils.uids import get_random_uids -MAX_SYNTHETIC_HISTORY_SIZE = 10 - class GenieValidator: def __init__(self, neuron: BaseNeuron): self.neuron = neuron self.config = neuron.config self.synthetic_history = [] - self.reward_manager = RewardManager() self.task_generators = [ TextTaskGenerator(), ImageTaskGenerator(), @@ -42,9 +39,9 @@ async def forward(self): solutions = [] for synapse, miner_uid in zip(all_synapse_results, miner_uids): - processed_synapse = await self.process_synapse(synapse, miner_uid) + processed_synapse = await self.process_synapse(synapse) if processed_synapse is not None: - solutions.append(processed_synapse.solution) + solutions.append(Solution(html = processed_synapse.html, miner_uid = miner_uid, process_time = processed_synapse.dendrite.process_time)) if len(solutions) == 0: bt.logging.warning(f"No valid solutions received") @@ -82,26 +79,18 @@ async def organic_forward(self, synapse: Union[WebgenieTextSynapse, WebgenieImag synapse=synapse, timeout=synapse.timeout, ) - - processed_synapse = await self.process_synapse(responses[0], best_miner_uid) + + processed_synapse = await self.process_synapse(responses[0]) if processed_synapse is None: raise Exception(f"No valid solution received") - + return processed_synapse except Exception as e: bt.logging.error(f"[forward_organic_synapse] Error querying dendrite: {e}") - synapse.solution = Solution( - html = f"Error: {e}", - process_time = 0, - miner_uid = best_miner_uid, - ) + synapse.html = f"Error: {e}" return synapse - async def process_synapse(self, synapse: bt.Synapse, miner_uid: int) -> bt.Synapse: + async def process_synapse(self, synapse: bt.Synapse) -> bt.Synapse: if synapse.dendrite.status_code == 200: - if synapse.solution is None: - return None - synapse.solution.miner_uid = miner_uid - synapse.solution.process_time = synapse.dendrite.process_time return synapse return None \ No newline at end of file diff --git a/neurons/validators/validator.py b/neurons/validators/validator.py index e5763f21..f57e6e6c 100644 --- a/neurons/validators/validator.py +++ b/neurons/validators/validator.py @@ -115,8 +115,8 @@ async def scoring_loop(self): await asyncio.sleep(5) async def __aenter__(self): - #self.loop.create_task(self.forward_loop()) - #self.loop.create_task(self.scoring_loop()) + self.loop.create_task(self.forward_loop()) + self.loop.create_task(self.scoring_loop()) self.is_running = True bt.logging.debug("Starting validator in background thread") return self diff --git a/requirements.txt b/requirements.txt index c8d4ef70..4d9dca97 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,11 +1,158 @@ -bittensor==7.4.0 -starlette>=0.30.0 -pydantic>=2 -rich>=13 -pytest>=8 -torch>=2 -numpy>=1 -setuptools>=68 -langchain -langchain-openai -python-dotenv \ No newline at end of file +aiohappyeyeballs==2.4.4 +aiosignal==1.3.1 +annotated-types==0.7.0 +ansible==8.5.0 +ansible-core==2.15.13 +ansible-vault==2.1.0 +anyio==4.7.0 +attrs==24.2.0 +backoff==2.2.1 +base58==2.1.1 +beautifulsoup4==4.12.3 +bittensor==8.4.5 +certifi==2024.7.4 +cffi==1.17.1 +charset-normalizer==3.4.0 +click==8.1.7 +colorama==0.4.6 +colormath==3.0.0 +contourpy==1.3.1 +cryptography~=43.0.1 +cycler==0.12.1 +cytoolz==1.0.0 +ddt==1.6.0 +decorator==5.1.1 +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 +fastapi==0.110.3 +filelock==3.16.1 +fonttools==4.55.3 +frozenlist==1.5.0 +fsspec==2024.10.0 +ftfy==6.3.1 +fuzzywuzzy==0.18.0 +gitdb==4.0.11 +GitPython==3.1.43 +greenlet==3.1.1 +h11==0.14.0 +httpcore==1.0.7 +httpx==0.28.1 +idna==3.10 +iniconfig==2.0.0 +Jinja2==3.1.4 +jiter==0.8.2 +joblib==1.4.2 +jsonpatch==1.33 +jsonpointer==3.0.0 +kiwisolver==1.4.7 +langchain==0.3.11 +langchain-core==0.3.24 +langchain-openai==0.2.12 +langchain-text-splitters==0.3.2 +langsmith==0.2.3 +Levenshtein==0.26.1 +markdown-it-py==3.0.0 +MarkupSafe==3.0.2 +matplotlib==3.9.3 +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 +munch==2.5.0 +nest-asyncio==1.6.0 +netaddr==1.3.0 +networkx==3.4.2 +numpy~=2.0.1 +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.57.2 +openai-clip==1.0.1 +opencv-python==4.10.0.84 +orjson==3.10.12 +packaging==24.2 +password-strength==0.0.3.post2 +pillow==11.0.0 +platformdirs==4.3.6 +playwright==1.49.1 +pluggy==1.5.0 +propcache==0.2.1 +protobuf==5.29.1 +psutil==6.1.0 +py==1.11.0 +py-ed25519-zebra-bindings==1.2.0 +py-sr25519-bindings==0.2.1 +pycparser==2.22 +pycryptodome==3.21.0 +pydantic==2.10.3 +pydantic_core==2.27.1 +pyee==12.0.0 +Pygments==2.18.0 +PyNaCl==1.5.0 +pyparsing==3.2.0 +pytest==8.3.4 +python-dateutil==2.9.0.post0 +python-dotenv==1.0.1 +python-Levenshtein==0.26.1 +python-statemachine==2.5.0 +PyYAML==6.0.2 +RapidFuzz==3.10.1 +regex==2024.11.6 +requests==2.32.3 +requests-toolbelt==1.0.0 +resolvelib==1.0.1 +retry==0.9.2 +rich==13.9.4 +scalecodec==1.2.11 +scikit-learn==1.6.0 +scipy==1.14.1 +sentry-sdk==2.19.2 +setproctitle==1.3.4 +setuptools~=70.0.0 +shtab==1.6.5 +six==1.17.0 +smmap==5.0.1 +sniffio==1.3.1 +soupsieve==2.6 +SQLAlchemy==2.0.36 +starlette==0.37.2 +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 +tinycss2==1.4.0 +toolz==1.0.0 +torch==2.5.1 +torchvision==0.20.1 +tqdm==4.67.1 +traitlets==5.14.3 +triton==3.1.0 +typing_extensions==4.12.2 +urllib3==2.2.3 +uvicorn==0.32.1 +wandb==0.19.0 +wcwidth==0.2.13 +webencodings==0.5.1 +websocket-client==1.8.0 +wheel==0.45.1 +xxhash==3.5.0 +yarl==1.18.3 diff --git a/run_miner.sh b/run_miner.sh index 0d2d9538..e53ded1a 100644 --- a/run_miner.sh +++ b/run_miner.sh @@ -1,4 +1,4 @@ export PYTHONPATH=. #--axon.port 5555 -python3.12 neurons/miners/miner.py --netuid 214 --subtensor.network test --wallet.name s-miner --wallet.hotkey miner1 --logging.debug --axon.port 8090 +python neurons/miners/miner.py --netuid 214 --subtensor.network test --wallet.name s-miner --wallet.hotkey miner1 --logging.debug --axon.port 8090 diff --git a/run_validator.sh b/run_validator.sh index 508d7d76..277a7ac0 100644 --- a/run_validator.sh +++ b/run_validator.sh @@ -1,3 +1,3 @@ export PYTHONASYNCIODEBUG=1 export PYTHONPATH=. -python3.12 neurons/validators/validator.py --netuid 214 --subtensor.network test --wallet.name sc-val1 --wallet.hotkey sh-val1 --logging.debug --neuron.axon_port 8091 +python neurons/validators/validator.py --netuid 214 --subtensor.network test --wallet.name sc-val1 --wallet.hotkey sh-val1 --logging.debug --neuron.axon_port 8091 diff --git a/webgenie/base/validator.py b/webgenie/base/validator.py index e7202051..5d953236 100644 --- a/webgenie/base/validator.py +++ b/webgenie/base/validator.py @@ -34,7 +34,6 @@ ) # TODO: Replace when bittensor switches to numpy from webgenie.mock import MockDendrite from webgenie.utils.config import add_validator_args -from webgenie.protocol import WebgenieStreamingSynapse class BaseValidatorNeuron(BaseNeuron): diff --git a/webgenie/constants.py b/webgenie/constants.py new file mode 100644 index 00000000..49ff9e30 --- /dev/null +++ b/webgenie/constants.py @@ -0,0 +1,4 @@ +MAX_SYNTHETIC_HISTORY_SIZE = 10 +PLACE_HOLDER_IMAGE_URL = "https://picsum.photos/seed/picsum/800/600" +SCREENSHOT_SCRIPT_PATH = "webgenie/rewards/metrics/screenshot_single.py" +WORK_DIR = "work" diff --git a/webgenie/datasets/dataset.py b/webgenie/datasets/dataset.py new file mode 100644 index 00000000..ce3f4373 --- /dev/null +++ b/webgenie/datasets/dataset.py @@ -0,0 +1,152 @@ +from pydantic import Field, BaseModel + +class DatasetEntry(BaseModel): + src: str = Field(default="", description="The source of the dataset entry") + topic: str = Field(default="", description="The topic of the dataset entry") + ground_truth_html: str = Field(default="", description="The ground truth html") + prompt: str = Field(default="", description="The prompt for the text task") + base64_image: str = Field(default="", description="The base64 encoded image") + +class Dataset: + async def generate_context(self)->DatasetEntry: + pass + +class MockUpDataset(Dataset): + async def generate_context(self)->DatasetEntry: + html = """ + + + + + +Tech Company + + + + +
+
+ + +
+
+
+Hero Image +
+
+

Welcome to Tech Company

+

At Tech Company, we are dedicated to providing the best technology solutions for your needs. Our team of experts is always ready to help you with any questions or problems you may have.

+
+ + + + """ + return DatasetEntry( + src="mockup", + topic="tech company", + ground_truth_html=html, + prompt="", + base64_image="" + ) + +class MockUpPromptDataset(Dataset): + async def generate_context(self)->DatasetEntry: + return DatasetEntry( + src="mockup", + topic="tech company", + ground_truth_html="", + prompt="CommingSoon Page with goback button, navHeader, and footer", + base64_image="" + ) \ No newline at end of file diff --git a/webgenie/helpers/htmls.py b/webgenie/helpers/htmls.py new file mode 100644 index 00000000..93fc6ff9 --- /dev/null +++ b/webgenie/helpers/htmls.py @@ -0,0 +1,14 @@ +import os +import time +import uuid +from webgenie.constants import SCREENSHOT_SCRIPT_PATH, WORK_DIR +from webgenie.helpers.images import image_to_base64 + +def html_to_screenshot(html: str) -> str: + html_path = f"{WORK_DIR}/screenshot_{uuid.uuid4()}.html" + with open(html_path, "w") as f: + f.write(html) + png_path = f"{WORK_DIR}/screenshot_{uuid.uuid4()}.png" + os.system(f"python3 {SCREENSHOT_SCRIPT_PATH} --html {html_path} --png {png_path}") + time.sleep(0.1) + return image_to_base64(png_path) \ No newline at end of file diff --git a/webgenie/protocol.py b/webgenie/protocol.py index d6d7824e..56ba95c7 100644 --- a/webgenie/protocol.py +++ b/webgenie/protocol.py @@ -3,12 +3,6 @@ import bittensor as bt import pydantic -import json -from typing import AsyncIterator, Union, Any -from starlette.responses import StreamingResponse - -from webgenie.solution import Solution -from webgenie.tasks import Task class WebgenieTextSynapse(bt.Synapse): """ @@ -20,10 +14,10 @@ class WebgenieTextSynapse(bt.Synapse): description="The prompt to be sent to miners." ) - solution: Union[Solution, None] = pydantic.Field( - None, - title="Solution", - description="A solution received from miners." + html: str = pydantic.Field( + "", + title="HTML", + description="The HTML received from miners." ) class WebgenieImageSynapse(bt.Synapse): @@ -36,96 +30,8 @@ class WebgenieImageSynapse(bt.Synapse): description="The base64 image to be sent to miners." ) - solution: Union[Solution, None] = pydantic.Field( - None, - title="Solution", - description="A solution received from miners." - ) - - -class WebgenieStreamingSynapse(bt.StreamingSynapse): - """ - A protocol for the webgenie streaming task. - """ - - task: Union[Task, None] = pydantic.Field( - None, - title="Task", - description="A task to be sent to miners." - ) - - solution: Union[Solution, None] = pydantic.Field( - None, - title="Solution", - description="A solution received from miners." - ) - - completion: str = pydantic.Field( + html: str = pydantic.Field( "", - title="Completion", - description="The completion response from miners." + title="HTML", + description="The HTML received from miners." ) - - async def process_streaming_response(self, response: StreamingResponse) -> AsyncIterator[str]: - """ - Processes a streaming response from a miner. - """ - if self.completion is None: - self.completion = "" - async for chunk in response.content.iter_any(): - tokens = chunk.decode("utf-8") - - for token in tokens: - if token: - self.completion += token - yield tokens - - def deserialize(self) -> Union[Any, None]: - """ - Deserializes the response. - """ - try: - json_response = json.loads(self.completion) - css = None - html = None - for key, value in json_response.items(): - if key.lower() == "css": - css = value - elif key.lower() == "html": - html = value - - if css is None and html is None: - bt.logging.error(f"Invalid response: {json_response}") - return None - - css = str(css) - html = str(html) - - process_time = self.dendrite.process_time - bt.logging.debug(f"css: {css}, html: {html}, process_time: {process_time}") - self.solution = Solution(css=css, html=html, process_time=process_time, miner_uid=0) - return self - except Exception as e: - bt.logging.error(f"Failed to parse completion: {e}") - return None - - - def extract_response_json(self, response: StreamingResponse) -> dict: - """ - Extracts the response JSON. - """ - headers = { - k.decode("utf-8"): v.decode("utf-8") - for k, v in response.__dict__["_raw_headers"] - } - - def extract_info(prefix:str) -> dict: - return { - key.split("_")[-1]: value - for key, value in headers.items() - if key.startswith(prefix) - } - return { - "completion": self.completion - } - \ No newline at end of file diff --git a/webgenie/rewards/__init__.py b/webgenie/rewards/__init__.py index e769de83..a6cba18e 100644 --- a/webgenie/rewards/__init__.py +++ b/webgenie/rewards/__init__.py @@ -1,5 +1,2 @@ from .reward import Reward -from .gpt import GPTReward -from .speed import SpeedReward -from .is_valid import IsValidReward -from .reward_manager import RewardManager +from .visual_reward import VisualReward diff --git a/webgenie/rewards/gpt.py b/webgenie/rewards/gpt.py deleted file mode 100644 index 056fc1c8..00000000 --- a/webgenie/rewards/gpt.py +++ /dev/null @@ -1,47 +0,0 @@ -import os -from dotenv import load_dotenv - -from langchain_openai import ChatOpenAI -import bittensor as bt -from webgenie.rewards import Reward -from webgenie.tasks import Task -from webgenie.solution import Solution - -class GPTReward(Reward): - def __init__(self): - load_dotenv() - api_key = os.getenv("OPENAI_API_KEY") - self.model = ChatOpenAI( - api_key=api_key, - model="gpt-4", - ) - def reward(self, task: Task, solution: Solution) -> float: - # Use GPT to evaluate the code - query = f""" - Task: {task.query} - Generated Code: - "```CSS" - {solution.css} - "```HTML" - {solution.html} - "```" - Evaluate the generated code based on the following criteria: - 1. Correctness: Does the code implement the required functionality? - 2. Code quality: Is the code well-structured and following best practices? - 3. Completeness: Does the code address all aspects of the task? - - Provide a score between 0 and 1, where 1 is perfect and 0 is completely incorrect. - Only return the score, without any explanations. - """ - - try: - messages=[ - {"role": "system", "content": "You are a code evaluation expert."}, - {"role": "user", "content": query} - ] - evaluation = self.model.invoke(messages) - bt.logging.info(f"Evaluation: {evaluation.content}") - return float(evaluation.content) - except Exception as e: - bt.logging.error(f"Error evaluating code: {e}") - return 0.0 \ No newline at end of file diff --git a/webgenie/rewards/is_valid.py b/webgenie/rewards/is_valid.py deleted file mode 100644 index 4a065508..00000000 --- a/webgenie/rewards/is_valid.py +++ /dev/null @@ -1,47 +0,0 @@ -from bs4 import BeautifulSoup -import tinycss2 - -from webgenie.rewards import Reward -from webgenie.tasks import Task -from webgenie.solution import Solution - -class IsValidReward(Reward): - - def __init__(self): - pass - - def is_valid_css(self, css: str) -> bool: - - try: - # Parse the CSS using tinycss2 - parsed_css = tinycss2.parse_stylesheet(css) - # Check if the CSS was parsed successfully - if parsed_css is not None: - return True - else: - return False - except Exception: - # If parsing fails, the CSS is invalid - return False - return True - - def is_valid_html(self, html: str) -> bool: - - try: - # Parse the HTML using BeautifulSoup - soup = BeautifulSoup(html, 'html.parser') - - # Check if the HTML has a valid structure - if soup.find(): - return True - else: - return False - except Exception: - # If parsing fails, the HTML is invalid - return False - - def reward(self, task: Task, solution: Solution) -> float: - if self.is_valid_css(solution.css) and self.is_valid_html(solution.html): - return 0.0 - else: - return 1.0 diff --git a/webgenie/rewards/metrics/__init__.py b/webgenie/rewards/metrics/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/webgenie/rewards/metrics/dedup_post_gen.py b/webgenie/rewards/metrics/dedup_post_gen.py new file mode 100644 index 00000000..2af9bd43 --- /dev/null +++ b/webgenie/rewards/metrics/dedup_post_gen.py @@ -0,0 +1,69 @@ +import difflib +import os +import re + +def map_positions(clean_text, original_text): + """ + Maps the positions from the clean text back to the original text. + """ + map_clean_to_original = [] + original_idx = 0 + + for clean_char in clean_text: + while original_text[original_idx] != clean_char: + original_idx += 1 + map_clean_to_original.append(original_idx) + original_idx += 1 + + return map_clean_to_original + +def check_repetitive_content(file_path, chunk_size=100, repetition_threshold=5, similarity_threshold=0.8, debug=False): + """ + Checks for repetitive content in a text file, considering both exact and similar chunks, + ignoring HTML tags but keeping the original position reference. + + :param file_path: Path to the text file. + :param chunk_size: The size of each chunk for comparison. + :param repetition_threshold: Minimum number of repetitions to consider it as repetitive content. + :param similarity_threshold: The threshold for considering two chunks as similar (0 to 1). + :return: A tuple indicating if repetitive content was found and the position where it starts in the original file. + """ + with open(file_path, 'r', encoding='utf-8') as file: + content = file.read() + + # Clean HTML content and keep a map of positions + content_no_html = re.sub('<.*?>', '', content) + position_map = map_positions(content_no_html, content) + + # Split content into chunks + chunks = [content_no_html[i:i + chunk_size] for i in range(0, len(content_no_html), chunk_size)] + + # Check for repetitive and similar chunks + seen = {} + repetitive_start = len(content_no_html) + for i, chunk in enumerate(chunks): + for seen_chunk, indexes in seen.items(): + similarity = difflib.SequenceMatcher(None, chunk, seen_chunk).ratio() + if similarity >= similarity_threshold: + indexes.append(i) + if len(indexes) >= repetition_threshold: + clean_start = min(repetitive_start, indexes[0] * chunk_size) + c_repetitive_start = position_map[clean_start] if clean_start < len(position_map) else len(content) + if c_repetitive_start < repetitive_start: + repetitive_start = c_repetitive_start + break + else: + seen[chunk] = [i] + + repetitive, start_position = repetitive_start != len(content_no_html), repetitive_start + + if repetitive: + print(f"[Warning] Repetitive content found in {file_path}, start at {start_position}") + print(f"[Warning] You might want to manually check whether the automatic repetition removal is correct.") + if not debug: + os.rename(file_path, file_path.replace(".html", "_old.txt")) + with open(file_path, 'w', encoding='utf-8') as file: + file.write(content[:start_position]) + else: + with open(file_path.replace(".html", "_new.html"), 'w', encoding='utf-8') as file: + file.write(content[:start_position]) diff --git a/webgenie/rewards/metrics/multi_processing_eval.py b/webgenie/rewards/metrics/multi_processing_eval.py new file mode 100644 index 00000000..a1f6b377 --- /dev/null +++ b/webgenie/rewards/metrics/multi_processing_eval.py @@ -0,0 +1,123 @@ +from metrics.visual_score import visual_eval_v3_multi +from multiprocessing import Pool +import contextlib, joblib +from joblib import Parallel, delayed +from tqdm import tqdm +import numpy as np +import json +import os +import shutil + +@contextlib.contextmanager +def tqdm_joblib(tqdm_object): + """Context manager to patch joblib to report into tqdm progress bar given as argument""" + class TqdmBatchCompletionCallback(joblib.parallel.BatchCompletionCallBack): + def __call__(self, *args, **kwargs): + tqdm_object.update(n=self.batch_size) + return super().__call__(*args, **kwargs) + + old_batch_callback = joblib.parallel.BatchCompletionCallBack + joblib.parallel.BatchCompletionCallBack = TqdmBatchCompletionCallback + try: + yield tqdm_object + finally: + joblib.parallel.BatchCompletionCallBack = old_batch_callback + tqdm_object.close() + + +def print_multi_score(multi_score): + _, final_size_score, final_matched_text_score, final_position_score, final_text_color_score, final_clip_score = multi_score + print() + print("Block-Match: ", final_size_score) + print("Text: ", final_matched_text_score) + print("Position: ", final_position_score) + print("Color: ", final_text_color_score) + print("CLIP: ", final_clip_score) + print("--------------------------------\n") + +if __name__ == "__main__": + debug = False + multiprocessing = True + + orig_reference_dir = "../testset_final" + eval_name = "testset_final" + + ## copy the original reference directory to a new directory + ## because we will be creating new screenshots + reference_dir = "../testset_final_" + eval_name + os.makedirs(reference_dir, exist_ok=True) + for filename in os.listdir(orig_reference_dir): + if filename.endswith(".html") or filename == "rick.jpg": + shutil.copy(os.path.join(orig_reference_dir, filename), os.path.join(reference_dir, filename)) + print ("copied original reference directory to ", reference_dir) + + test_dirs = { + "gpt4v_direct_prompting": "../predictions_final/gpt4v_direct_prompting", + "gemini_direct_prompting": "../predictions_final/gemini_direct_prompting" + } + + file_name_list = [] + + ## check if the file is in all prediction directories + for filename in os.listdir(reference_dir): + if filename.endswith(".html"): + if all([os.path.exists(os.path.join(test_dirs[key], filename)) for key in test_dirs]): + file_name_list.append(filename) + + print ("total #egs: ", len(file_name_list)) + + input_lists = [] + for filename in file_name_list: + + input_pred_list = [os.path.join(test_dirs[key], filename) for key in test_dirs] + original = os.path.join(reference_dir, filename) + + input_list = [input_pred_list, original] + input_lists.append(input_list) + + # print ("input_list: ", input_lists) + if multiprocessing: + with tqdm_joblib(tqdm(total=len(input_lists))) as progress_bar: + return_score_lists = list(tqdm(Parallel(n_jobs=8)(delayed(visual_eval_v3_multi)(input_list, debug=debug) for input_list in input_lists), total=len(input_lists))) + else: + return_score_lists = [] + for input_list in tqdm(input_lists): + return_score_list = visual_eval_v3_multi(input_list, debug=debug) + return_score_lists.append(return_score_list) + # print ("return lists: ", return_score_lists) + + res_dict = {} + for key in test_dirs: + res_dict[key] = {} + + for i, filename in enumerate(file_name_list): + idx = 0 + return_score_list = return_score_lists[i] + # print ("return score list: ", return_score_list) + if return_score_list: + for key in test_dirs: + if multiprocessing: + matched, final_score, multi_score = return_score_list[idx] + else: + matched = return_score_list[idx][0] + final_score = return_score_list[idx][1] + multi_score = return_score_list[idx][2] + idx += 1 + current_score = [final_score] + [item for item in multi_score] + res_dict[key][filename] = current_score + else: + print (filename + " didn't get a score") + for key in test_dirs: + res_dict[key][filename] = [0, 0, 0, 0, 0, 0] + + ## cache all scores + with open("metrics/res_dict_{}.json".format(eval_name), "w") as f: + json.dump(res_dict, f, indent=4) + + for key in test_dirs: + print(key) + values = list(res_dict[key].values()) + # print (values) + current_res = np.mean(np.array(values), axis=0) + # print(current_res) + print_multi_score(current_res) \ No newline at end of file diff --git a/webgenie/rewards/metrics/ocr_free_utils.py b/webgenie/rewards/metrics/ocr_free_utils.py new file mode 100644 index 00000000..ccd706a8 --- /dev/null +++ b/webgenie/rewards/metrics/ocr_free_utils.py @@ -0,0 +1,259 @@ +import cv2 +import numpy as np +from PIL import Image, ImageColor +import os +from bs4 import BeautifulSoup, NavigableString, Tag, Comment +from pathlib import Path + + +def rgb_to_hex(rgb): + """Convert an RGB tuple to hexadecimal format.""" + return '{:02X}{:02X}{:02X}'.format(*rgb) + + +class ColorPool: + def __init__(self, offset=0): + + color_values = list(range(10, 251, 16)) + color_list = [((r + offset) % 256, (g + offset) % 256, (b + offset) % 256) for r in color_values for g in color_values for b in color_values] + self.color_pool = [rgb_to_hex(color) for color in color_list] + + def pop_color(self): + if self.color_pool: + return self.color_pool.pop() + else: + raise NotImplementedError + + +def process_html(input_file_path, output_file_path, offset=0): + # Read the input HTML file + with open(input_file_path, 'r') as file: + soup = BeautifulSoup(file, 'html.parser') + + def update_style(element, property_name, value): + # Update the element's style attribute with the given property and value + # Adding !important to ensure the style overrides others + important_value = f"{value} !important" + styles = element.attrs.get('style', '').split(';') + updated_styles = [s for s in styles if not s.strip().startswith(property_name) and len(s.strip()) > 0] + updated_styles.append(f"{property_name}: {important_value}") + element['style'] = '; '.join(updated_styles).strip() + + # Set the background color of all elements to white + for element in soup.find_all(True): + update_style(element, 'background-color', 'rgba(255, 255, 255, 0.0)') + + color_pool = ColorPool(offset) + + # Assign a unique color to text within each text-containing element + text_tags = ['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'div', 'span', 'a', 'b', 'li', 'table', 'td', 'th', 'button', 'footer', 'header', 'figcaption'] # Add more tags as needed + for tag in soup.find_all(text_tags): + color = f"#{color_pool.pop_color()}" + update_style(tag, 'color', color) + update_style(tag, 'opacity', 1.0) + + # Write the modified HTML to a new file + with open(output_file_path, 'w') as file: + file.write(str(soup)) + + +def similar(n1, n2): + if abs(n1 - n2) <= 8: + return True + else: + return False + + +def find_different_pixels(image1_path, image2_path): + # Open the images + img1 = Image.open(image1_path) + img2 = Image.open(image2_path) + + # Ensure both images are of the same size + if img1.size != img2.size: + print(f"[Warning] Images are not the same size, {image1_path}, {image2_path}") + return None + + # Convert images to RGB if they are not + img1 = img1.convert('RGB') + img2 = img2.convert('RGB') + + # Get pixel data + pixels1 = img1.load() + pixels2 = img2.load() + + # List to store coordinates of different pixels + different_pixels = [] + + # Iterate through each pixel + for x in range(img1.size[0]): + for y in range(img1.size[1]): + # Compare pixel colors + r1, g1, b1 = pixels1[x, y] + r2, g2, b2 = pixels2[x, y] + if similar((r1 + 50) % 256, r2) and similar((g1 + 50) % 256, g2) and similar((b1 + 50) % 256, b2): + different_pixels.append((y, x)) + + if len(different_pixels) > 0: + return np.stack(different_pixels) + else: + return None + + +def extract_text_with_color(html_file): + def get_color(tag): + if 'style' in tag.attrs: + styles = tag['style'].split(';') + color_style = [s for s in styles if 'color' in s and 'background-color' not in s] + if color_style: + color = color_style[-1].split(':')[1].strip().replace(" !important", "") + if color[0] == "#": + return color + else: + try: + if color.startswith('rgb'): + color = tuple(map(int, color[4:-1].split(','))) # Extract the RGB values + else: + color = ImageColor.getrgb(color) # Convert named color to RGB + return '#{:02x}{:02x}{:02x}'.format(*color) # Convert RGB to hexadecimal + except ValueError: + print(f"Warning: unable to identify or convert color in {html_file}...", color) + return None + return None + + def extract_text_recursive(element, parent_color='#000000'): + if isinstance(element, Comment): + return None + elif isinstance(element, NavigableString): + text = element.strip() + return (text, parent_color) if text else None + + elif isinstance(element, Tag): + current_color = get_color(element) or parent_color + children_texts = filter(None, [extract_text_recursive(child, current_color) for child in element.children]) + return list(children_texts) + + with open(html_file, 'r', encoding='utf-8') as file: + soup = BeautifulSoup(file, 'html.parser') + body = soup.body + return extract_text_recursive(body) if body else [] + + +def flatten_tree(tree): + flat_list = [] + + # Helper function to recursively flatten the tree + def flatten(node): + if isinstance(node, list): + for item in node: + flatten(item) + else: + flat_list.append(node) + + # Flatten the tree + flatten(tree) + return flat_list + + +def average_color(image_path, coordinates): + """ + Calculates the average color of the specified coordinates in the given image. + + :param image: A PIL Image object. + :param coordinates: A 2D numpy array of coordinates, where each row represents [x, y]. + :return: A tuple representing the average color (R, G, B). + """ + # Convert image to numpy array + image_array = np.array(Image.open(image_path).convert('RGB')) + + # Extract colors at the specified coordinates + colors = [image_array[x, y] for x, y in coordinates] + + # Calculate the average color + avg_color = np.mean(colors, axis=0) + + return tuple(avg_color.astype(int)) + + +def get_blocks_from_image_diff_pixels(image_path, html_text_color_tree, different_pixels): + image = cv2.imread(image_path) + x_w = image.shape[0] + y_w = image.shape[1] + + def hex_to_bgr(hex_color): + """ + Converts a hex color string to a BGR color tuple. + """ + hex_color = hex_color.lstrip('#') + rgb = tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4)) + return rgb[::-1] + + + def get_intersect(arr1, arr2): + # Reshape the arrays to 1D + arr1_reshaped = arr1.view([('', arr1.dtype)] * arr1.shape[1]) + arr2_reshaped = arr2.view([('', arr2.dtype)] * arr2.shape[1]) + + # Find the intersection + common_rows = np.intersect1d(arr1_reshaped, arr2_reshaped) + + # Reshape the result back to 2D, if needed + common_rows = common_rows.view(arr1.dtype).reshape(-1, arr1.shape[1]) + return common_rows + + + blocks = [] + for item in html_text_color_tree: + try: + color = np.array(hex_to_bgr(item[1]), dtype="uint8") + except: + continue + + lower = color - 4 + upper = color + 4 + + mask = cv2.inRange(image, lower, upper) + + coords = np.column_stack(np.where(mask > 0)) + + coords = get_intersect(coords, different_pixels) + + if coords.size == 0: + continue + + x_min, y_min = np.min(coords, axis=0) + x_max, y_max = np.max(coords, axis=0) + color = average_color(image_path.replace("_p.png", ".png"), coords) + + blocks.append({'text': item[0].lower(), 'bbox': (y_min / y_w, x_min / x_w, (y_max - y_min + 1) / y_w, (x_max - x_min + 1) / x_w), 'color': color}) + return blocks + + +def get_itermediate_names(name): + return name.replace(".png", ".html"), name.replace(".png", "_p.html"), name.replace(".png", "_p_1.html"), name.replace(".png", "_p.png"), name.replace(".png", "_p_1.png") + +def get_blocks_ocr_free(image_path): + html, p_html, p_html_1, p_png, p_png_1 = get_itermediate_names(image_path) + process_html(html, p_html) + process_html(html, p_html_1, offset=50) + + os.system(f"python3 {Path(__file__).parent}/screenshot_single.py --html {p_html} --png {p_png}") + os.system(f"python3 {Path(__file__).parent}/screenshot_single.py --html {p_html_1} --png {p_png_1}") + + different_pixels = find_different_pixels(p_png, p_png_1) + + if different_pixels is None: + print(f"[Warning] Unable to get pixels with different colors from {p_png}, {p_png_1}...") + os.system(f"rm {p_html} {p_png} {p_html_1} {p_png_1}") + return [] + + html_text_color_tree = flatten_tree(extract_text_with_color(p_html)) + try: + blocks = get_blocks_from_image_diff_pixels(p_png, html_text_color_tree, different_pixels) + except: + print(f"[Warning] Unable to get blocks from {p_png}...") + os.system(f"rm {p_html} {p_png} {p_html_1} {p_png_1}") + return [] + + os.system(f"rm {p_html} {p_png} {p_html_1} {p_png_1}") + return blocks \ No newline at end of file diff --git a/webgenie/rewards/metrics/screenshot_single.py b/webgenie/rewards/metrics/screenshot_single.py new file mode 100644 index 00000000..86b0b6b6 --- /dev/null +++ b/webgenie/rewards/metrics/screenshot_single.py @@ -0,0 +1,48 @@ +import os +from playwright.sync_api import sync_playwright +import argparse +from PIL import Image + + +def take_screenshot(url, output_file="screenshot.png", do_it_again=False): + # Convert local path to file:// URL if it's a file + if os.path.exists(url): + url = "file://" + os.path.abspath(url) + + if os.path.exists(output_file) and not do_it_again: + print(f"{output_file} exists!") + return + + try: + with sync_playwright() as p: + # Choose a browser, e.g., Chromium, Firefox, or WebKit + browser = p.chromium.launch() + page = browser.new_page() + + # Navigate to the URL + page.goto(url, timeout=60000) + + # Take the screenshot + page.screenshot(path=output_file, full_page=True, animations="disabled", timeout=60000) + + browser.close() + except Exception as e: + print(f"Failed to take screenshot due to: {e}. Generating a blank image.") + # Generate a blank image + img = Image.new('RGB', (1280, 960), color = 'white') + img.save(output_file) + + +if __name__ == "__main__": + + # Initialize the parser + parser = argparse.ArgumentParser(description='Process two path strings.') + + # Define the arguments + parser.add_argument('--html', type=str) + parser.add_argument('--png', type=str) + + # Parse the arguments + args = parser.parse_args() + + take_screenshot(args.html, args.png, do_it_again=True) diff --git a/webgenie/rewards/metrics/visual_score.py b/webgenie/rewards/metrics/visual_score.py new file mode 100644 index 00000000..7bc330fd --- /dev/null +++ b/webgenie/rewards/metrics/visual_score.py @@ -0,0 +1,581 @@ +import cv2 +import numpy as np + +import sys +import os +# Add the current file's directory to Python path +current_dir = os.path.dirname(os.path.abspath(__file__)) +sys.path.append(current_dir) + +# This is a patch for color map, which is not updated for newer version of numpy +def patch_asscalar(a): + return a.item() +setattr(np, "asscalar", patch_asscalar) + +import matplotlib.pyplot as plt +from scipy.optimize import linear_sum_assignment +import random +from sklearn.metrics.pairwise import cosine_similarity +from difflib import SequenceMatcher +from tqdm import tqdm +from pathlib import Path +from PIL import Image, ImageDraw +import torch +import clip +from copy import deepcopy +from collections import Counter +from bs4 import BeautifulSoup, NavigableString, Comment +import re +import math +from colormath.color_objects import sRGBColor, LabColor +from colormath.color_conversions import convert_color +from colormath.color_diff import delta_e_cie2000 + +from ocr_free_utils import get_blocks_ocr_free +from dedup_post_gen import check_repetitive_content +from webgenie.constants import SCREENSHOT_SCRIPT_PATH, WORK_DIR + +device = "cuda" if torch.cuda.is_available() else "cpu" +model, preprocess = clip.load("ViT-B/32", device=device) + +def calculate_similarity(block1, block2, max_distance=1.42): + text_similarity = SequenceMatcher(None, block1['text'], block2['text']).ratio() + return text_similarity + + +def adjust_cost_for_context(cost_matrix, consecutive_bonus=1.0, window_size=20): + if window_size <= 0: + return cost_matrix + + n, m = cost_matrix.shape + adjusted_cost_matrix = np.copy(cost_matrix) + + for i in range(n): + for j in range(m): + bonus = 0 + if adjusted_cost_matrix[i][j] >= -0.5: + continue + nearby_matrix = cost_matrix[max(0, i - window_size):min(n, i + window_size + 1), max(0, j - window_size):min(m, j + window_size + 1)] + flattened_array = nearby_matrix.flatten() + sorted_array = np.sort(flattened_array)[::-1] + sorted_array = np.delete(sorted_array, np.where(sorted_array == cost_matrix[i, j])[0][0]) + top_k_elements = sorted_array[- window_size * 2:] + sum_top_k = np.sum(top_k_elements) + bonus = consecutive_bonus * sum_top_k + adjusted_cost_matrix[i][j] += bonus + return adjusted_cost_matrix + +def create_cost_matrix(A, B): + n = len(A) + m = len(B) + cost_matrix = np.zeros((n, m)) + for i in range(n): + for j in range(m): + cost_matrix[i, j] = -calculate_similarity(A[i], B[j]) + return cost_matrix + + +def draw_matched_bboxes(img1, img2, matched_bboxes): + # Create copies of images to draw on + img1_drawn = img1.copy() + img2_drawn = img2.copy() + + h1, w1, _ = img1.shape + h2, w2, _ = img2.shape + + + # Iterate over matched bounding boxes + for bbox_pair in matched_bboxes: + # Random color for each pair + color = (random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) + + # Ensure that the bounding box coordinates are integers + bbox1 = [int(bbox_pair[0][0] * w1), int(bbox_pair[0][1] * h1), int(bbox_pair[0][2] * w1), int(bbox_pair[0][3] * h1)] + bbox2 = [int(bbox_pair[1][0] * w2), int(bbox_pair[1][1] * h2), int(bbox_pair[1][2] * w2), int(bbox_pair[1][3] * h2)] + + # Draw bbox on the first image + top_left_1 = (bbox1[0], bbox1[1]) + bottom_right_1 = (bbox1[0] + bbox1[2], bbox1[1] + bbox1[3]) + img1_drawn = cv2.rectangle(img1_drawn, top_left_1, bottom_right_1, color, 2) + + # Draw bbox on the second image + top_left_2 = (bbox2[0], bbox2[1]) + bottom_right_2 = (bbox2[0] + bbox2[2], bbox2[1] + bbox2[3]) + img2_drawn = cv2.rectangle(img2_drawn, top_left_2, bottom_right_2, color, 2) + + return img1_drawn, img2_drawn + + +def calculate_distance_max_1d(x1, y1, x2, y2): + distance = max(abs(x2 - x1), abs(y2 - y1)) + return distance + + +def calculate_ratio(h1, h2): + return max(h1, h2) / min(h1, h2) + + +def rgb_to_lab(rgb): + """ + Convert an RGB color to Lab color space. + RGB values should be in the range [0, 255]. + """ + # Create an sRGBColor object from RGB values + rgb_color = sRGBColor(rgb[0], rgb[1], rgb[2], is_upscaled=True) + + # Convert to Lab color space + lab_color = convert_color(rgb_color, LabColor) + + return lab_color + +def color_similarity_ciede2000(rgb1, rgb2): + """ + Calculate the color similarity between two RGB colors using the CIEDE2000 formula. + Returns a similarity score between 0 and 1, where 1 means identical. + """ + # Convert RGB colors to Lab + lab1 = rgb_to_lab(rgb1) + lab2 = rgb_to_lab(rgb2) + + # Calculate the Delta E (CIEDE2000) + delta_e = delta_e_cie2000(lab1, lab2) + + # Normalize the Delta E value to get a similarity score + # Note: The normalization method here is arbitrary and can be adjusted based on your needs. + # A delta_e of 0 means identical colors. Higher values indicate more difference. + # For visualization purposes, we consider a delta_e of 100 to be completely different. + similarity = max(0, 1 - (delta_e / 100)) + + return similarity + + +def calculate_current_cost(cost_matrix, row_ind, col_ind): + return cost_matrix[row_ind, col_ind].sum() + + +def merge_blocks_wo_check(block1, block2): + # Concatenate text + merged_text = block1['text'] + " " + block2['text'] + + # Calculate bounding box + x_min = min(block1['bbox'][0], block2['bbox'][0]) + y_min = min(block1['bbox'][1], block2['bbox'][1]) + x_max = max(block1['bbox'][0] + block1['bbox'][2], block2['bbox'][0] + block2['bbox'][2]) + y_max = max(block1['bbox'][1] + block1['bbox'][3], block2['bbox'][1] + block2['bbox'][3]) + merged_bbox = (x_min, y_min, x_max - x_min, y_max - y_min) + + # Average color + merged_color = tuple( + (color1 + color2) // 2 for color1, color2 in zip(block1['color'], block2['color']) + ) + + return {'text': merged_text, 'bbox': merged_bbox, 'color': merged_color} + + +def calculate_current_cost(cost_matrix, row_ind, col_ind): + return cost_matrix[row_ind, col_ind].tolist() + + +def find_maximum_matching(A, B, consecutive_bonus, window_size): + cost_matrix = create_cost_matrix(A, B) + cost_matrix = adjust_cost_for_context(cost_matrix, consecutive_bonus, window_size) + row_ind, col_ind = linear_sum_assignment(cost_matrix) + current_cost = calculate_current_cost(cost_matrix, row_ind, col_ind) + return list(zip(row_ind, col_ind)), current_cost, cost_matrix + + +def remove_indices(lst, indices): + for index in sorted(indices, reverse=True): + if index < len(lst): + lst.pop(index) + return lst + + +def merge_blocks_by_list(blocks, merge_list): + pop_list = [] + while True: + if len(merge_list) == 0: + remove_indices(blocks, pop_list) + return blocks + + i = merge_list[0][0] + j = merge_list[0][1] + + blocks[i] = merge_blocks_wo_check(blocks[i], blocks[j]) + pop_list.append(j) + + merge_list.pop(0) + if len(merge_list) > 0: + new_merge_list = [] + for k in range(len(merge_list)): + if merge_list[k][0] != i and merge_list[k][1] != i and merge_list[k][0] != j and merge_list[k][1] != j: + new_merge_list.append(merge_list[k]) + merge_list = new_merge_list + + +def print_matching(matching, blocks1, blocks2, cost_matrix): + for i, j in matching: + print(f"{blocks1[i]} matched with {blocks2[j]}, cost {cost_matrix[i][j]}") + + +def difference_of_means(list1, list2): + counter1 = Counter(list1) + counter2 = Counter(list2) + + for element in set(list1) & set(list2): + common_count = min(counter1[element], counter2[element]) + counter1[element] -= common_count + counter2[element] -= common_count + + unique_list1 = [item for item in counter1.elements()] + unique_list2 = [item for item in counter2.elements()] + + mean_list1 = sum(unique_list1) / len(unique_list1) if unique_list1 else 0 + mean_list2 = sum(unique_list2) / len(unique_list2) if unique_list2 else 0 + + if mean_list1 - mean_list2 > 0: + if min(unique_list1) > min(unique_list2): + return mean_list1 - mean_list2 + else: + return 0.0 + else: + return mean_list1 - mean_list2 + + +def find_possible_merge(A, B, consecutive_bonus, window_size, debug=False): + merge_bonus = 0.0 + merge_windows = 1 + + def sortFn(value): + return value[2] + + while True: + A_changed = False + B_changed = False + + matching, current_cost, cost_matrix = find_maximum_matching(A, B, merge_bonus, merge_windows) + if debug: + print("Current cost of the solution:", current_cost) + print_matching(matching, A, B, cost_matrix) + + if len(A) >= 2: + merge_list = [] + for i in range(len(A) - 1): + new_A = deepcopy(A) + new_A[i] = merge_blocks_wo_check(new_A[i], new_A[i + 1]) + new_A.pop(i + 1) + + updated_matching, updated_cost, cost_matrix = find_maximum_matching(new_A, B, merge_bonus, merge_windows) + diff = difference_of_means(current_cost, updated_cost) + if diff > 0.05: + merge_list.append([i, i + 1, diff]) + if debug: + print(new_A[i]['text'], diff) + + merge_list.sort(key=sortFn, reverse=True) + if len(merge_list) > 0: + A_changed = True + A = merge_blocks_by_list(A, merge_list) + matching, current_cost, cost_matrix = find_maximum_matching(A, B, merge_bonus, merge_windows) + if debug: + print("Cost after optimization A:", current_cost) + + if len(B) >= 2: + merge_list = [] + for i in range(len(B) - 1): + new_B = deepcopy(B) + new_B[i] = merge_blocks_wo_check(new_B[i], new_B[i + 1]) + new_B.pop(i + 1) + + updated_matching, updated_cost, cost_matrix = find_maximum_matching(A, new_B, merge_bonus, merge_windows) + diff = difference_of_means(current_cost, updated_cost) + if diff > 0.05: + merge_list.append([i, i + 1, diff]) + if debug: + print(new_B[i]['text'], diff) + + merge_list.sort(key=sortFn, reverse=True) + if len(merge_list) > 0: + B_changed = True + B = merge_blocks_by_list(B, merge_list) + matching, current_cost, cost_matrix = find_maximum_matching(A, B, merge_bonus, merge_windows) + if debug: + print("Cost after optimization B:", current_cost) + + if not A_changed and not B_changed: + break + matching, _, _ = find_maximum_matching(A, B, consecutive_bonus, window_size) + return A, B, matching + + +def merge_blocks_by_bbox(blocks): + merged_blocks = {} + + # Traverse and merge blocks + for block in blocks: + bbox = tuple(block['bbox']) # Convert bbox to tuple for hashability + if bbox in merged_blocks: + # Merge with existing block + existing_block = merged_blocks[bbox] + existing_block['text'] += ' ' + block['text'] + existing_block['color'] = [(ec + c) / 2 for ec, c in zip(existing_block['color'], block['color'])] + else: + # Add new block + merged_blocks[bbox] = block + + return list(merged_blocks.values()) + + +def mask_bounding_boxes_with_inpainting(image, bounding_boxes): + # Convert PIL image to OpenCV format + image_cv = cv2.cvtColor(np.array(image), cv2.COLOR_RGB2BGR) + + # Create a black mask + mask = np.zeros(image_cv.shape[:2], dtype=np.uint8) + + height, width = image_cv.shape[:2] + + # Draw white rectangles on the mask + for bbox in bounding_boxes: + x_ratio, y_ratio, w_ratio, h_ratio = bbox + x = int(x_ratio * width) + y = int(y_ratio * height) + w = int(w_ratio * width) + h = int(h_ratio * height) + mask[y:y+h, x:x+w] = 255 + + # Use inpainting + inpainted_image = cv2.inpaint(image_cv, mask, 3, cv2.INPAINT_TELEA) + + # Convert back to PIL format + inpainted_image_pil = Image.fromarray(cv2.cvtColor(inpainted_image, cv2.COLOR_BGR2RGB)) + + return inpainted_image_pil + + +def rescale_and_mask(image_path, blocks): + # Load the image + with Image.open(image_path) as img: + if len(blocks) > 0: + # use inpainting instead of simple mask + img = mask_bounding_boxes_with_inpainting(img, blocks) + + width, height = img.size + + # Determine which side is shorter + if width < height: + # Width is shorter, scale height to match the width + new_size = (width, width) + else: + # Height is shorter, scale width to match the height + new_size = (height, height) + + # Resize the image while maintaining aspect ratio + img_resized = img.resize(new_size, Image.LANCZOS) + + return img_resized + + +def calculate_clip_similarity_with_blocks(image_path1, image_path2, blocks1, blocks2): + # Load and preprocess images + image1 = preprocess(rescale_and_mask(image_path1, [block['bbox'] for block in blocks1])).unsqueeze(0).to(device) + image2 = preprocess(rescale_and_mask(image_path2, [block['bbox'] for block in blocks2])).unsqueeze(0).to(device) + + # Calculate features + with torch.no_grad(): + image_features1 = model.encode_image(image1) + image_features2 = model.encode_image(image2) + + # Normalize features + image_features1 /= image_features1.norm(dim=-1, keepdim=True) + image_features2 /= image_features2.norm(dim=-1, keepdim=True) + + # Calculate cosine similarity + similarity = (image_features1 @ image_features2.T).item() + + return similarity + + +def truncate_repeated_html_elements(soup, max_count=50): + content_counts = {} + + for element in soup.find_all(True): + if isinstance(element, (NavigableString, Comment)): + continue + + try: + element_html = str(element) + except: + element.decompose() + continue + content_counts[element_html] = content_counts.get(element_html, 0) + 1 + + if content_counts[element_html] > max_count: + element.decompose() + + return str(soup) + + +def make_html(filename): + with open(filename, 'r') as file: + content = file.read() + + if not re.search(r']*>', content, re.IGNORECASE): + new_content = f'

{content}

' + with open(filename, 'w') as file: + file.write(new_content) + + +def pre_process(html_file): + #check_repetitive_content(html_file) + make_html(html_file) + with open(html_file, 'r') as file: + soup = BeautifulSoup(file, 'html.parser') + soup_str = truncate_repeated_html_elements(soup) + with open(html_file, 'w') as file: + file.write(soup_str) + + +def visual_eval_v3_multi(input_list, debug=False): + predict_html_list, original_html = input_list[0], input_list[1] + predict_img_list = [html.replace(".html", ".png") for html in predict_html_list] + # try: + predict_blocks_list = [] + for predict_html in predict_html_list: + predict_img = predict_html.replace(".html", ".png") + # This will help fix some html syntax error + pre_process(predict_html) + os.system(f"python3 {SCREENSHOT_SCRIPT_PATH} --html {predict_html} --png {predict_img}") + predict_blocks = get_blocks_ocr_free(predict_img) + predict_blocks_list.append(predict_blocks) + + original_img = original_html.replace(".html", ".png") + os.system(f"python3 {SCREENSHOT_SCRIPT_PATH} --html {original_html} --png {original_img}") + original_blocks = get_blocks_ocr_free(original_img) + original_blocks = merge_blocks_by_bbox(original_blocks) + + # Consider context similarity for block matching + consecutive_bonus, window_size = 0.1, 1 + + return_score_list = [] + + for k, predict_blocks in enumerate(predict_blocks_list): + if len(predict_blocks) == 0: + print("[Warning] No detected blocks in: ", predict_img_list[k]) + final_clip_score = calculate_clip_similarity_with_blocks(predict_img_list[k], original_img, predict_blocks, original_blocks) + return_score_list.append([0.0, 0.2 * final_clip_score, (0.0, 0.0, 0.0, 0.0, final_clip_score)]) + continue + elif len(original_blocks) == 0: + print("[Warning] No detected blocks in: ", original_img) + final_clip_score = calculate_clip_similarity_with_blocks(predict_img_list[k], original_img, predict_blocks, original_blocks) + return_score_list.append([0.0, 0.2 * final_clip_score, (0.0, 0.0, 0.0, 0.0, final_clip_score)]) + continue + + if debug: + print(predict_blocks) + print(original_blocks) + + predict_blocks = merge_blocks_by_bbox(predict_blocks) + predict_blocks_m, original_blocks_m, matching = find_possible_merge(predict_blocks, deepcopy(original_blocks), consecutive_bonus, window_size, debug=debug) + + filtered_matching = [] + for i, j in matching: + text_similarity = SequenceMatcher(None, predict_blocks_m[i]['text'], original_blocks_m[j]['text']).ratio() + # Filter out matching with low similarity + if text_similarity < 0.5: + continue + filtered_matching.append([i, j, text_similarity]) + matching = filtered_matching + + indices1 = [item[0] for item in matching] + indices2 = [item[1] for item in matching] + + matched_list = [] + sum_areas = [] + matched_areas = [] + matched_text_scores = [] + position_scores = [] + text_color_scores = [] + + unmatched_area_1 = 0.0 + for i in range(len(predict_blocks_m)): + if i not in indices1: + unmatched_area_1 += predict_blocks_m[i]['bbox'][2] * predict_blocks_m[i]['bbox'][3] + unmatched_area_2 = 0.0 + for j in range(len(original_blocks_m)): + if j not in indices2: + unmatched_area_2 += original_blocks_m[j]['bbox'][2] * original_blocks_m[j]['bbox'][3] + sum_areas.append(unmatched_area_1 + unmatched_area_2) + + for i, j, text_similarity in matching: + sum_block_area = predict_blocks_m[i]['bbox'][2] * predict_blocks_m[i]['bbox'][3] + original_blocks_m[j]['bbox'][2] * original_blocks_m[j]['bbox'][3] + + # Consider the max postion shift, either horizontally or vertically + position_similarity = 1 - calculate_distance_max_1d(predict_blocks_m[i]['bbox'][0] + predict_blocks_m[i]['bbox'][2] / 2, \ + predict_blocks_m[i]['bbox'][1] + predict_blocks_m[i]['bbox'][3] / 2, \ + original_blocks_m[j]['bbox'][0] + original_blocks_m[j]['bbox'][2] / 2, \ + original_blocks_m[j]['bbox'][1] + original_blocks_m[j]['bbox'][3] / 2) + # Normalized ciede2000 formula + text_color_similarity = color_similarity_ciede2000(predict_blocks_m[i]['color'], original_blocks_m[j]['color']) + matched_list.append([predict_blocks_m[i]['bbox'], original_blocks_m[j]['bbox']]) + + # validation check + if min(predict_blocks_m[i]['bbox'][2], original_blocks_m[j]['bbox'][2], predict_blocks_m[i]['bbox'][3], original_blocks_m[j]['bbox'][3]) == 0: + print(f"{predict_blocks_m[i]} matched with {original_blocks_m[j]}") + assert calculate_ratio(predict_blocks_m[i]['bbox'][2], original_blocks_m[j]['bbox'][2]) > 0 and calculate_ratio(predict_blocks_m[i]['bbox'][3], original_blocks_m[j]['bbox'][3]) > 0, f"{predict_blocks_m[i]} matched with {original_blocks_m[j]}" + + sum_areas.append(sum_block_area) + matched_areas.append(sum_block_area) + matched_text_scores.append(text_similarity) + position_scores.append(position_similarity) + text_color_scores.append(text_color_similarity) + + if debug: + print(f"{predict_blocks_m[i]} matched with {original_blocks_m[j]}") + print(SequenceMatcher(None, predict_blocks_m[i]['text'], original_blocks_m[j]['text']).ratio()) + print("text similarity score", text_similarity) + print("position score", position_similarity) + print("color score", text_color_similarity) + print("----------------------------------") + pass + """ + if debug: + img1 = cv2.imread(predict_img_list[k]) + img2 = cv2.imread(original_img) + img1_with_boxes, img2_with_boxes = draw_matched_bboxes(img1, img2, matched_list) + + plt.figure(figsize=(20, 10)) + plt.subplot(1, 2, 1) + plt.imshow(cv2.cvtColor(img1_with_boxes, cv2.COLOR_BGR2RGB)) + plt.axis('off') + plt.subplot(1, 2, 2) + plt.imshow(cv2.cvtColor(img2_with_boxes, cv2.COLOR_BGR2RGB)) + plt.axis('off') + plt.show() + # """ + + if len(matched_areas) > 0: + sum_sum_areas = np.sum(sum_areas) + + final_size_score = np.sum(matched_areas) / np.sum(sum_areas) + final_matched_text_score = np.mean(matched_text_scores) + final_position_score = np.mean(position_scores) + final_text_color_score = np.mean(text_color_scores) + final_clip_score = calculate_clip_similarity_with_blocks(predict_img_list[k], original_img, predict_blocks, original_blocks) + final_score = 0.2 * (final_size_score + final_matched_text_score + final_position_score + final_text_color_score + final_clip_score) + return_score_list.append([sum_sum_areas, final_score, (final_size_score, final_matched_text_score, final_position_score, final_text_color_score, final_clip_score)]) + else: + print("[Warning] No matched blocks in: ", predict_img_list[k]) + final_clip_score = calculate_clip_similarity_with_blocks(predict_img_list[k], original_img, predict_blocks, original_blocks) + return_score_list.append([0.0, 0.2 * final_clip_score, (0.0, 0.0, 0.0, 0.0, final_clip_score)]) + return return_score_list + + # except: + # print("[Warning] Error not handled in: ", input_list) + # return [[0.0, 0.0, (0.0, 0.0, 0.0, 0.0, 0.0)] for _ in range(len(predict_html_list))] + + +if __name__ == "__main__": + print(visual_eval_v3_multi([ + ["work/miner1.html", "work/miner2.html", "work/miner3.html"], + "work/original.html"], debug=True)) diff --git a/webgenie/rewards/reward.py b/webgenie/rewards/reward.py index 39f95bd1..b46e2b79 100644 --- a/webgenie/rewards/reward.py +++ b/webgenie/rewards/reward.py @@ -1,2 +1,11 @@ -class Reward: - pass \ No newline at end of file +from abc import ABC, abstractmethod +import numpy as np +from typing import List + +from webgenie.tasks import Task +from webgenie.tasks.solution import Solution + +class Reward(ABC): + @abstractmethod + async def reward(self, task: Task, solutions: List[Solution]) -> np.ndarray: + pass \ No newline at end of file diff --git a/webgenie/rewards/reward_manager.py b/webgenie/rewards/reward_manager.py deleted file mode 100644 index f68d255a..00000000 --- a/webgenie/rewards/reward_manager.py +++ /dev/null @@ -1,67 +0,0 @@ -from typing import Dict, List, Tuple - -from webgenie.rewards import Reward -from webgenie.rewards import GPTReward -from webgenie.rewards import SpeedReward -from webgenie.rewards import IsValidReward -from webgenie.tasks import Task -from webgenie.solution import Solution - -class RewardManager: - """ - A singleton manager for the reward models. - """ - reward_models: Dict[str, Reward] = {} - - def __init__(self): - self.reward_models = { - "gpt": GPTReward(), - "speed": SpeedReward(), - "is_valid": IsValidReward(), - } - - def _penalty(self, task: Task, solution: Solution) -> float: - """ - Penalize the solution based on the penalty models. - """ - penalty = 0 - for penalty_model in task.penalty_models: - model = self.reward_models[penalty_model[0]] - weight = penalty_model[1] - - penalty += weight * model.reward(task, solution) - return penalty - - def _reward(self, task: Task, solution: Solution) -> float: - """ - Reward the solution based on the reward models. - """ - reward = 0 - for reward_model in task.reward_models: - model = self.reward_models[reward_model[0]] - weight = reward_model[1] - reward += weight * model.reward(task, solution) - - return reward - - def _score(self, task: Task, solution: Solution) -> float: - """ - Score the solution based on the reward and penalty models. - """ - score = task.reward_weight * self._reward(task, solution) - task.penalty_weight * self._penalty(task, solution) - if score < 0: - score = 0 - return score - - def score(self, task: Task, results: List[Solution]) -> Tuple[List[float], List[int]]: - """ - Score the solutions based on the reward and penalty models. - """ - scores = [] - miner_uids = [] - for solution in results: - score = self._score(task, solution) - scores.append(score) - miner_uids.append(solution.miner_uid) - - return scores, miner_uids \ No newline at end of file diff --git a/webgenie/rewards/speed.py b/webgenie/rewards/speed.py deleted file mode 100644 index d9e905ff..00000000 --- a/webgenie/rewards/speed.py +++ /dev/null @@ -1,15 +0,0 @@ - -from webgenie.rewards import Reward -from webgenie.tasks import Task -from webgenie.solution import Solution - -class SpeedReward(Reward): - - def __init__(self): - pass - - def reward(self, task: Task, solution: Solution) -> float: - if (task.timeout == 0): - return 1.0 - return 1.0 - (solution.process_time / task.timeout) - \ No newline at end of file diff --git a/webgenie/rewards/visual_reward.py b/webgenie/rewards/visual_reward.py new file mode 100644 index 00000000..d4eef431 --- /dev/null +++ b/webgenie/rewards/visual_reward.py @@ -0,0 +1,33 @@ +import bittensor as bt +import numpy as np +from typing import List + +from webgenie.constants import WORK_DIR +from webgenie.rewards import Reward +from webgenie.rewards.metrics.visual_score import visual_eval_v3_multi +from webgenie.tasks.task import Task, ImageTask +from webgenie.tasks.solution import Solution + +class VisualReward(Reward): + def __init__(self): + pass + + async def reward(self, task: Task, solutions: List[Solution]) -> np.ndarray: + if not isinstance(task, ImageTask): + raise ValueError(f"Task is not a ImageTask: {type(task)}") + + bt.logging.debug(f"Rewarding image task in visual reward") + + original_html_path = f"{WORK_DIR}/original.html" + with open(original_html_path, "w") as f: + f.write(task.ground_truth_html) + + miner_html_paths = [] + for solution in solutions: + path = f"{WORK_DIR}/miner{solution.miner_uid}.html" + with open(path, "w") as f: + f.write(solution.html) + miner_html_paths.append(path) + + visual_scores = visual_eval_v3_multi([miner_html_paths, original_html_path]) + return np.array([score[1] for score in visual_scores]) diff --git a/webgenie/solution/__init__.py b/webgenie/solution/__init__.py deleted file mode 100644 index fb1bbb15..00000000 --- a/webgenie/solution/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .solution import Solution \ No newline at end of file diff --git a/webgenie/task_generator/__init__.py b/webgenie/task_generator/__init__.py deleted file mode 100644 index e4507b44..00000000 --- a/webgenie/task_generator/__init__.py +++ /dev/null @@ -1 +0,0 @@ -from .task_generator import TaskGenerator \ No newline at end of file diff --git a/webgenie/task_generator/image_task_generator.py b/webgenie/task_generator/image_task_generator.py deleted file mode 100644 index 96053562..00000000 --- a/webgenie/task_generator/image_task_generator.py +++ /dev/null @@ -1,27 +0,0 @@ -import bittensor as bt -from typing import List, Tuple - -from webgenie.helpers.images import image_to_base64 -from webgenie.protocol import WebgenieImageSynapse -from webgenie.solution import Solution -from webgenie.tasks.task import Task, ImageTask -from webgenie.task_generator.task_generator import TaskGenerator - -class ImageTaskGenerator(TaskGenerator): - def __init__(self): - super().__init__() - - async def generate_task(self) -> Tuple[Task, bt.Synapse]: - base64_image = image_to_base64("original.jpg") - return ImageTask( - base64_image=base64_image, - timeout=50, - generator=self - ), WebgenieImageSynapse(base64_image=base64_image) - - async def reward(self, task: Task, solutions: List[Solution]) -> List[float]: - if not isinstance(task, ImageTask): - raise ValueError(f"Task is not a ImageTask: {type(task)}") - bt.logging.debug(task.base64_image) - bt.logging.debug(f"Rewarding image task {task} with solutions {solutions}") - return [1.0] * len(solutions) diff --git a/webgenie/task_generator/text_task_generator.py b/webgenie/task_generator/text_task_generator.py deleted file mode 100644 index e46e88a2..00000000 --- a/webgenie/task_generator/text_task_generator.py +++ /dev/null @@ -1,26 +0,0 @@ -import bittensor as bt -from typing import List, Tuple - -from webgenie.protocol import WebgenieTextSynapse -from webgenie.solution import Solution -from webgenie.tasks.task import Task, TextTask -from webgenie.task_generator.task_generator import TaskGenerator - -class TextTaskGenerator(TaskGenerator): - def __init__(self): - super().__init__() - - async def generate_task(self) -> Tuple[Task, bt.Synapse]: - return TextTask( - prompt="CommingSoon Page with goback button, navHeader, and footer" , - timeout=50, - generator=self - ), WebgenieTextSynapse(prompt="CommingSoon Page with goback button, navHeader, and footer") - - async def reward(self, task: Task, solutions: List[Solution]) -> List[float]: - if not isinstance(task, TextTask): - raise ValueError(f"Task is not a TextTask: {type(task)}") - bt.logging.debug(task.prompt) - bt.logging.debug(f"Rewarding text task {task} with solutions {solutions}") - - return [1.0] * len(solutions) diff --git a/webgenie/tasks/__init__.py b/webgenie/tasks/__init__.py index 760a4128..0670b304 100644 --- a/webgenie/tasks/__init__.py +++ b/webgenie/tasks/__init__.py @@ -1 +1,5 @@ -from .task import Task \ No newline at end of file +from .solution import Solution +from .task import Task, ImageTask, TextTask +from .task_generator import TaskGenerator +from .image_task_generator import ImageTaskGenerator +from .text_task_generator import TextTaskGenerator \ No newline at end of file diff --git a/webgenie/tasks/image_task_generator.py b/webgenie/tasks/image_task_generator.py new file mode 100644 index 00000000..d7e937d5 --- /dev/null +++ b/webgenie/tasks/image_task_generator.py @@ -0,0 +1,33 @@ +import bittensor as bt +import numpy as np +import random +from typing import List, Tuple + +from webgenie.helpers.htmls import html_to_screenshot +from webgenie.protocol import WebgenieImageSynapse +from webgenie.tasks.solution import Solution +from webgenie.tasks.task import Task, ImageTask +from webgenie.tasks.task_generator import TaskGenerator +from webgenie.rewards.visual_reward import VisualReward +from webgenie.datasets.dataset import MockUpDataset + +class ImageTaskGenerator(TaskGenerator): + def __init__(self): + super().__init__() + self.rewards = [ + (VisualReward(), 1.0) + ] + self.datasets = [ + MockUpDataset() + ] + + async def generate_task(self) -> Tuple[Task, bt.Synapse]: + dataset_entry = await random.choice(self.datasets).generate_context() + base64_image = html_to_screenshot(dataset_entry.ground_truth_html) + return ImageTask( + base64_image=base64_image, + ground_truth_html=dataset_entry.ground_truth_html, + timeout=50, + generator=self, + ), WebgenieImageSynapse(base64_image=base64_image) + diff --git a/webgenie/solution/solution.py b/webgenie/tasks/solution.py similarity index 100% rename from webgenie/solution/solution.py rename to webgenie/tasks/solution.py diff --git a/webgenie/tasks/task.py b/webgenie/tasks/task.py index 8d633538..3a85d86d 100644 --- a/webgenie/tasks/task.py +++ b/webgenie/tasks/task.py @@ -6,7 +6,9 @@ class Task(BaseModel): generator: Any = Field(default=None) class ImageTask(Task): - base64_image: str = Field(default="") + base64_image: str = Field(default="", description="The base64 encoded image") + ground_truth_html: str = Field(default="", description="The ground truth html") class TextTask(Task): - prompt: str = Field(default="") + prompt: str = Field(default="", description="The prompt for the text task") + ground_truth_html: str = Field(default="", description="The ground truth html") diff --git a/webgenie/task_generator/task_generator.py b/webgenie/tasks/task_generator.py similarity index 52% rename from webgenie/task_generator/task_generator.py rename to webgenie/tasks/task_generator.py index a41b3a82..e27668ed 100644 --- a/webgenie/task_generator/task_generator.py +++ b/webgenie/tasks/task_generator.py @@ -1,8 +1,9 @@ import bittensor as bt +import numpy as np from typing import List, Tuple from webgenie.tasks import Task -from webgenie.solution import Solution +from webgenie.tasks.solution import Solution class TaskGenerator: """ @@ -13,7 +14,11 @@ def __init__(self): async def generate_task(self) -> Tuple[Task, bt.Synapse]: pass - - async def reward(self, task: Task, solutions: List[Solution]) -> List[float]: - pass + + async def reward(self, task: Task, solutions: List[Solution]) -> np.ndarray: + scores = np.zeros(len(solutions)) + for reward, weight in self.rewards: + reward_scores = await reward.reward(task, solutions) + scores += weight * np.array(reward_scores) + return scores diff --git a/webgenie/tasks/text_task_generator.py b/webgenie/tasks/text_task_generator.py new file mode 100644 index 00000000..415cee3b --- /dev/null +++ b/webgenie/tasks/text_task_generator.py @@ -0,0 +1,26 @@ +import bittensor as bt +import numpy as np +import random +from typing import List, Tuple + +from webgenie.datasets.dataset import MockUpPromptDataset +from webgenie.protocol import WebgenieTextSynapse +from webgenie.tasks.solution import Solution +from webgenie.tasks.task import Task, TextTask +from webgenie.tasks.task_generator import TaskGenerator + +class TextTaskGenerator(TaskGenerator): + def __init__(self): + super().__init__() + self.rewards = [] + self.datasets = [ + MockUpPromptDataset() + ] + + async def generate_task(self) -> Tuple[Task, bt.Synapse]: + dataset_entry = await random.choice(self.datasets).generate_context() + return TextTask( + prompt=dataset_entry.prompt, + timeout=50, + generator=self + ), WebgenieTextSynapse(prompt=dataset_entry.prompt) From ee552e4b348eba9557919038763e6990a0b30faa Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 06:02:50 -0600 Subject: [PATCH 037/129] chore: moved backend api hotkey to constants --- neurons/validators/validator.py | 7 +++---- webgenie/constants.py | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/neurons/validators/validator.py b/neurons/validators/validator.py index f57e6e6c..389e27d0 100644 --- a/neurons/validators/validator.py +++ b/neurons/validators/validator.py @@ -8,12 +8,11 @@ from typing import Tuple, Union from webgenie.base.validator import BaseValidatorNeuron +from webgenie.constants import API_HOTKEY from webgenie.helpers.weights import init_wandb from webgenie.protocol import WebgenieTextSynapse, WebgenieImageSynapse from neurons.validators.genie_validator import GenieValidator -API_HOTKEY = "5D72esHuc1DxD6PD8S6VyU24bTHGQjHHyodzTGsem1sejUYj" - class Validator(BaseValidatorNeuron): """ Your validator neuron class. You should use this class to define your validator's behavior. In particular, you should replace the forward function with your own logic. @@ -115,8 +114,8 @@ async def scoring_loop(self): await asyncio.sleep(5) async def __aenter__(self): - self.loop.create_task(self.forward_loop()) - self.loop.create_task(self.scoring_loop()) + #self.loop.create_task(self.forward_loop()) + #self.loop.create_task(self.scoring_loop()) self.is_running = True bt.logging.debug("Starting validator in background thread") return self diff --git a/webgenie/constants.py b/webgenie/constants.py index 49ff9e30..83553113 100644 --- a/webgenie/constants.py +++ b/webgenie/constants.py @@ -1,3 +1,4 @@ +API_HOTKEY = "5D72esHuc1DxD6PD8S6VyU24bTHGQjHHyodzTGsem1sejUYj" MAX_SYNTHETIC_HISTORY_SIZE = 10 PLACE_HOLDER_IMAGE_URL = "https://picsum.photos/seed/picsum/800/600" SCREENSHOT_SCRIPT_PATH = "webgenie/rewards/metrics/screenshot_single.py" From 7c90dd49cc783b39f4608280818134814db20468 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 06:04:26 -0600 Subject: [PATCH 038/129] docs: added comments in constants --- webgenie/constants.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/webgenie/constants.py b/webgenie/constants.py index 83553113..6041aba4 100644 --- a/webgenie/constants.py +++ b/webgenie/constants.py @@ -1,5 +1,14 @@ +# backend api hotkey API_HOTKEY = "5D72esHuc1DxD6PD8S6VyU24bTHGQjHHyodzTGsem1sejUYj" + +# max synthetic history size MAX_SYNTHETIC_HISTORY_SIZE = 10 + +# place holder image url PLACE_HOLDER_IMAGE_URL = "https://picsum.photos/seed/picsum/800/600" + +# screenshot script path SCREENSHOT_SCRIPT_PATH = "webgenie/rewards/metrics/screenshot_single.py" + +# work dir WORK_DIR = "work" From cb7fe17fb472a5eb100b852beb182a6a2e10cfe1 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 06:15:08 -0600 Subject: [PATCH 039/129] feat: added bert reward --- neurons/validators/validator.py | 4 +- webgenie/datasets/dataset.py | 125 +++++++++++++++++++++++++- webgenie/rewards/bert_reward.py | 28 ++++++ webgenie/tasks/text_task_generator.py | 7 +- 4 files changed, 159 insertions(+), 5 deletions(-) create mode 100644 webgenie/rewards/bert_reward.py diff --git a/neurons/validators/validator.py b/neurons/validators/validator.py index 389e27d0..2fc0abe2 100644 --- a/neurons/validators/validator.py +++ b/neurons/validators/validator.py @@ -114,8 +114,8 @@ async def scoring_loop(self): await asyncio.sleep(5) async def __aenter__(self): - #self.loop.create_task(self.forward_loop()) - #self.loop.create_task(self.scoring_loop()) + self.loop.create_task(self.forward_loop()) + self.loop.create_task(self.scoring_loop()) self.is_running = True bt.logging.debug("Starting validator in background thread") return self diff --git a/webgenie/datasets/dataset.py b/webgenie/datasets/dataset.py index ce3f4373..93c4ab12 100644 --- a/webgenie/datasets/dataset.py +++ b/webgenie/datasets/dataset.py @@ -143,10 +143,133 @@ async def generate_context(self)->DatasetEntry: class MockUpPromptDataset(Dataset): async def generate_context(self)->DatasetEntry: + html = """ + + + + + + Coming Soon + + + + + +
+ +
+ + +
+

Coming Soon!

+

We're working hard to launch something amazing. Stay tuned!

+ +
+ + + + + + + +""" return DatasetEntry( src="mockup", topic="tech company", - ground_truth_html="", + ground_truth_html=html, prompt="CommingSoon Page with goback button, navHeader, and footer", base64_image="" ) \ No newline at end of file diff --git a/webgenie/rewards/bert_reward.py b/webgenie/rewards/bert_reward.py new file mode 100644 index 00000000..264e5e47 --- /dev/null +++ b/webgenie/rewards/bert_reward.py @@ -0,0 +1,28 @@ +import bert_score +import bittensor as bt +import numpy as np +from typing import List + +from webgenie.rewards import Reward +from webgenie.tasks.task import Task +from webgenie.tasks.solution import Solution + +class BertReward(Reward): + def __init__(self): + pass + + async def reward(self, task: Task, solutions: List[Solution]) -> np.ndarray: + bt.logging.debug(f"Rewarding task in bert reward") + + original_htmls= [] + miner_htmls = [] + + for solution in solutions: + original_htmls.append(task.ground_truth_html) + miner_htmls.append(solution.html) + + P, R, F1 = bert_score.score(original_htmls, miner_htmls, lang='en') + + return np.array(F1) + + \ No newline at end of file diff --git a/webgenie/tasks/text_task_generator.py b/webgenie/tasks/text_task_generator.py index 415cee3b..2298cb62 100644 --- a/webgenie/tasks/text_task_generator.py +++ b/webgenie/tasks/text_task_generator.py @@ -5,14 +5,16 @@ from webgenie.datasets.dataset import MockUpPromptDataset from webgenie.protocol import WebgenieTextSynapse -from webgenie.tasks.solution import Solution +from webgenie.rewards.bert_reward import BertReward from webgenie.tasks.task import Task, TextTask from webgenie.tasks.task_generator import TaskGenerator class TextTaskGenerator(TaskGenerator): def __init__(self): super().__init__() - self.rewards = [] + self.rewards = [ + BertReward(), + ] self.datasets = [ MockUpPromptDataset() ] @@ -21,6 +23,7 @@ async def generate_task(self) -> Tuple[Task, bt.Synapse]: dataset_entry = await random.choice(self.datasets).generate_context() return TextTask( prompt=dataset_entry.prompt, + ground_truth_html=dataset_entry.ground_truth_html, timeout=50, generator=self ), WebgenieTextSynapse(prompt=dataset_entry.prompt) From 6b30f5ec2e6acdd7fa49a71bdf96a00416ef3cb2 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 06:19:14 -0600 Subject: [PATCH 040/129] fix: fixed bugs in bert --- webgenie/tasks/text_task_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webgenie/tasks/text_task_generator.py b/webgenie/tasks/text_task_generator.py index 2298cb62..374e227c 100644 --- a/webgenie/tasks/text_task_generator.py +++ b/webgenie/tasks/text_task_generator.py @@ -13,7 +13,7 @@ class TextTaskGenerator(TaskGenerator): def __init__(self): super().__init__() self.rewards = [ - BertReward(), + (BertReward(), 1.0) ] self.datasets = [ MockUpPromptDataset() From 25e5b1872eb8aeea9820f2cedcf1fe817a3e1eda Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 06:23:37 -0600 Subject: [PATCH 041/129] chore: implemented preprocess html pseudo code --- neurons/validators/genie_validator.py | 2 ++ webgenie/helpers/htmls.py | 5 ++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 0a6ba728..64d2a4b2 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -4,6 +4,7 @@ from webgenie.base.neuron import BaseNeuron from webgenie.constants import MAX_SYNTHETIC_HISTORY_SIZE +from webgenie.helpers.htmls import preprocess_html from webgenie.protocol import WebgenieImageSynapse, WebgenieTextSynapse from webgenie.tasks.solution import Solution from webgenie.tasks.image_task_generator import ImageTaskGenerator @@ -92,5 +93,6 @@ async def organic_forward(self, synapse: Union[WebgenieTextSynapse, WebgenieImag async def process_synapse(self, synapse: bt.Synapse) -> bt.Synapse: if synapse.dendrite.status_code == 200: + synapse.html = preprocess_html(synapse.html) return synapse return None \ No newline at end of file diff --git a/webgenie/helpers/htmls.py b/webgenie/helpers/htmls.py index 93fc6ff9..4e04119c 100644 --- a/webgenie/helpers/htmls.py +++ b/webgenie/helpers/htmls.py @@ -11,4 +11,7 @@ def html_to_screenshot(html: str) -> str: png_path = f"{WORK_DIR}/screenshot_{uuid.uuid4()}.png" os.system(f"python3 {SCREENSHOT_SCRIPT_PATH} --html {html_path} --png {png_path}") time.sleep(0.1) - return image_to_base64(png_path) \ No newline at end of file + return image_to_base64(png_path) + +def preprocess_html(html: str) -> str: + return html \ No newline at end of file From 488b6f0436b0785714be5f0946516b4a7b961add Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 06:33:06 -0600 Subject: [PATCH 042/129] chore: implemented preprocess func --- webgenie/helpers/htmls.py | 33 ++++++++++++++++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/webgenie/helpers/htmls.py b/webgenie/helpers/htmls.py index 4e04119c..1cff25f0 100644 --- a/webgenie/helpers/htmls.py +++ b/webgenie/helpers/htmls.py @@ -1,7 +1,9 @@ import os +from bs4 import BeautifulSoup import time +import re import uuid -from webgenie.constants import SCREENSHOT_SCRIPT_PATH, WORK_DIR +from webgenie.constants import SCREENSHOT_SCRIPT_PATH, WORK_DIR, PLACE_HOLDER_IMAGE_URL from webgenie.helpers.images import image_to_base64 def html_to_screenshot(html: str) -> str: @@ -13,5 +15,34 @@ def html_to_screenshot(html: str) -> str: time.sleep(0.1) return image_to_base64(png_path) +def beautify_html(html: str) -> str: + soup = BeautifulSoup(html, 'html.parser') + return str(soup) + +def replace_image_sources(html_content, new_url = PLACE_HOLDER_IMAGE_URL): + soup = BeautifulSoup(html_content, 'html.parser') + + for img_tag in soup.find_all('img'): + img_tag['src'] = new_url + + for source_tag in soup.find_all('source'): + if 'srcset' in source_tag.attrs: + source_tag['srcset'] = new_url + + for tag in soup.find_all(style=True): + style = tag['style'] + updated_style = re.sub(r'background-image\s*:\s*url\([^)]+\)', f'background-image: url({new_url})', style) + tag['style'] = updated_style + + for style_tag in soup.find_all('style'): + style_content = style_tag.string + if style_content: + updated_content = re.sub(r'background-image\s*:\s*url\([^)]+\)', f'background-image: url({new_url})', style_content) + style_tag.string.replace_with(updated_content) + + return str(soup) + def preprocess_html(html: str) -> str: + html = beautify_html(html) + html = replace_image_sources(html) return html \ No newline at end of file From 03b268dc6882c362b9efbaf7f935676fc81bfa29 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 06:52:56 -0600 Subject: [PATCH 043/129] feat: implemented rtc reward --- webgenie/prompts.py | 12 +++++++ webgenie/rewards/rtc_reward.py | 52 +++++++++++++++++++++++++++ webgenie/tasks/text_task_generator.py | 4 ++- 3 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 webgenie/prompts.py create mode 100644 webgenie/rewards/rtc_reward.py diff --git a/webgenie/prompts.py b/webgenie/prompts.py new file mode 100644 index 00000000..35058ec7 --- /dev/null +++ b/webgenie/prompts.py @@ -0,0 +1,12 @@ +# prompt to make rounded trip correctness +PROMPT_RTC = """ +You are an HTML, CSS expert. And you are well versed in the AI, ML. +I have a model that converts the prompt to html. +I want you to analyze the html code and make a prompt that generate the given html code. +The following is the given html code: +{html} +The following is the example of prompt: +{prompt} + +{instructions} +""" \ No newline at end of file diff --git a/webgenie/rewards/rtc_reward.py b/webgenie/rewards/rtc_reward.py new file mode 100644 index 00000000..69045cb2 --- /dev/null +++ b/webgenie/rewards/rtc_reward.py @@ -0,0 +1,52 @@ +import bittensor as bt +import bert_score +import os +import numpy as np +from typing import List + +from langchain_openai import ChatOpenAI +from langchain.prompts import ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate +from langchain_core.output_parsers import JsonOutputParser +from langchain_core.pydantic_v1 import BaseModel, Field + +from webgenie.prompts import PROMPT_RTC +from webgenie.rewards import Reward +from webgenie.tasks.task import Task +from webgenie.tasks.solution import Solution + +class PromptResponse(BaseModel): + prompt: str = Field(default="", description="The prompt that generates the given html code") + +class RtcReward(Reward): + def __init__(self): + self.model = ChatOpenAI( + api_key= os.getenv("OPENAI_API_KEY"), + model_name="gpt-4o", + ) + + self.prompt_response_parser = JsonOutputParser(pydantic_object=PromptResponse) + + def _get_prompt(self, task: Task, solutions: List[Solution]) -> str: + prompt = ChatPromptTemplate.from_messages([ + SystemMessagePromptTemplate.from_template(PROMPT_RTC) + ]) + + chain = prompt | self.model | self.prompt_response_parser + prompt_response = chain.invoke({ + "html": task.ground_truth_html, + "prompt": task.prompt, + "instructions": self.prompt_response_parser.get_format_instructions() + }) + + return prompt_response["prompt"] + + async def reward(self, task: Task, solutions: List[Solution]) -> np.ndarray: + bt.logging.debug(f"Rewarding task in rtc reward") + original_prompts = [task.prompt for _ in solutions] + miner_prompts = [self._get_prompt(task, solution) for solution in solutions] + P, R, F1 = bert_score.score(original_prompts, miner_prompts, lang='en') + return np.array(R) + + + + \ No newline at end of file diff --git a/webgenie/tasks/text_task_generator.py b/webgenie/tasks/text_task_generator.py index 374e227c..65c89128 100644 --- a/webgenie/tasks/text_task_generator.py +++ b/webgenie/tasks/text_task_generator.py @@ -6,6 +6,7 @@ from webgenie.datasets.dataset import MockUpPromptDataset from webgenie.protocol import WebgenieTextSynapse from webgenie.rewards.bert_reward import BertReward +from webgenie.rewards.rtc_reward import RtcReward from webgenie.tasks.task import Task, TextTask from webgenie.tasks.task_generator import TaskGenerator @@ -13,7 +14,8 @@ class TextTaskGenerator(TaskGenerator): def __init__(self): super().__init__() self.rewards = [ - (BertReward(), 1.0) + (BertReward(), 0.5), + (RtcReward(), 0.5) ] self.datasets = [ MockUpPromptDataset() From b2b5d138e495505a6e5f2e5f11857d571decd1dd Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 06:58:36 -0600 Subject: [PATCH 044/129] feat: added auto update script --- auto_update.sh | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 auto_update.sh diff --git a/auto_update.sh b/auto_update.sh new file mode 100644 index 00000000..99968193 --- /dev/null +++ b/auto_update.sh @@ -0,0 +1,32 @@ +#!/bin/bash + + +while true; do + # Log the start of the script execution + echo "$(date): Script started" + + # Save the current HEAD hash + current_head=$(git rev-parse HEAD) + + # Pull the latest changes from the repository + git stash + git pull -f + git reset --hard origin/main + + # Get the new HEAD hash + new_head=$(git rev-parse HEAD) + + # Check if the new HEAD is different from the current HEAD + if [ "$current_head" != "$new_head" ]; then + # The HEAD has changed, meaning there's a new version + echo "$(date): New version detected, installing requirements and restarting the validator." + pip install -e . + pm2 restart webgenie_validator + else + # No new version, no action needed + echo "$(date): No new version detected, no restart needed." + fi + + # Sleep until the beginning of the next hour + sleep 3600 +done From 46eb1e29e194a4256b643c4de3381c46cb9f4bdc Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 07:07:37 -0600 Subject: [PATCH 045/129] chore: removed hardcoded python cmd --- neurons/validators/validator.py | 4 ++-- webgenie/constants.py | 4 ++++ webgenie/helpers/htmls.py | 9 +++++++-- webgenie/rewards/metrics/visual_score.py | 6 +++--- 4 files changed, 16 insertions(+), 7 deletions(-) diff --git a/neurons/validators/validator.py b/neurons/validators/validator.py index 2fc0abe2..389e27d0 100644 --- a/neurons/validators/validator.py +++ b/neurons/validators/validator.py @@ -114,8 +114,8 @@ async def scoring_loop(self): await asyncio.sleep(5) async def __aenter__(self): - self.loop.create_task(self.forward_loop()) - self.loop.create_task(self.scoring_loop()) + #self.loop.create_task(self.forward_loop()) + #self.loop.create_task(self.scoring_loop()) self.is_running = True bt.logging.debug("Starting validator in background thread") return self diff --git a/webgenie/constants.py b/webgenie/constants.py index 6041aba4..81fa8f6f 100644 --- a/webgenie/constants.py +++ b/webgenie/constants.py @@ -7,8 +7,12 @@ # place holder image url PLACE_HOLDER_IMAGE_URL = "https://picsum.photos/seed/picsum/800/600" +# python command +PYTHON_CMD = "python" + # screenshot script path SCREENSHOT_SCRIPT_PATH = "webgenie/rewards/metrics/screenshot_single.py" # work dir WORK_DIR = "work" + diff --git a/webgenie/helpers/htmls.py b/webgenie/helpers/htmls.py index 1cff25f0..686a727d 100644 --- a/webgenie/helpers/htmls.py +++ b/webgenie/helpers/htmls.py @@ -3,7 +3,12 @@ import time import re import uuid -from webgenie.constants import SCREENSHOT_SCRIPT_PATH, WORK_DIR, PLACE_HOLDER_IMAGE_URL +from webgenie.constants import ( + SCREENSHOT_SCRIPT_PATH, + WORK_DIR, + PLACE_HOLDER_IMAGE_URL, + PYTHON_CMD +) from webgenie.helpers.images import image_to_base64 def html_to_screenshot(html: str) -> str: @@ -11,7 +16,7 @@ def html_to_screenshot(html: str) -> str: with open(html_path, "w") as f: f.write(html) png_path = f"{WORK_DIR}/screenshot_{uuid.uuid4()}.png" - os.system(f"python3 {SCREENSHOT_SCRIPT_PATH} --html {html_path} --png {png_path}") + os.system(f"{PYTHON_CMD} {SCREENSHOT_SCRIPT_PATH} --html {html_path} --png {png_path}") time.sleep(0.1) return image_to_base64(png_path) diff --git a/webgenie/rewards/metrics/visual_score.py b/webgenie/rewards/metrics/visual_score.py index 7bc330fd..bbc8f055 100644 --- a/webgenie/rewards/metrics/visual_score.py +++ b/webgenie/rewards/metrics/visual_score.py @@ -33,7 +33,7 @@ def patch_asscalar(a): from ocr_free_utils import get_blocks_ocr_free from dedup_post_gen import check_repetitive_content -from webgenie.constants import SCREENSHOT_SCRIPT_PATH, WORK_DIR +from webgenie.constants import SCREENSHOT_SCRIPT_PATH, WORK_DIR, PYTHON_CMD device = "cuda" if torch.cuda.is_available() else "cpu" model, preprocess = clip.load("ViT-B/32", device=device) @@ -445,12 +445,12 @@ def visual_eval_v3_multi(input_list, debug=False): predict_img = predict_html.replace(".html", ".png") # This will help fix some html syntax error pre_process(predict_html) - os.system(f"python3 {SCREENSHOT_SCRIPT_PATH} --html {predict_html} --png {predict_img}") + os.system(f"{PYTHON_CMD} {SCREENSHOT_SCRIPT_PATH} --html {predict_html} --png {predict_img}") predict_blocks = get_blocks_ocr_free(predict_img) predict_blocks_list.append(predict_blocks) original_img = original_html.replace(".html", ".png") - os.system(f"python3 {SCREENSHOT_SCRIPT_PATH} --html {original_html} --png {original_img}") + os.system(f"{PYTHON_CMD} {SCREENSHOT_SCRIPT_PATH} --html {original_html} --png {original_img}") original_blocks = get_blocks_ocr_free(original_img) original_blocks = merge_blocks_by_bbox(original_blocks) From 6c37c1b51eeec4d155497d23d46119feb70cbe35 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 07:09:51 -0600 Subject: [PATCH 046/129] chore: removed dummy miner --- webgenie/miners/dummy_miner.py | 96 ---------------------------------- 1 file changed, 96 deletions(-) delete mode 100644 webgenie/miners/dummy_miner.py diff --git a/webgenie/miners/dummy_miner.py b/webgenie/miners/dummy_miner.py deleted file mode 100644 index 5b616403..00000000 --- a/webgenie/miners/dummy_miner.py +++ /dev/null @@ -1,96 +0,0 @@ -import json -from functools import partial -from starlette.types import Send -import time -from typing import Dict - -from typing import Dict, Awaitable -from langchain_openai import ChatOpenAI -from dotenv import load_dotenv, find_dotenv -from langchain.prompts import ChatPromptTemplate -from langchain_core.output_parsers import StrOutputParser -from langchain_core.runnables.base import RunnableSequence - -import bittensor as bt -import webgenie -import os - -def miner_init(self): - bt.logging.debug(f"Dummy Miner initialized") - _ = load_dotenv(find_dotenv()) - api_key = os.getenv("OPENAI_API_KEY") - # Set openai key and other args - self.model = ChatOpenAI( - api_key=api_key, - model_name="gpt-4", - ) - -def miner_forward(self, synapse: webgenie.protocol.WebgenieStreamingSynapse)->Awaitable: - - async def _forward(self, chain: RunnableSequence, chain_formatter: Dict[str, str], timeout_threshold: float, init_time: float, send: Send): - try: - buffer = [] - - timeout_reached = False - - for token in chain.stream(chain_formatter): - buffer.append(token) - - if time.time() - init_time > timeout_threshold: - bt.logging.debug(f"⏰ Timeout reached, stopping streaming") - timeout_reached = True - break - - if len(buffer) == self.config.neuron.streaming_batch_size: - joined_buffer = "".join(buffer) - bt.logging.debug(f"Streamed tokens: {joined_buffer}") - - await send( - { - "type": "http.response.body", - "body": joined_buffer.encode("utf-8"), - "more_body": True, - } - ) - buffer = [] - - if ( - buffer and not timeout_reached - ): # Don't send the last buffer of data if timeout. - joined_buffer = "".join(buffer) - await send( - { - "type": "http.response.body", - "body": joined_buffer.encode("utf-8"), - "more_body": False, - } - ) - except Exception as e: - bt.logging.error(f"Dummy Miner Error: {e}") - - bt.logging.debug(f"Dummy Miner Query received, forwarding synapse: {synapse}") - - prompt = ChatPromptTemplate.from_messages([ - ("system", """You are an expert programmer. Generate code based on the following request without explanations:\n\n{query}\n\n Provide the code that satisfies the following requirements without any explanation. You must give me both the CSS file and the frontend HTML file( (the HTML file content should be whole content)).' The response should be in JSON format as shown below, (so that I can decode it such as json.loads(your response)) and it should be plaintext (without triple quotes): - {{ "CSS": "css_code_here", "HTML": "html_code_here" }}"""), - ("user", "{query}"), - ]) - chain = prompt | self.model | StrOutputParser() - - query = synapse.task.query - bt.logging.debug(f"Dummy Miner Query received: {synapse}") - time.sleep(2) - chain_formatter = {"query": query} - - init_time = time.time() - timeout_threshold = float(synapse.timeout) - - token_streamer = partial( - _forward, - self, - chain, - chain_formatter, - timeout_threshold, - init_time, - ) - return synapse.create_streaming_response(token_streamer) \ No newline at end of file From e93db2486b0f763169b7928d8423a35db69cdf69 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 07:16:50 -0600 Subject: [PATCH 047/129] docs: added developer info --- setup.py | 9 ++++----- webgenie/__init__.py | 3 +-- webgenie/base/validator.py | 2 +- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/setup.py b/setup.py index 440a4ffa..af66f894 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,6 @@ # The MIT License (MIT) -# Copyright © 2023 webgenie -# Sangar -# Copyright © 2023 +# Copyright © 2023 Yuma Rao +# Copyright © 2023 Sangar, pycorn0729 # 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 @@ -69,10 +68,10 @@ def read_requirements(path): long_description=long_description, long_description_content_type="text/markdown", url="https://github.com/webgenie/webgenie", - author="Sangar", + author="Sangar, pycorn0729", packages=find_packages(), include_package_data=True, - author_email="sangar.work1028@gmail.com", + author_email="sangar.work1028@gmail.com, hayesdominique0729@gmail.com", license="MIT", python_requires=">=3.8", install_requires=requirements, diff --git a/webgenie/__init__.py b/webgenie/__init__.py index 11da983c..5a811b92 100644 --- a/webgenie/__init__.py +++ b/webgenie/__init__.py @@ -1,7 +1,6 @@ # The MIT License (MIT) # Copyright © 2023 Yuma Rao -# Sangar -# Copyright © 2023 +# Copyright © 2023 Sangar, pycorn0729 # 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 diff --git a/webgenie/base/validator.py b/webgenie/base/validator.py index 5d953236..89af5768 100644 --- a/webgenie/base/validator.py +++ b/webgenie/base/validator.py @@ -1,6 +1,6 @@ # The MIT License (MIT) # Copyright © 2023 Yuma Rao -# Copyright © 2024 Sangar +# Copyright © 2024 Sangar, pycorn0729 # 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 From 3580ebf6714068d5e15e1140ddd27b440c58eabf Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 07:23:33 -0600 Subject: [PATCH 048/129] docs: changed developer info --- setup.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/setup.py b/setup.py index af66f894..a71f5056 100644 --- a/setup.py +++ b/setup.py @@ -67,13 +67,13 @@ def read_requirements(path): description="webgenie aims to transform diverse prompts—ranging from text and voice to images and Figma designs—into fully functional, ready-to-deploy projects.", long_description=long_description, long_description_content_type="text/markdown", - url="https://github.com/webgenie/webgenie", - author="Sangar, pycorn0729", + url="https://github.com/web-genie-ai/web-genie-ai", + author="Sangar, Dominique Hayes", packages=find_packages(), include_package_data=True, author_email="sangar.work1028@gmail.com, hayesdominique0729@gmail.com", license="MIT", - python_requires=">=3.8", + python_requires=">=3.12", install_requires=requirements, classifiers=[ "Development Status :: 3 - Alpha", From b3714a77119d6096007158699fdc6f084d4c0f36 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 07:56:02 -0600 Subject: [PATCH 049/129] chore: added code removing work file int htmls helper --- webgenie/helpers/htmls.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/webgenie/helpers/htmls.py b/webgenie/helpers/htmls.py index 686a727d..eef31b65 100644 --- a/webgenie/helpers/htmls.py +++ b/webgenie/helpers/htmls.py @@ -17,8 +17,14 @@ def html_to_screenshot(html: str) -> str: f.write(html) png_path = f"{WORK_DIR}/screenshot_{uuid.uuid4()}.png" os.system(f"{PYTHON_CMD} {SCREENSHOT_SCRIPT_PATH} --html {html_path} --png {png_path}") + + time.sleep(0.1) + base64_image = image_to_base64(png_path) + time.sleep(0.1) - return image_to_base64(png_path) + os.remove(html_path) + os.remove(png_path) + return base64_image def beautify_html(html: str) -> str: soup = BeautifulSoup(html, 'html.parser') From 60d407834ab71744cc60fbea59b667408d796e0a Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 07:59:46 -0600 Subject: [PATCH 050/129] chore:wrote code to make work dir --- neurons/validators/genie_validator.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 64d2a4b2..031eaf5e 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -22,6 +22,16 @@ def __init__(self, neuron: BaseNeuron): ImageTaskGenerator(), ] + self.make_work_dir() + + def make_work_dir(self): + import os + from webgenie.constants import WORK_DIR + + if not os.path.exists(WORK_DIR): + os.makedirs(WORK_DIR) + bt.logging.info(f"Created work directory at {WORK_DIR}") + async def forward(self): try: if len(self.synthetic_history) > MAX_SYNTHETIC_HISTORY_SIZE: From 513fdf11228f2829a23caf20a3ca1f36dbcf4f24 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 08:09:41 -0600 Subject: [PATCH 051/129] chore: added .env.example file --- .env.example | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..d4429700 --- /dev/null +++ b/.env.example @@ -0,0 +1,3 @@ +OPENAI_API_KEY = your_openai_api_key +WANDB_API_KEY = your_wandb_api_key +WANDB_ENTITY_NAME = your_wandb_entity_name \ No newline at end of file From ca9a45af051d25aa545ccdb8a0768afbcbb8f89d Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 08:17:57 -0600 Subject: [PATCH 052/129] chore: reconfigured .env files --- .env.example => .env.miner.example | 0 .env.validator.example | 3 +++ .gitignore | 3 ++- neurons/miners/miner.py | 7 +++---- neurons/validators/validator.py | 8 +++++--- 5 files changed, 13 insertions(+), 8 deletions(-) rename .env.example => .env.miner.example (100%) create mode 100644 .env.validator.example diff --git a/.env.example b/.env.miner.example similarity index 100% rename from .env.example rename to .env.miner.example diff --git a/.env.validator.example b/.env.validator.example new file mode 100644 index 00000000..d4429700 --- /dev/null +++ b/.env.validator.example @@ -0,0 +1,3 @@ +OPENAI_API_KEY = your_openai_api_key +WANDB_API_KEY = your_wandb_api_key +WANDB_ENTITY_NAME = your_wandb_entity_name \ No newline at end of file diff --git a/.gitignore b/.gitignore index a169e7c9..8528596b 100644 --- a/.gitignore +++ b/.gitignore @@ -168,7 +168,8 @@ testing/ temp.txt # env -.env +.env.miner +.env.validator # test test.py diff --git a/neurons/miners/miner.py b/neurons/miners/miner.py index 5b09a02f..f113b64f 100644 --- a/neurons/miners/miner.py +++ b/neurons/miners/miner.py @@ -1,6 +1,6 @@ # The MIT License (MIT) # Copyright © 2023 Yuma Rao -# Copyright © 2024 pycorn0729 +# Copyright © 2024 pycorn0729, Sangar # 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 @@ -15,9 +15,8 @@ # 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 dotenv import load_dotenv - -load_dotenv() +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv(filename=".env.miner")) import time diff --git a/neurons/validators/validator.py b/neurons/validators/validator.py index 389e27d0..3b83e259 100644 --- a/neurons/validators/validator.py +++ b/neurons/validators/validator.py @@ -1,10 +1,12 @@ # The MIT License (MIT) # Copyright © 2023 Yuma Rao -# Copyright © 2024 pycorn +# Copyright © 2024 pycorn, Sangar import bittensor as bt import asyncio -from dotenv import load_dotenv -load_dotenv() + +from dotenv import load_dotenv, find_dotenv +load_dotenv(find_dotenv(filename=".env.validator")) + from typing import Tuple, Union from webgenie.base.validator import BaseValidatorNeuron From e7060d57e342ebe5783b91e07ac39029e71aa5af Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 08:28:07 -0600 Subject: [PATCH 053/129] docs: updated requirements.txt --- requirements.txt | 147 ++--------------------------------------------- 1 file changed, 4 insertions(+), 143 deletions(-) diff --git a/requirements.txt b/requirements.txt index 4d9dca97..a86bab75 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,158 +1,19 @@ -aiohappyeyeballs==2.4.4 -aiosignal==1.3.1 -annotated-types==0.7.0 -ansible==8.5.0 -ansible-core==2.15.13 ansible-vault==2.1.0 -anyio==4.7.0 -attrs==24.2.0 -backoff==2.2.1 -base58==2.1.1 beautifulsoup4==4.12.3 +bert-score==0.3.13 bittensor==8.4.5 -certifi==2024.7.4 -cffi==1.17.1 -charset-normalizer==3.4.0 -click==8.1.7 -colorama==0.4.6 +clip==1.0 colormath==3.0.0 -contourpy==1.3.1 -cryptography~=43.0.1 -cycler==0.12.1 -cytoolz==1.0.0 ddt==1.6.0 -decorator==5.1.1 -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 -fastapi==0.110.3 -filelock==3.16.1 -fonttools==4.55.3 -frozenlist==1.5.0 -fsspec==2024.10.0 -ftfy==6.3.1 -fuzzywuzzy==0.18.0 -gitdb==4.0.11 -GitPython==3.1.43 -greenlet==3.1.1 -h11==0.14.0 -httpcore==1.0.7 -httpx==0.28.1 -idna==3.10 -iniconfig==2.0.0 -Jinja2==3.1.4 -jiter==0.8.2 -joblib==1.4.2 -jsonpatch==1.33 -jsonpointer==3.0.0 -kiwisolver==1.4.7 langchain==0.3.11 -langchain-core==0.3.24 langchain-openai==0.2.12 -langchain-text-splitters==0.3.2 -langsmith==0.2.3 -Levenshtein==0.26.1 -markdown-it-py==3.0.0 -MarkupSafe==3.0.2 -matplotlib==3.9.3 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 -munch==2.5.0 -nest-asyncio==1.6.0 -netaddr==1.3.0 -networkx==3.4.2 -numpy~=2.0.1 -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.57.2 -openai-clip==1.0.1 +open-clip-torch==2.29.0 opencv-python==4.10.0.84 -orjson==3.10.12 -packaging==24.2 -password-strength==0.0.3.post2 -pillow==11.0.0 -platformdirs==4.3.6 +pip-chill==1.0.3 playwright==1.49.1 -pluggy==1.5.0 -propcache==0.2.1 -protobuf==5.29.1 -psutil==6.1.0 -py==1.11.0 -py-ed25519-zebra-bindings==1.2.0 -py-sr25519-bindings==0.2.1 -pycparser==2.22 -pycryptodome==3.21.0 -pydantic==2.10.3 -pydantic_core==2.27.1 -pyee==12.0.0 -Pygments==2.18.0 -PyNaCl==1.5.0 -pyparsing==3.2.0 -pytest==8.3.4 -python-dateutil==2.9.0.post0 python-dotenv==1.0.1 -python-Levenshtein==0.26.1 -python-statemachine==2.5.0 -PyYAML==6.0.2 -RapidFuzz==3.10.1 -regex==2024.11.6 -requests==2.32.3 -requests-toolbelt==1.0.0 -resolvelib==1.0.1 -retry==0.9.2 -rich==13.9.4 -scalecodec==1.2.11 scikit-learn==1.6.0 -scipy==1.14.1 -sentry-sdk==2.19.2 -setproctitle==1.3.4 -setuptools~=70.0.0 shtab==1.6.5 -six==1.17.0 -smmap==5.0.1 -sniffio==1.3.1 -soupsieve==2.6 -SQLAlchemy==2.0.36 -starlette==0.37.2 -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 tinycss2==1.4.0 -toolz==1.0.0 -torch==2.5.1 -torchvision==0.20.1 -tqdm==4.67.1 -traitlets==5.14.3 -triton==3.1.0 -typing_extensions==4.12.2 -urllib3==2.2.3 -uvicorn==0.32.1 wandb==0.19.0 -wcwidth==0.2.13 -webencodings==0.5.1 -websocket-client==1.8.0 -wheel==0.45.1 -xxhash==3.5.0 -yarl==1.18.3 From d014476690397e39e69441672d0b098b89d7e80a Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 08:44:56 -0600 Subject: [PATCH 054/129] chore: updated beautify_html func --- webgenie/helpers/htmls.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webgenie/helpers/htmls.py b/webgenie/helpers/htmls.py index eef31b65..24be1335 100644 --- a/webgenie/helpers/htmls.py +++ b/webgenie/helpers/htmls.py @@ -28,7 +28,7 @@ def html_to_screenshot(html: str) -> str: def beautify_html(html: str) -> str: soup = BeautifulSoup(html, 'html.parser') - return str(soup) + return soup.prettify() def replace_image_sources(html_content, new_url = PLACE_HOLDER_IMAGE_URL): soup = BeautifulSoup(html_content, 'html.parser') From 7a07cce175826b846b36fe022f65b682102bd9e6 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 09:00:57 -0600 Subject: [PATCH 055/129] chore: updated imagetaskgenerator --- webgenie/tasks/image_task_generator.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/webgenie/tasks/image_task_generator.py b/webgenie/tasks/image_task_generator.py index d7e937d5..990fcd8f 100644 --- a/webgenie/tasks/image_task_generator.py +++ b/webgenie/tasks/image_task_generator.py @@ -3,7 +3,7 @@ import random from typing import List, Tuple -from webgenie.helpers.htmls import html_to_screenshot +from webgenie.helpers.htmls import html_to_screenshot, preprocess_html from webgenie.protocol import WebgenieImageSynapse from webgenie.tasks.solution import Solution from webgenie.tasks.task import Task, ImageTask @@ -23,10 +23,11 @@ def __init__(self): async def generate_task(self) -> Tuple[Task, bt.Synapse]: dataset_entry = await random.choice(self.datasets).generate_context() - base64_image = html_to_screenshot(dataset_entry.ground_truth_html) + ground_truth_html = preprocess_html(dataset_entry.ground_truth_html) + base64_image = html_to_screenshot(ground_truth_html) return ImageTask( base64_image=base64_image, - ground_truth_html=dataset_entry.ground_truth_html, + ground_truth_html=ground_truth_html, timeout=50, generator=self, ), WebgenieImageSynapse(base64_image=base64_image) From 6e1818b0408051ab0075fea4fae63f12e372683a Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 09:28:45 -0600 Subject: [PATCH 056/129] chore: added exception handling --- neurons/miners/openai_miner.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/neurons/miners/openai_miner.py b/neurons/miners/openai_miner.py index c7d2f1f5..7402f77a 100644 --- a/neurons/miners/openai_miner.py +++ b/neurons/miners/openai_miner.py @@ -45,8 +45,9 @@ async def forward_text(self, synapse: WebgenieTextSynapse) -> WebgenieTextSynaps synapse.html = html_response["html"] return synapse - except: + except Exception as e: bt.logging.error(f"Error in OpenaiMiner forward_text: {e}") + synapse.html = f"Error in OpenaiMiner forward_text: {e}" return synapse async def forward_image(self, synapse: WebgenieImageSynapse) -> WebgenieImageSynapse: @@ -84,4 +85,5 @@ async def forward_image(self, synapse: WebgenieImageSynapse) -> WebgenieImageSyn return synapse except Exception as e: bt.logging.error(f"Error in OpenaiMiner forward_image: {e}") + synapse.html = f"Error in OpenaiMiner forward_image: {e}" return synapse \ No newline at end of file From b2b18b37ffb715e343f267e590662cd0e86cf423 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 09:29:40 -0600 Subject: [PATCH 057/129] chore: removed logging --- neurons/validators/genie_validator.py | 1 - 1 file changed, 1 deletion(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 031eaf5e..dae46aee 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -84,7 +84,6 @@ async def organic_forward(self, synapse: Union[WebgenieTextSynapse, WebgenieImag axon = self.neuron.metagraph.axons[best_miner_uid] async with bt.dendrite(wallet=self.neuron.wallet) as dendrite: - bt.logging.info(f"Dendrite: {dendrite}") responses = await dendrite( axons=[axon], synapse=synapse, From 6313c10593c0b658a5364b04c82b7a3c3152789a Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 09:38:56 -0600 Subject: [PATCH 058/129] chore: added seperate_html_css --- webgenie/helpers/htmls.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/webgenie/helpers/htmls.py b/webgenie/helpers/htmls.py index 24be1335..4517bcac 100644 --- a/webgenie/helpers/htmls.py +++ b/webgenie/helpers/htmls.py @@ -11,6 +11,25 @@ ) from webgenie.helpers.images import image_to_base64 +def seperate_html_css(html_content: str): + soup = BeautifulSoup(html_content, 'lxml') + + css = '' + for style_tag in soup.find_all('style'): + css += style_tag.get_text() + for style_tag in soup.find_all('style'): + style_tag.decompose() + + head = soup.head + if not head: + head = soup.new_tag('head') + soup.html.insert(0, head) + + link_tag = soup.new_tag('link', rel='stylesheet', href='styles.css') + head.append(link_tag) + cleaned_html = str(soup) + return cleaned_html, css + def html_to_screenshot(html: str) -> str: html_path = f"{WORK_DIR}/screenshot_{uuid.uuid4()}.html" with open(html_path, "w") as f: From 0a465bbd6374527486dc6f94f171786fceb017aa Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 09:53:36 -0600 Subject: [PATCH 059/129] chore: added html validity check --- neurons/validators/genie_validator.py | 4 +++- webgenie/helpers/htmls.py | 24 +++++++++++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index dae46aee..23f15fb5 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -103,5 +103,7 @@ async def organic_forward(self, synapse: Union[WebgenieTextSynapse, WebgenieImag async def process_synapse(self, synapse: bt.Synapse) -> bt.Synapse: if synapse.dendrite.status_code == 200: synapse.html = preprocess_html(synapse.html) + if synapse.html == "": + return None return synapse - return None \ No newline at end of file + return None diff --git a/webgenie/helpers/htmls.py b/webgenie/helpers/htmls.py index 4517bcac..d0fbbcfa 100644 --- a/webgenie/helpers/htmls.py +++ b/webgenie/helpers/htmls.py @@ -1,5 +1,6 @@ import os from bs4 import BeautifulSoup +from lxml import etree import time import re import uuid @@ -11,6 +12,14 @@ ) from webgenie.helpers.images import image_to_base64 +def is_valid_html(html_code: str): + try: + parser = etree.XMLParser(recover=False) + etree.fromstring(html_code, parser) + return True + except etree.XMLSyntaxError as e: + return False + def seperate_html_css(html_content: str): soup = BeautifulSoup(html_content, 'lxml') @@ -73,6 +82,19 @@ def replace_image_sources(html_content, new_url = PLACE_HOLDER_IMAGE_URL): return str(soup) def preprocess_html(html: str) -> str: + if not is_valid_html(html): + return "" html = beautify_html(html) html = replace_image_sources(html) - return html \ No newline at end of file + return html + +if __name__ == "__main__": + html = """ + + +

Hello, World!

+ + + """ + + print(preprocess_html(html)) \ No newline at end of file From 514a1e846ab7cd459d1ae9cb1464cbe9b66565e5 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 10:40:22 -0600 Subject: [PATCH 060/129] chore: removed unneccessary file --- .dependencies_installed | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 .dependencies_installed diff --git a/.dependencies_installed b/.dependencies_installed deleted file mode 100644 index e69de29b..00000000 From ce4ffaf6a01e6113ef46113482375b8bab05c8f8 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 10:44:35 -0600 Subject: [PATCH 061/129] updated gitignore --- .gitignore | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index 8528596b..2cda507a 100644 --- a/.gitignore +++ b/.gitignore @@ -177,8 +177,9 @@ test.py # Wandb wandb/ -# test images -original.jpg - # work dir -work/ \ No newline at end of file +work/ + +# scripts +run_miner.sh +run_validator.sh \ No newline at end of file From 45b3dd05264271d0876a55c22c80fe7e01930672 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 13 Dec 2024 10:49:11 -0600 Subject: [PATCH 062/129] removed run scripts --- run_miner.sh | 4 ---- run_validator.sh | 3 --- 2 files changed, 7 deletions(-) delete mode 100644 run_miner.sh delete mode 100644 run_validator.sh diff --git a/run_miner.sh b/run_miner.sh deleted file mode 100644 index e53ded1a..00000000 --- a/run_miner.sh +++ /dev/null @@ -1,4 +0,0 @@ -export PYTHONPATH=. -#--axon.port 5555 -python neurons/miners/miner.py --netuid 214 --subtensor.network test --wallet.name s-miner --wallet.hotkey miner1 --logging.debug --axon.port 8090 - diff --git a/run_validator.sh b/run_validator.sh deleted file mode 100644 index 277a7ac0..00000000 --- a/run_validator.sh +++ /dev/null @@ -1,3 +0,0 @@ -export PYTHONASYNCIODEBUG=1 -export PYTHONPATH=. -python neurons/validators/validator.py --netuid 214 --subtensor.network test --wallet.name sc-val1 --wallet.hotkey sh-val1 --logging.debug --neuron.axon_port 8091 From 6f850031206b0338eb712b51fdc6de89028e6daa Mon Sep 17 00:00:00 2001 From: cardoso-topdev Date: Fri, 13 Dec 2024 11:59:24 -0600 Subject: [PATCH 063/129] feat: update readme for text prompted html generation model --- README.md | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/README.md b/README.md index e6d99564..c45dcfcd 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,8 @@ The WebGenieAI subnet incentivizes miners and validators to ensure high-quality ## Evaluation Process +1) Image to HTML Model + ### Automatic evaluation of ImageToHTML task for design-wise We automatically evaluate generated webpages by calculating the similarity between the original input image and the rendered screenshot of generated webpage. We break down the evaluation into both high-level visual similarity and low-level element matching. @@ -84,6 +86,20 @@ blocks are, the lower this score is. - Color: We use the [CIEDE2000](https://en.wikipedia.org/wiki/Color_difference) color difference formula to assess the perceptual difference between the colors of the generated text in block $g_q$ and the reference text in block $r_p$, denoted as **sim**color(rp, gq), where the formula considers the complexities of human color vision. The overall score is averaged across all matched pairs. +2) Text Prompt to Html Model + +### Unsupervised Evaluation of Model by Round-Trip Correctness +We draw inspiration from a software testing technique known as property-based testing. It allows defining properties that must hold between inputs and outputs of a program (e.g., all items in the input list must also appear in the output list) Round-trip correctness is one such property (e.g., compressing and subsequently decompressing data must yield the original data). + +Consider two forms of data X and Y, such as text prompt and HTML and two (probabilistic) models whose task is to “translate” from one form of data to the other, i.e., a forward model M : X → Y and a backward model M-1: Y → X. These models could be a single LLM prompted differently. + +The central idea for unsupervised evaluation is the concept of round-trip correctness (RTC). Intuitively, for a “good” forward and backward model we expect ̂x =M-1 M(x) to be semantically equivalent to x. For example, we can describe the HTML code with text prompt in the forward pass and then generate back the code from the text prompt. To compute RTC we need some function sim(x, ̂x) that estimates the semantic equivalence between the original x and each predicted sample ̂x. Such functions may include discrete or continuous metrics such as exact match, BLEU and so on. + +### Supervised Evaluation of Model by CodeBERTScore +Let x is prompt, y is the ground truth html, ̂y is the generated html. +To evaluate the performance of the model, we can use [CodeBERTScore](https://github.com/neulab/code-bert-score). sim(y, ̂y ) = bert_score(y, ̂y) +CodeBERTScore is an evaluation metric for code generation, which builds on BERTScore. Instead of encoding only the generated tokens as in [BERTScore](https://huggingface.co/spaces/evaluate-metric/bertscore), CodeBERTScore also encodes the natural language input preceding the generated code, thus modeling the consistency between the generated code and its given natural language context as well. + ### Example Scenario From 5a2d2cfd0c91cf134de79fcea62ad5c23c74e086 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sat, 14 Dec 2024 05:55:10 -0600 Subject: [PATCH 064/129] feat: added synthetic dataset --- webgenie/datasets/synthetic_dataset.py | 64 ++++++++++++++++++++++++++ webgenie/prompts.py | 24 +++++++++- 2 files changed, 87 insertions(+), 1 deletion(-) create mode 100644 webgenie/datasets/synthetic_dataset.py diff --git a/webgenie/datasets/synthetic_dataset.py b/webgenie/datasets/synthetic_dataset.py new file mode 100644 index 00000000..6c807253 --- /dev/null +++ b/webgenie/datasets/synthetic_dataset.py @@ -0,0 +1,64 @@ +# The paper [Unlocking the conversion of Web Screenshots into HTML Code with the WebSight Dataset](https://arxiv.org/pdf/2403.09029v1#bib.bib5) is our inspiration. +# We should use Mistral-7B-Instruct to generate concepts and use Deepseek-Coder-33b-instruct to generate html, but now we are using openai models here. + +import bittensor as bt +import os +from typing import List + +from langchain_openai import ChatOpenAI +from langchain.prompts import ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate +from langchain_core.output_parsers import JsonOutputParser +from langchain_core.pydantic_v1 import BaseModel, Field + +from webgenie.datasets.dataset import Dataset, DatasetEntry +from webgenie.prompts import PROMPT_GEN_CONCEPT, PROMPT_GEN_HTML + +class ConceptResponse(BaseModel): + concepts: List[str] = Field(description="The concept of the website") + +class HTMLResponse(BaseModel): + html: str = Field(description="The html code of the website") + +class SyntheticDataset(Dataset): + def __init__(self): + self.model = ChatOpenAI( + api_key= os.getenv("OPENAI_API_KEY"), + model_name="gpt-4o", + temperature=0.6, + ) + + self.concept_parser = JsonOutputParser(pydantic_object=ConceptResponse) + self.html_parser = JsonOutputParser(pydantic_object=HTMLResponse) + + async def _generate_concepts(self): + prompt = ChatPromptTemplate.from_messages([ + ("system", PROMPT_GEN_CONCEPT), + ]) + chain = prompt | self.model | self.concept_parser + response = chain.invoke({ + "instructions": self.concept_parser.get_format_instructions() + }) + return response["concepts"] + + async def _generate_html(self, concept: str): + prompt = ChatPromptTemplate.from_messages([ + ("system", PROMPT_GEN_HTML), + ]) + chain = prompt | self.model | self.html_parser + response = chain.invoke({ + "concept": concept, + "instructions": self.html_parser.get_format_instructions() + }) + return response["html"] + + async def generate_context(self)->DatasetEntry: + if len(self.concepts) == 0: + self.concepts = await self._generate_concepts() + + concept = self.concepts.pop(0) + ground_truth_html = await self._generate_html(concept) + return DatasetEntry( + src="synthetic", + prompt=concept, + ground_truth_html=ground_truth_html, + ) diff --git a/webgenie/prompts.py b/webgenie/prompts.py index 35058ec7..ed29915e 100644 --- a/webgenie/prompts.py +++ b/webgenie/prompts.py @@ -9,4 +9,26 @@ {prompt} {instructions} -""" \ No newline at end of file +""" + +PROMPT_GEN_CONCEPT = """ +Generate diverse website layout ideas for different companies, each with a unique design element. +Examples include: a car company site with a left column, a webpage footer with a centered logo. +Explore variations in colors, positions, and company fields. Don’t give any explanations or recognition that you have understood the request, just give the list of 10 ideas, with a line break between +each. +{instructions} +""" + +PROMPT_GEN_HTML = """ +Code a complete website with a good design in HTML and Tailwind CSS about this: {concept} +Write the code inside a tag . +Write real and long sentences about the business. NEVER USE sentences starting with Lorem +ipsum, NEVER. +You don’t have to include images, but if you do, use only this source +"https://source.unsplash.com/random/WxH/?keyword", by replacing ‘W‘ and ‘H‘ in the URL +by the desired width and height, and ‘?keyword‘ by a keyword describing the picture, for example +"https://source.unsplash.com/random/300x200/?gym" for an image about gym of size 300x200, or +"https://source.unsplash.com/random/100x200/?cake" for an image of a cake of size 100x200. + +{instructions} +""" From 6fbeeb5f0ba2cd159488cd505815d951c8d702c5 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sat, 14 Dec 2024 05:58:20 -0600 Subject: [PATCH 065/129] refactor: moved mockup dataset to seperate file --- webgenie/datasets/dataset.py | 263 --------------------------- webgenie/datasets/mockup_dataset.py | 264 ++++++++++++++++++++++++++++ 2 files changed, 264 insertions(+), 263 deletions(-) create mode 100644 webgenie/datasets/mockup_dataset.py diff --git a/webgenie/datasets/dataset.py b/webgenie/datasets/dataset.py index 93c4ab12..8b8cf31b 100644 --- a/webgenie/datasets/dataset.py +++ b/webgenie/datasets/dataset.py @@ -10,266 +10,3 @@ class DatasetEntry(BaseModel): class Dataset: async def generate_context(self)->DatasetEntry: pass - -class MockUpDataset(Dataset): - async def generate_context(self)->DatasetEntry: - html = """ - - - - - -Tech Company - - - - -
-
- - -
-
-
-Hero Image -
-
-

Welcome to Tech Company

-

At Tech Company, we are dedicated to providing the best technology solutions for your needs. Our team of experts is always ready to help you with any questions or problems you may have.

-
- - - - """ - return DatasetEntry( - src="mockup", - topic="tech company", - ground_truth_html=html, - prompt="", - base64_image="" - ) - -class MockUpPromptDataset(Dataset): - async def generate_context(self)->DatasetEntry: - html = """ - - - - - - Coming Soon - - - - - -
- -
- - -
-

Coming Soon!

-

We're working hard to launch something amazing. Stay tuned!

- -
- - - - - - - -""" - return DatasetEntry( - src="mockup", - topic="tech company", - ground_truth_html=html, - prompt="CommingSoon Page with goback button, navHeader, and footer", - base64_image="" - ) \ No newline at end of file diff --git a/webgenie/datasets/mockup_dataset.py b/webgenie/datasets/mockup_dataset.py new file mode 100644 index 00000000..09b53ef1 --- /dev/null +++ b/webgenie/datasets/mockup_dataset.py @@ -0,0 +1,264 @@ +from dataset import Dataset, DatasetEntry + +class MockUpDataset(Dataset): + async def generate_context(self)->DatasetEntry: + html = """ + + + + + +Tech Company + + + + +
+
+ + +
+
+
+Hero Image +
+
+

Welcome to Tech Company

+

At Tech Company, we are dedicated to providing the best technology solutions for your needs. Our team of experts is always ready to help you with any questions or problems you may have.

+
+ + + + """ + return DatasetEntry( + src="mockup", + topic="tech company", + ground_truth_html=html, + prompt="", + base64_image="" + ) + +class MockUpPromptDataset(Dataset): + async def generate_context(self)->DatasetEntry: + html = """ + + + + + + Coming Soon + + + + + +
+ +
+ + +
+

Coming Soon!

+

We're working hard to launch something amazing. Stay tuned!

+ +
+ + + + + + + +""" + return DatasetEntry( + src="mockup", + topic="tech company", + ground_truth_html=html, + prompt="CommingSoon Page with goback button, navHeader, and footer", + base64_image="" + ) \ No newline at end of file From 4b0b59a40a72b7b917d7ea49e1909e69a6f97621 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sat, 14 Dec 2024 05:59:44 -0600 Subject: [PATCH 066/129] style: removed unneccessary imports --- webgenie/datasets/synthetic_dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webgenie/datasets/synthetic_dataset.py b/webgenie/datasets/synthetic_dataset.py index 6c807253..b3e6e74a 100644 --- a/webgenie/datasets/synthetic_dataset.py +++ b/webgenie/datasets/synthetic_dataset.py @@ -6,7 +6,7 @@ from typing import List from langchain_openai import ChatOpenAI -from langchain.prompts import ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate +from langchain.prompts import ChatPromptTemplate from langchain_core.output_parsers import JsonOutputParser from langchain_core.pydantic_v1 import BaseModel, Field From 9b1a00615bd1641f6fac5b2f49addaa017600276 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sat, 14 Dec 2024 06:05:41 -0600 Subject: [PATCH 067/129] feat: added synthetic dataset to image task generator --- webgenie/tasks/image_task_generator.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/webgenie/tasks/image_task_generator.py b/webgenie/tasks/image_task_generator.py index 990fcd8f..8b6590e7 100644 --- a/webgenie/tasks/image_task_generator.py +++ b/webgenie/tasks/image_task_generator.py @@ -9,7 +9,8 @@ from webgenie.tasks.task import Task, ImageTask from webgenie.tasks.task_generator import TaskGenerator from webgenie.rewards.visual_reward import VisualReward -from webgenie.datasets.dataset import MockUpDataset +from webgenie.datasets.mockup_dataset import MockUpDataset +from webgenie.datasets.synthetic_dataset import SyntheticDataset class ImageTaskGenerator(TaskGenerator): def __init__(self): @@ -18,12 +19,16 @@ def __init__(self): (VisualReward(), 1.0) ] self.datasets = [ - MockUpDataset() + MockUpDataset(), + SyntheticDataset() ] async def generate_task(self) -> Tuple[Task, bt.Synapse]: dataset_entry = await random.choice(self.datasets).generate_context() ground_truth_html = preprocess_html(dataset_entry.ground_truth_html) + if ground_truth_html == "": + raise ValueError("Invalid ground truth html") + base64_image = html_to_screenshot(ground_truth_html) return ImageTask( base64_image=base64_image, From 548dde3b15a8452a39c7608608bc4cd5991b2973 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sat, 14 Dec 2024 06:28:17 -0600 Subject: [PATCH 068/129] feat: updated synthetic dataset --- .gitignore | 5 +++- webgenie/datasets/__init__.py | 3 +++ webgenie/datasets/synthetic_dataset.py | 11 +++++++-- webgenie/rewards/bert_reward.py | 4 ++- webgenie/tasks/text_task_generator.py | 34 ++++++++++++++++++-------- 5 files changed, 43 insertions(+), 14 deletions(-) create mode 100644 webgenie/datasets/__init__.py diff --git a/.gitignore b/.gitignore index 2cda507a..b92ec6a6 100644 --- a/.gitignore +++ b/.gitignore @@ -182,4 +182,7 @@ work/ # scripts run_miner.sh -run_validator.sh \ No newline at end of file +run_validator.sh + +# developer doc +developer_doc.md \ No newline at end of file diff --git a/webgenie/datasets/__init__.py b/webgenie/datasets/__init__.py new file mode 100644 index 00000000..4e1069f2 --- /dev/null +++ b/webgenie/datasets/__init__.py @@ -0,0 +1,3 @@ +from .dataset import Dataset, DatasetEntry +from .synthetic_dataset import SyntheticDataset +from .mockup_dataset import MockUpDataset, MockUpPromptDataset diff --git a/webgenie/datasets/synthetic_dataset.py b/webgenie/datasets/synthetic_dataset.py index b3e6e74a..f3016ecb 100644 --- a/webgenie/datasets/synthetic_dataset.py +++ b/webgenie/datasets/synthetic_dataset.py @@ -20,7 +20,9 @@ class HTMLResponse(BaseModel): html: str = Field(description="The html code of the website") class SyntheticDataset(Dataset): - def __init__(self): + def __init__(self, has_ground_truth_html: bool = True): + self.has_ground_truth_html = has_ground_truth_html + self.model = ChatOpenAI( api_key= os.getenv("OPENAI_API_KEY"), model_name="gpt-4o", @@ -56,7 +58,12 @@ async def generate_context(self)->DatasetEntry: self.concepts = await self._generate_concepts() concept = self.concepts.pop(0) - ground_truth_html = await self._generate_html(concept) + + if self.has_ground_truth_html == True: + ground_truth_html = await self._generate_html(concept) + else: + ground_truth_html = "" + return DatasetEntry( src="synthetic", prompt=concept, diff --git a/webgenie/rewards/bert_reward.py b/webgenie/rewards/bert_reward.py index 264e5e47..6afc1013 100644 --- a/webgenie/rewards/bert_reward.py +++ b/webgenie/rewards/bert_reward.py @@ -13,7 +13,9 @@ def __init__(self): async def reward(self, task: Task, solutions: List[Solution]) -> np.ndarray: bt.logging.debug(f"Rewarding task in bert reward") - + if task.ground_truth_html == "": + raise ValueError(f"Ground truth html is empty") + original_htmls= [] miner_htmls = [] diff --git a/webgenie/tasks/text_task_generator.py b/webgenie/tasks/text_task_generator.py index 65c89128..0d06cfcf 100644 --- a/webgenie/tasks/text_task_generator.py +++ b/webgenie/tasks/text_task_generator.py @@ -2,8 +2,10 @@ import numpy as np import random from typing import List, Tuple - -from webgenie.datasets.dataset import MockUpPromptDataset +from webgenie.datasets import ( + MockUpPromptDataset, + SyntheticDataset, +) from webgenie.protocol import WebgenieTextSynapse from webgenie.rewards.bert_reward import BertReward from webgenie.rewards.rtc_reward import RtcReward @@ -11,15 +13,27 @@ from webgenie.tasks.task_generator import TaskGenerator class TextTaskGenerator(TaskGenerator): - def __init__(self): + def __init__(self, has_ground_truth_html: bool = True): super().__init__() - self.rewards = [ - (BertReward(), 0.5), - (RtcReward(), 0.5) - ] - self.datasets = [ - MockUpPromptDataset() - ] + if has_ground_truth_html: + self.rewards = [ + (BertReward(), 0.5), + (RtcReward(), 0.5) + ] + + self.datasets = [ + MockUpPromptDataset(), + SyntheticDataset(has_ground_truth_html = True) + ] + else: + self.rewards = [ + (RtcReward(), 1.0) + ] + + self.datasets = [ + MockUpPromptDataset(), + SyntheticDataset(has_ground_truth_html = False) + ] async def generate_task(self) -> Tuple[Task, bt.Synapse]: dataset_entry = await random.choice(self.datasets).generate_context() From 7fd16092b1a9cf01c1dfe3634f78f41c23a0b41b Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sat, 14 Dec 2024 06:41:25 -0600 Subject: [PATCH 069/129] fix: fixed bugs in import --- webgenie/datasets/mockup_dataset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webgenie/datasets/mockup_dataset.py b/webgenie/datasets/mockup_dataset.py index 09b53ef1..f480a504 100644 --- a/webgenie/datasets/mockup_dataset.py +++ b/webgenie/datasets/mockup_dataset.py @@ -1,4 +1,4 @@ -from dataset import Dataset, DatasetEntry +from webgenie.datasets.dataset import Dataset, DatasetEntry class MockUpDataset(Dataset): async def generate_context(self)->DatasetEntry: From 78cb9adf9d7e7d8e659cfa7192834f37850363b3 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sat, 14 Dec 2024 06:51:51 -0600 Subject: [PATCH 070/129] docs: added paper link --- neurons/validators/validator.py | 4 ++-- webgenie/rewards/rtc_reward.py | 3 +++ webgenie/rewards/visual_reward.py | 3 +++ 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/neurons/validators/validator.py b/neurons/validators/validator.py index 3b83e259..e164e09a 100644 --- a/neurons/validators/validator.py +++ b/neurons/validators/validator.py @@ -116,8 +116,8 @@ async def scoring_loop(self): await asyncio.sleep(5) async def __aenter__(self): - #self.loop.create_task(self.forward_loop()) - #self.loop.create_task(self.scoring_loop()) + self.loop.create_task(self.forward_loop()) + self.loop.create_task(self.scoring_loop()) self.is_running = True bt.logging.debug("Starting validator in background thread") return self diff --git a/webgenie/rewards/rtc_reward.py b/webgenie/rewards/rtc_reward.py index 69045cb2..25b7ba9a 100644 --- a/webgenie/rewards/rtc_reward.py +++ b/webgenie/rewards/rtc_reward.py @@ -1,3 +1,6 @@ +# The paper [Unsupervised Evaluation of Code LLMs with Round-Trip Correctness] +# (https://arxiv.org/pdf/2402.08699#page=11&zoom=100,384,458) is our inspiration for this reward. + import bittensor as bt import bert_score import os diff --git a/webgenie/rewards/visual_reward.py b/webgenie/rewards/visual_reward.py index d4eef431..3a676ed1 100644 --- a/webgenie/rewards/visual_reward.py +++ b/webgenie/rewards/visual_reward.py @@ -1,3 +1,6 @@ +# The paper [Design2Code: Benchmarking Multimodal Code Generation for Automated Front-End Engineering] +# (https://arxiv.org/pdf/2403.03163) is our inspiration for this reward. + import bittensor as bt import numpy as np from typing import List From ad4cef93596eea6488758d977f1e4e7863256ca9 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sat, 14 Dec 2024 06:55:26 -0600 Subject: [PATCH 071/129] docs: updated comment --- webgenie/datasets/synthetic_dataset.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/webgenie/datasets/synthetic_dataset.py b/webgenie/datasets/synthetic_dataset.py index f3016ecb..2e517cd4 100644 --- a/webgenie/datasets/synthetic_dataset.py +++ b/webgenie/datasets/synthetic_dataset.py @@ -1,5 +1,7 @@ -# The paper [Unlocking the conversion of Web Screenshots into HTML Code with the WebSight Dataset](https://arxiv.org/pdf/2403.09029v1#bib.bib5) is our inspiration. -# We should use Mistral-7B-Instruct to generate concepts and use Deepseek-Coder-33b-instruct to generate html, but now we are using openai models here. +# The paper [Unlocking the conversion of Web Screenshots into HTML Code with the WebSight Dataset] +# (https://arxiv.org/pdf/2403.09029v1#bib.bib5) is our inspiration. +# The paper suggests using Mistral-7B-Instruct to generate concepts and use Deepseek-Coder-33b-instruct +# to generate html, but now we are using openai models here. We are going to use that models on the mainnet import bittensor as bt import os From a56f827b51755308df4126232371c8d07691d5e3 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sat, 14 Dec 2024 08:40:48 -0600 Subject: [PATCH 072/129] fix: fixed synthetic data gen --- webgenie/helpers/htmls.py | 11 ++++++----- webgenie/prompts.py | 1 - 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/webgenie/helpers/htmls.py b/webgenie/helpers/htmls.py index d0fbbcfa..b217a523 100644 --- a/webgenie/helpers/htmls.py +++ b/webgenie/helpers/htmls.py @@ -1,3 +1,4 @@ +import bittensor as bt import os from bs4 import BeautifulSoup from lxml import etree @@ -12,16 +13,16 @@ ) from webgenie.helpers.images import image_to_base64 -def is_valid_html(html_code: str): +def is_valid_html(html: str): try: - parser = etree.XMLParser(recover=False) - etree.fromstring(html_code, parser) + soup = BeautifulSoup(html, 'html.parser') return True - except etree.XMLSyntaxError as e: + except Exception as e: + bt.logging.debug(f"Error during HTML parsing: {e}") return False def seperate_html_css(html_content: str): - soup = BeautifulSoup(html_content, 'lxml') + soup = BeautifulSoup(html_content, 'html.parser') css = '' for style_tag in soup.find_all('style'): diff --git a/webgenie/prompts.py b/webgenie/prompts.py index ed29915e..f3e7dc69 100644 --- a/webgenie/prompts.py +++ b/webgenie/prompts.py @@ -21,7 +21,6 @@ PROMPT_GEN_HTML = """ Code a complete website with a good design in HTML and Tailwind CSS about this: {concept} -Write the code inside a tag . Write real and long sentences about the business. NEVER USE sentences starting with Lorem ipsum, NEVER. You don’t have to include images, but if you do, use only this source From 1ec8a85f1c1a2c3c30e642565344c2bbcb06f55b Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sat, 14 Dec 2024 09:09:57 -0600 Subject: [PATCH 073/129] fix: fixed bugs --- neurons/validators/validator.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/neurons/validators/validator.py b/neurons/validators/validator.py index e164e09a..3b83e259 100644 --- a/neurons/validators/validator.py +++ b/neurons/validators/validator.py @@ -116,8 +116,8 @@ async def scoring_loop(self): await asyncio.sleep(5) async def __aenter__(self): - self.loop.create_task(self.forward_loop()) - self.loop.create_task(self.scoring_loop()) + #self.loop.create_task(self.forward_loop()) + #self.loop.create_task(self.scoring_loop()) self.is_running = True bt.logging.debug("Starting validator in background thread") return self From 2f5dd01ab3b3c5d29b0acd22e82b45af5293a94b Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sat, 14 Dec 2024 09:13:06 -0600 Subject: [PATCH 074/129] fix: fixed bugs in synthetic dataset --- neurons/validators/validator.py | 4 ++-- webgenie/datasets/synthetic_dataset.py | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/neurons/validators/validator.py b/neurons/validators/validator.py index 3b83e259..e164e09a 100644 --- a/neurons/validators/validator.py +++ b/neurons/validators/validator.py @@ -116,8 +116,8 @@ async def scoring_loop(self): await asyncio.sleep(5) async def __aenter__(self): - #self.loop.create_task(self.forward_loop()) - #self.loop.create_task(self.scoring_loop()) + self.loop.create_task(self.forward_loop()) + self.loop.create_task(self.scoring_loop()) self.is_running = True bt.logging.debug("Starting validator in background thread") return self diff --git a/webgenie/datasets/synthetic_dataset.py b/webgenie/datasets/synthetic_dataset.py index 2e517cd4..382fd719 100644 --- a/webgenie/datasets/synthetic_dataset.py +++ b/webgenie/datasets/synthetic_dataset.py @@ -33,6 +33,7 @@ def __init__(self, has_ground_truth_html: bool = True): self.concept_parser = JsonOutputParser(pydantic_object=ConceptResponse) self.html_parser = JsonOutputParser(pydantic_object=HTMLResponse) + self.concepts = [] async def _generate_concepts(self): prompt = ChatPromptTemplate.from_messages([ From b7edac0068a8acd77458426f79499145821fcd2c Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sat, 14 Dec 2024 10:28:47 -0600 Subject: [PATCH 075/129] style: removed some empty lines --- neurons/miners/miner.py | 3 --- neurons/validators/validator.py | 7 ++++--- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/neurons/miners/miner.py b/neurons/miners/miner.py index f113b64f..ff438169 100644 --- a/neurons/miners/miner.py +++ b/neurons/miners/miner.py @@ -57,19 +57,16 @@ def __init__(self, config=None): init_wandb(self) - async def forward_text( self, synapse: WebgenieTextSynapse ) -> WebgenieTextSynapse: bt.logging.debug(f"Miner text forward called with synapse: {synapse}") - return await self.genie_miner.forward_text(synapse) async def forward_image( self, synapse: WebgenieImageSynapse ) -> WebgenieImageSynapse: bt.logging.debug(f"Miner image forward called with synapse: {synapse}") - return await self.genie_miner.forward_image(synapse) async def blacklist_text(self, synapse: WebgenieTextSynapse) -> typing.Tuple[bool, str]: diff --git a/neurons/validators/validator.py b/neurons/validators/validator.py index e164e09a..167973b2 100644 --- a/neurons/validators/validator.py +++ b/neurons/validators/validator.py @@ -1,6 +1,7 @@ # The MIT License (MIT) # Copyright © 2023 Yuma Rao # Copyright © 2024 pycorn, Sangar + import bittensor as bt import asyncio @@ -42,6 +43,7 @@ async def blacklist_text(self, synapse: WebgenieTextSynapse) -> Tuple[bool, str] if synapse.dendrite.hotkey == API_HOTKEY: return False, "Subnet owner hotkey" return True, "Blacklisted" + async def blacklist_image(self, synapse: WebgenieImageSynapse) -> Tuple[bool, str]: """ Only allow the subnet owner to send synapse to the validator. @@ -93,7 +95,6 @@ async def concurrent_forward(self): async def forward_loop(self): self.sync() bt.logging.info(f"Validator starting at block: {self.block}") - while True: try: bt.logging.info(f"step({self.step}) block({self.block})") @@ -116,8 +117,8 @@ async def scoring_loop(self): await asyncio.sleep(5) async def __aenter__(self): - self.loop.create_task(self.forward_loop()) - self.loop.create_task(self.scoring_loop()) + #self.loop.create_task(self.forward_loop()) + #self.loop.create_task(self.scoring_loop()) self.is_running = True bt.logging.debug("Starting validator in background thread") return self From 14accdb0d626af5089fa82413237118c7a110222 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sat, 14 Dec 2024 10:55:40 -0600 Subject: [PATCH 076/129] style: styled --- neurons/validators/genie_validator.py | 16 +++++----------- webgenie/datasets/synthetic_dataset.py | 2 +- webgenie/rewards/bert_reward.py | 2 +- webgenie/tasks/image_task_generator.py | 2 +- 4 files changed, 8 insertions(+), 14 deletions(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 23f15fb5..3e09c94b 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -37,10 +37,8 @@ async def forward(self): if len(self.synthetic_history) > MAX_SYNTHETIC_HISTORY_SIZE: return - miner_uids = get_random_uids(self.neuron, k=self.config.neuron.sample_size) - + miner_uids = get_random_uids(self.neuron, k=self.config.neuron.sample_size) task, synapse = await random.choice(self.task_generators).generate_task() - all_synapse_results = await self.neuron.dendrite( axons = [self.neuron.metagraph.axons[uid] for uid in miner_uids], synapse=synapse, @@ -48,30 +46,27 @@ async def forward(self): ) solutions = [] - for synapse, miner_uid in zip(all_synapse_results, miner_uids): processed_synapse = await self.process_synapse(synapse) if processed_synapse is not None: solutions.append(Solution(html = processed_synapse.html, miner_uid = miner_uid, process_time = processed_synapse.dendrite.process_time)) - if len(solutions) == 0: + if not solutions: bt.logging.warning(f"No valid solutions received") return - - bt.logging.debug(f"Processed solutions: {solutions}") + bt.logging.debug(f"Processed solutions: {solutions}") self.synthetic_history.append((task, solutions)) except Exception as e: bt.logging.error(f"Error in forward: {e}") raise e async def score(self): - if len(self.synthetic_history) == 0: + if not self.synthetic_history: bt.logging.warning(f"No synthetic history to score") return task, solutions = self.synthetic_history.pop(0) - task_generator = task.generator scores = await task_generator.reward(task, solutions) self.neuron.update_scores(scores, [solution.miner_uid for solution in solutions]) @@ -82,7 +77,6 @@ async def organic_forward(self, synapse: Union[WebgenieTextSynapse, WebgenieImag best_miner_uid = 1 try: axon = self.neuron.metagraph.axons[best_miner_uid] - async with bt.dendrite(wallet=self.neuron.wallet) as dendrite: responses = await dendrite( axons=[axon], @@ -103,7 +97,7 @@ async def organic_forward(self, synapse: Union[WebgenieTextSynapse, WebgenieImag async def process_synapse(self, synapse: bt.Synapse) -> bt.Synapse: if synapse.dendrite.status_code == 200: synapse.html = preprocess_html(synapse.html) - if synapse.html == "": + if not synapse.html: return None return synapse return None diff --git a/webgenie/datasets/synthetic_dataset.py b/webgenie/datasets/synthetic_dataset.py index 382fd719..19b1e68b 100644 --- a/webgenie/datasets/synthetic_dataset.py +++ b/webgenie/datasets/synthetic_dataset.py @@ -57,7 +57,7 @@ async def _generate_html(self, concept: str): return response["html"] async def generate_context(self)->DatasetEntry: - if len(self.concepts) == 0: + if not self.concepts: self.concepts = await self._generate_concepts() concept = self.concepts.pop(0) diff --git a/webgenie/rewards/bert_reward.py b/webgenie/rewards/bert_reward.py index 6afc1013..eda471a4 100644 --- a/webgenie/rewards/bert_reward.py +++ b/webgenie/rewards/bert_reward.py @@ -13,7 +13,7 @@ def __init__(self): async def reward(self, task: Task, solutions: List[Solution]) -> np.ndarray: bt.logging.debug(f"Rewarding task in bert reward") - if task.ground_truth_html == "": + if not task.ground_truth_html: raise ValueError(f"Ground truth html is empty") original_htmls= [] diff --git a/webgenie/tasks/image_task_generator.py b/webgenie/tasks/image_task_generator.py index 8b6590e7..ee3dc119 100644 --- a/webgenie/tasks/image_task_generator.py +++ b/webgenie/tasks/image_task_generator.py @@ -26,7 +26,7 @@ def __init__(self): async def generate_task(self) -> Tuple[Task, bt.Synapse]: dataset_entry = await random.choice(self.datasets).generate_context() ground_truth_html = preprocess_html(dataset_entry.ground_truth_html) - if ground_truth_html == "": + if not ground_truth_html : raise ValueError("Invalid ground truth html") base64_image = html_to_screenshot(ground_truth_html) From b79d74a9cb956a77f723fdf1b1c1300581bb026c Mon Sep 17 00:00:00 2001 From: cardoso-topdev Date: Mon, 16 Dec 2024 07:24:09 -0600 Subject: [PATCH 077/129] feat: add workflow diagram --- README.md | 11 +++-------- docs/webgenie-workflow.png | Bin 0 -> 334436 bytes 2 files changed, 3 insertions(+), 8 deletions(-) create mode 100644 docs/webgenie-workflow.png diff --git a/README.md b/README.md index 90d29cb8..25d73d70 100644 --- a/README.md +++ b/README.md @@ -67,14 +67,7 @@ Based on the two sets of detected blocks, we use the Jonker-Volgenant algorithm Given R, G, and matched pairs in M, we evaluate similarity along the following aspects: - **Block-Match**: The first desideratum of the task is that all visual elements from the image should be reproduced in the generated webpage, and the generated webpage should not hallucinate non-existent new elements. We measure this by computing the total sizes of all matched blocks divided by the total sizes of all blocks, including unmatched ones (either because the generated webpages missed them or because the generated webpages contain hallucinated blocks): -![Incentive Mechanism Fomula](docs/incentive-fomula.png) - +![Incentive Mechanism Formula](docs/incentive-formula.png "WebGenieAI Incentive Formula") where S(·) returns the size of the blocks, $U_R$ and $U_G$ denotes the unmatched blocks in R and G. The intuition here is that unmatched blocks will lower the score as they indicate @@ -101,6 +94,8 @@ Let x is prompt, y is the ground truth html, ̂y is the generated html. To evaluate the performance of the model, we can use [CodeBERTScore](https://github.com/neulab/code-bert-score). sim(y, ̂y ) = bert_score(y, ̂y) CodeBERTScore is an evaluation metric for code generation, which builds on BERTScore. Instead of encoding only the generated tokens as in [BERTScore](https://huggingface.co/spaces/evaluate-metric/bertscore), CodeBERTScore also encodes the natural language input preceding the generated code, thus modeling the consistency between the generated code and its given natural language context as well. +![Webgenie Subnet workflow](docs/webgenie-workflow.png "WebGenieAI workflow") + ### Example Scenario diff --git a/docs/webgenie-workflow.png b/docs/webgenie-workflow.png new file mode 100644 index 0000000000000000000000000000000000000000..2a74fd929f4b392171ebbf6dc2e470d7f6511ac4 GIT binary patch literal 334436 zcmeEP2|QKX_wV(*S6(lLqLc;_m6^;_hA1HsN$BF@X1H>%d8%FmDbk!mQBOsbvBB_? zA~IJ*cp+ml&(nYJbMCpWq4D2q`u~5Q_shNeo^$p-drjZ9*4}&hZq`(t{Ri70)22(}xN zf}eQw33ld0XA=0RsSExQ5(Ld6ir^FYK};0==prV%3bd@Vx3|IT<4x5G;JNG9iiq=z zh=FDvbv0d$OIq#W{slqL(J4dv}94ff#0$6Wy%bS6zHY}F^2ldaHeCDu}g z2iw)~1WPNdXCXn#FSa=BVoC$a3I`Yoy&q|!r3yNa*laowiQsuOM5Fx~yU5l>32y@@ z8=FT;KY(vO{a)&9I8Y_wfAcs|6H(>0wid3!>cT22>(}j;Ad@8c#3WGqIpJ&^DRYiC zkjSnW2RjfQ?ac9zL{Q3{tq5eijy=u{UhWJS476F1ZEe6eA@C8%xs4Lhh6q{Rj%Wui zQZ&brtdQH`@95L*abxd9c}*ZoWIV>A@T&LE_1KK7;(@f19bCbzQ*MX9LH?$&x1I0}PWQXdT;_c25S*HC!i{lkZfa007LNgr`+cK$>9kjM^1 zYdl6YbG!x4(FU;kx1ig2!U+iqi=qUfp`7qkl*1I*_d~d~Ux9GnTCmnq#D#`BKZ;zf zr5Ng;O7Dq@jF%r_(a8i75~Ey0tN4V3zB|PSQ5sab|0RLG8^tF&o^B#SlA;tV)dsl7cn1*x;XVM?O;KdO z$rMBHrZ_WeO9bz0Ig-I+;PW7AC!szdFcV(~!HbVqh*ZMS5OXR!hUv)fhm;ijN@K;| zMLyvCK+sL&O5tn>mN2$p2HZawI#7fz5&^iu>nK0i63oq^lQt!p$9rukNBy^M*94+r zUq3)I46}ScG^1c48k%jeg%JrvC}{+4Y=)tTd_dH`1_4AwC_~50k@#dCIF;CZeHB?w6k}2IRQ3cN(<`|CBi16!@WKGgKXNVSltJ&+T4 z8|8QGL4Y67M}t>JA2z-N&`i@Qq}y0u!XRD>;>Pc?VzlOvWRC`uEeI|!oCpR2ANAkb zGzp5NQ8-d)s(=&)tI)~~D|!Lz`p#6iV7k=A3t={te7qL^EUkxpC~Ah(H9NA(eD#SSCs_oIh}fO3SE zfkx~mQQ#AyEfuDscou#&phd0kfT^DYiP51>VRAnRZ7Br%AhgBU`zM38qL{kH?1m)8 zZeX1G<)Q6b%no5OPe2w-UovTO`#1fi;lZJ%$EF-!YllS~Oq z{bVM2qPh~n>?Ov}U*053VBsZ-hNLFru`WN`B!6?eC5|#8jk=s(lyr0^3Cnkf?x^aKqPs*z#isDWfqWwFEiFzErRWBXslGi< zz%)G#aV8PqGqn}J=vu_KQ*umT0l5(NeasJsDJM9MR7(%DOcPWVqUheEv7r0@Ex^Ff znR=owucB}_mD@3<{qmYnRG1rVg{M7zAmiwg9 z9C`$jBMvBbidMHXB|+tm1zt(^I6I)?#}Zyx@@#CW4WAjhULNMy2zCTA0X%K0G@FhY z(SbIgi#{4lwZWj`STDlgo&6XK24D<>g$rohOf1GT&U}1#xio0uCd6<-upRG!BZIu8 zu%M2a10HWjvI03&O7wkV=pAbZqC)f2Bop-A(U2O0!$>$K29WqMFaSk;(Ma+ZyagWY zO94wXer6(K5DhasSQv*!41W*`ekGst^IXmk;Wt#;KPQ!fsh)0+#qpR|tk+C~nY4FSYwbC+PEqNQmOcd{~Gm#{MT4 zBI?F1jHCYphi8HiiTpf5^s@zfgeQ*KgPo;ScIJ3RwTy)Elxpzsg7x<|ISZtOeG$L5m7E@=RZNVk~v>2W>> zwl4fDDR)X3{)2wXPa4ep2FhJ{;(E`YCJqCZUQA5PVX3xv=3zf}lOy&Z>P|+WZK-=} zsEssj?@iBs-@6{E=AL3dg?`vwZrBl3G&T_<(U<24z(cS6Tw_-_Obu6Ya*-tA;W9f9E_gm5cUY@dV(%pOq(sCt!&mnJ2J7 z(^tWlfd770o)E&4a}+cIcmj3hemFEC_U`i;zC4=vnY9F#LBsH+7$v;<<8j1Pd7J;r zZv4zT0)q*dju51*UHI`hVsfM&Q|(qjebB7q$948mAf_04z9oz^&1ZTE7fWKf5HxB%PX?8G=)cKxj*x+rbla5_VZ4ic3(8VFu zN{EhNQZOpMV^D$?VDi9KU>M+E%E62l<(X>9K?tc1?HOEN#NebX=PnQIoDvEHDs5^jYxXBc# zgU^~h$m&pkV7!ki7uZ5GjeuGrB9dU81?-V3I3Vpy<=pRttt8<*sX%`TY?lHbQHqnD zA#;q)8}O0dF+sbOQ9Avv%=XCNVcoK z$ds4@%|71bd=$rh=r(AZX}0lAUVb&zIm1M7GBjf4MU<_;nb3G}YP90m=CSv#zWIV> zcc=xJFnl)2gA>CL+_*pwR_fvViT+{9jVZkPFO1JN&@iY5#-M-&{x@I^vjvlqlK<<2 zUe`I2iJ*oSatu8l6=7KZ3?Ll~@DHM`b%|(UH9@eX5Gs8Oc$%ajC>I9KGUsz9!1D&F z0O-SK#ZoVQH&W4uN(6@u3}<5l)g@(c@F!Y3<%~Cl#lfIbK>!3eOWn*7o@|W32vif1 z2(|;E$!7yEmYGw2?u!#^Hz>jPO9jDTe zX_{g{P$eSoVd{X#!C3=E$;LVzH`CyRHK-0L#>5I#IpedzJ0S($oG2TS1;@PtIxgS> zP_M(=;BA3c1Wy69G_wLP5Bf)Eda`z!qGh7q`(Lw_8tq$9*A(l}5Q54FUxa0_w|(cp z!37hJIM#)ePQz6wou{60Nac4zm8o9f3xYP_^HJw&D*xgOc7^psa-=GnEm#}G8e`40 zt;lxx@pkjej%l1jiZK5>Y4IOV(6^<9D($R)8(KiG&Kc@pN2vBGDs-H3LKGs9B?2@O zN}D!p27mCjrg(F6L?y!q05n5H_tgV@fL;WQT*6R~v;j-av(0Cot*L9HG1MG)_D44RCg8}I{lbhP3QU=oNF zQVKNIsO{xq@eB4VMe(Of$#9) z;Pe(WT1y5|0_uerLIw?ptwL`?9|9N@N3fw87-Ul@KyX_X>;V-C^kP_|7V(=1WD?#6 zj>MdZFf*`j$iuS2RFFuaC%1(-_!fv6SU1~^25uxPX$5wWed zM6}4WGw>hm@FWrq<4=L$K~Li$7@+BAlX?>0yc1T#A3mm@h%Q1qdr-~{P6^jqMPYt)sD8+q6=5)OS9E=JQZC0@Z zP8}W`whG+9waPT?{|S=vZU+^6-VdQfc&x}|SjBrC;0W*^&cTJ?#7}epQTFxr=74M0 z3yTVh@C%9viHU&JkstwbKd5R1KiZM(2|$QwC!B1BT;PE04h51y)x@7Lu^;A3VbJ?e z5;CJMyrxNMiebxTXoF^xj%)b#%P6rG9A7?YriSD%QJ8`hLPTm3VY-~MdzFT}K9tB* zrDTEt-++h^3Dh$OpFmF23~!GB93(cRK>%e&aW;J7ihLXFz(+?2x2O#KZgEGFbW`>~ z$sEk?yZsCAgo`9#DmsEo&hQXP0}KB(Lr_(ViZ`GCBA{GhOre+6B=)y{@!M$yaE zmjlL-L=!0l`5iC=I^$3%C9p+_9#WLJGXRU=O7uD4E*%glg65Kv%lr?u^_QQik5sLa zg2IwcAQLZQN-*UI|6luVJ^Ps&m4@}1PcRYh`_zqA^X8ATT4!{IDIOr zVJn!wBdSr@jYa}dGSZL>+DsFuC@uwd=2tZN<9r90h$_o_=VP;oQ zjIsv}W1la7UKt4HpmRy-lgijwm!Eyi%eOw(RQ%n0I={SU2iHv_L46P(`*37y0&M)g zPXfTu29+tXgMet@$5iM8Yr`1n+lQybDbh4f^`T_@@*DwZ{IhbzqzcG_m=gK%SYoo@ ztv9wXd4ilyUWxgmp_qv!kR=6y;(~8UL%1IU#$a%G3R$M9rvT!GAP)Pk8pygJS@*-1 zKo$}bm6)RX(hpJtS%SiMb= zxFV924SQ5Hfi1MgV8WM&xYPq%seNM6EUeGZ#<&pUfVK7iahWB|x>Cn4M)BbQ*!+3x zAWHys05t-QiuhscAphXXn&NKLP=d0fZi2@C@d47rLdZgbl0Pg$njD@(1?uwyJ>W23 zzyHB~sdxBVe`y3jU4WvP~ znXMXj&i^?vqS&7gixDM3{3)Cl+w~8p*RsYsH0yCBIk>;j0AE zM6m6em{$Sb0CizM&0)a?JIQSUX%+N&)DAE|=F>Vrd=#)B0tcR>!X*gKML<^lyzAd@ z?$S(-(!qs}*mC4A7?qnC_(MyKQdd2XKek~)bm+%lDl)P1s3f+kj23!8CV_8G8%)Bu z5ObcteNaybTLw&nJwC`UAA6Jw_8{sKd*CZjd4$?X)AnAz`M&o`OssVz^y99K7lJhc zAnSe=o&al=epa5CSS1gv1)LoGpGr?)ZQqlgnC$0cgnOr5`D{J$f2eXIfGSfh=EpdI z)_P!V-xF5=>HTV&0?5wKswpN}kEs(>7zcdsTtQjs|5fY<@VtLku9#RW?;UB&AB8I@ zw(qNO1@MG^R<4*>Ee~=9s)cFXj~|66CVQ+H;l4bo_?dYEqc=6+fo-$<0eNEb#$1di zzHt4G3H8y?SjlG=Nc+zhLQxM``CtJy5aSx}NTZtYodWggMbZAhE}Nn~;7Lm|STHlW zE*qBr#7?N7C@p49LKX$Jf^m=F|&XB1Nktzl%OEzX72nVPc38kCT+ zid>&qTuny@^gOP(8jJ+e&c}buwP2RV)rY`l{T=cGnkUg?Qo|P{5CF|eVvstN9d?3W zzvS9@`hM55g%qcwIcb&3%i}og{reBlQ@tr1576mY4K;8WzCFCS5IPGa*x(BO0mJ4YIhw&!R6)=HG$LzuL8UctMmeeu zz*0?1ct52{5KlJy{|aeve-L&`0hlp7Wy=uK^l^D0TNhP45uW-EsX)1XVudsbZ2tib zMEg+=EfvBtcvCoYUl^plvq&0LR$z8v_=zIs;ILe(jK1d>XzVK5)0&V1T;^!8*9nK2 zqh(ZKnFcXxXX{9Uf2@c$NId{*RWhUq8N45@@&&4O zfTCpZI}uhGfCC0k1BC=&!3`cfKP>bG5dfw10jQ7&uO--le^7*i6*aQ~hhKw6O1C&0 z60ACav$x0N9LC>(_6XYG9q^>$Eks8y~V+pDaAQOuafikph;8*lj3{V=-DrJa*!0%u> zkXpx-3n@ooqj#ZC<^h+O;!Fu}wh3l%zLCNYaAKXU&`J=Lx_iiEBS)rlsR(lcJtL_E zjx)C-fogYntOY1J42{7UJnus`W@qMorE~vh1X~)fRXNApzYw{=oly^;dJnpL^xSnH%$Cx>gzR$$Q4g^@o0)8Zcd-zD8Fa?li_)=a(cJ!=gP*&BJxQj6H zqN#7CeT#S8C!z=*jm#W!-aayKLSmG{;|S?2k!>D)X6RAVa9$lOO?iaZLL3Xe(0*72 z{3I~431#&FpHS8HRFLYO1O;_|U?mZ#F_CeZf`UKbV0>8q2N^b?C7ut9k;8$ZMNkFp ziQu?@coIEwmjfOae{_Q9V}ncJwozDViH~xy7rYHBZul(p3TpXvKCre5Z;LkmKW|Os zxR2$nsiO{`$U=Po&qDu))zcSYA%N;XQwss}HCYD&AV-EOd@6;9>K$kVX^rAG2ncnp z@MEQ%phY($I#S>s_yUXrsOxHHiRk-Ho1lS3;R1py%?TEueiVX^z@y&#GM@}@z_{RG z;Mu-t;ahX0U>jOe$)nhXU z0`fR?8Yqa4QUL}lZZZM<(ZF%=%9Qu9#~~1!f(5PccAzKAu`)LBMd83uQV}N?m>GvK z5pIKs9)f5`r1}k*b!_aQ5*CDVB;dS)KNgO5W@t^P$&-cpCeRH6Wzs0_3hms1+o|(r z27h=@Ib|2{Gx~OT91^Xt2lbN)l=^=p%KQq#k?Wwo12!+CiG*>Y#fnSHz&Zt zgvg5_1On^_Ts23cBXTjdo*XR%2Q)rkJZ9q_`y6~gqchbr_^hq>jvM?kDfB3ALY+J`mz6uNjVAa16A*Yn^ z0lEs#I)&L$_fQvg50z-#!~d@C+Bcf8k16*f%r7V*CMgNRj<75_IF&{Snj+MkqyM@Q zi2!CQ3&A52ltd&%!M~zwC7|Q?6-uwsFfMf6XpRm;)QN~9P05+5&T4@*5G_D}1H6D0 z!5kFw1tt+u{XoHpD5HoRKKQ>q!ewI64HTl8YUelgG$71t00u=^R8oB6y88cO!7j3D z+<^!xK~9b>Vk^oWaQ0RjK(hl}L9-4W`4!fIgfSlU2g(hg1`yyVc&W42RnBSCmQCBB zyiV7}_)UuYOWkLlt!bvmPds(GfA+-UBYRh>MiyNzyEs2=pY8_5Sv;x^%GQGPbz4># zn{NNt<;EEw4W32vo?qgyuZmzfIF zCV$-P&)N37@{s=`W;VL1uRvRU=;pScoB7@+F>vkisk-#5Qrx@xLZ0N)_vC&3>`^$x zq>#~Cu=kR|`#SqY*`w~=F9kZds6@Z4+PUGM0^TWK8yfuirt{^IeZx_j^+q0Gh1VsbT!>5fn+Cyyb zK5ut-l}ArpsO{OB&V2c$IWO)yS|>X_JUkzlzMp>fnebUoZ(lBao7|aapTn25-+L(2 z`_HtI?ie#k_m%{Gm#V=^){)H7zNN2EM3VC9W&} zTq1Kjd$n+m+rN8fq%R#VTza6y#5<*Dq+Wis-@fLkc-`{~D?@?QzUsCs0-mc*_r7&b zlkREx_pdo`wMQIFj94vlgSNW2Fo*Uj_YIV!Rhsc%KWZ85sCRp~r@Y2>S8s)L6?^5t z^Q0cO2=Yk&IT7wB7bN3!J=6MgiRo3t6;%gb#91EVGfd2|uIx!nde|Hw)pRxSi1W3B zXJi^fM;b%ZeYryxBoeEZjO35@%fAX=I&Ago)Yi&!uJfhV5d@pHz0t`X*~=Kj606(K zc=w;dmEOc%Y3?C;cQq8g8X0VzGx#Phe@V$1$yWWefvtT_`e|%(efQ@aJfS6bhrB3A z^47Wb`1;(vGnOoPe#4N}tJm2(>D=`@zXLietN;X6oIEj(Ing{9vDZ1`_>a|8GZjOCzhvXMO{I_c0M%Cf4G?tlcvO$ao zi=>|Z?i&%>|HLxvm`Q72HEY^}hY|_*j8wzZUFNsUi_r3H(s{M0s&9QjHG}+68_&&@ zW~0{ZsJ{ynqj%g9{^PAwW!K;02NJAH9!cUe(?$o=4i#%k^Zjw(NsaE=Vz*4`N^SR+ zhpZ-lGD(Wtmd!J{R~KZRK007B8u`b(JNS5x>il47aVxtkhgfbiNHtva;;Xh^_eN`^ zR;!xh#jVUGi$+FTN9Eq6jgA~J(|(vz5&4%xjdLHzMqsY zo0Q3+Yut{{a5XX{hrGpC=)2Vo=cEm(tNVp=3GKIdr1M6uBQRjUci$oJnM(&tGtJMsb*!$6HdUA7d2Lx$@njtB>dxKFU2W zeu!C9;!>h}`k6cA#v&eF3-+Ul-I?+uyCvS1B+*7VuuEDf#;3O~C1;^>O)f(nH-QqmFOR)Lm63mgg3okrudq zR(!SDv+G-hIz|#ojAwz_a=l%Bcuf;aMN^7V(mpwfb!N`54zR8dOl>H2Z%uh{aQ33~ zwpS0D6R-cln8t2%{)P_ewZKTVK(#*)-~J=)(uw?>nvnC3sQ}&N`pcM(0cwgodvE3v zcitmxYgfOiOp)#}C@iRXbkxtgUS(Ir^sT!{VON{*Ij7fh`G~Li^Ti(7zJLGHE*%aS z?F;BD@)~SmRTj)>&> z44=AkEXwYQ*oNsNRU<7`7XG+!4zJbwwa$z2btEaB-oTVx%$X3~|2!#mK4*{PP||2m zRl3p~*?G2sZk<+o8CuS>v)ojAG@L}UZ;B6=TD=%;9UV?>baq*ZEl29;lT~hcU61;-SJEvLyg74O^_-~o z%i`lKi^4B^IsBMLLpXfqUmH!IC6H0%A=)kA)$V6?#jJ85ma#vbHFb03*3)9Ka)Gvf zMgju0jl~g%w7fb_$lBP%?w5NOndRnst@&8!Z8{f&c$ZPH{&iYGqN~O8(A%oa@ceyw)L2mzqUYr8PSYl7{+2M_uPNm@nsO2IBuf{)ia=;Xiu2 z2NTs_o?I6FkTH0$?ZKSPtM~+!z^kzw?P5&b%0}H9=}DzQX?L@?>`!u1u(}l%sH7N0 zCn_X(zg=nRz-Eo7`DLeYuS=}0^`!HpTa#}WuR9b)j^7sFnmYJsbgGgVz3B7ZLqrPp(aPR0y)r0=}(52>!PL*-F3TYR% z-PPnxI2(Se@a++G{yQs%phC6wTjYgXCq1hxBu}f}Q1<@aKOd|gsBwOBSsK6PUf4+^ zw^5ds^v&|2ZcW!Ddb#viU2jKpUi9&;Yp;IjSHoo*C9}Z0$I_dHGr_T{!S&Y8`}?+q zzGf>B!IdZ4?q;y^!26zZTw2;$Uw5A9Z+rURXm^K4M;7ir;+fLj!R}&xmArhFQP%}2 z$6dEu63d$^Pv1D)7h<&iT*4*szUSv_T-kePnN{3&+3u`pKb>yb=AdaDUIg6>GslTT zm-BR$$BOtHA{I~1F1xSKB6;|tSkB_5wyn0BTNcf8ZCWD~&8rj3K<}S(Mj(buO5?yK z`H>oV2By3fwO0<=$gFr1wBW6Zfp0VZxNY6h(A#QzbVB2ro;#ZEZb>M4-qdT$I^#9n zF_%!s%RRCxVRoxoB3`G|%eMM1Dc$~($GvaI3=56Uy>mR4tz7Ln&=_GR#8kmW{#$k7 zFH$LwR0{`xn=Y^{l6@8{{}H`KO>wLnblll|Z*E^ubm`Xdf7aZRBSzJB#)!E@^2nRS zH-sstcQ7X@`7Mt9r;wka{L)U2)Z`S$IcJUhS&oQS#fM)~(_LF=IP%)nNh-H1(~#*( zVf!^B?bUMb40tDrv+Rc@`gdOy@6A5km)7f^#wse$$Cz+x&3*k1Cf&&erh0B!r&Y|} zoJikwUp;@Wx?FRN_7-dUA?KI7RXy%(DyjykgL~ypJ^#76TnU*h;kpM+teSKg(^jYE z9`0KeA^fsAm#^~BG`hPd012G^rBe>lpf`mQrZ%shSnmGH-c;5TK?5_jRj#XF+(H*k zr>C+vCr_D$*Z5+0jCzBv@(q>M^}n3XicyX~d{XnkrLc8@5wUc#%t~oJ2{8||wzC_g z4j=vVCX>MG>Pnk~9nq(|x!JJm*yA%Kkj((@k|l z{hi?zhZu5j%kC{tS-Eb9s&RUeg|bIlL3nI#;^SFaLDEb)2@|)kzlwL9R2ZD$-8N%4W42#k89wat zm86#r>dDI0u}ppkE#2fmay{6OLE7 zlxlI*F^GHJJ^gqc#hSCu4QzZt4OUw>Ri=8n$8$jNVCL%Tx07HvAjJgc`G!y5hzO^( zzQ(mf3%J)!n{K2vjWMk`0{S9j>|S6*=aG?;p-zALaQ5ko+_$!79_Lc89&K4GaJIie zM_{^Jz9W5bQk9o`d_~|iZtd;WNh2cyqp#EUJ2pQ*5Xh)iaL%y!zyjFbny? z7x~XK9Eo>2Ah6>XZMe$4p5^Gx-l6BN+i!2y9dQTnaoHtJTWu4v%O&ag2_EH+Z{NI# zvpy|6ke0EX&!HiU`O*BxF%2%RLy^(VmwQb~}!)R>hCy8O!=8TQC0stKEF_JZm|Z(r+ghUNWM;YdlMk$^ErH zzxrZkPfw=jx)~*lpIz8Gn!9pUcSc&%^~h?)eUH{1QpWQstST#C+D(@ybMQtNx7Lx~ z-iU-kjs_jWE253|ZlQe-Pp%(o+p5XQk?NWo$&mZY(QE8tn%SP0R2Eh=CzjS7aPe+! z*v|L6k@s--y3-olb+^dPEO!j+HDqjeO!Mf^G4b!&Ar*i7+Cz0&ktTNIj2P$Wl+dyT z&(0GY)3?m`Xntn!l-0Y{_f%Ao&F&k#zvle~#9<2fe|V4b;~IbI=(+^{`^M?FQtHAV zrTn{Md0UaXK+E!;^K(y^b)B`c-dWt6t13VL;Jo%IAH7!D*6^&9X|)Hg^RDr83+#CA zB%oUsb}(x9o3_@RjY_5!fvQXH98x=%D4Vc$6=(I}0eX5O+oJsRw9u7Da?fW}7Rwwp z)_m9#ReK@Vr|+7{?fv?EQhi3huUzK7H{VHDjVFnHj`2TT)u}A(@iQ-}u*&E2-+L2# zd+y~2PVC7w?5(AoO!C&zS2#v~nX!0ohT5UhYjYw(y#|(CZKl8A`e$TQK#R`{@6loR zH!IXscJi^6dKy0p-JxAq9dqEK@N?-Un+zCDq*umYji^wN8cIJt(#rYLk7>>z`}$bP zBF*`H9i@SXhk!=uXcz@BG3Pc9{!rB=oz@4JV^oH&LyfO8+>@UkWoaX8kvs>m{W33x zG&}ce|BeSh3KwV1ysWpF$!>naF2)zGVb_e$Y~vEx#Yg9|PGWvZ6Hc^HcK*TGH&V&R z8p8LcTnxq+Zq~~Xh&6cJbjY`z&`;1M1vZ@GZelTryKKAay!lhVgT_x2JCf^NSNAnu z0J_7dp7Y_>7^cUXAu^IYt~ZyerXAZ!zpp;`yngxczpO0Do6lQbsy(nft>L&o>G_UG z@qo_#Rc_NQmpsrLs43so8^&_yq}sM6Z?~Ty*s$Qi^A`?2KH61T+cexQZ-@=kQiIL3}e3E zcEa>Bz5UnpREBu1&#$+1*tL91-M}iHzm-oD-HOjwH|G9v2l&UP@uu3fDGXfNr*9ne zk0*M`WlvYRGqct`#9#HC`6E0>d*@I^oU#X(dPOW=J@?S)P-p&}vQ_`u`eXAYF_3yV zVW{d<{yMWHr|QR4xb1x8U?ON&Xg%tGed`!uFP!CWPNjcNZ$9nBYi zoeC(`Vz%&lW46>Uu!HE;d4ATy2t||Uj+M+Nifz5Ay(tZ$eAct*N1Q8NEu)lLD}V-g z<-_Tp6(ypTp>FWh@Xwp+&Gbn?;mC0qr4FspkN$JU5|^FDouX?V6H?N*yh=7$E*{LB zS}9s4pIf}M{%^*?#%i6okUqSMM}JjLmrJ@06na_3m)%~K;jfmN%*RetmB zy({oWPJ91tzRL~9PtRs)OXMHs30{=zpOW2Z%~)l~jOPm4%0G)Mb8C?mJHCaSq0`T7 zcjxtKmx|Vrx2*>Os<8~HSkV1;jr&qY`3$-y;3%!}JJJblsxk=aY@(x`7ln(>Gqczf z(}#lATDx`0uGzO%t4-^`f~8xd6b0_o9=Cek4qTnJ9)smo3?0`hZ<(nc+4($yNhOY# zIMSsxD!Rk*+17Qw;t6|}-tb;OB%{ZF4u79(lYKP%!j}Vj7+Lv*kh^IVK|x zeK1(D-CH9=okVxFvYePE>vpJFHrUd4ONyn3?5jjzFg2Q1S%_Dp5`uJ+{)+j>|f zZg7970kg!>{rMYfca>aW987(qw||E~)OI5S`YoMwqoS@c99?TG6Bj9K4>G;z4tlgP zO0;!FV)N~xj-2qU^Jx1MSF^*7`q$tannARy%VGWb}fw5j&`jFL{J{Mm8rQouWUaD8iJn5TZ6 zD$%FUyHnhoS#G2~AhZ5rhXOe$*njlT$A${lr@2c2oT*R(aK`;nP>ABVc%8q3-bts^ zvo%lu6U5b!(w06;d(&`)_dtYVUQK4e9B&YfVXZx3oS#(5x?CTHptS#!y`3s_E2!E z2X}I3aO)G<77*+4IjzdX;dr^QbtL_%bQO25tx86(lYpK{kX5m9uO!2p1YUP8*IGaA zly=R92^`gtvOy&qSt1`Y_FwXgIWBYN+$@zp&$k2!ZDjT8x|i zd!AQ$dFAuJf{zDguUw~S`FuI9+Z*`VMtBpiJK%7C(FIF>VG+RHFDYfl@Kvx?_ zi6ql&-M^z8LFCZDnODBuS1Utp^shN`x1FN1H$UxTP3wF3vYD%LO;I`Tz9!%8K&r3r z7-@!~#S8ZqaLC)ec$3A%)wX?F8Sc%hJ|>T$O*UG zx{9?j&lC+xXf!lCpLlxDpvY*o+++I`>%r|AN$#q%*2oVO2J~1}F0{PB$jTY-uBy6_ zv5K1^x}+*~UVFmL>sf;!@X>9h7jd|6yI{ip9D{o)O}EggdH)dbq3O!cYc(E{ zZjFM7s&SS8yII^0f=Y&#%1-s$t8dB4Zdr5QEw$ymIFVs{sdlV(%k;uaw}Rw6-Qsn& z)ZUigKEHjd-re$y4(_MtXWWZ z+3&fkI&!u=c*AhVvrG%ES@(s^37S!2>~5PpLuwUMRqMd&^IbB|J6%BTqG3DGvqsWg z3h!tUUIc2XMG8k3KI|Pb?R~$|@xl1Oz?B`tm zg8Qo7)2g%@sqz4}T4#?dzK;jB#jRF@9KkOzN3e6Qm15Q0%?I7s%k1g{l1o|^r^N7` z+vp#@!kpEJT)2dBR#_#li6+;-WtO3y4$|iMToA@4N@?(ogB-vOC}8?UMJv>!HnHB$Dij(#Adleu7>nEGh2Q7DrDjsq{qch z1&`5Zk&Jx9TR}#kEik}i1+Ph}N5F+&g9}E}Mw-+7(_BqJV&G+6R*}l|k)=cTmy#7v z%pfm1S#DNzc|%}trd-dhwp^lD-01}#3eFeufZ|S31d3b#<`ye%rYxp6DK}#>#N#Y- z4{PXexix!{*#1`!4joEX^H9&bOv?0UUnI*k-#12yaqzTYmIpH{@3zfrObi#!KN=Iu z_sT!Tg8s(IJ&ljKL;rM{?^JCQrZH~n6vP?(Mtakhj`YY+Ge$BrAd~ajxaHf~pd9VFUhD`ac-l&lh{D-Xz#M%xqYsm;T&0-6v z8Xc}$x>NSDGzjFIz?77F+0$oE)2})D(|5(@v}+HHB=a(wnCw}9lx_ZLK0_7|mSR?M zXJ=~1-wRhtlx+w#N~?Me^`?7cZckXB%$ammMj>{m$!H03d2-{swS!p4r$ze+?5*OC?dBK}$(LacZ%W85Vu;Jwz)F|AnK*04%4$olf)=wKx-bSm&LCqX zi9ZMA?EH*9O@pWRs61NgJ=Ff{Ue&Oj;3XN2P31N#M%bNSGHS{XcWs~NxhAmBr_WPb z{Oat<5{^7RSPhJoS@nQe_v&BhyEg8WHqScNpepau_V2$c9xvV;Ss=u*^5WyTV#$Gq z@EXa^`ux^czM+TvP7jG4%{Zf>DXZn7JY*dF`#c~QQw?^n*E;ZglRue{MjijhEI^i} z#crim$PV?z*7R0Mb`@+7he3uZ^K!!Jt9rh~Q#bWaORtPRuMm@Hn4ZEix9-U~=1~Fn;D>&!nWQ%fqHbx~sXFh>BNn6qzpOW= zJuCs6!YRgDpaN9QJ*%6IK<$jta>B>}OTlBfsbGyNHz$h*{YEzaz)pOA{!B+VN{o z(_Zl%yC2SvF#-SF${FbG$Poze9(`ysq%tHQ^I&iPKc4qVd+HTTWM{k)x#}l(h?X*k zyD zZ|ur-WU0Jdlu4T0ktMQHae47kzRcXuje|riH3RNky(7Qt>D8O=y>DGKw_Bw2c4p0v zcRk&s(`lFC!-yLh>WA6tD=UwA=e+@`=iQ41?ly!Km7KYL`QeG6b1{TdtCjMLq6Y7W zN*C0t`6=Ch4JJx{Mccu-wZ?lq*U2xI4*GK(1B-4@P%MZth*LCJZoKap1M`r^o5fOO zE2HIU=aq+69tt-TXcl9v@{8(L-puzq%v#!%-dxUaUn+Y^We(r!a|&rTFJgm^MF}U_ zykPpR)H1|%1cucgLk`uK)}y0Oi^b7Zx~W>t+2xY0nODvjvk5>4Ful^f zmHoFeV2hrWS^F(XoEb)Hkr5Jn)mwW)%m3b--)FPU3%~2=qnhHiz1;Ry&3}w=(G)n^D z5oDhySx+ZhQmo(G~S@ zAG?ypPNg{QBeCq$ToogU=l5@Hj#O6TTfHe%PIkTeV3rm0ixq$PFBJJ(RW{@l-CBWE zH!T*h^oKrG%(YCX?t*@VT(bMozcAG54CM$g^Qi4 zz^aSgg^Feby_MG*Ld0{6u6C05W+s{J3oE<0yML%&eyJD0OBuVL zb3w6M&8=Wso*lSl8DjB7mG)#L=5DTGLrHKz;S@+a$M;)V0)Q0KHzFfoH|TYxhjE0> zcbh>-pDT-8W3x(NVTcxEB=0O1mFkFl^EOH3HCwKJ+25RULxb&p@cPs4jL)1_pUpb6 z{(gDbhP!Gttg_iI)g{acagPiZ$6pPr0jl%p=*B&oB+<)Z_LaBIeKW0!%#@Wrp!*t>ZT#+C(DE?p@# zctFh1LwL?0nK3V0!=zW5!=~5IY28^LqHS+)e$%r^cbDvld}eEtvNN%r>UWfqEj`a! zUTDg8e`H=!W#G4Y)%n<27jAeQw1@V8Zz@=YQgtT&8oKhx0mSn}BoV-HF@`>Vb+x66 z*FbHO#fxJguzdXG(~*bSf`bbM9N0MH?gy#`b5%#|J8yI*HRN!O@!}k2KON~gr)+x` zT+8+4>eM(Ok^j#?W2XFk-{yp3zk?&&^SQf|1UkHl>oc@Yu+|@qt9!6j-)KJn?SJ># zgH=3-pj*Ft#zA(%@#>RCfj+I7`F>~dK7OC zt0Tg;8?Q)ZO~~mgcE1?Gu{*mgDGoHyAMIB5xRt=;;`c=Vvd0Uxq4=~)hpZ&+ZZ!$$ zlN^)3%W;*M-2SL9iLUeMtkf9gAu|8DhTzw~hSvMG+FrB~$`;soxpO;jMt|VKwS9TdG&#-v;PrwBAtjuy&8kKe1kAD7k*?-D$5`tJscaq=B4K zdSgWBlGgl_5l-u0c!-Jb=zQ9nepRO3Q@Yu;`po?H+{hg#ZY|Url3|o*w{$(4Gs|SdF0N(j|IBCh z?SshKz_=Fw=!(r{GI;!R)4d$mJ2~BRWchC4zQ`J^cnh3Cmt}Hemf;}yH@`p za!;S=<*Lfkb(maw8OjbcN8;0*Z(=KW{J` zZ-6(uL>FoLx;%9Lv)2C0J<1@+?b`qh+8@<;8QUF8J-Z6eUfAkYZJ_ersAzc}hWQ1~lIEXXtcqZuLZ&t?l30til{-x(n?ZN@2n$e%!; z2dyYjtA4sNvSGJ@!E!CdYa9<>6Y>iO>C6Q#XZ40%9a?P|XvutX>mim#f62PDvTp*e z+swgJ?maTi*Y?~j1Z)*B(y7^$Gd@t@q|lRn-9;T04;3a5To0F?UN+5I*@W-x({hbt z2ez7~Iaz1f@f>Aay7Pg8GpZq#V5%%jq2NzwNGddepHpr*Z5S+Lz}N$cLIG&G%num- zC$kWH(=c;-X(Ks9uwW+g6}tp|F)27YX&VJAhDpyrFF>CB59n8fbz&zp7fhSZ4)qiR z`+}g~K=&8RXYPzBw&m3Hp292Ofz}Yuvm5QPO9}{okCf@^@#8|@cPWf>9Qteh3HX>l zVS?k=-MWp;Qw;S6B;VG3J7AJ^+VoR%!8fKO^QMnO*@zeN&ooY8ntu_yNS-lH+l=r2 zcLv~%?iD%RHD3BAT(sbqH{{<-_Y|?iK8H_%DIa#MwGy9Vcqtn$HZzrnc=o)&fptc* zXElSR*1mKqw{?`1?>2d1uo$ zI7=1EfKBY!YzL-r^A!xH8=WZ?Q_m1D91ip2|6>L_dGnlgqj#I5{dy#s%0pf2&IJYM zCEAud7cl2|NF+~!lh^33XrDEM^qZSYq^I31UL}o}obyilD)i1($e449 zMN)v#B(Q5_bM4T}JEY#Z?K)Z>-XW~*o(5j)c7}TP_%e>__1vis@#FUDed*jaZ|r#& zkJ5RB@Rs8ZPj;@ZxYm3gtgmb&&P-`dYi_>hZNlz)e_i8SGwT3(lQeza1=noc{RSGH zdCK`~IavPK6I+pgT|Q;K=YqAnbW2m*oo$T6TO-ms;|JYZQ+gQ|iP`u_$GSJS?T9?f zx;o#x4$AT#AKC(A&eO_dqwZEX+6iCCK;k{d>J( znB1;_Z$2wYvJg4D=cEcUKhgmkxZC}JDrAb*-MgDVdw3ba|V4L=;*yM9U zX6w6>&uk85^%a09VCc+B&xP#bc00DkTWN0tD~#>T)l&o#*hS2&W&%mtYs>-U)p)x4 zErjzot0`3V4)+eY-QL0|e{>0q4DMLgmd2@D*VkV200cCxTMZbZ!)7)`d2DA9H2+1R zl1*2;%k@rTlS*}@jP;2DLD!Dj+tw}x7uUT4hV!WF{drxY`0SGl=!vdrhgsIJF}4>Q z<(SqD{yiE|quV9TKO9`o+2Uz`YT$ZJd*;$PT5|p7H%#Psat3y_NJ4Kp*|l@m;tWq9 z)d?>NUiycYK9Gs-Dc5Pqx-K4k!XT>XpZF-(YhIUKD|HjAhIe)*70M7nXs$)bn>@!J zQAj{}Z)V>c&D!3Q<2$NoYr+5*nD^lgRhxVPF7J{d9z9X-8zT@Xw|%;FaMQ}QT)?ah z!&Pgei^aFkhRQaLIg3fzZ=hDiW03FCMRpZ(L|Ck{{p&|t{CVTvcGX{YjrB;eU=Ppg z6f%2MTUXqnnAEa5uk4kfR)+pyvCnSbfwHYO-Nedd$={{>s}u*yc@>Li+A|+dm}~#f zFEcW#bPcqtS7q{!AU@Sug11iVFz@o0QGXTjIPKmBkn)?zeaXD(RT8DGO3cw*{>{A? zyrj!^v~@qL*_Ec!+#+*RxU(+mw}V$~+s$Hx|JFFq(wX|AwKS@~`PYK%I@br95oz+X z320cDEW9^5zF7O@etF(9g_wSyU;80KPQjyF5}1ov!PxO%Bz5s;9^+1F3fBSt`lu48`-$Wy){5ApBt zO=Ed1=TMuq$?vSF)@jz;#WS7L%>FUlEyhvpF7H~UIyn3I;as_niq^$Fr+HO4Qipr4 z)k}x=T=Gc^2+LocR5_^UbXQ#=#<0ufu6o&{9e47RH-~O7u5F3tuin*q1s^G^^7@LC zluG@e>$T2J8CsnRCts)-pRDH-5MQjd9J9jPxeFY9Z4Fh|(~r~X3f|oRa#qAK71wnO z%x^EgoV1cxJwt!RAXxvtI0P(Oy=ngV_<{q*C%JR{u3y{M^jnRE%Fu=rD-XQRIn%3F z9Rw7pk+PL;MsJDw%dO`3-nj6mbQLXF_IhypO0W=0rpaLW{FG43?i0^W>ss~RTb;bj zSTCbjnzbl){))OeOYQ4emjS`Db*@a_1Q2|uP5tfP{yFuy^Ka53jUz9sLOJ6p7`;Fr z1bzn(i*{o?jG83&bqeWMk}Je*N(+<<_=_b3sxAAE8OrhcWt=&1Bq@*AT5^jRlf13c zR`BBU+vCDsaCOwR$neMd1j?sn;MnCDZP_G^yHa~IN?cXEhPyf1-E}gOV*Tn&WwnOF z1LUOME-MN1Wx3kH(Y{r+(ltc?+|1#Vgmdn`Bn+GIXkBc6L(UF=wF#N#26cday0~nc z_CbzFp$ocI&>nwS*~2#y1OV|n7Plek1f4j+!e%)Rxo)$hyG^du#22w#Zcok&+*8@; zeAp+He|jmy{-`X6?iVbae`|$SK5)x&FOHC=-_kk|)~nNemV+@Zgxd=&7>Mm?uO5O+ z?f-e}TKY1JseM=`)>dyTx(yPMw=c(w2r1E*PGH?;io0U1xobJWhCl_?srm28|)F^6%n55BDTvr zs8+lsru9r^r({N#w_zzXM9J}my`G+c+w*k1b2mP6nsd2w7%$uIcl&Q?5swttSxXE^ zrTGCTQ$o+MT!|r_yshtfQuc&@R43cx&bmR@O4s^lDQDPHoFy?1$%Ux_*RDpG(A&!Z zxH&2tv>R2;uD#Z#NXmjh78E0E2`@rg+BK;{Xt4OjDY1 z(1HTLr@r`RF^&OYT?JK$6*&*?T|b_ak$wws5U=BYRYdrUyax2{)SOjJ#6+0p*@q!S zmPZ;cazdE?Zous%#V)o?6v9u1BnVp#RYSaBKz!^SkdK>{0Zx80XNtb4-G1cWv^V6l zzoPHM2z8?9NV zQ*`aM&rrLz8<_D4H=f*vQ6@r8KzwY4u%Ys+C?9@Xayaa6!ghe|NQkRBvrwDIsl#S z3R*vpDlNID6LGNI?%oaEG!xg?rv$x*`fhp+50v$j>YmVxSmyCMdJ;p(JufT|f$T-M zt6|wz;*uQNO)pUZVsAPtc&X%OWHgrYfPwLmB{DAdI*!+qDJuRDNNtp4w;fsQ43e(8 zZh6;_4Zgi6@C59CdI{FB;Q(rFEEJQEPz_iT?CPJUfY-PnA>=&bJ|eY>FRpS>V{Z)y z%1;WcknLyvevTi;8XD$6(##20UIZPjF7N3~j4^kcu0vB(y3n@+EHaB+<0TKkHuL~q zWiV?+(O>N;1Gv+DD=)uNN^wS)ulGfv$Be{V~)JrT*z`0aFZZ}D#jEiw-@Q+WH=K!w-7wp{5j z*A;kbBn7PZs;NqrvzI76%aE~XpB#;FlUeu12Ifj>F_sWSNp2|tx`{mIhP(?v{$yLS z%Wk?i&eQrDYOzEGPmm!#7_6eh4feFWI&0?d>nO18=EX$6odA6M?`s0VOzW5zd3&Oq z%zZC=>F+zmy;rZgIY%IEvEGgC^TW$J!{RmiWI4Q@@Bs0sBy0SDywfy&$fBVb@{cAPnhi= zzPRqMbg;Q{QTg&7rn0pbUi`HxFsh|%Yi#2@_+Uta+g<<_=spkg8*pdg?_ z3Id{pv?!gTgybkl$B5(*gD6UgbT^1}cg-k*C?VZ2BOo0kG1M^d?+5jL&-Z<2{cEvU zoO9;r!_40IzV}u4O2u}rgGX|ie(J0zhL}>Dba$Qh_vXBxd}j)VB5{eNOHqGZPLm2| zvi%2m@C&p^-y$9it<^n74nFa1;`BPXNrfd;o;$fh6~>dco$~5j0OcNSJDw$)grvI7 zM&?ebIWr%^7!sWJ-CEv?L9?(9v}3Z;+7s6F(zb8w{0bc)EjFjwf|G2c^4I<+Li~FO z!Z*HmoDW96-{P%UyDGs~D1jAap91>i=C_v~GB8g@Z|K0P>&+8LqY5l5J$poQ$A#!* zx;fWg>XoLxz~zb?DX&i~bO5qsG6pGa=r!ZNRdF%(HSjCmzA~W3QD~dw8%dZ5(5{s@h&Jgz}Xrrg42os|VPBqpkU#aV}#HkBP zcMt~M>kjSu9puR93JT3$a8V+~|eh z-PZYwFUYvmZ34+}9WwV{pqdmbYL3QArzDa+VzuG$oQH}T=y|#=)~{2^hHhm)vNkS? zOqYH6w~5xPwV=F0v*MX{-_ZPJ4jGCMZN}CwsGlUT4M4)WcK`Wn`bN5xxUf2c}arYZIAb6!iX} zqrJMiqsclk5ZH)UrNp$ku9y2IFPjxLMdo=NMV{7;Yc)@>mwN=1R6KBR8?VJ*4u;hX zjz8;zIZ(Okw+aq6g}1j#%5yPBnHXNN z6nTxSuu^8%b91axzTibk(pG;zF!ODm_Yq{5g`q&xi=|pwo0g4Ad{_CiSR`h-r0AHp zBX1>@f8qOPGl%f<&z}bMcu%x*$J4bCuP~(L$&yPYm%j+#=)k}`KKRc3ejo3o#Glkm z8le^!6k~EPV)(q6;xffP= zigWYqdv$moi~?+0kVwnGKu{ODC+4#J_DJ!$4zz+r#cK+-3RM>oS6;J8mct{be*nGn z+vMSc`giPDY3jFt9pRDpb>e4$)I6UwkD7-yzC6Wyi7FK85g-KjIANJ=hHZ@x+( zs1xHO$UL1`#us9F@6S4W;#fq?c=pK;{_bo;}nz5 zBhdZH&Ycm!PPgaYho^1flF!NP?g{7xRhIY2cr19!id^f83i~AJaAiXHUOuAmNv1k( z^@Gr#ej$@Gi*9Pnaw$XNzt3Js4wh8~#_t9&)_0@}Pf2W!QZu>DrrvN~9GvCYPNneV zyzJ1%KOTheFqty>1*9sz^30wB9(Z@$X#gVz1|IiXm-o7$CPC<}qInQ_G!eBPy|U!^ zE^%0rZ(@Y@hxDuCuOWusZYau+msw>=W<|f5BxHa&HVVrsGt@h6B3b~t6wXFsei;F_ zP%5*#E{_eM;ojgJsAMCM_uY-FNEWW;?94V9Ode^1J@I0a82%T@)g1(7mO&%lkx~xr zciGwt@6yLQr&Lh)BOLza;of$C^} z1=-zRzTSDo@Gpw9n1zB`V||rIDbQMcF0<^hLS-!dP)Qsj6D6&^vWMV|KNbJKcZ8)Y z_p1}tG6QZB{edBSAGw*d7}ePs5g2ax=m}wE@G{ zw0g3uR}Su6&#FyWbhH@&zp^_m;9W*!HvyR$_%iIPy(8^m=N-p>*UsNG_kW)hS}>xo zpngtF)h85FKfFH4ynnkbPLMkRK)Pqc540cmR7aqw8@8J^nqpr zccU8Jbf*XTo#uNjD${o8c8_$W%Qq{acnY$ypq$B_PJyx` z24Oxnd7Ai7Czvmr0WACen;_Kxyg2+_{bwE@i45FIY*c#r6B4xWNbuJEYWNUW;kq(m z@i~=q4}@D_>@Eq9KD+leO-Hldr%n4@xqje@9DR+}#AlB4YJqBxZ=E0ReCt=}{uDs} z;Jqjb-2$5A_VSKwjtTBnto*7zE6S>Q5-znl>M;2-X1Nupn<#^%&a+?l{GadbpS59l z2OL+4YL7SOa|=P)08e#Fc+Bx3l$kN0S<0#g(R-udq~R~ATul86M=n9iS)S9|kYc%P zAGuG&G25ZHr%P04%h%4YxSzF^zc8BPhtT+?L^bBefu4t0aULr)B_+=J$SnO-ZAgCP z(9~1p1osi#W+}_Z9zTT_iIV@zfb!>7TLcbv>X8b4pKU05VCm)sdtzT9s(g+=`t^4W zc}5y0)YkYLGLPK*clW-AvV5KlmcoXWzl+?2to6Gf;UXYucS(F$0==~8F5Ei?Ol7=S zwA^WUYZOnQ#)fE{)eJEGoD_3j%tHQbJpr#~g(`YLo};FmRi;V5c*(D>@__k)mx<0~ zK&QkndDQW}v*`33KHoE9FfRQTd6B~4rO7+o|96X>pS=!!$V;?OSTD3h8>Ja>`}#Bk zjnA~+EN6qx-Bb_ET3xwmeyLSoPF&28lDp#)hvbTX5jaL(7kZHv@@bn0=Vu*`P%Uw& z#$AZwgR4CllFYlN#~qTAJy_asndO#gC8$6mdUG#+vGzX2TXr}4-R_l989din>6UuTQMelWTI@E#9S z=ADxs(|6s);z>rXtKT?Iv#dB3)6zmACZHA9`9{8n+b4RZOMC;7BcJs_nN(`Ac^~2& zgkZ7#Y4H01`L_~w^fh!Hsr_i+&@L!_cMk~0^z5J%2@?^^oBE&=e~DS|RyHvN1*i8s za-OmNROB-9bNP{y%?>&z&h}(WMDk{3TicWMDGk z5q^RpZJh3q*fi+GAmuNqdl=S&t@rHLrxVRyxuQ?$-m@lAi2u5yB^$+if)XRZrRD>e z6IRgt9rv~u{g+y7D4nv}Sg}O)1k$VtA+VRv-Tv_+>O{fY`-a+$Iwma<0}AONmm~T3 z&^D+Nr-KZ%zf~{MD+ z19Zvw6Yp(U(c&vphWkJ>q$QLD+%kL(P@~a8%fn@T@fdW|w(veA=Bf9hIpbd*@XVtl zksKAa(c~-g*Ok4GuaL>oL;T>c0yr-kU9`65r^#tpzSw6J#pJ6Z!! z&5zPjVhktsFNvRm(AmES+($1e*S+l-lOOMnxU#Fn-2W;oEs`!5bgI?`|8C~&rJJmr zkIey3de%_bZGHCHK55ezu&SA1_MP~!OSjAxyJ!ivf5hoewtkI|Lz+^MPI4N6Fg7_x z1g}3-s}pfl^VnIVh>gOidA;6e6^O(-3{Csh$a;|YxX7om5UlUcFC5z zccQEd9~GxHTk8TTV)g)gt~Hc-)C&08o^^@TyK&WgyO*zvA&3*=EBXL}$mi<7@XZ(2 zoQs!#n5h=DSglGLFVlQ- zAjjWjr(O-gmGZabk62aWp1A{!XMc;$Q_8|FNt#6X2R^NSoez2I)e(e6u7_$IiRr|< z!~OALnH_PB-Z?W`J zGP_vVh*wMgs9L6>VRY4X_%1VPR({+Q?+$Nmzc26+G{jI`2+mO#I))#WTWRgSH0oP{ znvct%OC1NnHuo9}PjNw_Nk(aKc>RSj?+l#3G423#ni;T_cuH&u*@l2smpr*1Yw~ev zjQpE(jCz4@30(jmdSL_yI>ulH59W-jO{}~8%ET82#F-XS#<4(19b31$>U?p_&m_=* zQNZFp^oq#>vL&;S`)P@%BwI8_0WnT96ItGL`dUDMeut`o2tf9wm4e<4H zOp+xJ2c!TwISw}2;wPP%ApsV+ec&Nf0kPQPgOt!- zjW(um6X&(hq|pWFu3Fu+L3vfKO@(NR@nPfED$fJ6S!)8?%o7PVW$p z(A_kl_lKK(fA1m4tOo33t`z>^w4}0yEA{F<7RK?ydng5bhU2Dm|4Y-PtZejb5wUSF zyQ4-2%Ex%!qb?<}DH1t=t=rBqRNbTZ!bi`^?~G3^P@0U`W}psINgSjd8j{Q*28?L=Z9C9lAUDsJB7GB9v`~bjV=G0X;u&yUs_CHfd8O#PH>x80rgD#8Q#D3h6 z9)6yFmpn7c%__tkqrH=(^m9a^c32P{R2J%R^^o_TB3NT2hfxbbQ0=_;6h5X^5s1>p zx|e|5FOU6t_a^9p^JEWC`!oA8IzLjaiv9)DkoNWY0CKVVVJ9g-vegz*rZ$8|Hp@8! zhAwC==_y`gU2dUQFiu73yDF#FB_7H4o`y5S9%+)1KACHz zskNV1SF)r?%v>$g*R}mcQnn52R`^oJOl8%xApwq6K_g3K5~h#4>!In^o*N~6kq_W^ z!&|XgnQm=S4IL-Axe+JiYm`Ul%aKpD1x3|i;gz(CvW_QRzGThr$PrD}`sEu&>VMGD zt;f;QL$>8#<{(UG#ig)7(3+FGd0AHqPUDonXW7+~Z0=BI^fNy>9M!h8lSkAjjBSsg z4Y5g0vH1R!Cv0o`vFH6EA?972UeT&;-)-IcJ)>>|g2Rmub4E zD6fSLAsya{N@bs7H+7J?_3Z0a-$uUxZHlq7QuB^;U@5xXyNgEOPI4Z$93QqW45H-K zVsvwr1SxO)!x@GNx$~zFMBNG}e;Jsl9W(}a;FLG)6>xn(Z@t7rBx zb>ekcuZ%q3BloWR#&?rAT%!CCuoya~^*FlWwH9UBUCjhx3<(zeOWeLHcB6M~ck`uI z(&O*|150AvY`poq`gpB+xx4gfJD{QTKdpyc>q`stT(6(+nD>Q8^t|(3 zPkQK{AIz>zHx_KQCIT|&WGzZ(zsBDJpy6qXrBNHcxkEDfnE9~a&S$Ojzuo;1ovYXnm#QNE7oQ+sFv7@8iE%}}#T(*BkKaTH9 zvdL+|>@9=owRBD$Zytl4F=?c2ZgeXfzB*#FrjP>&4?E`8FFj7Ci}Pw9-ITo?zr8!3 zi#OW&n|A3#LCU18u1LhaJt|EIr`H&!uV7>}&vjm2wi+rHLXD!L8pi(%)#;zvo zDs?=LkLn}zyZAy;a_*1jvY3%Dv@EZ69_ymp!a2T)83Ko)D<@TxN|BCxuexmL$oIoM z;xR>d9=7gnu2#;DEQQ_@>xcZ})@?nqFY;sssaIOtVgdWT8vs-<*4y$w3l5KcW`*Dy9>B~&6K4Jup{GZHSn z1$jiA)WdiH3)ihmN&)TidTci1qv6=cV%)nkj{#CmXu|1ZpL5}z7QDcg=}5`LpLF&j z%CeSME>0NsCBu)d>0>`GYXJmfgsow#>6H%LQxNL|`N7w_tovBt{i1Ym5Bs+-2Xac8 zwrYUcy$W0>I15^JRHVqtPXCCrF34`XE6=o*OdOptzDeup@7Ai1r80k7OREfyhoeNH z_b&hFtUYBv$~O^3TUjfCPB>}68FAvtjBBbg=Bmg0=SJIFzz{I;vXP4sc|?<|H)b#< zfEx*WTS<4V#8Odq_vTVI7Znf^7t(oKO$6KL-)-!Oqp*~i^H644A?;6X=HZHf_n2rM z1=AhKOpE(|tz<0pP9e&jzgN6H|GIw!fLgVp8--5h@nLqK^C-S|RWgY1+6Ezt`qUe1 z)ABXMhJ7iThe6UtGPULrr^g&!*+rc3=@cG6j*bKT z>0RyFfU2M61B;(LSe-?^@)NbatV$nll)`A-lfi-d3%uluf*0f5S{v*Kz{u;6(Y!CW zH!|=UwpQ!g@Uq7tr0uX53p+KzJ}Qk4eco2xbp z)SDg#z7-DMbAzGr>Y*IyoyTc$;srF9Gi8}iKBa9htgw*$u$h<4OtrgYbUzkV>1#M6 zvHgdccsMe0X`5eIEb+SiK;S1-SB}MZ;JDC-sr4mR)PAML*{DaHX5p@U)l&MJ!DMo# zZaQ+?nfSAp;Cv=ysomN{+p%Og!fk?%@EI4M{W=&n$WsG%D23$gO+RJ~Lze=R3&HQT zhvIYF6Yf5@{}!}la}C!0Ga=4Mg~{$T-{Bd&S4TBSM#^hCHAd9kzPz|S>%h4s)3>Ml z0Xf|SxXNql>xCQ>naP_`fpU|B`Y;qrGTh_Rq8AxgWsfD@OL6;cB5ogbf4vGe>_nXe zz{4dg<2g43xXf1tWHFLKDV19!V3d|9u#Jwv7Pz%G^gL=^Q}f-Ukz%YncVZeOz0jHB zJKSc9dor1>Liul?5z9IG{^jG)X23XTRg*>7W2}5uHh&Pmr@>EUoD?}+A(%BQB z=F&=P|Kx`5?APhb-YK6P4fCTd&*Y@SIm2gNT=new>l1h-XA|a-j>yL@{Z`5flrM=YB77L=E^y_*^X*0oJ^b3yW3?$L%Za%Qee(*HoVX!J! z60Q9;t3nyXG5z_F1@GiSRAz*iL?N^3>hW1*IB(9=p;t8iGjk3DUdJ!UP7uxO3N)V0 zxnPK6dN6GSMPJ(*CW}$B*}G+<175mXydBbaZbUcSX+JahEQ<0YcCF(}+KgSgv*?Dw zjnh}!sdd>cGZl>1kyf9ll4mS=$@Lf8+{ZTSll+oV?=IrkYvpvs5_6<5IeTid$a?iZ zXv(~QYuw^$`TkG`idJ$hw!;U?4O;nJ@bH{x)^0m)SG8(;I{tGlmQXO8n_TdYSkb?L zKoD{bCr9*2sQ`CMaUd^SsO}dbvp9KxFE}1K?5`XkNzj;0>@4seeN%#NIvOjD*l0g3 zQ?IM8F_ix^)IukoXi{H~xZZ2pKI)?*Jvr)HMn0avp|eMtxSyE0tvJkOl5}iAbk1c zY7MiPvrUK;)>m>p^;sK#vyU)(B`C7v4Q_W@Sb9VkvssUqEktYOYRgg_M5!hKm~kAl zH?Xf)^IeXpU-JWnGVV(=mTHz>yV{vcmV-KHzrQj6Oyl-{L7V^JH>3y%=Le@`8I0W) zP+Gv$%I+pB39tug$sGW&KfOKS$I_7M%R~>Z(ezl-vWt9td?kjo)Mc)YHG$ZySAE0< z_`WCJQB2+Gn5ts4*6>eoqrTS|M|!c-VQ)!0}>1oP2N-B zHX;~XlaE=NP^kz1o1}LR9l^DLf8_TvM)w_i(%H4Fxk(lGolg6b4=dgd#Tpepdu^GDSd8v6VFcfcJW2 zMhP?WCuQ0`(Ksagzgn;}Hx4jyWgknO`lUFEv3^`hLM|;$Ykw>9{H)v&Eh*nBuUdQB zE^`P1_EZP_6^B~FMeV#tlu!o7mtbaQ2$F4bKDi5gyn~Z?U?um3LXU-ZD@N=VRw{k= zxC`rUp4x2*X0wL}9-(Ge@te6e&=1)cfMP2djlV7U4_I|w{}0~zuso)Bj%E1ZJ#Kv` zUt1Mn%RD3f_zL9@6oW##A{=_d94UbAQ#cjdxD9k)O|wK8-ou{|FJv1Kx&w)-WU<^_ zxIGMjW`}7+N|0?v3f>Hz61DP1-zL5fCmo3Sb&K#oJ zS5IozXEi$n4IMuaetSx%Tmt89*fR{Y=0H-%4fl=3BH(1WXo^TfW5Y3ffFgag_EpF5 z56XNFt8~I^7k$xk{x2SS=*Usu0}6ra2@vkpez;pnsJuRtBimF$c+Nyt6MCnwVvDHx?lQ?akiYqDpKB6@urcT<97= z+Viw{p1_*{X89QyYuw3qAy{*3cmPccC9K?VobA*j+XoR3p8?}z6)F@^f`pR#AsY1^ zfGwfMd)7(B{Ck3)A@=A)Y;3UjszTlX(Gu&i*1qBq2&C;Gv7x5FB0|5masJ?8uW&`N zHGyIveqNAbNq}~G^}Wv?v0@v0eAU`c~Bg{kM&x6y=x}ciT)UKu3}H))3XpK8=b$Oko@HhQG)Ud^wc}0joWz&z%q;@ zE)lOq(M95z0X1+5?*Hg7h&M{9vC+x<2AW%70C@F`*Sk=Cu$`(iKH@2I;$k40?yS32_Cp|oAb2Y8-Ze#QyUSg&E zS<$oc28+VUbbJry539^H5?V1bsmu_2)Dw5lDlg#Uof@yeB+m)c`^=BWoWAcAH|`+y|FYnav) zrBMga@<1ahpr+&i3F!7HSTZ?+boh8W2CXQ2SJELR_(b8^zVd}?Z6_V@AllK+=Iubr zvL~$GIN;t!%#rt9{a^1Rco0(XpY3ZlFL+hsvo?FI4aZr;Ez;7$#{XbTgL6n~WJ6Q4 zA%spSz@k(@X~Yfa*L;^oDt(b_(*!5stiof^(>P~C%BmGCq$VEqPGkLI=Gk=F;;;v^ z;qccqqgZx=?~Gn7>Sw!qZ9l%d+gcVT*@3>2~ijFEbV`R2pNUf2I&=}&-vQ5G0jJ_m{x1)MlM&;ZRU@By$~Il+55 zgbKSFRp|qgvxJ~YGiN~_j@g`cWb9G1d`;FLxeX-_DMphQ)7^YpFt}x6zK1e#qb&`I zr_RO!dk#Nbxofo03cJa3*y>+&S1C#mn4~3C5L!~Bs$!+%#gJXuR?Yr{ZhL%0ob#bk ztOI?lPEZ1|i4VmO;t_e-#rB|qDX4`?RBmXi*v=@NG_Nxb=ym5R5dPf&jeHBEKo8ETbvPH=~O#%awIe1 zxl;DD?$7Ei$SUC+)VQP?FVtjkxV^2?ZD4Sh`Q}pvyFG6=#t`31+Uxy=a5%m783`=eWTJ2Y?vBKOFm7@6E zWrnGLSPcFiYn_{9Ty!hob{mne1N(5!|GlWnqf5YOuvA|6B#R@Y;-&{lwh{3@n9j)c z$!$;=KeI&}bWh%-E&{P_!;=KC3B-g?5rIDI928#!2xjZ|Cq12)A;ruu5d4?scmX9Q z2rP}jj~@7{9FS#~p`TRw7M zvLfvSO0sl5B$afm+%zp;OQ=I8S7g6URLfmMCtl9_HxG~1f$$XUdz*T|8os5P`Fosz zdzw)om2jhyXx7&jN)|b?lO428bwGRu&2t)7c=WtMGi6=qB&~NrVA~0!m-FC?Z!Ch0 zIyLt!dI=%e^S=54qyepR7zcqL%Nj4tf8aQ&Rw({aQ)z@gSZNg7bTBXm0~B-GI~gI2 z^Kcl=xXR5Six2E8F>P&ld+F9hf&fUCOU>q@gsZe{fMn3Dx9gJGl`J0?FMnkyEwSYl zn@&U-!PhJ2MTedCwI!lt*RTUxa*YTltj?Hl3$%A{+$Y~k39G}WnCw1}EvVu_ZwnIWA zn@?60U7pU^DX{V&M_hBJE2n}G)xl3W? z;Zk@PGd-PKYl1wVD?ux>VOb`onh0le@#13ykxD#L;XM^Yo=!ui(S6H!P4||U_k*pq z?-=MZUZZdua!*s*Q&oMjU-wn2bp1i><-q*j=JN9(d+oQ_=ApJq3)RZf)hxcQ-!^$T zvt3Dt#`-EqM|@b?6H4->+an<*TXdPv{8hS_ohi0nyw~7-NMPs2^$}bShhF+*E;9$@ zRGCTkLiMOtkH2AP00GDo7kkBqn=-oG7W}ue6WSxGrE#P z;sfuzK@L^K-gsnFUFhBh;qupbmY}j~toQ;@M9X>+)86fU6wM7vyoo1rOAQ|wm4#2f zZ?Kg3)HobQ5QEuf?xUSR6W#B_P><@@HGd5S^1h`FHRTa-55HGx$}(XdZY_6ViHs7r zPpkbS4*ty2`JKRXL*11kn^;qnj5h!O^xqHv+B%0klb^L>QF={HN~{hZ<*s8Xx0Tqso-y>J|~oNf{wI?EyI*dIC4aF7Hp>8vqs- zRQa%TkicO3@LbS(W#P=7%&}YTH|+>DU|$c_&>QwzS0wGZ8fLZ;_rAUpI*87*J+hWnfG`E7=x5Eb$J!nSK_>ybD8HN z_w7k2uyoF)R?g+PKJwHJJ)lsMnQ&3u!H2qej@3BvUwH~{uR%*i28xyfSqR@H*Q%8- z46njI=C-5cv**_d7p9)As`N7PVV|yaBlt+?JhrEzO`e6ozI*;OGpc8A_}0G$j8)k~ z4~BWI7Q)fFBj;=Ocy!@olbj4xo$VkmjhfRG$_- zR}gWtrF?_n6E8ktTs zhf^(`F7mW6xBXU5A?o>Up2FX@_4&r+^>cr3DL`s~x|r}+Y9Iw`xeS^EhN*59R`fpX zr_}paFtp{KuF(CR$9*m3l8rXj4wul*YU!kZa=x3SUwaO zF_GLna{42{pmLV3bizV+kU;ZFcU*WIr;RGeFiCpn&)>$t>>rFWx~Px2k$UrCl#th= zkw73D0+Y6FEb`ss{NrJie`WU-$B4v2x8rk?W$rCv$A8M+j_+KcyRVr^_~fIRm}&Sk zD!MZU1cu2DjFb*0rRsKEuQ%IFmcV%Ytm#Xp@rSRU$=kFkVbs|P;+Z+Ngzm=0g~#E( zXd471xx~Gq2RReE8~UiE9g#`KD*;JsAl}Zu`6*lED)X6Kox<;`?0Or%-m38>qhrXHUnkd`4wyejQ!H)7{C4LIzeO2_D|5^tC00oI<_XcI z(Y-<3-r%&`Bz74IFZ4VZmXgA7(sle3OiNYfYd{1y!)v}M$sFCZ$demJZ6NZgXV-we zSCRFXZ7J^ZUyEM)vslyiaPy_G%3HU23ON-T*M7llu?8(vDAnsLbq_xA2fOdoa*t6a zHYuN`rXtbj3zOQTH@Y0u_Ia&*>3Y^sCw6f8vR$itb>dH|lKc>#ED0VNYp6Bac42C8 z8I_->H2S%XU${2$fqeS}v&RkLN8dVl0vm)sRp&KnbZsd-7Fg*DPQ($wOIkw5kx%`% z=E{)-e=n>@drhVab+I8w_MyU6$HW1(Zu3{)i(gCmr^ptZaNGEB`jW25y$6)IByZUo zW$bPn)k9hqGEh~zja z{C%Ei0KpEwTR?yRYc7)17;h(~{%SMCQyvJ@ZC&7P(^HvA3BJdTkJgZOq zWQojv_K5YdY2V~X7+pP&J>Qh+28FX{Kd(Q`TPBc6x9-BFc^Ap({DHs^RF@tS$;x|P z8b9zZvl!i-P!7FlYwQJZsqfYxHBeTA#H)?eop4Wz^u7JF{ndVOy|6bXhih|xpiqFi zN#ry~!PJGhz6SsEJ~FL$6F5T^ZYA)hdB13?cUoN^60((iH z>Y~tFy$jy5UWrU%u!o`I?0X3q^+uN-t#6)h-5Um6?A7$98w`^kW_1`I z^Og(~@(kZ1>rhp6;pmvjn$e9qbJ1#qi(%&)Aw7$Tp({`_S3gl2dKC0~~;7Cg;rKHkstP}TDy0We;bj#k-VI6NI zOj3@x9keE!^0t*R;etOtaCgJMz@8xIFDZIgke5m-h%u`+7KY|l_(t^QQ+>@x1;?F6 zE4blimPwiWaxbETU8scXCW%6GXKDPC&BEC1?$C(6lh}Av>zhDZX!;;ENx@_NlK@HZ z8kH39+3@(Lk2DXU8EVk52S=q07JTMTQgBP$k0g#y!ON$7?uTwk0u9F?nf{X#WX~2c z-U^O>%@^TM_*N$;9qI;}$CT|FtM*2M!lL)4Kq%+$4|s(;-w!{WAVh_0zC1A5ncJDh zegp1F&{_jodwuL7Vco*tekvHSqVbz|<7RAH_p6MLGTgng^`)Zy{1eH&Et55Wb&rN0 z`%BM#Xgj)}Tfgz@8Gf@CF!53sx+!)hfN298QaWvXIoRvAh@aJ_HV6&LZa(tW9|Q4c zOvmcvb+8=JO*v@(EW?s(KJPanl+%Vevex`GjWQU~>vVa|G}i2xwD~?^%TmUe zq+br+`}&(lysb3ii{;9dmyg!d-+Az6meUTtAA-Y_aQK+z^(e}&(0c2kUs13H(F{)p zestzoHQ4`(I$u&H_L#2>axqj?V?0Gx1aBqk7zA!9 zk_34Fs>w;Nfw{~3t3$vKsM~rabh>(+dc{d?x5{21 z*pJzTxp}H_pyTL(n6+ z79yP#UxB_^QOU_%4MXpywA3Pl-`(hvY8hkpwSRe+UP`b*-ZZ&a`Lq8_$f1+qtBm6? zJm*aoCn>8AjrLetCiCW&F{c3uyoC>~zgj5?2Ujl0j!Ak%m)#uX>7aS#lyVqm%T_~a zy3l)7!#MGVZq>6?8kp&%hH&CJpWBA&qi;Spkj(&QX;>UgnANUZUFo*GUDCS{plOG> zB!#LQJEJLd^b1}_cZzXJm~!{2)9u|kW#WXs$Q6uqy6ZIM%Gd?z^YMD^AYeyz9Dgev+7pEx?@ZrFXB&i-|}>cdi@=#B(&#?=fGU-iug`B|1D0wr9sv`hfQ7k^acB* z!Zy}|s{ZyH<7SQuYomVp_K}TJR)XGqiSJNqRiyK{Cl1e5E-!<5$=Z`t%#Hj5xYRQe zc0cf0eOC5!j*jl8>T>ncLd(|%XBm>a#m)Kmi0rf@Yta|=6MdoLjeJXP-_GQ|_~Z&F zm^f;5HGtdclERb&xatr4BGZHA$X^l4vK2J!(}RR=0ta8|bbj@89Al%$Pr{5+h?_Xw z^f1ihXTX-*w=BGG07K|i3Txq#paTadOUP4&6%GYH+wXc8AP{OI+kZ-Q&`4Jf(63IKg znQhW17@x8{8&+-P3wn6@-d!RmSK>PvH&ByKv~m!-<#ZZ5N|5SQUVdA-`4w^7V5;q< zpZix^SBHF53qx-4=e8ql@4?5V|2%ozp``XlVZo~8$cXS44Bm5c`%yhs(4)%&48UsL zEJ-wnzL&t{w>0&7wX)w(h+P1@_%!E#;hDCA(dSq;2HF1pZ?yh|M`AF`HHJ(qFI3ZOp<%Sr|El@ zW6t`;y>X9rKz{dQhmosNV!_|Z0`yKk2e86OBt@J5KHqeEU56(L&B@%yJy9FK`M%vB zKhnmte$4pwkA&#&nI#`b-y?G>L&eM`h6i5crvz4NV7VqMyFLWn=Z^; zEK2JOv5t}SQ7HWXJX{F0xlQlvfCu_~DiKjnh=M$-+=(pABENz`_fh)kI71BZZN&)o zKT6N`Yvr96P}V%t)I_t`G<4qm>I0S&+gE)r8*hxcz;6NOPY)h)foE_K`QxKE7T@K2 z_j2kmLqk-VX~d^#pqv}nF$Wg#ew#d|7-2+GPy>Z$L~6b6NN*ldI5%m%f&d}$^H=?``I483!;w)*N07f>O?M%(sTmtEZPeJ7Pk=mC(K-7@{d~Y>c z8R1aNa4`p`+*tpF5}#8SFV<)l15tc28I65TXC3*GwjZ6r;xY@lFhlGm|REKR>I3pDnIIB{QcJIT<9p$es#HP<*VE7kUVUO{fCh zR2!IarBw>1n#DkZ(@*iMFiN+ASB4!ju4TfddOt@2Cs z9^E6bA$2!i=h3~~1xfR!ApG|6Yd6Xb`L`FJY|4eG+Q^1@-#%= ztC`V70KPjx*WRy>u&r^y+X(^VfBdxBtFuBkfNAm_u=@q-dJ_7quW?Im{WuB?>s+PA zO{dnt%fb9GTjyGgTxp_f2f{e(B-e?P@i;UXcl-f?Lz{vBev`qj>dE$t#z(#6 zPl}kXv{TVQIV15Pb246Vz@U_A&3d3^x@`3@Z1L)k*27o5=6kctF{LPYa3~(LkJbg4 zP&;maZ__Qna;rSmv-j8Wr&&cgl2-ks_`|mPiFz9===_9FU&8)R%_$Rvj_8V)Du%7NOUIl?Vgd2;DX;hoedO}9{2w^j;!=?!!(r*~`E!k2wVHd+&E12pY~S zA^~LtS>P8w7Ihid&AFatwMSkzh_Ej9{1E#_g|NnSa;?5--iJ> z8+=&6u099f`7GkgX3BA1PqWSV6ENqq)DCg-ooRGp6aESb?(`o<+3^UFMqy360}#ZH3(q+gYt?O_iQU>eJt<*!TGWa zCYK2-wDxkH=)MCc+1pJ+kiG#Kdg15}r$M3328p(S?fSKVL!?&Jza6quSD=C@Uuq$% zLDs@MA2EjRL|ImDyqRuZooBhfp;*78*PJVYE}{UxWZ6w`AgJ$MUm6)$c6jaS02I|~ zr5nT6?(yQ*{nu|H25xg1{-!#K4p?U>H{t`xx-$b|m)Ym* zKop)CI8L15A3mQGYx{q6eRWt>Ti3lJN=c_Qhg3@GkOl$iP?S~#>FyK(IkZSgOC#M~ zDguH?O2a|A;m~#9x3>4b-+R5^_j?}xz~^yu_St)_x#k>mj4>hh)=w7?)prdL4Rlq{ zC=s+}GoTZ+nv;`!|0{6PO9oA?o~_b10%pa6YE)D{Dv$lXw1C;5(;I-SWH#3xLUT^A zOs8K7wp-G-7}n%8F|S-pvv;irsO?6C)}z>YrfG==m-YCpz_Ntc-n9IYU*hekSiX{A zSk@W-pq z4Mmb$Y*@y^=_*hR-37Jj%RYEPWvQlbB^j~M5o7T%Wya~ziuC-rhi0*%G=hYK?km4li*){)su$D{sxM~v&X^TMv`oHo##n?JB(j8$~5s>X0 zh7HDd`63=(q49`Z=LS|Yy2GBs4_^a&yx+*J84#G8B^bDtYvdn)*TJ6vmMz*=m9d;I zpH)v(C*~{VC+cTB*AoF3WZJs?E14M}r|@Tp70}+L1E$f%TUW2$7=HY~d3EsX_hi9R z8Io$V@NMwEY#Z@^0wH}b7>#T}Kr`Lycd45#jvWtQWjiAn7kkk~&`2cXL@J@AcQj^# z&kM#msV=W}StHZUaLvG6!W#B_-umhZw&Zn?Sa_+6F^cc$T7<4t=4JpVm{3(B$Ncg@ zbYmE$az<_nb>2oWX5kmCh0OM;Wr=(inizg22Xe7Abq|hs^)G?#kryM2_vAV`6#CohI<+EjK=_>)W-HAsmr}h44vRicJ zD&4M=8;?ROf?i@Z!9bffq>%USvb}^)OP#@8LYX7+XH18y=Z~r|pzu}rg z4-LiuD=p}A;uRvYFPX%G0c9t>*7RgkN>qU!-G3G)ZgeVB7X+4<2wD?j&|&WBR{~72 zkDV9#;pw~a5B(OHJJ-Q!*R3ikdnJ2{;2xpCa!9*d&mb|nQUX^sj0@IDD8+SKb6*(N z53eg7U(L7W?Egib$|Z}=lAY^+mlMrhU(9oE4k-O=(L~Ti2|sHDE2AkSf|gfFPUyfP zd@M7_*#dv99`J37)G=r^@dxokreRIkbeQ6R55BU1&zbwcLB<0{Eu~K0sKL#`Q{_YO zMvm>Z!YX#ioQ^QQX;(Y`SY4B|mx~BZ?F^fQe9lnFTvT8Cn>ybtous;pKm|672ZN_H znc1oqS;Al301z>yq`_{;2-AS3*SLRcvRS5*OvE_Ks(@m^F$h!$4nU>}TObuCSKedhy=uoM9(G zRUGb+n+m2-b4Gk=HQl`eyGRjsYC5XjziuCWrh$;)>a0U*mkb>$N`$QU=yJTFIrTj% zE^zELTvemM$SfIc8HTmc?(H|TwZy!9OIzr2`IV{CPJhqAnek+RjM{dBN2guQE9%%f z{@!OAr`cq_`FL841NU{+s(|Y7@+~K}H4nW*1Y=bcdIb??nrbjN>Zw87o(vHt=bblI z#qbLER#K@r(_ChPhRf}(}jU-c-ZI@p# zFEwnogoRR_Y%5)x?Qz%;MUduuAJm}{O{`(B!G>23%?XY6LJD+b$pnK!&6dH`_l+zt z&1NVf?dNm)KocEz;(Ox>pFfcj^E2uf9*OjC$~)JRAzhlq{Q&YIej(E5rY~W?`32#D z+QLq-WvaofERfs86e$QG*)KL3b0&L$srJm)w41MJ57FYQsuyoPFb+9RsB?qK?) z+G4P5W}bR$0tD^e<1GVa&C^4H=ye}h3s6A&XkNN(V;rLuCS(fbs&vkW9xEQfMX?AD ziil?#Qa$Ggi0?R{`VtDdIn_>c&ZytTshxWC7)dhqTcW?VvsNO^{n?<>k8wpC!bK6Y8KT**N;)cVvMsSnPIJA|02s+}s27%T{w(9QuS%daMZs!9~FNt(W| zlV?I;+Y*Z*vIx&6-pRRi_)2Ib_O3#1kQa`tknrmPM&oPsSTT(Q=@-!zveeRIx_m>g zUuuh77u@!Vg>%ovQytK-HmrNb0HRV^w+owI*lNQ~o%Yc=BhV?RRchB#&Q!q8tn-cG z^N9JmJs5Q$#^rMkkmrpu;c``c#$}~em+O0MqIWZLvo40PXD#H^M-hrJ-`Kli&)YFY zHRf%*sriwNH@#7!Dy?odu0b~08ug%I*$WsJi__fFh}yE-cY+f1qPQuiNs&P^B<(*?IM0}T7{UT7Axy4mAioV2W`4KHyX(WesX4J zJklV^{84HEWppP1XLObOU~`Sp7ssMXOupt|kb0}b$w|A&G-B?Yi~V6{W)i=WPP=Mo zdsL|8(aC_A;E$Png>xqV!35P#QZhN}Y4fZIksRYev_h)tqNjNVQVgSY^D)UR(3BCg zv&~vTNHXx^;a#^JjWEk+ApUN)(Ltu6~fmYhX&A6+kA;T*(}5^{Em z{X4mQeduDUtIIZndnt$*$)8a6qs4!vN|4IGB1~kHrlD2gCLX07?Z1&TyF*9q@i7a^A9utngX!NcpZ`+^-^>GFZLXOi#sQb80^ z=C9!VUQ1I=Wps`4Hl5RIT03c=nwhzgk3sA@cw>Wg*7m%h^9fj%^A-T&_ravPY!DuI zp~^ZRDm9YWUci7g9u!z)WF1qr*M`$Kyd@v|;W{Wl?`BnNZorl3HH>S?V*(a^ALG$$@| znieVAM&IHt2}*;@ub&cxlQMMY?i-3GSNs{c9%^&gJI$?E@jvOl1j_9R6%L1ES$%_6 z?zl+RYX6EVg}hSo=x@C^oM^OxY}B@5BFz5Tu?j@;48sbcH_KTquD>dg-eR zoX65+l1;VZp~o1<#=0i%ylo>UB6r}1K&6>c6C|JQubME{PM--` zd>=fz?j3w@6_7P+R@JK7Zt1qWZObFQ-Zi%%he%n;)`?W^D{wnC&~surT!d-MhtsdOE1~t&XfygE#zPDw9dz3_GJ@djb|1 zHlJhD(R|xS*z&vLP+wMuRMq%qWtmuy(+V{2QUyVBm~dT*(SHsLgq@Q?qo$!J*RU|J z&e(0>^fD26YDS^z6K6K`1^@G^e#0AGGHt2O-C8$FZ*%+C#ok@4PKvMQxf&^4ykSsN zJdXZ4Z|IkjU!}ps%mIKndsJhN^v1=8Ck}(Q{Gd(O>=t-vH$JHZra=BiAwG~|)+P6x z!W{H6iZ^F&z;ggF9`d~pv1L7W(Lp%-en@DfDCF<6sao{WIBL>1Q>(GMwtBl{tvV7_ z>M7py{2nSfh^&wKe*rP{{&P?~4dabtPVko;mrJdfB%cW09GU4qe`4r^-Yx!7$7tQ1 zzmxp%e_slqCJg`CRp;&j4qXkB|Hcow1DW{AB=m{R3(iJz;g;BG8IY^|3r)$^F_;GJs3;hJIjQ#uSUW0E0q5aFs(}!0?Y5T?1#aBZ;rfl-wEYpf!-nzb8j?S zrvp4Q-V&rwMR4wAz{42z^#xGSd(`1%Uz!)F%_jQVj6u0%2JoNXNY2lKM`mH1HM1qi z@w1mp?9ySOiMZxF*B><@D8u1DNfqdwFRg0NY9&X}Z;Mp<1yFpI{@>qM;R7yIB|?u5 za}Ovt?m?{k#QPoTR%FF74mzO`ePHU@C zJtNWo3m(!AhMv8M%4Ka1Yz|1FI=;WZ_XYb5B;@{D+E=; zC=CH-LH2c~%*?^tg2M8#?B5NX{{ldgXywqx`Utb*ocrR7dBU1vFRkJXO4JlmBt95R zg*R{@L{4~kj0f_}RfaiyO1}JSkz581WBESK4z@qt;|zu^1T_E#ldpT3+uY0CZ+m3t zWWhT61Ie67*a!8r|MlblfB>_l+=f(+f^%*g@M4yLPcT3Dpn#|4&w~mg`c>LN9FEBZiAa*edwL?yqyczH;V7sJ)k<=(q5)ll}j|9_g7t z3Fk{{bXpBZwEFq651Y*Ej#oX-+>CLYvm!+FhD4p80(~E;U@h*rmze3Z%wNEztpYK3 z31kx9Fa0y@160|wj8JK_@3RgFca{dA@W%Io7nu>oT`=AiyxB{-*V z?E{RxCzF2pq?*JvX&B?MKfktrCIo|OeNFJ?oMRkqX0Nj%H2M+L6(tjRk)Lw3tJwg6 z%^~cM&Y@f391^1bcV_Iy^}exB1YOZNjqMo%xx`}BqPIeco#e#JcuT4Jq_uCc!`=p#mjkw@U@Vi|R*F#tJ23HfWBo0`N=-SX6!1@1t#(+iBFjltx0Yl#?zbD({1hlKTql|a;*DN`n^-*%k^$NqyK zm_N|w4BU{X?9FNZ4bjcgbWrSpu~UA^6ijbJugIlpzo&F1LJiy`f_}Cm!Dd{v8|(q@kcwz?b5>#3-r3N>`g%k|_}i>v2q$dd2ROBsFcs+L_>Zk`-4> zNJ#h5`K`D-`WXmBo%&$@{1PN>$%acnn~F~?^N^fV_jmW>KO0>)!!S>I>P3u-^+Xx~ zj;iA;^5W_%Z2q~A!0*K}5LJZanQoqwY<1v&A9=1gNK6ipI6nL+!X&nyJ;MGoY#?p? zag+A`81^o#dm9Ynj)q+TC8_Qt<|rvLf#d05h6w!Z+Krp}IrGmbX%6N8wV*(u#5K2< z)B12LulNyb+gb4%Z8j@E??p$I|ocgCe1*6F#`%lfv0lb!mW`1 zdD*Hb+gz<<)WcDMY+dPxEY!f!=B14^+twPS!Bb8ZsUEuhp6OjP_2AEo(#o~X7|6wQ z^TPhjD}{zp;S3hrX9Vl=-i znjx{*K;bGc1CPmPeiYtsu9?UTM|ZFc2MxP`QJfWW5CE22)x-0k-o>P;#4QDTG1;?o zQ{A<0k;EbDL-7+*k6!-K_rSNj{er8omOIy`)wjh4z(5dSid!sEoO;^SkK%jbu|kLW z&LZ}5cW0h|70S-PBwRSpdXQlS(zl&)qRYi=kfG_XYLTg(_uxfxFYd~->tz|iAtc`D zLU6*Ik)30<3pOX?-VY0eV|4{YIg3=>)6>vNh?!0U7Dt~m{7_hrW z_N{K|<@g1=Y)<@qp!bcX`ji>3CJZwQj>>lyAwbH#crWT!VpAfkNPgl~8m?~=NI^fOUYj6zZ#1Vuk+XedB z0ozmG_FON1X$!g!m0!v9oI}6I{lw5<$8+`=Yn=l|6lH}L^rw;w&A=$!GWpTE!o%i* zPqpu)d4LPB!yf2Wk8F7vi8WYHn^j?Cl1BcHHzehKw8P~;MApY5 zYJM>yxx3*7@<$j6f-Nye@`_hS^3>=W`!FbsrHNkW>qQ-2>+WwMe_WT6F~$5JE`To> zkRkv++?I22qEp_lWO9C8zM_D!cP+l?Q1(O@F@5S~G+et~KYOZB+Knt8`8L>Bm8wf_ z|JB|gVIRwb7Iu0oxYTH^X0vWS@=#w^tx>wE#0XaOA70wn#cZgzhL6nJ1Zbgt55yw2 zu4AI!%&thLc>?ewVlEEnaZ(MPu<zG^7z!%xRyZY$Y78 zR}~$0StjPk%Gt)&Zqxp(u`H*osXroE`-F}?CSNUo<^Y&OhmPUI11^^%7V|H5aa?Jj z9GV1nEZXZ+a8|0dQmO{^;P3Ht(u7ls%>WNp!a35wjU`i*`*sQ#n#od_)<#OC&C z3&~{L^Z{9DB(cXg$sC zAclun=4t5?%S`9xsmICC@3b_X3;JC`MwCk0BTo)DIq&rJefRK|qE4PwR>eb1 zHcAlvVF0_(!!G+^YmhMHz;>tei~NH*_7 z&kJM68H2>o!}V|Uz1ffa)~hWWK@w>pz$W%3Xq@xD@j^#fsQTMvnt_`QNR!uq`|uCzy-wALuUFUk|RUA};})K7(TxI^zJl zpmHuXqbN6KYH&&xm~D`gUhNl7_IGi;~LbYjEi3FC-G3Jg=0b9Z)C zWxQ!B5ZBIB{b=rBF+$9s6pqMmX`Es(r;GPcR z%mGoK+}X$uze_6_I6`b*B2;wNc<3)H1geSysAFBQD%x^*s#$*gW>^)ef6#02s)#fmc4J-9G}TPK`&IhEg27{nXRjs0+ATJb zVu&g@{*9Ipd$8|2pNQf0WIr9Q3&k3*Tb3QBSdH{*abRUv(`k1@MYd+8;n<>-KWcq7 zDoVA%11C+T9EA~6P|NuAVyXxcnqVRKJE=>|LBUro5BfN|Hxlj_`4C=t)UEi_`m(0O zhP;H7Wc+ja3&md6wu>omI&++rD94k45vUK${6Pyab1yHJ`ur{`Bko6>8PI~UP}enO zYb~F(<1!Kbq8nwBn`^XqXdj_Z$2_OhO_MJ<_fV$73NAVg@cx>c?Mx-X>x<)WE7hmO<7LjTr-!7E@P_e=LD9o zq=r7kLJqK&E%~j+Y$~Y|L4$a0ToKEsM;fUm(uk*XMm~PWgEaLqf<*PW$R7!r-Oa&+ zhl1&MX&0CC#qXeGC=}nOv2$VL969M+ar=7AcN9{ZZk)+|habZ_n$6@~w7}@Ipt#0V zs$?wna-E=E-LD(TH!ytE+Mg>|{F-@dewHs#&#t-evm83zV5B%H*B(WgH>LPW2=JDL z$iI>h0=6z?CXdhxo1+h1u42HOm#;=*=h{!%MCIIr)%Gp1>T8*bLsc&Ut&nG&$+zvj z3y`=aGDh96sF5o2O1G3)MKS{(-+k_BMHw-?m9yIsVivOAtLg9AzE;YiyB`S`cDkev zA#6aODC|;veAKrWoLgLdUxZV#6@^7wFbliNuTPG(9o1Eo3VSer2#ZeO31 zr7%e7OA{(*S7Y*}1P2a9FHuPP0)m5Gq^c`LkO=WoZR!pIXYL70VAb}%*n;W=pa(@C zYY-l^^cv?VPrs?FYTaso1K>v@q=AOm52LbcrZP9ZPgnD7uxSXxfY&5qa_X4E&Y9~X zKjexsM;B9)A(Dt+YPn`w1tSYJ7g{JzNJosTQNK8!MxdeWGWiaMtOPW3M&x{lCZWFD z*fWeuNb#_AZxjUp{{w95kDWGoYVB&F?QX0ebL_f9LjcyzcbH9x{3U21I1zO5s&7!d zvl4#XLaJz?vvd5IGAES;iQE_SW1AyC5-nF|#!$-}`kvr>^lSxBbBpMS8!uL(%6@8a zaLR>h->iHu2ds}#+$|xjf$)my8bMW#E_#wj>!UD|;$pO`CHuD%5`^wd{T zMBJ|*X}@rLOlTy3y~peIneXca&uPriEq@Yq}> zk`D`RM{&V@(KvL;3)052!%XN5@M9Qd<^^Dpmr!IlU0G+dpm|DF{VM~rX~I`9C%C4w z)}n^JyIwfN$3k+-`y+;cG9qmZBYQUDKEN;wlK{PzAwT=;j>fvXu8Hfq6+U zL{VYER!Xyme|;jy_}ouBxji@kBFOw@$eR%=_MVp&nKEhT?(kf4RTg+q8>w+od_WjTMZzEv^F)!(31~wkl2W%r4~Bl}4Y;0FKtttI92W%nkPyFS8JNndnapO1KTe zRHHr&jn=jF&wV(XS*$e5D|@>y3zI%=*Gw0)7|LD|fX@cj(C;XPflFyys_-Ya&2ctb z0qxRiH6#179@V|bcHyEm%yc4OGUV-?gnxwBXBh?WuEWsRn~GJM?~JNV9st@DTi-*r z=vxQojXs--Yke(evXie!3?ZTuXK@HEWBZMVeFdiCkS^4<=qBgpGNF0LSik*<{z!iFXpSso;aS^;*Z>5vuEb=a$I>ueW z?=^A<$Y$aCQnQuBo8h6yhpF7@Jz_-shV_LUbD(cXl1GX(H4V&*JB~ zHM-;72;jiYhg`yH8=uUYcZ<-t!I`@q0CY1v7O>m)Q@+&v;&st`TY>fVpD6rg1O`+q zE2LOf>{Gc@_1?kpz3h$OvGf+?ES-y(6e~h}EJac>RI?omfW_tb5 zsijBOZ6|J3SjWeuzv6Dz)(mMOWm6mtIu^GjRfX?9ePDuhm(JJ3gZ!x;V9Ux;Cg)m< zRfZ0Ts;s&0&8hQH82{~~nfDz=gXxsaEfg4-vim8^E6-Oj1W z*iWoTOe-o=tsSyZ(`;KzWR7A#oOI%t45)U!a*ePQE9*_4rM%r&=&cT{9##4aY*}+r ztS`;;>sh1=wT3?8V-$cve9onfRQv*2x_AE%B$%G=FLuYC@N1oDNm{_}*j1nBH)RG? zA+ElDx_{i~Kd?)4BeYLLt2;TZKBB~WGw$Fb%DxnSGU(fdnk47Fh~)OjM}nr)KQ8&} zcTBav?(?(gM`h+gzGJu=$7)U|p89SFaFvP??<8#HB7 zH-!q`J6}F*enV$lW8$@3xV10OXi>>U1W4M{MwIh*s*!?kc&+|k%m4Gj{qGP1Vo6^2 zU2`va*@@A_5} z33a4;68z^5D`%Ah%*6Lq(4`?xuw(zQ& zEz#XuAN8742}WM|`rYvW|L@)F4+A&N`T9=J9|U&)oxg8-NA-?Y$i7`-)3cj$HNR_s zzgFhMnHPHhTuguc{(D6jnNn1VB?5@}#e<0ZU>z{oj6gaPXkVQ7enWQHn{oaNWc~eq zhE}z2-LO$(0XT1>24mj`2};Fe7gCj1&&>t`E93f*%v7aKFJqhx9hoqA~aSiMiujnAB-*M}75h89c4ita}%;3+~W`p~oB7{`dmW>iQi=x~KhX zbRw>7*A{q$A9fIeomg`537MmR@qwR79lB{bb<@N@E+zPV(+53QE_{IT$kRgPC2&m{ zx=2p1y%D@KU4HN|1(3lL@0q-TdA@_0@TJS z+`O)MDjII^8Wrf&j{BVAx%P1hpwv8IG7&ET zr|*h+?*ArgL%kb_p_(Y>RRghg#w`nrzk4jFzH|kyUlVpcjT6v@lr82YX)RQyii6lPt;KkO!yE@ z01#jLjKi(WpUYoM`m-=~-u=LPh2zD5JdsBpm zAd;O z`5=)1R5mBs8RO`-DXfq=TLvYyJJd@5$o%-v)n%jdYSnpS0%h&3fOZW7j}K@H-13}> zf#^7Yy4@9YU~3x0z)-g-@K((L%C)5}Hu!W3{HMb+J`AIlA@w=oM+}npDyh@nnR}PN z(Kue*!6~Ny|Hg~DavS$tj&fWq;J=HwNhdyaA506R(Y*xit;zgzAS-b`roQd;_L{`n zm~G?cZ#mNTOf#{fX1khfJ26XB1coc3Ha+^dGP<41^i$=%@U zl9|X`L5N$lEH=QL@)?*ccXbaepZ9=(ngqm4NyW7@6Or41e5=Zj#m>x2BqEQo$o$VI z3;o`2hW=hPub2AA5}C{g(10-;%vMZw{B;H@U{zJ`j7~q-1d{Z} z-yNbr^ym+#5-d4%fR--MY}^dI%)j=$-z?e2OfDlkU`~yQ!Pkfx?a^l6*!FXvEfGj1 z%d7x;z$=-H(IrJjt-d$r?c+^=pkfJH?vTZOo{w5)_ooXM;8$B=G4xiPbIV-2^`%7b z9C9gDPfeoN#3k$0h<4H)_aDa@INI7;!CrW2M-Pl1=L}ynoJ(FA1Z07!XF%~LVb^P6 zR4|lA{vm|pH99t{diBLQ;Gqr}zuN+;p|a0Akf418*qMG7E`RNf4rZ&+Ktk_vtOUkL zq0bQA!TT4Ekpjof`J-dxd@t~c6fLd^ZKNnHz!N_Vbb! zvqkym(QYCc;x>x2>22RpYT$LZ1Z-HtIeb9_&J2XMtypRKLs6+vO5wvPC$&cZ`o>W# z&Gp}rq45FjMqWg=fxdyosf9yz<%>U#U`QG80d7R5)*tzhm_p36YTW({u8nqycxE82 z_cpQD4o#*^0;irC7^e5OD-h-jgQZELNnsghjRF8{Q1$b(Jx$u5EU#27&ki@Oz$jVI z#^_SBopp*Qu2nNd-S1X+6HdvoOwXmv={ZBda zpA>F~&{%}#5SIv3l3sPgA?RG_DqU`Sh*-%-667_ZD7n}-l7!FARyv?w&=MqygDlKe zGM^g8V4m+LwDPTZDWqoMBNjM*d@^JO9Bue?wtfPsps-%M&=22<>>t|3)h)idc5#m= z+}3RvOaIV!`9G#_i?GrA6lN`R8}oRO>%~1TtBD|3dpgJAu09c_;)e~C$Lap6KoaFg z4fiUttJ&YBhEYJAW{JB#UYXaM+6qcgH}eh9NVd8=)0Zk*d+0H^z%iH&Mtlw42Q%7I z+k&=^Hu8#p3c6Tjo?0{)<4#|$RR@p$D|pHhe}MO;Nul{;@|j$dZSlr%qz$`$-M`}K zC1#V+<>_cIp**z)wjm#k+8;=(K7o)ab_1=Bx&^n#G+-AB+jr?zyP_t`?mIK_omtfR zYSg@gK20FrXR5f?rz!1^#i<(vc1=|%>v*Il!Fv&x0!Xe7P+qgVm0qE1ggG9-#5Nf^ zv5kqRs-QFG-JHiGdIH_=H>a04Yo`Vu{Ng3|HLvL9#ux4+Ts#MQh0$UD9l4_x!|uJl6XzG&vDYTQ(w zHxRPwPk!P1y*DSsv`Gf+s+n)(g&BJ0Qoc7BIK*;zy=m zp65LUgF$-%)S_-x2dsxeZ+3Wxqd*@hcpP9CBB?~=A#_RL>6m9O-sUbykM9)G{Z|p9 z&U7{OSZ>gC+(zljM;9<>T00=W9yqZl0@28OS}{)wxHN$i8K5Yb^`+SWpKY@&&py%Z z`vDP{m9!+tN}~A_rnl)JvpnA@u+LdX0IK)6GH@y$DkykbX_g4=@5dE&lv^HprUC=Q zA*zt5pN~OAO_3sL^$Ls(G?OTnw(GHDL?x%wl>V9L-yt>ztS+ypnuN5r^OL<^V0Tzl zwg>E3lKyiTLdGRP_|XY58}_%RMcqV?dD20m$-&^?54H}=t}l*CmHuM=1x$}kt%c$>bd|Q>m*?Coe0>J&Zi>It=I;J^-Z!?(r2|B^IT%8f4niK9d5L#v3N0m z^gGJ*`#s)48vXlZ^y-Hf`mSfXA8`1pu}b{zVTpPl+kA5d?yT(!0C z5$}JFE*+MbfmGkXhMtPt3He#_1{le)1hi3Wu~ZTf2VFskLLKhCMxBI=$ptr{De#ujTB;t%;72DMJyve zYUL#%=R1=Vd0(0l?J~^LdGCB$I^XN7O`a84GnTL+SzQ{IxfBQ%wfckh=x+uh1Hu}= z%$F!X;EK`G`m0VQMS4cS=)C3sXk%aKQo0LRWq#>hOfX8jQ;qlE9i!2PPlMI#t6LtK zFra@b$apvH&$q}a4f0Oi_Gs$|UPQA!-d?B8kMwB39`6k~(gQo>4)?cKg<>ZNy991_ z-)>R~e7{L?w++x}zW~0Lh#9~y%?2{%%nmok&A^OQGhko919^29kccQASU$A#A~KHp z4;KJPzsHROaG!rNy}Qs`0I z$vv>va>0ykmPnXxS#xhliO)UQ{(Bev7PSKBWApzhxW>(>h5$R^KLyutgt8tt_W58! z%L^zAaT<^#Pt1@BelQZ9(=_~Gw|$TRa}&|O7*F#J_KOK<8h%?pFd3B%aq*XcL)Zf?F0u=ViAb z@aK4L7NC45PG~K38vwBjTbBi|9+p34;Uxq z{@8a;QmIwB`3C#~tZ{Wv3 zPdyBImdDdv_)NQuCdR0We1lkfW-%%P_dsS)3K3mV7eLs{S3BcbIPN-WV{?ZVzD}^t z@Igt{O+TQ^OLqKd8-Yj*=JEdMs?LS(LwK6*cj56HGyoLAi31^wm@5z)_7=|zcrQ(q z%;2izc~pbpAUc1c3EVGAbW3P&Fa5G9!1xl>=@V>jC_32ebl;BKNFN~7KI`F9Ibk~k zHa+DR0d-N=&@RBpZ(&}boN)p!q?+w^k1;IOn|o=4DTkFG|4XGj3)T~jD;81y-K#C% zb;dmSnu+}@@?5|d&)u85MjNOP-zE6z00jleS?+fb0L2x52KB&^NGKhQA5ZW)eu=~0 z);ionc27o9eBrH(2qx-X8ph(9;uU3)6~*iDIUF~WR-bVL~q4lfmKmSa{(o>xT!GzE>(&PAl1Y^!y=U*)i<=e~KeIM%??%T$;6 zPb?iG)o1wz}(?&q5LegPH-snDKHBNE)1?3EPw7i*b+YV${v)`_DfRmh95t_*6vO1=u;`G_4HhiJGi1I3meUY z)S9EvZ+)}&UsBZ#S@8(0hh_@<9l%R*&GM6zX;84Q!FYH@h54`aEorTZEWhdL1+D?u9n$pzcUW7pJ=P|w3nShn zph%eR_yK|iKR`BD%31@gE5cE?=;!PjR&!mmUZ%MH5MCZW4DEOAc>q*xS^COob+n1VQHZ$a;0wmTKRDES+nWlhu$;b<>b!^0itiBe2o69q;0i!<`10-W{W4rH zZYVYwjZledwX5yITK}jhuM(M)UCkY`AoCbeLc59?`=|)3pO0+d&F_?x@b5&t8VtFb zhy;W^azbvP*K3zA6$Y~K-h`HYW-wQ|?>%(nFSW??iT{wH^>NF335XRD$44lG653Pl zzp`@MTRxJ>e?I?P(DUcbuT&+%l!X#=`pPFav8tFruE@DMqMN1rwbj%{UQ8HiKuDK) z4*AT0nQE3pbCSb;AkF*UD~PZbtosSO=#FE@&D=aYSRcJjME7HKxyD5Ge59ptT;z|8 z*PI3%ih7+1){0a_U;z%jDKUU-NcHhZ*hjb*62n}LD<3aFNoz>~YMS&T9F97%~w`oGE(s@in z`Q?`p@-rxqKu<W1GO)<)laLjGrF z$q2m?`O?Z>PMKHSdz|wR1r{#VhzUW$i_5={dQ3)0hjsz1k8lOB4FD^&SaJfO_UxW8 ze#J&=@k{BZ)|Je#qATGFHrCmcyO`6Z(h1}h#TMBsZiW8Dp|1wZZJ29}JLbB1o+>BP z!HSbHd!X+}R%dJ&vXpEaD3kyk(BQoK+@}I$)Hhn4KtK-O%KJq19c>*(0F{M>g$_;c z|9DFZZQOI|U>Xu#I+zXmiQt#%KBPri{B@}*AaO}<;z%F-_UL-|DX0R+OE2th&ZB;g zo#9Z)Nsrxhfg85|!^>#Py`&g(AizVSVY)W#Gy%Yz_e+$EBF` zb0)cS0`;d4?9nnhGiGm8Nd6IF=>4T3vEPZK20Z-<8;1tZko?~2$ z$o~E*xAG|r8Es_bxQxTE?+4)8B#=FgFHI&~TeV*B7JeB_<61_zO3*hAcOS*0v7sXm ziQ3+;w?)||rNWwN5>#InSC3Sj>@cVP0M}VSFs7mBDsw={o8K*lnh7w<1_9*IJc+kF zc{rRUdWR{}^o6dT3f`PEa2@{&A$&DuprD-irdSsDWjgFyx(OZrxRdM;FCggIe18tC zV#lLOT23t+Gg;SvmArZ9BU=$4u%}b>sz1&F*mW1c0sfpf3PZ&~d6FD} zC(=djGOXeH-SY6)BuH@DjL@dPMoAt0u|fQ%edjm~jgRu@5)fo{7zgcw`5M*|$Iu9MJ(uw_KQdcTS7bn> zP+>3)3|DBjZQE?84ur>zhjUf(6D`S0_wVCqTtfa_l>lPTrtt>@1aR$Pp=*o`=!WGV| zgptuow9&g2tDL)&^924zzVJwjdzoaLk$Z#@QJ(fbx`cFdH9vZ}>o4R!yh}NT?2CvC_=nww9Rp= z(q>JUPyMJn&K@5rKgCc2&oe0BA-jGEij}I>MtfN-Ctzt^G;Mwkj;Mk}0_NT49Agke zmZAxy;7Op)r&K0*A-0Qo#g?Y**RsTAy)ZC@+T|gdjch z&@2&L!yN^WVsl4*A+s5DCY$Q)ESNAoD!{f2+T!mwQkjJ%A8Fw__s;z zE`!-8yzD+H=R2B9^t+_O2M+haK#uAv;j+G?fF=!Y5An;;_mG#sJtF)YNcl1qy=?2o@1lXLFnJkh6pE+Is zQ?HY|4>wvjDtb1K!>$dyp*X52>G)LbfqrG$6EnB$`e*O>YcPz=Y#jhJb0l0Bt-U2Z z0%QZ$peLZECze4K@W_H7J2Ub=m?b=F{2^IgU2}48$NB{?h^E7Iq@0m$fnY9ZYm5g8 za$amCAK@l*(!eXZ@q0&r#1Q!1|nMoqW$8v!vu=c|j$Bo-Y*1T|hZ$%ONe%H{$N9_BHGg~5E@ z%R3j2mbwVmgk0BdhD@VOmJR{UlNAz zg|p&XYOYyMVQFONezAOXU+1kWwjZR*t7*!1Hr2Ff3^_inRhZgit=4jXu15f|9ys&M zqnXr+&^AwZ4T#Qd>>jLKkb_vX`JsOk*Cw6)VNh4V9SzO0N3hfN^2Sh@xNI3WXk@y| zNsU}1Y#`W$Y?6u+mc&~(P<(D*+%uR#+F?rP7Un2=Slvbu(kx-0;x^;46sx6LemR1W zdMlrO0rD-uv(z6Rt-$d(`Jueyx=FYYG<$}HF$cF>Zi1|yR@g4O;AWwRGi5T{C*lKG zg)b0j*L+WPo%n{mLqO=Fi?%>50kUNqYz%5hASw029F%N~NU2xt1zgG^-hfXnIB8v} zlJ`x~Uif4orkEf=oAAlpWp$}fFOM`yV~Nou9wE+aAsD^G7UT|?rS%L4h8OK9orj}9 zRM9V>eE$`vX4@~xr|+M19EW}j<-kA}#AG%G^Q8uX>*3F&e7fp8GTmww{N0g440;K{ zEpTmW0<@fB_;=ZdtmhGr=ru47U$lvZlcED4$1no_l;ZJIS>(4lJwZ)M!ew{bwU%-a zP9{klq3*OtnL}OSS=A=YS{@mAoOi|(=&+HTg`fKRb2BT_E&lN90gX<3=fek8mrm(t zDbvIl#AjRX{$Bf#-VH@39}7?K6j%QItU?8$wL+(`9`Ms2jy8JrMhQVE>i%8v6o-ym zOOWl;5&hOB0FB4CTo!0GPgU=V@&%07<&(Js{j6f+>8(oqOy-! zwnFwiNkS@n91)Vevq#23_RNfo$`+1!NXJfQ$nMDAd-Hpp?$77`-kSEH+|E>?oGK5eHTwlX z&&L|~OoI6`mG4fRv}BNJNM>tABPb+eTApy4Px?ms;#{maD{S>hT75j5jEY~@%bb=v znEv}{UVVx+@5qMFr}bYN)-%G^dpo{`c5pQ_j|9Ih;m|9#TU03rax}7N0hQWVWP0UD zc~(A$R?p7jw4kr^`$bn5zQ-$a=zy6B8u8Im#dSENZFGVO<&zjEQd3z1HblwYm?z8% zxK8HEjFjd;DLlO^hW*?ztmHuC>2EhiMIMNuTV1&8Nlp#QeIA!J^7PB3GI* zVE$41qtup&X!SDAmdvul-~sybY=^*k#)fx$c(P_YP-5D_w21AkC>lp_b)UuIrB$0n zQ?FVFvOT!lwg3x1q+}S;HM@cxg}J>XLF?~cb$>F4y$;Z@V}e=J2j7atgnD3h?&?-K7de5-!;%>U6>RS;Op9ib8Tgjv8QI2FZM5ytgM}t7 z_N;|~r*B2Aq)Iwv!v9PYa6nIH;1A=_c?(~C2wAn;2y7(@>=9>wSQhj5)DES=C`Owh znfT=abXWN|eY-AqXbFY6Dd8+gj2`$XgGe}3+nW>c`{_#uEQXwD3f~KUT$X$!Kwt&g zp1gBw@uX&95})47?2RIm(}?=?+5Bhuf$SNthZQE<(4JUr^t9`U(G_nD)6APN*@yQ{ zs9L0R9_JmkkyT#jL2d#+C1%`PASU{=S33(MW1vqNT@pi$rC_c9`y!|>H_8L%UDlue zsiD?6)wkEVg%T@do`b$I(H0z{o`r?-^j!k$x>N%BwktxloIewppMkJth<* zXdDH&G^FFG0>>}f(^y7Tpz3FpW>PvZ&*yUGW zLUbuKujf$aIIuD)P(oN${i5#w0;g{;kBllQ%eErf*P0m;9LHi8SU%Q3@}AKwqH8=W z_?qbG*WCAr7?kzJg5P<8wk7TON)na5m0T>O;(V|MLjpfLx~xGs^p1 zbL~gVZQlbE44EnKX;>IOViIddPJJ)s1NxVG$@7mIpLfCzC16)pn}3rnCtoJ0q^4G} zyKvCSJwA;PE`NDnyR>EjmIsP;(h;-ZFF+(}+0;go1!{fnCAaEnKyjZXF#tu)740Ax zm~Oal&_}-zjn7n670M*udGO;vFEU zxG_1FF@DztG};-Q9~M+792A0yShs2==fCRSB}YbiQdUOQO`&H*&zSmGfel%j#YIwY z<)e?SN{rd1&LwQyG~zq4ZbUiMN;1UjB{*{=IFyBy&%%feiM5auP7#cvzXB$p?Jonq%?%cT#>g|Dvv-?2Hzo7C--j zXA0$fx@j*q;xu7VS$9!m6OIqx%IH zm#I+NVXbZyiqZ9P|kx{@xpgA369S^RQT@p@IK}) zXfth}*LyX!?m=>Ae}bj6#`#k(cmo(YfHA%W+tkHYw4ZL0@LRlbLEOkh36sbz{9d|R z<~`j)9kQCJx^X8H4V-Z$DO7cci&M3>L@COppYfla(?aanM2=Nh2 z=Wg+rFpe3QDU9WQxT9mGyP3Yv0$_u2H{a_oqWQmSCxv05UE!+8>Vz!0#VynyR5h`w zJz-p7qzO^dLOKOZy9d5F9F;fr-=qJ@JccM!&XsDp@y zppo?q)IMM~%vw0Vmmog&`m%x;m-oP{1Z`kBCEmE*#VF>jP*HD+W5#(LH5FtOaSBce zY|~x~y7ZS`-~3-2*CwJlx<8TM>|Xn-r*ul`{Y+UJit@d8FS1wV+o8`1*5m@otWcdo zkgEJH1CB~l&JZCD=sOc|@yM3eaywB#A}70?&munDM?8I{Hx%0N%DmIy2sFAsNzSM; z3c9V4on>w-I@#m&o(zCVVt(6l>TfQ<;y$fjCeN~54Q$FWtYf&vKWeOffs!KzpCRW1 zZP4djX8CcY-_C^oA1cZ81qYxJw7+wbFyL$)%+I0BLR{NODiym>qXNI6wxI+qQX}^& zYt>!$*rAj+iHZ@rCC$k?zg6kOn5(i&q9dmwSc_-kN04@YZd_s*r&O*meaD=SQF3%& z7))!81Ktfb|F(rs98z&$6pIqigg5DNJeq}mMdP0_%@RRBPu@Rm*k4M3kJ{t&1i5^7 z%sLx$({X4QQMsuq=n>edR;1oSfWt}s+}X~MnG2&lX@ENRJ|o%#ejH;Zy9kNC4TyNu z9mP>)d2)9@%2D!uIJtC5P5f&QrBkU91fLbm`yg|0q*ikrDI- ze%Y$F9G!3Wq#)RrX>>wg+oOUibr=9d$QkCY;!-;dC~0Sm6OT&?^>_;=^6Cqeh2o zkLK#`MI4r_Sq0nhk%iE1m22Qa`C}imt!0Y|w2F8T)JxgYop|rH!rmT_CFv+dQ7>ug zXhLPWhU0J5j8OLQ(Niw;1oM06tP3O?B_N_{=%68{c{tFmauOxZCw-w+y0_w8JdU`+ z)@1xvR&vmO@LI~=@-bY|L%y5E#w^7ovfK2c09P3 zbl+Qyg^L1S$#KODp0-je+9_~~68U3WH3`3@|2eHp3mE1fo1==>a*gKL`|M_=H;_*I zUmt11!VVEbKe{6|lLa1dubbm{M_}vG@Wbr%A8m6E3?wVwVsyuKD<#%bz+4=7Iz47{ zid408N1SQM9Oa%pSSWBOye4)_dQDX-C}zX=pq^3v0ESt)BUB@2?{Hl!OM`u&MfvIT zSNh|M5r>)|Z7cTiGR$Pxsyqju8pbP^@hG52Vh^eE$g@S)J66Wo<;(9w;3s59^BNLM z?De%bgXU{?uj?J#G2A{YN`88ivh*1~^z9cWfTfuk<@5^aMOTUnlr}!pls5s!szXo$}t`u&u3`{`|akvi^JM7^}MKGMQ~g)M#k5((kY0qvxm}RzQ-CC!-j8}5uEDTg(ixu}% z$zm$vbBK3{e@cXkZf|3&Zi}y1#J)}5bn&nvb!;83Kpd8^EXG(PlCR_mZGmC*qo>a;*#dwA#Mv+~=KJ^x4||T#f)QKNJOG(` z&c5EMl;#y+2*9$y=8+&PBH21vfk1q>PqC4HoE*>%fes`uNgYQZ0SL1pDq@o^@jk3> z(S5Ub|2Gd+WuzVxk}i;wo>{qf`Fx!eIrWNi4nP@J79jsZG>_Kl4jbL=9QVEaAkCZh zF+0~yuc9RpLRirY*0YGS4oWtFW?9xmg_Z^PxJTc`5cBLR=E^ftV)#*qraM?C0Hq0C|^FXO?cQ%xKoTg<`T6ZDX~kOvK?g@$&c*S)&o^u{uif6 z`ts6l{s;sGe0gHi(O03q>;?g(!}CdRB}$D-@3RtyE$#;WdQLR7)ayp(dSlSO60Blj zt=@6=HAt;H;p~dJ^2vxv_-EFLOrsdivb-RUultO6^fb>VPtJl7zly?#4KK!zC0tyZ zzJ{4+4SPq&(H3u9%wMpL@rK%)tGtJK8^JZ{$3q?*`)D*=$gL43Bdf3_q+Wzs;FfmP#PH5?1}lJ3jNZ zqtvc_MCIzv_MB_6JXTG!b}rm^V=}P#14%$1Mnh{%O|n~_W%m_)|D3F!K-`rzeDeh# z9wi-M*)XLUXptj`fNi4?HfXz`?cSVqlxo52P8=mgXj7qA(EmYbF1S9T46{9&{O$k_Il~#gYbQGEi~?o2KW!hhLe8gHrDB zvR=b7xhs1`87NIqfC|Q*6cFjddOUNVN?;p2eUcN&>7ytkO-gr`P=89&2XzvF%W?gi_U56oi+{FS(6gcp8)5jNu~PgQlm(4zs?LpRoLuV74s5Rv#;gT+ zNJ^`Q^%*~T0>R9x)2pl++-IxU>=Qmy&4@F^Qj7B{rj6ab^vRrR>)CBi_~y3(s~HyqO511})egrfwrKl!g4UW1p{3vsFKKad zNpLKe9TCYM21L>%;Zv_1>ltZGN7k#j$o93Q(rcBTBuMsi#U6Sb%}N~7VI(+deTX-(Xoak?^{K%0uR zJ2)q8@ws07r9ee)!py16fibp0>&mlv(7pwM1cuNLUi>zMjb;(7#|@e=fkqP%e|)3xR5z( zB~>;$DFS&0lMCz&I4Ntz?58f$1}Zc5`h&y7SIX*EjTG;Yj>xcUzgyj-dBG7(6ORhL z_NG$(J}$n{4b*FHKfoLqx3qrDE@LyobJOJp*K}h>o{!_KV!B|l&c=$f+pOdn53Yh{s;AqYE!03#g zj!zgAisg~`gBm1?{KvbcK2WU==}qn%Y>j|S7-BRd? zjdR;_&08vHHTZJl-<+X40a$xjl3#rq10+Ap**_exZss9xy`R)Vs%V5L*%}+|?f0LX zVn^`Zythn8Y<{~bPJO&nTOTfO9Qn9h$rY$Qgus;NUF7I5O?5nT)O4518|_%RGlH0x zGhps5A~yBv{SqkL&Il`l@+sxev;*oasiYrodJ`q7vZXR9$ZX545I>z3!M3LIxzq&S zN@!C>)oIP4=hsfGp44e=J{a5|oVoh>->iTDT|5cfr$hPO=3feQRGT?-l@y;!_zu>k zv74FS2$V0w6^S;zyqM^ZI+F>n^3MYv1K0#InyXkMY&|bocd?-EvmMUgcYpul@}I0P ze?uNfn1rv9BKTz$JZB|sVLbDO=?5W*`KJguO;Za9vEa=&3!PYir7ZUvb(Yj8d7Sve zjuQ`XMUHWdS#fARg{L+|p37Wdt9Vz87gzL)i;LfBrm zXAE;IvR-ezZRalm;JX)i_oZJ?d-rLS)oy~Yj))Rr=cCayJgbfYhWlC}-Um|QTr4N6 zO18Ri@3o@rPgo@1pgI|s_^`u^u%6PkolO!~Dcw}Cz0HVb`|}RZb~v;(3%X|U6WsOS zPOmzD!)>ipEmyfVQkpe+O7pSf6%YsxEIy0Z`~50}Sk`S-m)0V zJRmIoo$Dao=zCHmow$uc=oNuM2H1hu{@@*B?p|@$W2>ZLn#qfb(H7^we}|U@Wz+cD zZl-Kj1QF1k*HiD`k*$3>#~+Qx6K!KWTnoSAa3u4jVWf-#x55}ZhjxRL7HuY7nT;sT z5CP@_z=_vJu~o@L4M+w4is(ayq%4rM9%13=r8cdRITL!fZb)Hi2tqrN^^ zewhYsfT-coDfwok{+Zu<7G!ZTaTlXfak>Lp>lD4)+hX?^5OHlgl(k*IoD(9YsW#Kt zc-q0ehH}$t%o?68y=6pSk(F=dt_7qAwzJ}Y5o7v@@vwOD(bx#eLM)_JQXcpq-O#kn zmk|8v|Lz;OzR@hnO*SEnNEN4il2=Zgc%!-cQz~Ws2gyyRa}P&$e((RuQhWQz^t!kO zRV;0Y@)-d(w9dIto+wXU`jKBGFKZYM92HNzbpplS(veiRBc4BQtT z&ktGv+jI#a@-N0`68NV8t z9FI6hmIb>)hS4JPK{YXv`;E=-83?*uni5|V7h`q24{#ud9X?@K3_R{+;&6S%x_A6{ zaMxeP{HpW2EX=87KCWHvDbR68HG5RE^+}N6?s~|t zj>rE#IkA5MTNoQ0IM^*GEzo8K-C|A0_KAPYt9xZH8q}I@`y9LQ)Xmm|>+$E1Bx*;g z3@E9+_+S)hUxpg;7OaTSMj|s$HF<1TN`EnV#bFAohgfUA_z0pu&~u7}^%M%Y{d z$E$7o29QSU0j>SgaIrzC3ZA*sf3wiAe|f}$HDV{J6mjm~Ye=byJw&*T zK9LZ{(ZpBaZ^J_r?U4!QN5^d zf)^Wso+fF)1?=lXLJm1}reJS_2~E$k*NoRlHN&$Dsq-;mv4muF$eyW&RO ze21G~f~8MYK4gUKc`-k*__T+&~mG-cYv;B|jzLp<8Ev$Y%xHyvi zLE~Qw*7?(W{yTQ&i*JSgK}V&5#z>XfW*A@6homU1yO?aD!2Exs`VtZED$E}_RE%Jj z&@dQ_KnRpLu&0#d78=cidc)4Wf_KYoj%P@9sIyp*FO948J)&3Ukp`Y?>c)!72gwKE z4M;XTAJK77NX`3)fbd_tJYb47#t>Z^-!c9`FZ>uCDsYTVw^K_I%!W*e{-%a!ajm;l z_LcZxB2<|Vf3`y^&`6mBjrHN-qX14G@hsoc^%Zqy~wUQYUVGYFz9PAi;PTtkh8ccYIy(eNFw3%D! zt%*^fRhKzumgv>T%u21~D|>HR>S4xr=Tdc1Jrc0tjbcGt4!AKrXY;;E@1h1Uq0t^p z1zTY1m6@2w_V1S@eY3SrOdDI|j~w^JH?${kzWr13>O^&)X|!5h0N-2a`%hAm^n3Y) zCayD2tCfjZZbxtj4DCptIE!RNLVZOhICS<;ULGBIuCMwvPT}kN{l``cu>11_pR^vC z#-T(y?nn=6KYs32sSrI<{eZ>wLN&v5CeVG`D9q=nQu&1s5PgX zGDbbqU~s}|iu<270QZ}z@2{&Ds2l#@pZPxJ=gWwMdM3)ngc%As;!%OBYRn0?YK5V*1!o@lQCSF5@(w@b4z&i1Y^yxZQXqdM7 z+%o}4oH4?B`TtaG{k2bH5OPkCCiNWCjIOv3jkh#H9`e`+$`JnfB;6ZSi}&y-H;|*I zr0%C%?e1n<^{8)#WGly<&k6r=Mq-c2HG;VM!H@ADbgOv*7c(X8wxv4$IXLFmu^gsq zv3vbwSou8?2;XY~={@h!5r5qx6lwEQ*c~h^nw|j>v*Q6(f!Y9kB3kuix&*eqBI&NY zuEP88ZUA$i!e8WDz8x{Fc0hUbgXUek^PJSwEaN-Ovh!4N4Q`6`KQ`TO_P;h=SHm^M zW;n{bwtnZk{Dv zuD`0xl6xyNb^cf=)R;BK6J_aFlD{C=aDnEM`NgT8uB(RYDFv1g(+18}ovgD`%q5fk ze;j@>@!-+QTA~=mix(rp5E!5%bbONk&?*7aB12woIFfW+Hh{fK8eykE54X$&OqvUt z>Mzbu&Z@>zFQO#Z9n9LJ_NJq6h;D-xJD zA9}lkI#tYe?2ca#;K+<}dvRw0_8Btv00>?6M{i6E`v5$Tk@qC_0`U3P-KKrq1%B@= zk_F`WG~CWJ6*9jtZklnuy&S>_~=kz=AHmbsB&pX@@8VTYk9zQ8-D z4&0R|o`f>y3P z%HS_ox)aMtP|?A1l^3^8f@YGl}2869@VD5*}Vr$ z2sZg&?YeHlWc|)hL-wQze2ITi{%TFc!5SrZq>7``Y{14H5S_<=@-C$kv;%%G{+i81s7R+X^qu35*ZzA zT6zX5M5H3R4In05i}J&uHyn0$qMAvwb)u?T!Ss)%_lFODmWbWjMGjNs%{RMpf}IdY zln1bMq-h;NN>v1o6O-ZOjDKE}3|g8TH4T8{21$l`6Bg7_RT^V%HvTvi4%wpPHypJr zmvg&ff}5=cVGisC|6w2I-`Wv}Sl=D@cK;35<=H3aOBXxHO2aWBw-n$GCbvQ9Pk7#! ztvCs~Y_Ea=kxw`2zP=o~#!K>#JqpyoLkV(D?h-eHcAgf6ClNyL@^*r#l9fy345Qp3 zHCb4n0_J`xRrJWZy^BwU;m_s_^@nJRyl36-VW)2Tn6FL@m@}|cR#7_{8?OqzPMKwn z0lyjyd_JbeEP;>TrNj+?`{Rx%`UK0QT?eLx#rZhWnb!T5iA?RS%UobJk9Zu!n3}vd z)4eLs=oQKxcdu}v1!plvk=zlRQQ_Wn^OD@xPzw8196OQdrz6PsHr3|?B@)KEpSI~# zxM)Vcp)7MQ64$(7U61Dtg?Qud+KXNhx;N-Luz+x+M}3WQ5Z4&+0SH<{x1Qs5>&QtL zec*^4k_L3;9YVH?T z-Y&(q2?6SVUu@>aZ)_kn_qsEMUI#;?Y+w z876IqDNrxdue&HfWl91ze02L~zqB8tMjAUPw~|%wfjhR;)ASwdckX1dLYY_+G=WQ zUgi5RclCMn<;yu4-6wnLl?@a69^@AZL2)x9@M zfV`n2F#lBx3qVd{sqvZj%s+t*Q^oqoEecQeNA4Q(GV@EHo27WtC59~}EAM0f#Nn*-Jj zidvIup@ou&|4voPHK&n}(~6aOcecj>nZrk7j7paFfM>XAS2DKRoBZvuJn%-HZQNjz#f>nQie3g8^@6chy@C)OQ~C=JvLyycbZY?;F;x zT(h|)Atuw(=~`P^Wro|k#eM69X-#&sQqG+~8~baE l*O2Yu-l6(g%h}5ZAR^ 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 Date: Mon, 16 Dec 2024 07:29:58 -0600 Subject: [PATCH 078/129] fix: incentive block match scoring formula --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 25d73d70..5944bce7 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ Based on the two sets of detected blocks, we use the Jonker-Volgenant algorithm Given R, G, and matched pairs in M, we evaluate similarity along the following aspects: - **Block-Match**: The first desideratum of the task is that all visual elements from the image should be reproduced in the generated webpage, and the generated webpage should not hallucinate non-existent new elements. We measure this by computing the total sizes of all matched blocks divided by the total sizes of all blocks, including unmatched ones (either because the generated webpages missed them or because the generated webpages contain hallucinated blocks): -![Incentive Mechanism Formula](docs/incentive-formula.png "WebGenieAI Incentive Formula") +![Incentive Mechanism Formula](docs/incentive-fomula.png "WebGenieAI Incentive Formula") where S(·) returns the size of the blocks, $U_R$ and $U_G$ denotes the unmatched blocks in R and G. The intuition here is that unmatched blocks will lower the score as they indicate From ee527a2f11a1441b2b98b1ec8db2c75ac262a254 Mon Sep 17 00:00:00 2001 From: cardoso-topdev Date: Mon, 16 Dec 2024 07:33:00 -0600 Subject: [PATCH 079/129] hotfix: hyperlink anchor issue --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5944bce7..dd113af1 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ Welcome to WebGenieAI Subnet, a pioneering Bittensor-based subnet designed to re - [Overview](#overview) - [Features](#features) -- [Incentive Mechanism](#incentive-mechanism) +- [Incentive Mechanism](#incentive-mechanism-v1) - [Roadmap](#roadmap) ## Overview From 3453113f07385d62a1e200fa092ffe75ad6ae153 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Tue, 17 Dec 2024 06:02:54 -0600 Subject: [PATCH 080/129] chore: seperated making tasks loop --- neurons/validators/genie_validator.py | 18 ++++++++++++++++-- neurons/validators/validator.py | 11 +++++++++-- webgenie/constants.py | 3 +++ 3 files changed, 28 insertions(+), 4 deletions(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 3e09c94b..34216cd7 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -3,7 +3,7 @@ from typing import Union from webgenie.base.neuron import BaseNeuron -from webgenie.constants import MAX_SYNTHETIC_HISTORY_SIZE +from webgenie.constants import MAX_SYNTHETIC_HISTORY_SIZE, MAX_SYNTHETIC_TASK_SIZE from webgenie.helpers.htmls import preprocess_html from webgenie.protocol import WebgenieImageSynapse, WebgenieTextSynapse from webgenie.tasks.solution import Solution @@ -16,6 +16,7 @@ def __init__(self, neuron: BaseNeuron): self.neuron = neuron self.config = neuron.config self.synthetic_history = [] + self.synthetic_tasks = [] self.task_generators = [ TextTaskGenerator(), @@ -37,8 +38,11 @@ async def forward(self): if len(self.synthetic_history) > MAX_SYNTHETIC_HISTORY_SIZE: return + if not self.synthetic_tasks: + return + + task, synapse = self.synthetic_tasks.pop(0) miner_uids = get_random_uids(self.neuron, k=self.config.neuron.sample_size) - task, synapse = await random.choice(self.task_generators).generate_task() all_synapse_results = await self.neuron.dendrite( axons = [self.neuron.metagraph.axons[uid] for uid in miner_uids], synapse=synapse, @@ -72,6 +76,16 @@ async def score(self): self.neuron.update_scores(scores, [solution.miner_uid for solution in solutions]) self.neuron.sync() + async def synthensize_task(self): + try: + if len(self.synthetic_tasks) > MAX_SYNTHETIC_TASK_SIZE: + return + + task, synapse = await random.choice(self.task_generators).generate_task() + self.synthetic_tasks.append((task, synapse)) + except Exception as e: + bt.logging.error(f"Error in synthensize_task: {e}") + async def organic_forward(self, synapse: Union[WebgenieTextSynapse, WebgenieImageSynapse]): bt.logging.debug(f"Organic forward: {synapse}") best_miner_uid = 1 diff --git a/neurons/validators/validator.py b/neurons/validators/validator.py index 167973b2..f05af3aa 100644 --- a/neurons/validators/validator.py +++ b/neurons/validators/validator.py @@ -116,9 +116,16 @@ async def scoring_loop(self): await asyncio.sleep(5) + async def synthensize_task_loop(self): + bt.logging.info(f"Synthensize task loop starting") + while True: + await self.genie_validator.synthensize_task() + await asyncio.sleep(5) + async def __aenter__(self): - #self.loop.create_task(self.forward_loop()) - #self.loop.create_task(self.scoring_loop()) + self.loop.create_task(self.synthensize_task_loop()) + self.loop.create_task(self.forward_loop()) + self.loop.create_task(self.scoring_loop()) self.is_running = True bt.logging.debug("Starting validator in background thread") return self diff --git a/webgenie/constants.py b/webgenie/constants.py index 81fa8f6f..898e50ea 100644 --- a/webgenie/constants.py +++ b/webgenie/constants.py @@ -4,6 +4,9 @@ # max synthetic history size MAX_SYNTHETIC_HISTORY_SIZE = 10 +# max synthensize task size +MAX_SYNTHETIC_TASK_SIZE = 10 + # place holder image url PLACE_HOLDER_IMAGE_URL = "https://picsum.photos/seed/picsum/800/600" From bcd0585db306e4618ee5fda2745022df915b0239 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Tue, 17 Dec 2024 06:08:33 -0600 Subject: [PATCH 081/129] chore: reduced sleep time --- neurons/validators/validator.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/neurons/validators/validator.py b/neurons/validators/validator.py index f05af3aa..47c97f34 100644 --- a/neurons/validators/validator.py +++ b/neurons/validators/validator.py @@ -103,8 +103,7 @@ async def forward_loop(self): self.step += 1 except Exception as e: bt.logging.error(f"Error during forward loop: {str(e)}") - - await asyncio.sleep(5) + await asyncio.sleep(1) async def scoring_loop(self): bt.logging.info(f"Scoring loop starting") @@ -113,14 +112,16 @@ async def scoring_loop(self): await self.genie_validator.score() except Exception as e: bt.logging.error(f"Error during scoring: {str(e)}") - - await asyncio.sleep(5) + await asyncio.sleep(1) async def synthensize_task_loop(self): bt.logging.info(f"Synthensize task loop starting") while True: - await self.genie_validator.synthensize_task() - await asyncio.sleep(5) + try: + await self.genie_validator.synthensize_task() + except Exception as e: + bt.logging.error(f"Error during synthensize task: {str(e)}") + await asyncio.sleep(1) async def __aenter__(self): self.loop.create_task(self.synthensize_task_loop()) From af12c6f2e59861ec28220512221fa0de428dfcf0 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Tue, 17 Dec 2024 06:15:27 -0600 Subject: [PATCH 082/129] chore: updated logging info --- neurons/miners/miner.py | 5 +++-- neurons/miners/openai_miner.py | 1 - webgenie/constants.py | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/neurons/miners/miner.py b/neurons/miners/miner.py index ff438169..350a9c70 100644 --- a/neurons/miners/miner.py +++ b/neurons/miners/miner.py @@ -24,6 +24,7 @@ import bittensor as bt from webgenie.base.miner import BaseMinerNeuron +from webgenie.constants import MAX_DEBUG_IMAGE_STRING_LENGTH from webgenie.helpers.weights import init_wandb from webgenie.protocol import WebgenieTextSynapse, WebgenieImageSynapse from webgenie.tasks import Solution @@ -60,13 +61,13 @@ def __init__(self, config=None): async def forward_text( self, synapse: WebgenieTextSynapse ) -> WebgenieTextSynapse: - bt.logging.debug(f"Miner text forward called with synapse: {synapse}") + bt.logging.debug(f"Miner text forward called with prompt: {synapse.prompt}") return await self.genie_miner.forward_text(synapse) async def forward_image( self, synapse: WebgenieImageSynapse ) -> WebgenieImageSynapse: - bt.logging.debug(f"Miner image forward called with synapse: {synapse}") + bt.logging.debug(f"Miner image forward called with image: {synapse.base64_image[:MAX_DEBUG_IMAGE_STRING_LENGTH]}") return await self.genie_miner.forward_image(synapse) async def blacklist_text(self, synapse: WebgenieTextSynapse) -> typing.Tuple[bool, str]: diff --git a/neurons/miners/openai_miner.py b/neurons/miners/openai_miner.py index 7402f77a..1c4f4877 100644 --- a/neurons/miners/openai_miner.py +++ b/neurons/miners/openai_miner.py @@ -52,7 +52,6 @@ async def forward_text(self, synapse: WebgenieTextSynapse) -> WebgenieTextSynaps async def forward_image(self, synapse: WebgenieImageSynapse) -> WebgenieImageSynapse: try: - prompt_messages = [ SystemMessagePromptTemplate.from_template(""" You are an expert web developer who specializes in HTML and CSS. diff --git a/webgenie/constants.py b/webgenie/constants.py index 898e50ea..18b47c15 100644 --- a/webgenie/constants.py +++ b/webgenie/constants.py @@ -7,6 +7,9 @@ # max synthensize task size MAX_SYNTHETIC_TASK_SIZE = 10 +# max debug image string length +MAX_DEBUG_IMAGE_STRING_LENGTH = 20 + # place holder image url PLACE_HOLDER_IMAGE_URL = "https://picsum.photos/seed/picsum/800/600" From d2202432bf7bcb41708808373767f24d2506a978 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Tue, 17 Dec 2024 06:20:17 -0600 Subject: [PATCH 083/129] chore: updated logging infos --- neurons/validators/genie_validator.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 34216cd7..72c67112 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -43,6 +43,8 @@ async def forward(self): task, synapse = self.synthetic_tasks.pop(0) miner_uids = get_random_uids(self.neuron, k=self.config.neuron.sample_size) + bt.logging.debug(f"Selected miner uids: {miner_uids}") + all_synapse_results = await self.neuron.dendrite( axons = [self.neuron.metagraph.axons[uid] for uid in miner_uids], synapse=synapse, @@ -73,7 +75,11 @@ async def score(self): task, solutions = self.synthetic_history.pop(0) task_generator = task.generator scores = await task_generator.reward(task, solutions) - self.neuron.update_scores(scores, [solution.miner_uid for solution in solutions]) + miner_uids = [solution.miner_uid for solution in solutions] + bt.logging.debug(f"Miner uids: {miner_uids}") + bt.logging.debug(f"Scores: {scores}") + + self.neuron.update_scores(scores, miner_uids) self.neuron.sync() async def synthensize_task(self): From aaf539330e5d355db7d1b5636f139fffe411134d Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Tue, 17 Dec 2024 06:22:48 -0600 Subject: [PATCH 084/129] chore: updated logging info --- neurons/miners/miner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neurons/miners/miner.py b/neurons/miners/miner.py index 350a9c70..bb673cee 100644 --- a/neurons/miners/miner.py +++ b/neurons/miners/miner.py @@ -67,7 +67,7 @@ async def forward_text( async def forward_image( self, synapse: WebgenieImageSynapse ) -> WebgenieImageSynapse: - bt.logging.debug(f"Miner image forward called with image: {synapse.base64_image[:MAX_DEBUG_IMAGE_STRING_LENGTH]}") + bt.logging.debug(f"Miner image forward called with image: {synapse.base64_image[:MAX_DEBUG_IMAGE_STRING_LENGTH]}...") return await self.genie_miner.forward_image(synapse) async def blacklist_text(self, synapse: WebgenieTextSynapse) -> typing.Tuple[bool, str]: From ac0d77935776ec7e07b862f52602da68f02cf586 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Tue, 17 Dec 2024 08:36:20 -0600 Subject: [PATCH 085/129] feat: added git design2code dataset --- neurons/validators/genie_validator.py | 7 +++- webgenie/datasets/huggingface_dataset.py | 47 ++++++++++++++++++++++++ webgenie/prompts.py | 9 +++++ webgenie/tasks/image_task_generator.py | 6 ++- 4 files changed, 65 insertions(+), 4 deletions(-) create mode 100644 webgenie/datasets/huggingface_dataset.py diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 72c67112..4475d7b4 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -19,7 +19,7 @@ def __init__(self, neuron: BaseNeuron): self.synthetic_tasks = [] self.task_generators = [ - TextTaskGenerator(), + # TextTaskGenerator(), ImageTaskGenerator(), ] @@ -34,13 +34,15 @@ def make_work_dir(self): bt.logging.info(f"Created work directory at {WORK_DIR}") async def forward(self): + bt.logging.debug(f"Forward") try: if len(self.synthetic_history) > MAX_SYNTHETIC_HISTORY_SIZE: return if not self.synthetic_tasks: + bt.logging.debug(f"No synthetic tasks") return - + bt.logging.debug(f"Synthetic tasks: {self.synthetic_tasks}") task, synapse = self.synthetic_tasks.pop(0) miner_uids = get_random_uids(self.neuron, k=self.config.neuron.sample_size) bt.logging.debug(f"Selected miner uids: {miner_uids}") @@ -83,6 +85,7 @@ async def score(self): self.neuron.sync() async def synthensize_task(self): + bt.logging.debug(f"Synthensize task") try: if len(self.synthetic_tasks) > MAX_SYNTHETIC_TASK_SIZE: return diff --git a/webgenie/datasets/huggingface_dataset.py b/webgenie/datasets/huggingface_dataset.py new file mode 100644 index 00000000..459201db --- /dev/null +++ b/webgenie/datasets/huggingface_dataset.py @@ -0,0 +1,47 @@ +# https://huggingface.co/datasets/SALT-NLP/Design2Code_human_eval_pairwise + +import bittensor as bt +import random +from datasets import load_dataset + +from langchain_openai import ChatOpenAI +from langchain.prompts import ChatPromptTemplate +from langchain_core.output_parsers import JsonOutputParser +from langchain_core.pydantic_v1 import BaseModel, Field + +from webgenie.datasets.dataset import Dataset, DatasetEntry +from webgenie.prompts import PROMPT_MAKE_HTML_COMPLEX + + +class HTMLResponse(BaseModel): + complex_html: str = Field(description="the complex html code") + +class HuggingfaceDesign2CodeDataset(Dataset): + def __init__(self): + self.dataset = load_dataset("SALT-NLP/Design2Code_human_eval_pairwise", split="train") + self.model = ChatOpenAI(model="gpt-4o-mini") + self.output_parser = JsonOutputParser(pydantic_object=HTMLResponse) + + async def _make_html_complex(self, html: str)->str: + prompt = ChatPromptTemplate.from_messages([ + ("system", PROMPT_MAKE_HTML_COMPLEX), + ]) + chain = prompt | self.model | self.output_parser + response = chain.invoke({ + "html": html, + "instructions": self.output_parser.get_format_instructions() + }) + return response["complex_html"] + + async def generate_context(self)->DatasetEntry: + random_index = random.randint(0, len(self.dataset) - 1) + html = self.dataset[random_index]["ref_html"] + complex_html = await self._make_html_complex(html) + bt.logging.debug(f"Complex HTML: {complex_html}") + return DatasetEntry( + src="huggingface", + topic="design2code", + ground_truth_html=complex_html, + prompt="", + base64_image="" + ) diff --git a/webgenie/prompts.py b/webgenie/prompts.py index f3e7dc69..c4b89048 100644 --- a/webgenie/prompts.py +++ b/webgenie/prompts.py @@ -31,3 +31,12 @@ {instructions} """ + +PROMPT_MAKE_HTML_COMPLEX = """ +You are an HTML, CSS expert. I have an HTML code. +I want you to make the html code more complex. +The following is the given html code: +{html} + +{instructions} +""" diff --git a/webgenie/tasks/image_task_generator.py b/webgenie/tasks/image_task_generator.py index ee3dc119..2b3bcab8 100644 --- a/webgenie/tasks/image_task_generator.py +++ b/webgenie/tasks/image_task_generator.py @@ -11,6 +11,7 @@ from webgenie.rewards.visual_reward import VisualReward from webgenie.datasets.mockup_dataset import MockUpDataset from webgenie.datasets.synthetic_dataset import SyntheticDataset +from webgenie.datasets.huggingface_dataset import HuggingfaceDesign2CodeDataset class ImageTaskGenerator(TaskGenerator): def __init__(self): @@ -19,8 +20,9 @@ def __init__(self): (VisualReward(), 1.0) ] self.datasets = [ - MockUpDataset(), - SyntheticDataset() + # MockUpDataset(), + # SyntheticDataset(), + HuggingfaceDesign2CodeDataset() ] async def generate_task(self) -> Tuple[Task, bt.Synapse]: From 8b139f1534d1fe656012a0c0485715b0a884b5f4 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Tue, 17 Dec 2024 09:24:00 -0600 Subject: [PATCH 086/129] chore: added env variables for llm --- .env.validator.example | 5 +++- webgenie/constants.py | 4 +-- webgenie/datasets/huggingface_dataset.py | 35 ++++++++++++++++-------- webgenie/datasets/synthetic_dataset.py | 3 +- webgenie/rewards/rtc_reward.py | 3 +- 5 files changed, 33 insertions(+), 17 deletions(-) diff --git a/.env.validator.example b/.env.validator.example index d4429700..0798cad9 100644 --- a/.env.validator.example +++ b/.env.validator.example @@ -1,3 +1,6 @@ OPENAI_API_KEY = your_openai_api_key WANDB_API_KEY = your_wandb_api_key -WANDB_ENTITY_NAME = your_wandb_entity_name \ No newline at end of file +WANDB_ENTITY_NAME = your_wandb_entity_name + +LLM_MODEL_ID = gpt-4o-mini +LLM_MODEL_URL = https://api.openai.com/v1/chat/completions \ No newline at end of file diff --git a/webgenie/constants.py b/webgenie/constants.py index 18b47c15..eb997497 100644 --- a/webgenie/constants.py +++ b/webgenie/constants.py @@ -2,10 +2,10 @@ API_HOTKEY = "5D72esHuc1DxD6PD8S6VyU24bTHGQjHHyodzTGsem1sejUYj" # max synthetic history size -MAX_SYNTHETIC_HISTORY_SIZE = 10 +MAX_SYNTHETIC_HISTORY_SIZE = 3 # max synthensize task size -MAX_SYNTHETIC_TASK_SIZE = 10 +MAX_SYNTHETIC_TASK_SIZE = 3 # max debug image string length MAX_DEBUG_IMAGE_STRING_LENGTH = 20 diff --git a/webgenie/datasets/huggingface_dataset.py b/webgenie/datasets/huggingface_dataset.py index 459201db..9100a933 100644 --- a/webgenie/datasets/huggingface_dataset.py +++ b/webgenie/datasets/huggingface_dataset.py @@ -1,6 +1,7 @@ # https://huggingface.co/datasets/SALT-NLP/Design2Code_human_eval_pairwise import bittensor as bt +import os import random from datasets import load_dataset @@ -19,7 +20,11 @@ class HTMLResponse(BaseModel): class HuggingfaceDesign2CodeDataset(Dataset): def __init__(self): self.dataset = load_dataset("SALT-NLP/Design2Code_human_eval_pairwise", split="train") - self.model = ChatOpenAI(model="gpt-4o-mini") + self.model = ChatOpenAI( + base_url=os.getenv("LLM_MODEL_URL"), + model=os.getenv("LLM_MODEL_ID"), + api_key=os.getenv("OPENAI_API_KEY") + ) self.output_parser = JsonOutputParser(pydantic_object=HTMLResponse) async def _make_html_complex(self, html: str)->str: @@ -34,14 +39,20 @@ async def _make_html_complex(self, html: str)->str: return response["complex_html"] async def generate_context(self)->DatasetEntry: - random_index = random.randint(0, len(self.dataset) - 1) - html = self.dataset[random_index]["ref_html"] - complex_html = await self._make_html_complex(html) - bt.logging.debug(f"Complex HTML: {complex_html}") - return DatasetEntry( - src="huggingface", - topic="design2code", - ground_truth_html=complex_html, - prompt="", - base64_image="" - ) + try: + random_index = random.randint(0, len(self.dataset) - 1) + html = self.dataset[random_index]["ref_html"] + bt.logging.debug(f"HTML: {html}") + complex_html = await self._make_html_complex(html) + bt.logging.debug(f"Complex HTML: {complex_html}") + return DatasetEntry( + src="huggingface", + topic="design2code", + ground_truth_html=complex_html, + prompt="", + base64_image="" + ) + except Exception as e: + bt.logging.error(f"Error in generate_context: {e}") + raise e + diff --git a/webgenie/datasets/synthetic_dataset.py b/webgenie/datasets/synthetic_dataset.py index 19b1e68b..e0e61f7b 100644 --- a/webgenie/datasets/synthetic_dataset.py +++ b/webgenie/datasets/synthetic_dataset.py @@ -27,7 +27,8 @@ def __init__(self, has_ground_truth_html: bool = True): self.model = ChatOpenAI( api_key= os.getenv("OPENAI_API_KEY"), - model_name="gpt-4o", + model_name=os.getenv("LLM_MODEL_ID"), + base_url=os.getenv("LLM_MODEL_URL"), temperature=0.6, ) diff --git a/webgenie/rewards/rtc_reward.py b/webgenie/rewards/rtc_reward.py index 25b7ba9a..6fbcc3f0 100644 --- a/webgenie/rewards/rtc_reward.py +++ b/webgenie/rewards/rtc_reward.py @@ -24,7 +24,8 @@ class RtcReward(Reward): def __init__(self): self.model = ChatOpenAI( api_key= os.getenv("OPENAI_API_KEY"), - model_name="gpt-4o", + model_name=os.getenv("LLM_MODEL_ID"), + base_url=os.getenv("LLM_MODEL_URL"), ) self.prompt_response_parser = JsonOutputParser(pydantic_object=PromptResponse) From 91869a42e14d268f37c4c6db9300bc862e42c8dc Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Tue, 17 Dec 2024 10:25:12 -0600 Subject: [PATCH 087/129] chore: implemented async langchain --- .env.validator.example | 2 +- neurons/miners/openai_miner.py | 2 +- neurons/validators/genie_validator.py | 2 +- webgenie/datasets/huggingface_dataset.py | 2 +- webgenie/datasets/synthetic_dataset.py | 4 ++-- webgenie/rewards/rtc_reward.py | 6 +++--- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.env.validator.example b/.env.validator.example index 0798cad9..8fac36d4 100644 --- a/.env.validator.example +++ b/.env.validator.example @@ -3,4 +3,4 @@ WANDB_API_KEY = your_wandb_api_key WANDB_ENTITY_NAME = your_wandb_entity_name LLM_MODEL_ID = gpt-4o-mini -LLM_MODEL_URL = https://api.openai.com/v1/chat/completions \ No newline at end of file +LLM_MODEL_URL = \ No newline at end of file diff --git a/neurons/miners/openai_miner.py b/neurons/miners/openai_miner.py index 1c4f4877..7bcda60d 100644 --- a/neurons/miners/openai_miner.py +++ b/neurons/miners/openai_miner.py @@ -38,7 +38,7 @@ async def forward_text(self, synapse: WebgenieTextSynapse) -> WebgenieTextSynaps ]) chain = prompt | self.model | self.html_response_parser - html_response = chain.invoke({ + html_response = await chain.ainvoke({ "query": synapse.prompt, "instructions": self.html_response_parser.get_format_instructions() }) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 4475d7b4..e110c01b 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -40,7 +40,7 @@ async def forward(self): return if not self.synthetic_tasks: - bt.logging.debug(f"No synthetic tasks") + bt.logging.warning(f"No synthetic tasks") return bt.logging.debug(f"Synthetic tasks: {self.synthetic_tasks}") task, synapse = self.synthetic_tasks.pop(0) diff --git a/webgenie/datasets/huggingface_dataset.py b/webgenie/datasets/huggingface_dataset.py index 9100a933..f88a9173 100644 --- a/webgenie/datasets/huggingface_dataset.py +++ b/webgenie/datasets/huggingface_dataset.py @@ -32,7 +32,7 @@ async def _make_html_complex(self, html: str)->str: ("system", PROMPT_MAKE_HTML_COMPLEX), ]) chain = prompt | self.model | self.output_parser - response = chain.invoke({ + response = await chain.ainvoke({ "html": html, "instructions": self.output_parser.get_format_instructions() }) diff --git a/webgenie/datasets/synthetic_dataset.py b/webgenie/datasets/synthetic_dataset.py index e0e61f7b..aec88df1 100644 --- a/webgenie/datasets/synthetic_dataset.py +++ b/webgenie/datasets/synthetic_dataset.py @@ -41,7 +41,7 @@ async def _generate_concepts(self): ("system", PROMPT_GEN_CONCEPT), ]) chain = prompt | self.model | self.concept_parser - response = chain.invoke({ + response = await chain.ainvoke({ "instructions": self.concept_parser.get_format_instructions() }) return response["concepts"] @@ -51,7 +51,7 @@ async def _generate_html(self, concept: str): ("system", PROMPT_GEN_HTML), ]) chain = prompt | self.model | self.html_parser - response = chain.invoke({ + response = await chain.ainvoke({ "concept": concept, "instructions": self.html_parser.get_format_instructions() }) diff --git a/webgenie/rewards/rtc_reward.py b/webgenie/rewards/rtc_reward.py index 6fbcc3f0..e4b75f0b 100644 --- a/webgenie/rewards/rtc_reward.py +++ b/webgenie/rewards/rtc_reward.py @@ -30,13 +30,13 @@ def __init__(self): self.prompt_response_parser = JsonOutputParser(pydantic_object=PromptResponse) - def _get_prompt(self, task: Task, solutions: List[Solution]) -> str: + async def _get_prompt(self, task: Task, solutions: List[Solution]) -> str: prompt = ChatPromptTemplate.from_messages([ SystemMessagePromptTemplate.from_template(PROMPT_RTC) ]) chain = prompt | self.model | self.prompt_response_parser - prompt_response = chain.invoke({ + prompt_response = await chain.ainvoke({ "html": task.ground_truth_html, "prompt": task.prompt, "instructions": self.prompt_response_parser.get_format_instructions() @@ -47,7 +47,7 @@ def _get_prompt(self, task: Task, solutions: List[Solution]) -> str: async def reward(self, task: Task, solutions: List[Solution]) -> np.ndarray: bt.logging.debug(f"Rewarding task in rtc reward") original_prompts = [task.prompt for _ in solutions] - miner_prompts = [self._get_prompt(task, solution) for solution in solutions] + miner_prompts = [await self._get_prompt(task, solution) for solution in solutions] P, R, F1 = bert_score.score(original_prompts, miner_prompts, lang='en') return np.array(R) From 757bc7ed8966930bd361f08b51da53aa2a51e55e Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Tue, 17 Dec 2024 12:33:59 -0600 Subject: [PATCH 088/129] chore: set weights for tasks --- neurons/validators/genie_validator.py | 11 ++++++++--- webgenie/tasks/image_task_generator.py | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index e110c01b..1c234f0b 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -19,8 +19,8 @@ def __init__(self, neuron: BaseNeuron): self.synthetic_tasks = [] self.task_generators = [ - # TextTaskGenerator(), - ImageTaskGenerator(), + (TextTaskGenerator(), 0.1), + (ImageTaskGenerator(), 0.9), ] self.make_work_dir() @@ -90,7 +90,12 @@ async def synthensize_task(self): if len(self.synthetic_tasks) > MAX_SYNTHETIC_TASK_SIZE: return - task, synapse = await random.choice(self.task_generators).generate_task() + task_generator, _ = random.choices( + self.task_generators, + weights=[weight for _, weight in self.task_generators] + )[0] + + task, synapse = await task_generator.generate_task() self.synthetic_tasks.append((task, synapse)) except Exception as e: bt.logging.error(f"Error in synthensize_task: {e}") diff --git a/webgenie/tasks/image_task_generator.py b/webgenie/tasks/image_task_generator.py index 2b3bcab8..a8a5b2a1 100644 --- a/webgenie/tasks/image_task_generator.py +++ b/webgenie/tasks/image_task_generator.py @@ -21,7 +21,7 @@ def __init__(self): ] self.datasets = [ # MockUpDataset(), - # SyntheticDataset(), + SyntheticDataset(), HuggingfaceDesign2CodeDataset() ] From b44c4ba951d0db965a5ec215b053533b131893f7 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Tue, 17 Dec 2024 12:47:05 -0600 Subject: [PATCH 089/129] chore: updated hf-dataset --- webgenie/datasets/huggingface_dataset.py | 9 +++++---- webgenie/tasks/image_task_generator.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/webgenie/datasets/huggingface_dataset.py b/webgenie/datasets/huggingface_dataset.py index f88a9173..c9931108 100644 --- a/webgenie/datasets/huggingface_dataset.py +++ b/webgenie/datasets/huggingface_dataset.py @@ -17,9 +17,10 @@ class HTMLResponse(BaseModel): complex_html: str = Field(description="the complex html code") -class HuggingfaceDesign2CodeDataset(Dataset): - def __init__(self): - self.dataset = load_dataset("SALT-NLP/Design2Code_human_eval_pairwise", split="train") +class HuggingfaceDataset(Dataset): + def __init__(self , dataset_name: str, split: str, html_field: str): + self.dataset = load_dataset(dataset_name, split=split) + self.html_field = html_field self.model = ChatOpenAI( base_url=os.getenv("LLM_MODEL_URL"), model=os.getenv("LLM_MODEL_ID"), @@ -41,7 +42,7 @@ async def _make_html_complex(self, html: str)->str: async def generate_context(self)->DatasetEntry: try: random_index = random.randint(0, len(self.dataset) - 1) - html = self.dataset[random_index]["ref_html"] + html = self.dataset[random_index][self.html_field] bt.logging.debug(f"HTML: {html}") complex_html = await self._make_html_complex(html) bt.logging.debug(f"Complex HTML: {complex_html}") diff --git a/webgenie/tasks/image_task_generator.py b/webgenie/tasks/image_task_generator.py index a8a5b2a1..1b7dbb8c 100644 --- a/webgenie/tasks/image_task_generator.py +++ b/webgenie/tasks/image_task_generator.py @@ -11,7 +11,7 @@ from webgenie.rewards.visual_reward import VisualReward from webgenie.datasets.mockup_dataset import MockUpDataset from webgenie.datasets.synthetic_dataset import SyntheticDataset -from webgenie.datasets.huggingface_dataset import HuggingfaceDesign2CodeDataset +from webgenie.datasets.huggingface_dataset import HuggingfaceDataset class ImageTaskGenerator(TaskGenerator): def __init__(self): @@ -22,7 +22,7 @@ def __init__(self): self.datasets = [ # MockUpDataset(), SyntheticDataset(), - HuggingfaceDesign2CodeDataset() + HuggingfaceDataset("SALT-NLP/Design2Code-hf", "train", "text"), ] async def generate_task(self) -> Tuple[Task, bt.Synapse]: From 55b3c7d4708aa170456cc954b1b21282c3219673 Mon Sep 17 00:00:00 2001 From: donbusha Date: Tue, 17 Dec 2024 20:26:15 -0600 Subject: [PATCH 090/129] feat: updated incentive reward --- .gitignore | 1 + neurons/validators/genie_validator.py | 7 ++-- webgenie/rewards/__init__.py | 1 + webgenie/rewards/incentive_rewards.py | 47 +++++++++++++++++++++++++++ 4 files changed, 53 insertions(+), 3 deletions(-) create mode 100644 webgenie/rewards/incentive_rewards.py diff --git a/.gitignore b/.gitignore index b92ec6a6..9307a961 100644 --- a/.gitignore +++ b/.gitignore @@ -182,6 +182,7 @@ work/ # scripts run_miner.sh +run_miner_2.sh run_validator.sh # developer doc diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 1c234f0b..8ba501bc 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -6,11 +6,11 @@ from webgenie.constants import MAX_SYNTHETIC_HISTORY_SIZE, MAX_SYNTHETIC_TASK_SIZE from webgenie.helpers.htmls import preprocess_html from webgenie.protocol import WebgenieImageSynapse, WebgenieTextSynapse +from webgenie.rewards import get_incentive_rewards from webgenie.tasks.solution import Solution from webgenie.tasks.image_task_generator import ImageTaskGenerator from webgenie.tasks.text_task_generator import TextTaskGenerator from webgenie.utils.uids import get_random_uids - class GenieValidator: def __init__(self, neuron: BaseNeuron): self.neuron = neuron @@ -80,8 +80,9 @@ async def score(self): miner_uids = [solution.miner_uid for solution in solutions] bt.logging.debug(f"Miner uids: {miner_uids}") bt.logging.debug(f"Scores: {scores}") - - self.neuron.update_scores(scores, miner_uids) + rewards = get_incentive_rewards(scores) + bt.logging.debug(f"Incentive rewards: {rewards}") + self.neuron.update_scores(rewards, miner_uids) self.neuron.sync() async def synthensize_task(self): diff --git a/webgenie/rewards/__init__.py b/webgenie/rewards/__init__.py index a6cba18e..53f7d6e2 100644 --- a/webgenie/rewards/__init__.py +++ b/webgenie/rewards/__init__.py @@ -1,2 +1,3 @@ from .reward import Reward from .visual_reward import VisualReward +from .incentive_rewards import get_incentive_rewards \ No newline at end of file diff --git a/webgenie/rewards/incentive_rewards.py b/webgenie/rewards/incentive_rewards.py new file mode 100644 index 00000000..f048d06b --- /dev/null +++ b/webgenie/rewards/incentive_rewards.py @@ -0,0 +1,47 @@ +import numpy as np + +def get_incentive_rewards(scores: np.ndarray, base_reward=100, alpha=1.5) -> np.ndarray: + """ + Calculate rewards based on the piecewise linear with exponential growth method, + preserving the original order of the scores, and returning rewards as a NumPy array. + + Parameters: + - scores: NumPy array of raw scores. + - base_reward: The minimum reward for the highest rank (rank 1). + - alpha: The exponential scaling factor for ranks above the threshold. + + Returns: + - rewards: NumPy array of rewards corresponding to the original order of scores. + """ + threshold = scores.shape[0] // 2 + + # Ensure input is a NumPy array + scores = np.array(scores) + + # Rank players based on scores (highest score gets better rank) + sorted_scores = np.sort(scores) # Sort in ascending order + score_to_rank = {score: idx + 1 for idx, score in enumerate(sorted_scores)} # Map each score to its rank + # Calculate rewards for each score based on its rank + rewards = np.zeros_like(scores, dtype=float) # Initialize the rewards array + + for idx, score in enumerate(scores): + rank = score_to_rank[score] + + if rank <= threshold: + # Linear reward scaling for ranks <= threshold + reward = base_reward + (rank - 1) * (base_reward / 2) # Linear scaling + else: + # Exponential reward scaling for ranks > threshold + reward = base_reward * (alpha ** (rank - threshold)) # Exponential scaling + + rewards[idx] = reward # Assign reward to the corresponding index + + return rewards + + +if __name__ == "__main__": + scores = np.array([500, 450, 400, 750, 300, 250, 200]) # Raw scores as a NumPy array + rewards = get_incentive_rewards(scores, base_reward=100, alpha=1.5) + + for score, reward in zip(scores, rewards): + print(f"Score: {score}, Reward = {reward:.2f}") \ No newline at end of file From ec04cb97d941d63d60e850fde8d45712d85055f9 Mon Sep 17 00:00:00 2001 From: donbusha Date: Tue, 17 Dec 2024 20:29:55 -0600 Subject: [PATCH 091/129] style: styled code --- neurons/validators/genie_validator.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 8ba501bc..3bff7dfe 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -11,6 +11,7 @@ from webgenie.tasks.image_task_generator import ImageTaskGenerator from webgenie.tasks.text_task_generator import TextTaskGenerator from webgenie.utils.uids import get_random_uids + class GenieValidator: def __init__(self, neuron: BaseNeuron): self.neuron = neuron @@ -76,12 +77,16 @@ async def score(self): task, solutions = self.synthetic_history.pop(0) task_generator = task.generator - scores = await task_generator.reward(task, solutions) + miner_uids = [solution.miner_uid for solution in solutions] bt.logging.debug(f"Miner uids: {miner_uids}") + + scores = await task_generator.reward(task, solutions) bt.logging.debug(f"Scores: {scores}") + rewards = get_incentive_rewards(scores) bt.logging.debug(f"Incentive rewards: {rewards}") + self.neuron.update_scores(rewards, miner_uids) self.neuron.sync() From 198d90b2279d3ecb669bf54890c701052845e1ec Mon Sep 17 00:00:00 2001 From: donbusha Date: Wed, 18 Dec 2024 05:31:05 -0600 Subject: [PATCH 092/129] fix: fixed circular import --- neurons/validators/genie_validator.py | 2 +- webgenie/rewards/__init__.py | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 3bff7dfe..04291c4c 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -6,7 +6,7 @@ from webgenie.constants import MAX_SYNTHETIC_HISTORY_SIZE, MAX_SYNTHETIC_TASK_SIZE from webgenie.helpers.htmls import preprocess_html from webgenie.protocol import WebgenieImageSynapse, WebgenieTextSynapse -from webgenie.rewards import get_incentive_rewards +from webgenie.rewards.incentive_rewards import get_incentive_rewards from webgenie.tasks.solution import Solution from webgenie.tasks.image_task_generator import ImageTaskGenerator from webgenie.tasks.text_task_generator import TextTaskGenerator diff --git a/webgenie/rewards/__init__.py b/webgenie/rewards/__init__.py index 53f7d6e2..a4073de3 100644 --- a/webgenie/rewards/__init__.py +++ b/webgenie/rewards/__init__.py @@ -1,3 +1,2 @@ from .reward import Reward -from .visual_reward import VisualReward -from .incentive_rewards import get_incentive_rewards \ No newline at end of file +from .visual_reward import VisualReward \ No newline at end of file From 36e05a39ad6dbbcba6a54e72a2d34fe960053e6a Mon Sep 17 00:00:00 2001 From: donbusha Date: Wed, 18 Dec 2024 06:35:37 -0600 Subject: [PATCH 093/129] fix: fixed circular import --- neurons/validators/genie_validator.py | 6 +----- webgenie/rewards/visual_reward.py | 2 +- webgenie/tasks/task_generator.py | 4 +++- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 04291c4c..29ee9afb 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -6,7 +6,6 @@ from webgenie.constants import MAX_SYNTHETIC_HISTORY_SIZE, MAX_SYNTHETIC_TASK_SIZE from webgenie.helpers.htmls import preprocess_html from webgenie.protocol import WebgenieImageSynapse, WebgenieTextSynapse -from webgenie.rewards.incentive_rewards import get_incentive_rewards from webgenie.tasks.solution import Solution from webgenie.tasks.image_task_generator import ImageTaskGenerator from webgenie.tasks.text_task_generator import TextTaskGenerator @@ -81,10 +80,7 @@ async def score(self): miner_uids = [solution.miner_uid for solution in solutions] bt.logging.debug(f"Miner uids: {miner_uids}") - scores = await task_generator.reward(task, solutions) - bt.logging.debug(f"Scores: {scores}") - - rewards = get_incentive_rewards(scores) + rewards = await task_generator.reward(task, solutions) bt.logging.debug(f"Incentive rewards: {rewards}") self.neuron.update_scores(rewards, miner_uids) diff --git a/webgenie/rewards/visual_reward.py b/webgenie/rewards/visual_reward.py index 3a676ed1..68824984 100644 --- a/webgenie/rewards/visual_reward.py +++ b/webgenie/rewards/visual_reward.py @@ -6,7 +6,7 @@ from typing import List from webgenie.constants import WORK_DIR -from webgenie.rewards import Reward +from webgenie.rewards.reward import Reward from webgenie.rewards.metrics.visual_score import visual_eval_v3_multi from webgenie.tasks.task import Task, ImageTask from webgenie.tasks.solution import Solution diff --git a/webgenie/tasks/task_generator.py b/webgenie/tasks/task_generator.py index e27668ed..6a546498 100644 --- a/webgenie/tasks/task_generator.py +++ b/webgenie/tasks/task_generator.py @@ -2,6 +2,7 @@ import numpy as np from typing import List, Tuple +from webgenie.rewards.incentive_rewards import get_incentive_rewards from webgenie.tasks import Task from webgenie.tasks.solution import Solution @@ -20,5 +21,6 @@ async def reward(self, task: Task, solutions: List[Solution]) -> np.ndarray: for reward, weight in self.rewards: reward_scores = await reward.reward(task, solutions) scores += weight * np.array(reward_scores) - return scores + rewards = get_incentive_rewards(scores) + return rewards From 966ef049fb36ed7fe4c51d3b3449b1854c3ebfe7 Mon Sep 17 00:00:00 2001 From: donbusha Date: Wed, 18 Dec 2024 06:51:53 -0600 Subject: [PATCH 094/129] chore: updated dependencies --- requirements.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/requirements.txt b/requirements.txt index a86bab75..d398930e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,9 +4,11 @@ bert-score==0.3.13 bittensor==8.4.5 clip==1.0 colormath==3.0.0 +datasets==3.2.0 ddt==1.6.0 langchain==0.3.11 langchain-openai==0.2.12 +lxml==5.3.0 matplotlib-inline==0.1.7 open-clip-torch==2.29.0 opencv-python==4.10.0.84 From 3f1114bffa83d5b356d588882a33d12686f3394a Mon Sep 17 00:00:00 2001 From: donbusha Date: Wed, 18 Dec 2024 18:43:38 +0000 Subject: [PATCH 095/129] feat: added base image2html model --- .env.miner.example | 4 +- neurons/miners/hf_miner.py | 30 +++++++++ .../miners/hf_models/websight_finetuned.py | 66 +++++++++++++++++++ requirements.txt | 8 ++- webgenie/helpers/images.py | 5 ++ 5 files changed, 109 insertions(+), 4 deletions(-) create mode 100644 neurons/miners/hf_miner.py create mode 100644 neurons/miners/hf_models/websight_finetuned.py diff --git a/.env.miner.example b/.env.miner.example index d4429700..4a124b0b 100644 --- a/.env.miner.example +++ b/.env.miner.example @@ -1,3 +1,5 @@ OPENAI_API_KEY = your_openai_api_key WANDB_API_KEY = your_wandb_api_key -WANDB_ENTITY_NAME = your_wandb_entity_name \ No newline at end of file +WANDB_ENTITY_NAME = your_wandb_entity_name + +HF_TOKEN = your_hf_token \ No newline at end of file diff --git a/neurons/miners/hf_miner.py b/neurons/miners/hf_miner.py new file mode 100644 index 00000000..d2ac6ef8 --- /dev/null +++ b/neurons/miners/hf_miner.py @@ -0,0 +1,30 @@ +import bittensor as bt +import os + +from webgenie.base.neuron import BaseNeuron +from webgenie.protocol import WebgenieTextSynapse, WebgenieImageSynapse +from webgenie.helpers.images import base64_to_image + +from neurons.miners.hf_models.websight_finetuned import generate_html_from_image + +class OpenaiMiner: + def __init__(self, neuron: BaseNeuron): + self.neuron = neuron + + async def forward_text(self, synapse: WebgenieTextSynapse) -> WebgenieTextSynapse: + try: + synapse.html = "dummy text response" + return synapse + except Exception as e: + bt.logging.error(f"Error in OpenaiMiner forward_text: {e}") + synapse.html = f"Error in OpenaiMiner forward_text: {e}" + return synapse + + async def forward_image(self, synapse: WebgenieImageSynapse) -> WebgenieImageSynapse: + try: + synapse.html = generate_html_from_image(base64_to_image(synapse.base64_image)) + return synapse + except Exception as e: + bt.logging.error(f"Error in OpenaiMiner forward_image: {e}") + synapse.html = f"Error in OpenaiMiner forward_image: {e}" + return synapse \ No newline at end of file diff --git a/neurons/miners/hf_models/websight_finetuned.py b/neurons/miners/hf_models/websight_finetuned.py new file mode 100644 index 00000000..7590707d --- /dev/null +++ b/neurons/miners/hf_models/websight_finetuned.py @@ -0,0 +1,66 @@ +import torch + +from PIL import Image +from transformers import AutoModelForCausalLM, AutoProcessor + +from transformers.image_utils import to_numpy_array, PILImageResampling, ChannelDimension +from transformers.image_transforms import resize, to_channel_dimension_format\ + +API_TOKEN = "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" +DEVICE = torch.device("cuda") +PROCESSOR = AutoProcessor.from_pretrained( + "HuggingFaceM4/VLM_WebSight_finetuned", + token=API_TOKEN, +) +MODEL = AutoModelForCausalLM.from_pretrained( + "HuggingFaceM4/VLM_WebSight_finetuned", + token=API_TOKEN, + trust_remote_code=True, + torch_dtype=torch.bfloat16, +).to(DEVICE) +image_seq_len = MODEL.config.perceiver_config.resampler_n_latents +BOS_TOKEN = PROCESSOR.tokenizer.bos_token +BAD_WORDS_IDS = PROCESSOR.tokenizer(["", ""], add_special_tokens=False).input_ids + + +def convert_to_rgb(image): + # `image.convert("RGB")` would only work for .jpg images, as it creates a wrong background + # for transparent images. The call to `alpha_composite` handles this case + if image.mode == "RGB": + return image + + image_rgba = image.convert("RGBA") + background = Image.new("RGBA", image_rgba.size, (255, 255, 255)) + alpha_composite = Image.alpha_composite(background, image_rgba) + alpha_composite = alpha_composite.convert("RGB") + return alpha_composite + +# The processor is the same as the Idefics processor except for the BILINEAR interpolation, +# so this is a hack in order to redefine ONLY the transform method +def custom_transform(x): + x = convert_to_rgb(x) + x = to_numpy_array(x) + x = resize(x, (960, 960), resample=PILImageResampling.BILINEAR) + x = PROCESSOR.image_processor.rescale(x, scale=1 / 255) + x = PROCESSOR.image_processor.normalize( + x, + mean=PROCESSOR.image_processor.image_mean, + std=PROCESSOR.image_processor.image_std + ) + x = to_channel_dimension_format(x, ChannelDimension.FIRST) + x = torch.tensor(x) + return x + +def generate_html_from_image(image): + global MODEL, PROCESSOR, image_seq_len, BOS_TOKEN, BAD_WORDS_IDS, DEVICE + inputs = PROCESSOR.tokenizer( + f"{BOS_TOKEN}{'' * image_seq_len}", + return_tensors="pt", + add_special_tokens=False, + ) + inputs["pixel_values"] = PROCESSOR.image_processor([image], transform=custom_transform) + inputs = {k: v.to(DEVICE) for k, v in inputs.items()} + generated_ids = MODEL.generate(**inputs, bad_words_ids=BAD_WORDS_IDS, max_length=4096) + generated_text = PROCESSOR.batch_decode(generated_ids, skip_special_tokens=True)[0] + + return generated_text diff --git a/requirements.txt b/requirements.txt index a86bab75..ec98b5fd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,16 @@ ansible-vault==2.1.0 beautifulsoup4==4.12.3 bert-score==0.3.13 -bittensor==8.4.5 -clip==1.0 +bittensor==7.4.0 colormath==3.0.0 ddt==1.6.0 +einops +flash-attn langchain==0.3.11 langchain-openai==0.2.12 matplotlib-inline==0.1.7 -open-clip-torch==2.29.0 opencv-python==4.10.0.84 +peft pip-chill==1.0.3 playwright==1.49.1 python-dotenv==1.0.1 @@ -17,3 +18,4 @@ scikit-learn==1.6.0 shtab==1.6.5 tinycss2==1.4.0 wandb==0.19.0 +git+https://github.com/openai/CLIP.git \ No newline at end of file diff --git a/webgenie/helpers/images.py b/webgenie/helpers/images.py index c0938be3..c2e7f57a 100644 --- a/webgenie/helpers/images.py +++ b/webgenie/helpers/images.py @@ -13,3 +13,8 @@ def pil_image_to_base64(img: Image.Image) -> str: def image_to_base64(image_path: str) -> str: img = Image.open(image_path) return pil_image_to_base64(img) + +def base64_to_image(base64_str: str) -> Image.Image: + img_bytes = base64.b64decode(base64_str) + img = Image.open(io.BytesIO(img_bytes)) + return img From 2704e499001b63b05c7d3d0f38ee15e9938a8a06 Mon Sep 17 00:00:00 2001 From: donbusha Date: Wed, 18 Dec 2024 19:43:30 +0000 Subject: [PATCH 096/129] fix: fixed bugs for backend hotkey --- neurons/miners/hf_miner.py | 2 +- neurons/miners/hf_models/websight_finetuned.py | 3 ++- neurons/miners/miner.py | 6 +++--- neurons/validators/genie_validator.py | 14 +++++++------- neurons/validators/validator.py | 8 ++++---- requirements.txt | 1 + webgenie/constants.py | 2 +- webgenie/datasets/huggingface_dataset.py | 2 -- 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/neurons/miners/hf_miner.py b/neurons/miners/hf_miner.py index d2ac6ef8..cc5b1f9f 100644 --- a/neurons/miners/hf_miner.py +++ b/neurons/miners/hf_miner.py @@ -7,7 +7,7 @@ from neurons.miners.hf_models.websight_finetuned import generate_html_from_image -class OpenaiMiner: +class HfMiner: def __init__(self, neuron: BaseNeuron): self.neuron = neuron diff --git a/neurons/miners/hf_models/websight_finetuned.py b/neurons/miners/hf_models/websight_finetuned.py index 7590707d..3ff85a11 100644 --- a/neurons/miners/hf_models/websight_finetuned.py +++ b/neurons/miners/hf_models/websight_finetuned.py @@ -1,3 +1,4 @@ +import os import torch from PIL import Image @@ -6,7 +7,7 @@ from transformers.image_utils import to_numpy_array, PILImageResampling, ChannelDimension from transformers.image_transforms import resize, to_channel_dimension_format\ -API_TOKEN = "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" +API_TOKEN = os.getenv("HF_TOKEN") DEVICE = torch.device("cuda") PROCESSOR = AutoProcessor.from_pretrained( "HuggingFaceM4/VLM_WebSight_finetuned", diff --git a/neurons/miners/miner.py b/neurons/miners/miner.py index bb673cee..57795ee7 100644 --- a/neurons/miners/miner.py +++ b/neurons/miners/miner.py @@ -27,8 +27,8 @@ from webgenie.constants import MAX_DEBUG_IMAGE_STRING_LENGTH from webgenie.helpers.weights import init_wandb from webgenie.protocol import WebgenieTextSynapse, WebgenieImageSynapse -from webgenie.tasks import Solution -from neurons.miners.openai_miner import OpenaiMiner + +from neurons.miners.hf_miner import HfMiner class Miner(BaseMinerNeuron): """ @@ -54,7 +54,7 @@ def __init__(self, config=None): priority_fn=self.priority_image, ) - self.genie_miner = OpenaiMiner(self) + self.genie_miner = HfMiner(self) init_wandb(self) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index e110c01b..fb1467f2 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -3,7 +3,7 @@ from typing import Union from webgenie.base.neuron import BaseNeuron -from webgenie.constants import MAX_SYNTHETIC_HISTORY_SIZE, MAX_SYNTHETIC_TASK_SIZE +from webgenie.constants import MAX_SYNTHETIC_HISTORY_SIZE, MAX_SYNTHETIC_TASK_SIZE, MAX_DEBUG_IMAGE_STRING_LENGTH from webgenie.helpers.htmls import preprocess_html from webgenie.protocol import WebgenieImageSynapse, WebgenieTextSynapse from webgenie.tasks.solution import Solution @@ -34,15 +34,13 @@ def make_work_dir(self): bt.logging.info(f"Created work directory at {WORK_DIR}") async def forward(self): - bt.logging.debug(f"Forward") try: if len(self.synthetic_history) > MAX_SYNTHETIC_HISTORY_SIZE: return if not self.synthetic_tasks: - bt.logging.warning(f"No synthetic tasks") return - bt.logging.debug(f"Synthetic tasks: {self.synthetic_tasks}") + task, synapse = self.synthetic_tasks.pop(0) miner_uids = get_random_uids(self.neuron, k=self.config.neuron.sample_size) bt.logging.debug(f"Selected miner uids: {miner_uids}") @@ -63,7 +61,6 @@ async def forward(self): bt.logging.warning(f"No valid solutions received") return - bt.logging.debug(f"Processed solutions: {solutions}") self.synthetic_history.append((task, solutions)) except Exception as e: bt.logging.error(f"Error in forward: {e}") @@ -71,7 +68,6 @@ async def forward(self): async def score(self): if not self.synthetic_history: - bt.logging.warning(f"No synthetic history to score") return task, solutions = self.synthetic_history.pop(0) @@ -96,7 +92,11 @@ async def synthensize_task(self): bt.logging.error(f"Error in synthensize_task: {e}") async def organic_forward(self, synapse: Union[WebgenieTextSynapse, WebgenieImageSynapse]): - bt.logging.debug(f"Organic forward: {synapse}") + if isinstance(synapse, WebgenieTextSynapse): + bt.logging.debug(f"Organic text forward: {synapse.prompt}") + else: + bt.logging.debug(f"Organic image forward: {synapse.base64_image[:MAX_DEBUG_IMAGE_STRING_LENGTH]}...") + best_miner_uid = 1 try: axon = self.neuron.metagraph.axons[best_miner_uid] diff --git a/neurons/validators/validator.py b/neurons/validators/validator.py index 47c97f34..acd0a573 100644 --- a/neurons/validators/validator.py +++ b/neurons/validators/validator.py @@ -38,18 +38,18 @@ def __init__(self, config=None): async def blacklist_text(self, synapse: WebgenieTextSynapse) -> Tuple[bool, str]: """ - Only allow the subnet owner to send synapse to the validator. + Only allow the backend owner to send synapse to the validator. """ if synapse.dendrite.hotkey == API_HOTKEY: - return False, "Subnet owner hotkey" + return False, "Backend hotkey" return True, "Blacklisted" async def blacklist_image(self, synapse: WebgenieImageSynapse) -> Tuple[bool, str]: """ - Only allow the subnet owner to send synapse to the validator. + Only allow the backend owner to send synapse to the validator. """ if synapse.dendrite.hotkey == API_HOTKEY: - return False, "Subnet owner hotkey" + return False, "Backend hotkey" return True, "Blacklisted" async def organic_forward_text(self, synapse: WebgenieTextSynapse): diff --git a/requirements.txt b/requirements.txt index ec98b5fd..5ea119cd 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,6 +4,7 @@ bert-score==0.3.13 bittensor==7.4.0 colormath==3.0.0 ddt==1.6.0 +datasets einops flash-attn langchain==0.3.11 diff --git a/webgenie/constants.py b/webgenie/constants.py index eb997497..df5f284d 100644 --- a/webgenie/constants.py +++ b/webgenie/constants.py @@ -1,5 +1,5 @@ # backend api hotkey -API_HOTKEY = "5D72esHuc1DxD6PD8S6VyU24bTHGQjHHyodzTGsem1sejUYj" +API_HOTKEY = "5DXDCYTuPfLqQXbxfvvnarG31SdTDtaubqpQrzjrcMgoP9dp" # max synthetic history size MAX_SYNTHETIC_HISTORY_SIZE = 3 diff --git a/webgenie/datasets/huggingface_dataset.py b/webgenie/datasets/huggingface_dataset.py index f88a9173..4c9f123d 100644 --- a/webgenie/datasets/huggingface_dataset.py +++ b/webgenie/datasets/huggingface_dataset.py @@ -42,9 +42,7 @@ async def generate_context(self)->DatasetEntry: try: random_index = random.randint(0, len(self.dataset) - 1) html = self.dataset[random_index]["ref_html"] - bt.logging.debug(f"HTML: {html}") complex_html = await self._make_html_complex(html) - bt.logging.debug(f"Complex HTML: {complex_html}") return DatasetEntry( src="huggingface", topic="design2code", From 7388fdddafe7fbaf20114b2ced3a71c90dca9e03 Mon Sep 17 00:00:00 2001 From: donbusha Date: Wed, 18 Dec 2024 19:50:30 +0000 Subject: [PATCH 097/129] fix: fixed bugs for empty html --- webgenie/helpers/htmls.py | 22 ++++++++++++++++++++++ webgenie/tasks/image_task_generator.py | 5 ++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/webgenie/helpers/htmls.py b/webgenie/helpers/htmls.py index b217a523..2c7a224c 100644 --- a/webgenie/helpers/htmls.py +++ b/webgenie/helpers/htmls.py @@ -89,6 +89,28 @@ def preprocess_html(html: str) -> str: html = replace_image_sources(html) return html +def is_empty_html(html: str) -> bool: + """Check if HTML body is empty or missing. + + Args: + html (str): HTML string to check + + Returns: + bool: True if body is empty or missing, False otherwise + """ + soup = BeautifulSoup(html, 'html.parser') + body = soup.find('body') + + # Return True if no body tag exists + if not body: + return True + + # Return True if body has no content (whitespace is stripped) + if not body.get_text(strip=True): + return True + + return False + if __name__ == "__main__": html = """ diff --git a/webgenie/tasks/image_task_generator.py b/webgenie/tasks/image_task_generator.py index 2b3bcab8..7021b8f2 100644 --- a/webgenie/tasks/image_task_generator.py +++ b/webgenie/tasks/image_task_generator.py @@ -3,7 +3,7 @@ import random from typing import List, Tuple -from webgenie.helpers.htmls import html_to_screenshot, preprocess_html +from webgenie.helpers.htmls import html_to_screenshot, preprocess_html, is_empty_html from webgenie.protocol import WebgenieImageSynapse from webgenie.tasks.solution import Solution from webgenie.tasks.task import Task, ImageTask @@ -30,6 +30,9 @@ async def generate_task(self) -> Tuple[Task, bt.Synapse]: ground_truth_html = preprocess_html(dataset_entry.ground_truth_html) if not ground_truth_html : raise ValueError("Invalid ground truth html") + + if is_empty_html(ground_truth_html): + raise ValueError("Empty ground truth html") base64_image = html_to_screenshot(ground_truth_html) return ImageTask( From 1d5298d88f982b0c95def02d52ff81d2e905a449 Mon Sep 17 00:00:00 2001 From: donbusha Date: Wed, 18 Dec 2024 20:02:41 +0000 Subject: [PATCH 098/129] chore: added miner logs --- neurons/miners/hf_miner.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/neurons/miners/hf_miner.py b/neurons/miners/hf_miner.py index cc5b1f9f..c8165c37 100644 --- a/neurons/miners/hf_miner.py +++ b/neurons/miners/hf_miner.py @@ -22,7 +22,9 @@ async def forward_text(self, synapse: WebgenieTextSynapse) -> WebgenieTextSynaps async def forward_image(self, synapse: WebgenieImageSynapse) -> WebgenieImageSynapse: try: + bt.logging.debug(f"Generating HTML from image") synapse.html = generate_html_from_image(base64_to_image(synapse.base64_image)) + bt.logging.debug(f"Generated HTML: {synapse.html}") return synapse except Exception as e: bt.logging.error(f"Error in OpenaiMiner forward_image: {e}") From 57a6dca1893c94b32697f34db843a1f86e895e62 Mon Sep 17 00:00:00 2001 From: donbusha Date: Wed, 18 Dec 2024 20:17:58 +0000 Subject: [PATCH 099/129] fix: fixed bugs --- .gitignore | 1 + neurons/validators/genie_validator.py | 3 ++- webgenie/helpers/htmls.py | 17 ++++++++++++++--- webgenie/tasks/image_task_generator.py | 2 +- 4 files changed, 18 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 9307a961..63e1eab1 100644 --- a/.gitignore +++ b/.gitignore @@ -179,6 +179,7 @@ wandb/ # work dir work/ +work_save/ # scripts run_miner.sh diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 25bf675a..74a08fa8 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -83,11 +83,12 @@ async def score(self): self.neuron.sync() async def synthensize_task(self): - bt.logging.debug(f"Synthensize task") try: if len(self.synthetic_tasks) > MAX_SYNTHETIC_TASK_SIZE: return + bt.logging.debug(f"Synthensize task") + task_generator, _ = random.choices( self.task_generators, weights=[weight for _, weight in self.task_generators] diff --git a/webgenie/helpers/htmls.py b/webgenie/helpers/htmls.py index 2c7a224c..74014f66 100644 --- a/webgenie/helpers/htmls.py +++ b/webgenie/helpers/htmls.py @@ -59,25 +59,36 @@ def beautify_html(html: str) -> str: soup = BeautifulSoup(html, 'html.parser') return soup.prettify() -def replace_image_sources(html_content, new_url = PLACE_HOLDER_IMAGE_URL): +import re +from bs4 import BeautifulSoup + +def replace_image_sources(html_content, new_url="PLACE_HOLDER_IMAGE_URL"): soup = BeautifulSoup(html_content, 'html.parser') + # Replace 'src' attribute in tags for img_tag in soup.find_all('img'): img_tag['src'] = new_url + # Replace 'srcset' attribute in tags for source_tag in soup.find_all('source'): if 'srcset' in source_tag.attrs: source_tag['srcset'] = new_url + # Replace URLs in inline styles (background-image) in elements for tag in soup.find_all(style=True): style = tag['style'] - updated_style = re.sub(r'background-image\s*:\s*url\([^)]+\)', f'background-image: url({new_url})', style) + # Match both background-image and shorthand background property + updated_style = re.sub(r'background\s*:\s*[^;]*url\([^)]+\)', f'background: url({new_url})', style) + updated_style = re.sub(r'background-image\s*:\s*url\([^)]+\)', f'background-image: url({new_url})', updated_style) tag['style'] = updated_style + # Replace URLs in + + + +
+

+ Welcome to Fashion Brand +

+

+ Discover the latest trends and styles in our collection. +

+
+ + diff --git a/work _save/miner1.png b/work _save/miner1.png new file mode 100644 index 0000000000000000000000000000000000000000..964e31c8207afa29506d3d9d8c76b6a6e8d34e5e GIT binary patch literal 20679 zcmeIaXH=72)GZnn6zNJ6PyrQ?-UOt-sDRRYZ&C#Wr1zj87OIN$D!oT~C!!!tK#KID z6zQE%134@28Ta=cz+Zi`ag?osSo~ug;1T*5%16ghFUB0 zh6-tauUj4~u|xS%F5TVrH#=1lmy=!N8UY3$m`!;SV_dc5nL(VWRjJlq(V>M6zrwk(EXN}CMzW>1{ZEMRBW^!YSY-=-bKan$r?BvL2 zeX^c~dsW-rUETcm`&zeLTE6vLJxB9#V)dTIK9e-shE-3S;UnkH?|xMKF}|}bX=`aI z5to5!F8Kaddp4 zt)(S2@bxQGF{Hm=xlM$K(?n{uQAa{v-gq>7U~YW;LLAL?N$O%UuTmEW?Y^W)?7P-pzi{E$as2eQ z)_3c*F&+}yWM=rpyoCK;=q4wXgJ=BwsXeW0S+TN{mCB6j#yAY$EF!ysIsZ)o`H zwZf02>Ib2nO#VMDM?Rk^DdB}dOH02HviX4y#BI3SIyyGGFHLx^87^Xc`A{a)$a;0u za3IzNZBWh@!)v0zJ%BRXe}D5rZCz|=l-lN=w1nGV3bj;8Dgu^Ub1l6xxb;=z2BYdo zPfrn;5i0f`U+jThj*g0|DlLI*=Qlb&UmSL4qdQ5|sXyyQl2O;e!OM$O@}3)0`Q9$M z3%zF7#ocF)Hv$!vm8nsPCHFeGLily5Ebpz1_wW0(a(+k$o@Ou;<>cg)+o&w}WtQjX zcl2fEmU$%nM%NZ?ZdQ}j?%n_CyDanjzK2O%VF5+3&V5$6$z=cSLoNgJb) zT{Erdw@ml%@yb1zFs^x~i&@qQ3g&BCAIttWP;O~q(O+aB^K=U92b(t|!)amQm0~RS z`l9|_HC|^vK;tP$^ zF}{uF{12obZlP0|adEqxDtwC;(YsNmIa7hB%YVWK;|UtwN!;bRd3a8-ZU&l~uG3B5 zBs`XxsHwNxm}J8Ch3-62UwF-AQ)FCQs#BQCguGk#?9aWfINWWXgI2n<^MoPZzT~He z3)8R2jz25KRfP)gua3^QF`-AR)=)#me&pB0n{2uZR)%q$J-!%DkHNgCZ{Kb*h^+oB zv)(tXEQnx|5zJ&rOiYYVOf=MwkK@}AcNofg^Jej;f@)~Frlx7Y!DCdR9%_GG>J|%& z%faT0Ooc$fz*9cQv0Vlkr0?+_54+lRu2^Li6~~`nCMO$hj`so_e%WuWj_$~XHPP5c z(C>6SKO<$^ee@~dxCx0|U{}8KKJ#0KcA>5?AByJjV|ECr$!h0J?9^Wh=|vap!QLAp z^ykmfXk63h2N&7xV!aJmBqtV@f0YZ_s@@S05NOiLG()dXO-VRUk&==~RKncfkUKq< zfAuo?U@rP}AXkluRMHi*JSyxEOU<(!A5*|U`s(9HJwCHjH-pEEKhMBqFwoPhr$0|& zCYr4CkaTb7lAK`YUY%S1rDSYk((<7zmPfw{`?@KhK1IBe9d7TZ|^|~ zSF4o>#wLZxr^UL(nj2FaY0`}gW0h_&6vLWlETvX|zJ?%A509TezYmx2+jnX&Oe*aD z_cgC|_AKumhanY3bbh|wHSr2@5BuKkq!(?Gj&ZL`!h1~gP^X9c({zzn#qKiyR)6pk z_u^cVEB&vzqur%CuZ>Saw%Cn{+Tn6ry4Hydxog8k2ClQOob^XK^0nHO_OI9up59a- zZ$eJ$88$q)m|O34f2_jp?;j05=HKs_iIB((rw5iUPMpSh?}Bq}+kO=`X6RZ(agT6*u( z>p_{d!^WiF;(E!|BzuV3*A#wyCUu;Gf@8y?PsS>0EA4fthPjEpMlill65UW$QmPNc z^}VICotO~B*1Zl%sqy|v?zmoFVhHqpQk=9>O0M5QwDr!VN4MZ}2TqfSmyB4rpn4&!TD z8nBHwMVXb&ZD_n0>@`WcW4E}xyglW&QD0w;+*>{Dj0xMG^BLZr@2aUEo~ZMPVkR1| zI{5{&5_%R=u;=!im|Aa?Zjru>{h&hdd5o?zE}VeC_==b-fd1O}{xu0tMW z-Q0FY%h4sKjiHSJXSO63eygifzAU#rAwB;s@>*eIgJ-Jvyx&5P)Yf?$jeounE^Xwh zrHzh`o;px;|0vKYtg!86u~U55ZtX(A=F;31_buCi&$NY_`$<|~4^N{SOWfcjAJWf25(^+uxmm-7{6c|JmJu zMOau^{HO=^O;d~6%{}5a=qa(%ao5Ps^$d#Es+mfTCXIzHFD~$C#b)H*YSdc}_r1T}Y#F9{C)ZHZ6lH5OZIA>D_sa*0WM2$!45>gB<2~jDP>OcGEcQsX; zqS9o8j|29A9%rfsYhm3LJ5}c~KU#ij`Q86y^N%GJRdjm#O^X+h=l7?X?%EHwY32Bz zexo4lO+()eIBFoF``S`FQ(k!^xH-K>-_p|3(Zb>~9UZ&M+psV?g65&Ujbv_XjrX0c zsY-Drk0?$c{%05Up9~c}(nLhQJ+!5o4wR|2?d63%%|!W(g@eOxcTr7J3g!NKvU z)Fwssb7|?&lKZy7#fzhNE1Wm1L!8kjw@-VYSX!>Fn_5ma)k+Af(cxcT>jm+6o1 zwIAQTJ2+ezXu=^i3UxOc_f5aqbW8f~t>3l^lk)U!RdbtHV!d_iJOKeC zs@P$8&&DPvQ$BzddDy}fH3u8lvP~7pl-E5m%nMtVM4PIa*N2KxI~S68z* z2#H5RQzB|dJOB-p;=a~YoL%TiseAm*)5Tl#&^n~VWSq~a%6w~PI!7gmk&0^g^Wzj6 zuG3mu2^n54$Y%}FGqY4#?wpD$D$%D^<>djpiyi2$w(-#uwO(+J5IB)_@m-v z=U1a21%d2s-~v;PujyknP><_!=ZQI4vdv|W>%Exfx_h$Nho!#wHk(tfvzwCn)k@r$ zGEzndF*hjJyB&4(V^(M$-x?p$(=8Ts6z++`7MlOuDLy;BIJD(YOT%~YY?jZnvl{9Gj!Up#^~-Ip7bs!7B-{-CY|lHev9ZN)#fe^@ zZV1Gs+V&jaHor})IlQ4zE$o`N#!C)lp%D>OYHPPVJv}L|OZE&geE9HTymIAcTKrOc zjlrMg;7nLkk(a3S1u1u)*Pdh&92Z~_3!H| zeKy@RnVcL-dVQj8vhjN~doSc(D_h&~GVADHg@NmX`6^5OJ--U|t_x=2*2cXyrW(^) zeR++lssNoKlel~G_SLwYIHONG+t}5YJ$}@kC!3IxTn2LFFkEtoJsgnmaw>oJM{w2x zWtweT=U(i!Q3AN6S8V7qS(n`wnd15EGoRU%P6LkRX1(XNC(}*VxKk-ky%K(lmUIT; z?cdTDFkQs3sF!gvjx*%$)Yb z+3ZV4{+o>Znk)lu!@%0^-o>7@n-&}XYBkDt)6&_(pvS8>NXQzwPe_Pro*e-OPvJ;I zex=J^TiO__EPVZ1p`beO>C@!O%9w=F>2jFLtP!X6wsqgF8#d!rjVAR~&QlGg{=WO` zDUf2BodnvZDf4|fSy;JO3+nxV9h2A%0XfPhbVCQ%`jdTN;hmQo32W@T*c58O5=O;z zlzlPi_wt_+Y*?pnuq;YkJ(ihBAu<^|jT=%<5;>jgFd<_Yq!)D>tMf2|n;c<=XQ4pa zk6`rr5ijs(u46JuglBlk9SUOc70g8A@s~KW9y})g0z8S41-P?RxVshM7 z>6uDtgZ@&r?J?m{Go zya`>3Okk;j5QW!%Vowk2-<}jsQ!7`uLMLnM{IXMQO%bf@Cb?0)XL)7iudgBHoiT33 zn%Q50-iL*SL8z(?4dYise!4Fx1|$v>~0Vnqug+Ha0a{+XDP+dDv^Z&I9Fp3|d*=k~_$H#6jv!4TTMl zT5}!8E9?wQEovtQjjv`d%(QZ?44nu%PgQ_&>Fw)j+;DKrs$_#QDE9$~6FYs?;C)G} z;>r8-WXpFY;?ys4Fp4}C78NzBb9ZX=by^#%j2CglLe>P43+2fl*Na#QVDg<5sQqYj zFkFm74p4naV zLAOgunVi!S_r(5#wZz!-xjYblA2+PZhf4WwRHV0I=7mM>PiAa18Ce-Wo$rj`(#uKn z7%=Ib@A{*u@&JO17N<@+4(W!&jt;~RmE^lh(sxrWHeCOyE8=(i+cMkMi;o|?3?d(p z9GjCSVA)h#dl*hn6-wg}ztL+;R7W57$E?c8qN3QFPeKF%G45R12l7Nr6t+ zDbABXNvW$;tq>m^*{fb1DjRp7p~xcm41=H9RL2Djq*9(HbEr*gI69Ah$m;y{D1vG zy3iF;R6RNVDu_7=-~X}e{-2NRzbpJtt?_>|`QJ?b&!PrI)N|a!BoRkSR22up~Mmkd0Wl<_+2T*|3auvq52)Sr#2pJ~UAz@J9O$3dg5a~!XVW>?$8U;-v} z&LifC&s_Opg!0+>yB&o@BI#r9q8n71GkuQb<|SzPpRq!yPF}Nuacs85oZ0RtOR8Eak%{XAtc?a)`+AaDVK9 zOJ9Z@P3zZh-#}+7h>zDm06S72C78O6GHqm#@~s$=bGEU88^YIalDW9KDXFN$NYma{ z|6QrOfPMx;f)W&5r#y{Jc~m_w;`!5?h;!a6ViLd#&7^PKxR*Gyu`yliW-C|fzF|U2 zOsx3mQR(N;>dNd0v>f?$$#$}3RM*!K5;HHF?GLrJwQoDnPLWDVO40288p_H)(q)5r zO&gxr*&R;nQX<|NL2}p#`rDmk%kVtF-#?sASy7RgoZM;T^C$lquvzZiyEmt`sO=x^ zT&~y5NuTaX%31^-7H2vzeliC%>D6fh8XI zt`H&GY0mOL_mbWD+l@PzrC748i#pSF?pbpXp;?nZ$7#U&gqR$HoQw=Cxmao4Lfwd> z?eAc>O_baAu9UQ*skwBE?u8Je>S^1BLP+S=`!W^EZMtF6Z=r|3jE^UQ_wIb{eDm2& zUUg3ylg|IA7hob|bz{Ss|H_=!%xc%MT-R*6`rpJOtyS^up*CeEm7Bk^q)STMwU%zH}Z_N}NRwjx% zPeEV_nn?2VKkNL+LB=TJ?&4BfR>rCS**t`pM%-(?!K&i}D1cTK+7S%GS&yAfOiWte z?r%&p3E3!v48X(9O-#c%2%3J1gjZ)=Xk_GdF;}Z!1v+c1tHgA?6A&D5(L6mqit%EI zdhwK=@88*Yc~uiZ3v;(q=%1WUuIvqDyM6n~*OwRJ4xEM+c_ASoDJdqkuJcErG<&S* zV)h<^BLF7q=f~fm5Sv|GeDe75V@F4m0_}XbCXfDS=CJylfh7W2V1cG(R*z=CN35-_ z6&DqOl4gN62NzeNX11co>WB;6!p_bvi21e?;f3d3 zug@T+F804oUj#FHyS6QgDb5BK19`YT-{1CuRf$tq*s7iKu%Mx#ftnBkTR-E$+wfDf zVy)aBSg8nykvb0tpVH^g5wn8Q($d2AgY8ecVHK5PxjXU*Gcq%O9cMmubVMKiO#;Py z)|&Co6K0z7>T1y|q|MFEa-q}$rXAJONG@L2z17hmLJ|hLD|eo}GexOYzY=q(4zH=L z&0j6L@_VNsU{I&T*q|v@B}v5Ei|l;xpV4yLMqi9(eri(EI8^TgCL(w4Xg3OjOGL-5 zZ|-kQJ>5Y9i`eK_ZPT6j-GN_N_~dXQEkZOU#@S;EDI`?z>FwK&47sqf@zqgrPw=T~ z8#Z4vA@gplqmco2POvfHE;ICYOiWDq&myC|Q!6^U;`nHMO#2~V*ws=SaHq%HZ$Z-U zaT-7SCFw1K^!GJ^CqJ1Fm&Rc2=iNX{Ezivu7UkqrG_BDsHr%e=U!RQbHL0|Z z8PUke03YGfrAwgSzjvXs&|tlR7)1v$BHN31KfK0H!Z*5;n7u+z$a z4r&@$VKYHN4{`^<^jPhPX6LL9LFt!%t$8KpR2^=`g;*#H7@U_zu;02hn6Je?P*+=1 zb6c~q*}TRVQxF_{U)R%dd#)qYnr6D=^Jji9FRvKktJ41Z0c78&mm5xO*x1<%hc^K= zM(q!uUXzex!>-qFP!j^s-slE-I$-nniy1l|!rv2DwHYGyBN@iIm!uwp_ql7Nx?GMh)`X9f)(3V@ml=(=QWOwi78EQEt7Zu%)tr@9gF(( z@cj+IJAl>!NIhW1v84(7A3SR<3%xY}ze@mH?`Q=D@rIp?R$Eht zz&sNd7mwvJ8aiNu-Lk@2RdQO~v*Y~tM}kl`n_~4VkLAG*-7edck&xG~$45rmNnVG9 zaCCx#aL@F2{YI0Pwi@x5TCtg0XV9zkxH`2PuvJSkNfJ0*+uX%uJn#qRkfptix?ZZM(NB9qYp)rXyt=<=7e`wtLL^<&EFxS7t zS6e+?YCK1UkpoRn`ih22w|ya&*TfKsUflfiZQc!bGd%+XXZNoa3@qk*x}Qt#uzNW@ z%E9n)bEA^@o+jm&bN3Hz8Xu2F#shQn^k#E#ny`zRQ7;2YE?(>?^wfg36~V)@?>^v|O|e4rkdZfI=Gt`&8f;1@c!ZBaBb8qdnQWw#Dq{rn4sEf3);VA=aSsEsH}t z=(k|ZGWhiA)3!mS{V&0i`}Lk{S!Y4ei0;*VOYF5i@jh)EOe4t2N~Jb(uOy%Set!34 zYql*^b&QH#^@^Ud%l*s@qemK|;4LzWIz4pD%*dE)oXNPU5c!Z06xZbB6FJL#;&{|-Ee z>~`CP#6$)euk;#ze7NyGej|g}AU_=U)zZ|=+L1gYd1pVE$KvU9$7w=8@yg}PAKtxV zJ}tf?s?*eysSwp^n=SD&9835a5=>M?M7Lr$#nrpaOk0(}1Vb4W2xSju!uVdbH~3-{ zlbb+;g*eEMPfALfkyh@@O+#|xz#TD-IdWeaQkj~bUtR_qPs-;7gmR_bfcy~FDAhd3NT4AI(ML$ULXjwctqRmJX&FVBR?Nux(|Y z`3?%ID3F2KC77n~W#ep+7e4mdPt=rJxCJr$Z+EaKNS9I|oH?e>k^Y|SYw+2D^!148 z1dDB+e8W1DXLxO`ts%ACKK$6y(sFXV4}<>%TM0h0JW@IvC370z8jueyQCkn>%G;xg z>ab4^8;U;(xIxKqGgR>X`}e@83$^q6A$1K7=ScUs^+eW`u2yw4`t8q^E5`ArtlM*G z=e@&-Bfc2DgzPw}k*!E`XqPq;mju4@<;yozKA!!on(Hu__x_HFf2^kekZNKc(U;yd znO>)uROx`k7>nLisp!Bv*I&qoWaw^d1kzA{6GnS)-N^jV8b+t5tPvhqc(+l6H(m2v zQM*OGwH&fSEtQ0rxWKS-S%E2_!m=&$&6_s|2M0xb5)MN}(2AKW!6hwy3Rxa7hZ`)h z3rzm$NcQ5bFX$Tp!9{xVyfTK;&JItW0!}@W{}3Gw_E973kgdqe`5$1D>Fwrl<~86r zL=}u#>K(!Oxu2c2wYZMhbi(JL+;%3H1|XRbvb@GM%u>87i;K56UVWG*e?@_Ht%+>kf*|OruN5rkORLtZ}&s2K8x$eZFQkb#Emtr}o2B7l4g~$UAra_Ew9ujY|JMm1?<- zes4ti5(-2cZO!+bG$eo1@UeHWG0q;A`Hzy?Yb55E-u6vXs6e5KQ%mJeyFrdlp2W;` zTRM22Vhp!*c}CTH;>@=c`}BFCMiJmA&C-`lKyz2Gh?JY&m`3N z>COsu?*wPvt3eKE1n8>~<}+W1%;aMa##p#qA## zOHX;+_}m}21H`603x0mJ;qie@^|z&`GshsF+S=-6%9Ab4rb_wEsqrveFh8Vv9>i?i z=qrpWD890H2;TU2>ni7IQ)77s+k?jasc1S0B-Xnw>8|H$Jk}DA-5yESPB3nIq4h|= zZ{rK6C&$}fgb7JWdWE_wrp&CY^FXx3^xYr<9__6`L)R%E8mpO@=gs5UFw?P@V=Gf$Y)vUuDgh?X@9P!&%00-gU4mr+*57HO2GZa)86je(yesHq4i+qK0LBwyl6)foKIdhw|42=q<0W!p}^h8M+B8+%;W@m3!0b;@4!K_b?H#f6LN@AfG z`eW`8=vu&TY)i+$Oe_FEb9c@{p`1Z_fFk9wt?k0bM&L}#MV~+Lxpp&6LBN=R+lrO zO^i|3r3%c@NO+r=n1Iv?IRSifzu#Y8{QhljHxE^+0J-_$f{N3jJD~8xEk*m%Xpu=L zH$T5)h-4ctD!|&&X?E5W)%{jFvv4LE)5ObHuIK|thJ8=VYdkVMybp0ZQ{M!HH%n0L z$_fc$O5K1y|1x6g1|S$P`|r3(fPUV|?Ir1FHLz*IhtTdz{Y4F4RuE>Pghtg)JbaXt zlT?>xxkmk&yVX9woR%#t zErom^FTwNRWd@ET?ee?C6;?3KoEQ#+vN_<1(BonwsbuW`GhNnTl+Q&6B)1RF;sdiz z{8u1w+4zK@gnL@df<`AK45^z$9)Bw?E}l30Tr@1QuOPYa_~GM6Ks~7#aeqK{pcrv# zVo;?H8POo7tR~Kq{Wih(mP3I*yR@{2APM@Jr*U)kEj?MHGI@Q*uAn;}@ z@HedMRR=h_0Mu!IgP9D)~rs^>+TG^zo_??6frG(Guvikd!!qrNIQf ze*N0QI8C%R019B}XkLKlIzB#@2{_Wm)`9x=3}PPSgqb&;uqh8g9fAH&=#vNUyA{fX z&@QO%xG(1_r$EHm-JdU+Sgm>FEJk4aC3%I0VE;K=&I&edHAZi-XNSsi~=iB(xK7bwC=>xwFpV+XJK7R7@SNxh(?T zoV!5oFS?xGlq&jIZJHQW{b_0{tv~R-i~AE^M7z;0;@op)qI;S05y>ej*--#5cy${F zio|Xyu(PwvQbKTV8*;_tYk$8tw;||jo~o<=>`9RT=xs3o zc=CGojMDb-_!tmgLR{RCk^m?){8`j-Oq=rM(m<{ZUik{85l>O&w&TA7ec=YcZ=gR) zm-6M$pFg3Xe(>M{5p?k^FE1?_0PdB}&2>ayyLuH;&t4l-AXIttU3}<|8|`@Ech7Qv zX`rpU+Y!|9AZ9pj0)n0`usXabBr56*p!4O$IXLmqDyLnQy$%bo9Tes;DsWI*VW z)Ph+HUrsNJ2~QNA#N@5o)$wGOO>$`EjEs%~$8tD1!oXZsl$P3!eEtL10@a^D_U8%) z3WlkvDL5mRtrVxbn_dveS~j9yTNHecHVL{jwm><9Qd3mma3SZjfaP~Evlyp6lY%pC zj0FzW9t8wOSnrZtCU^|WKL}6L0sAT1-oQ}s$Uu!rdwcr=+Sp@RCfi*!ZhcrENm8Mx zQZ^osNLR@LAeRa@oBhev;6ezuNG9k|kkJ5K!PL0u(hTJ$ zkUl?OU)?-)S>zix&%tmmHXKL(1}dnN?;+(&AQT4r`W5!S6gdOu+Aj$S3AuzG_1D`ZP=b8}xN-S-0Ayq++A^HlFhfgPV{kQPrYz7wQ1S&lUsj$fs5+8FWISRf&0rT$C^5)V^0e~Jzu?_;2_)R#aUYj3$Zz!o0>_Ct!#C*21>g(&V z^}{tTmaXFWMKStE)jBYOq!1gVE%LrEPL{5ICwWlDo~Shk8`SK!vyyCi@w| zd>{O4(BO}R@K7x{x#>txC2|(^NG%&yiJ17c3-`=mYVKz5MYFskw>yN=# zybi8NwUdcmES#Fml|f=h;It={RNz_9aN`@o7uZvYWdcvJ_;0Ta{aV3r{QJt!&tG3( z2dbh9CoQ@_&4&YRJS)f6wzlj!W~B_Os;XdO!-)xNBl@Mu`u*Ldfo!9i)^$8OGsJ06 zhbr#a1YBUAiS373Gbj0RO=sSi?)*U+ zF5wOf%d^j8`_7Rf=r3D-0@CWA0c~qpkD`J5h&zr|KnB8FSQwf|P*x1EzIf3TPSuPo5rhAAf*eSdW1e2OFEe|GxJ66qxX4 zD3EWw&~dk+8v;Jd!ng4h2sZQuTA(9FUf;YV6Pn@;$<4{h2?SZ7)))-N@n`yryPk!E zf3zU^`cKU!JFmdX{rU3;D!})tBFg)J7mA!&C;t{h3{=_21nYd&fzDP+JN*9zShIPs zs_%EWoqea(oL|ys`+;<ZZm>Wl$Ou*Diaf*%B8g2h_EoA_->w_Pwo6#z=8Zr}CtHA*XQLO=YN`jAcwG%}L0yNT+l47|{8)_mVRGvNid}khT*CIx+ zxZF60EM`{xwpBGfm=BNnuMDBga6ojR9fT3ZFRH+!W_3hTR#{22VMPnL58}L)%X}?% zo!;oa4%S3wX7#Pe5ZV8#raL285)aJ*cIg5$~@w9)8r zD;OZ8c;Hu2Z{OBJ1jNO~WoOSnKXdL!cQ;r)5HWo~Fd*3(*SXt)kN^~cUefzBcwV5- zLx}kw?>Rz00T>Q`Kq+fot$_mfmsbM1LGC4?pnyXcjNnbW8(F@;As5LcbDe>~9(r9? zN6R6M)}W0DkaXruW z9$7dz_(7=;`Y8MvdR?BvrGUP{6#$mt+(=Po<}9rCoQFCdMmhIb>-`cZV?P?o=MF&D zJv=;=0*x$p05&tQ%6#+YAlM5t63*Jo+^EMnWFBCPUmccZG3wS&|K|l{JeUU%#>k-G z;%L2NdL?=`Gbb+|o11m&sI878Y;9>dI6S1et)32*H}Lf%a0-6>_yNF?J;6Km~zFB<2oGRU|CLKNh7PKW{jkKBl#%a9CpzXn~y_oH|oKTnT|ef_CKr5dhThSH6})qA7HA01-6< zGyn+(Dk7jFOW>S-q!g4Y=Yu2-awGsaB=bQy1BofO!Z>m~%sqyaOEH`}cohuf_2A%O zFgfsgo{^#9(cvK>0Rf0J+0d!8EuQyOuF;3)_`{@Bx06z*Q4W}Pa0h@C$a(lS6L6S; zbu!Y?0Z2(;#JjA=)H%=SH~&X}ghQ@gFvpmwLx0W8%uM8U zsXD*?^(0Yea)vvLP@%wOE#`$z66Ox#k~9#yxU&4GsxIzx-Pxv8nD61Cp}?E*Dqdls zgM0)6VVK@EdW94L5~u6`W}uijGo*%3-WA02>TA95Pesx_Af8{9pHNXuK#~W}3u$k+uJ9bnA}f z6prxvKWO^>uOamRZe;`d5x(Fm%`W`gXdZ+uzEI137NJjR4pBu*0G;H|3GpFjE{pF5 z|L@oT&B=cO@!xLv4+{Q+$NzG}e~Iz`_pBfUvERN + + + + + + + Fashion Brand + + + + + +
+

+ Welcome to Fashion Brand +

+

+ Discover the latest trends and styles in our collection. +

+
+ + diff --git a/work _save/miner3.png b/work _save/miner3.png new file mode 100644 index 0000000000000000000000000000000000000000..84cd31df58f466e037d9fb742adac7e490431dd7 GIT binary patch literal 21033 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 + + +
+ + +
+Fashion Brand +
+

+ Welcome to Fashion Brand +

+

+ Discover the latest trends and styles in our collection. +

+
+ + diff --git a/work _save/miner6.png b/work _save/miner6.png new file mode 100644 index 0000000000000000000000000000000000000000..8b6192c625aed336fca01c6585cfce009f026af0 GIT binary patch literal 25187 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 sfH!HwVyVlj<9`G=8Rq|81Z-{>eYVpN7i@bQ3WM + + + + + + + Fashion Brand + + + + +
+
+

+ Fashion Brand +

+ +
+
+
+Fashion Brand +
+
+
+

+ Welcome to Fashion Brand +

+

+ Discover the latest trends and styles in our collection. +

+
+
+ + diff --git a/work _save/original.png b/work _save/original.png new file mode 100644 index 0000000000000000000000000000000000000000..4f6490dbf9dd8a1d5e7a401f0af6a9f6551fd392 GIT binary patch literal 25237 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 I{psib0~w8X?EnA( literal 0 HcmV?d00001 From 03f28d7b4ad9ef9ac51d58f8b5a50d7e6896c1f5 Mon Sep 17 00:00:00 2001 From: donbusha Date: Thu, 19 Dec 2024 01:01:48 +0000 Subject: [PATCH 101/129] feat: added text2html basemodel --- .gitignore | 1 + neurons/miners/hf_miner.py | 7 ++- neurons/miners/hf_models/falcon7b.py | 33 ++++++++++ work _save/miner1.html | 86 --------------------------- work _save/miner1.png | Bin 20679 -> 0 bytes work _save/miner3.html | 82 ------------------------- work _save/miner3.png | Bin 21033 -> 0 bytes work _save/miner6.html | 75 ----------------------- work _save/miner6.png | Bin 25187 -> 0 bytes work _save/original.html | 58 ------------------ work _save/original.png | Bin 25237 -> 0 bytes 11 files changed, 38 insertions(+), 304 deletions(-) create mode 100644 neurons/miners/hf_models/falcon7b.py delete mode 100644 work _save/miner1.html delete mode 100644 work _save/miner1.png delete mode 100644 work _save/miner3.html delete mode 100644 work _save/miner3.png delete mode 100644 work _save/miner6.html delete mode 100644 work _save/miner6.png delete mode 100644 work _save/original.html delete mode 100644 work _save/original.png diff --git a/.gitignore b/.gitignore index 63e1eab1..b064338d 100644 --- a/.gitignore +++ b/.gitignore @@ -180,6 +180,7 @@ wandb/ # work dir work/ work_save/ +*.png # scripts run_miner.sh diff --git a/neurons/miners/hf_miner.py b/neurons/miners/hf_miner.py index c8165c37..f29e7f45 100644 --- a/neurons/miners/hf_miner.py +++ b/neurons/miners/hf_miner.py @@ -5,6 +5,7 @@ from webgenie.protocol import WebgenieTextSynapse, WebgenieImageSynapse from webgenie.helpers.images import base64_to_image +from neurons.miners.hf_models.falcon7b import generate_html_from_text from neurons.miners.hf_models.websight_finetuned import generate_html_from_image class HfMiner: @@ -13,11 +14,11 @@ def __init__(self, neuron: BaseNeuron): async def forward_text(self, synapse: WebgenieTextSynapse) -> WebgenieTextSynapse: try: - synapse.html = "dummy text response" + synapse.html = generate_html_from_text(synapse.prompt) return synapse except Exception as e: - bt.logging.error(f"Error in OpenaiMiner forward_text: {e}") - synapse.html = f"Error in OpenaiMiner forward_text: {e}" + bt.logging.error(f"Error in HfMiner forward_text: {e}") + synapse.html = f"Error in HfMiner forward_text: {e}" return synapse async def forward_image(self, synapse: WebgenieImageSynapse) -> WebgenieImageSynapse: diff --git a/neurons/miners/hf_models/falcon7b.py b/neurons/miners/hf_models/falcon7b.py new file mode 100644 index 00000000..25d92372 --- /dev/null +++ b/neurons/miners/hf_models/falcon7b.py @@ -0,0 +1,33 @@ +from peft import PeftModel +from transformers import AutoModelForCausalLM + +base_model = AutoModelForCausalLM.from_pretrained("ybelkada/falcon-7b-sharded-bf16") +model = PeftModel.from_pretrained(base_model, "kasperius/falcon-7b-sharded-bf16-finetuned-html-code-generation-the-css-v2") + +def generate_html_from_text(prompt, max_length=1024 * 10, num_return_sequences=1): + """ + Generate text from a prompt using the Falcon-7B model + + Args: + prompt (str): Input text prompt + max_length (int): Maximum length of generated text + num_return_sequences (int): Number of sequences to generate + + Returns: + str: Generated text response + """ + inputs = model.tokenizer(prompt, return_tensors="pt", padding=True) + + outputs = model.generate( + **inputs, + max_length=max_length, + num_return_sequences=num_return_sequences, + pad_token_id=model.config.eos_token_id, + do_sample=True + ) + + response = model.tokenizer.decode(outputs[0], skip_special_tokens=True) + return response + +if __name__ == "__main__": + print(generate_html_from_text("Write a simple HTML page with a header, a paragraph, and a footer.")) \ No newline at end of file diff --git a/work _save/miner1.html b/work _save/miner1.html deleted file mode 100644 index b320b6ac..00000000 --- a/work _save/miner1.html +++ /dev/null @@ -1,86 +0,0 @@ - - - - - - - - Fashion Brand - - - - -
- - -
-
-

- Welcome to Fashion Brand -

-

- Discover the latest trends and styles in our collection. -

-
- - diff --git a/work _save/miner1.png b/work _save/miner1.png deleted file mode 100644 index 964e31c8207afa29506d3d9d8c76b6a6e8d34e5e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20679 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 - - - - - - - Fashion Brand - - - - - -
-

- Welcome to Fashion Brand -

-

- Discover the latest trends and styles in our collection. -

-
- - diff --git a/work _save/miner3.png b/work _save/miner3.png deleted file mode 100644 index 84cd31df58f466e037d9fb742adac7e490431dd7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 21033 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 - - -
- - -
-Fashion Brand -
-

- Welcome to Fashion Brand -

-

- Discover the latest trends and styles in our collection. -

-
- - diff --git a/work _save/miner6.png b/work _save/miner6.png deleted file mode 100644 index 8b6192c625aed336fca01c6585cfce009f026af0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25187 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 sfH!HwVyVlj<9`G=8Rq|81Z-{>eYVpN7i@bQ3WM - - - - - - - Fashion Brand - - - - -
-
-

- Fashion Brand -

- -
-
-
-Fashion Brand -
-
-
-

- Welcome to Fashion Brand -

-

- Discover the latest trends and styles in our collection. -

-
-
- - diff --git a/work _save/original.png b/work _save/original.png deleted file mode 100644 index 4f6490dbf9dd8a1d5e7a401f0af6a9f6551fd392..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25237 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 I{psib0~w8X?EnA( From a5dc545c8932daead8d709546722b3177d4ac8e7 Mon Sep 17 00:00:00 2001 From: donbusha Date: Thu, 19 Dec 2024 02:59:29 +0000 Subject: [PATCH 102/129] wip: ext2html --- neurons/miners/hf_models/falcon7b.py | 31 ++++++++++++------- .../miners/hf_models/websight_finetuned.py | 2 +- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/neurons/miners/hf_models/falcon7b.py b/neurons/miners/hf_models/falcon7b.py index 25d92372..65f5c55f 100644 --- a/neurons/miners/hf_models/falcon7b.py +++ b/neurons/miners/hf_models/falcon7b.py @@ -1,10 +1,19 @@ +import torch from peft import PeftModel -from transformers import AutoModelForCausalLM +from transformers import AutoModelForCausalLM, AutoTokenizer -base_model = AutoModelForCausalLM.from_pretrained("ybelkada/falcon-7b-sharded-bf16") -model = PeftModel.from_pretrained(base_model, "kasperius/falcon-7b-sharded-bf16-finetuned-html-code-generation-the-css-v2") +DEVICE = "cuda" if torch.cuda.is_available() else "cpu" -def generate_html_from_text(prompt, max_length=1024 * 10, num_return_sequences=1): +BASE_MODEL = AutoModelForCausalLM.from_pretrained("ybelkada/falcon-7b-sharded-bf16") +MODEL = PeftModel.from_pretrained( + BASE_MODEL, + "PrincySinghal991/falcon-7b-sharded-bf16-finetuned-html-code-generation" +).to(DEVICE) + +TOKENIZER = AutoTokenizer.from_pretrained("ybelkada/falcon-7b-sharded-bf16") +TOKENIZER.pad_token = TOKENIZER.eos_token + +def generate_html_from_text(prompt, max_length=4096, num_return_sequences=1): """ Generate text from a prompt using the Falcon-7B model @@ -15,18 +24,18 @@ def generate_html_from_text(prompt, max_length=1024 * 10, num_return_sequences=1 Returns: str: Generated text response - """ - inputs = model.tokenizer(prompt, return_tensors="pt", padding=True) - - outputs = model.generate( - **inputs, + """ + input_ids = TOKENIZER(prompt, return_tensors="pt").input_ids.to(DEVICE) + + outputs = MODEL.generate( + input_ids=input_ids, max_length=max_length, num_return_sequences=num_return_sequences, - pad_token_id=model.config.eos_token_id, + pad_token_id=MODEL.config.eos_token_id, do_sample=True ) - response = model.tokenizer.decode(outputs[0], skip_special_tokens=True) + response = TOKENIZER.decode(outputs[0], skip_special_tokens=True) return response if __name__ == "__main__": diff --git a/neurons/miners/hf_models/websight_finetuned.py b/neurons/miners/hf_models/websight_finetuned.py index 3ff85a11..24769dae 100644 --- a/neurons/miners/hf_models/websight_finetuned.py +++ b/neurons/miners/hf_models/websight_finetuned.py @@ -5,7 +5,7 @@ from transformers import AutoModelForCausalLM, AutoProcessor from transformers.image_utils import to_numpy_array, PILImageResampling, ChannelDimension -from transformers.image_transforms import resize, to_channel_dimension_format\ +from transformers.image_transforms import resize, to_channel_dimension_format API_TOKEN = os.getenv("HF_TOKEN") DEVICE = torch.device("cuda") From 0f76a0906806a7ce2754bb59b3f1e68b5c7720dc Mon Sep 17 00:00:00 2001 From: donbusha Date: Thu, 19 Dec 2024 05:27:05 +0000 Subject: [PATCH 103/129] feat: added text2html model --- neurons/miners/hf_models/falcon7b.py | 84 +++++++++++++++++----------- 1 file changed, 51 insertions(+), 33 deletions(-) diff --git a/neurons/miners/hf_models/falcon7b.py b/neurons/miners/hf_models/falcon7b.py index 65f5c55f..9df0b024 100644 --- a/neurons/miners/hf_models/falcon7b.py +++ b/neurons/miners/hf_models/falcon7b.py @@ -1,42 +1,60 @@ import torch -from peft import PeftModel -from transformers import AutoModelForCausalLM, AutoTokenizer +from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig -DEVICE = "cuda" if torch.cuda.is_available() else "cpu" +# Loading original model +model_name = "ybelkada/falcon-7b-sharded-bf16" -BASE_MODEL = AutoModelForCausalLM.from_pretrained("ybelkada/falcon-7b-sharded-bf16") -MODEL = PeftModel.from_pretrained( - BASE_MODEL, - "PrincySinghal991/falcon-7b-sharded-bf16-finetuned-html-code-generation" -).to(DEVICE) +bnb_config = BitsAndBytesConfig( + load_in_4bit=True, + bnb_4bit_quant_type="nf4", + bnb_4bit_use_double_quant=True, + bnb_4bit_compute_dtype=torch.float16, +) -TOKENIZER = AutoTokenizer.from_pretrained("ybelkada/falcon-7b-sharded-bf16") -TOKENIZER.pad_token = TOKENIZER.eos_token +model = AutoModelForCausalLM.from_pretrained( + model_name, + quantization_config=bnb_config, + device_map="auto", + trust_remote_code=True, +) -def generate_html_from_text(prompt, max_length=4096, num_return_sequences=1): - """ - Generate text from a prompt using the Falcon-7B model - - Args: - prompt (str): Input text prompt - max_length (int): Maximum length of generated text - num_return_sequences (int): Number of sequences to generate - - Returns: - str: Generated text response - """ - input_ids = TOKENIZER(prompt, return_tensors="pt").input_ids.to(DEVICE) +tokenizer = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True) +tokenizer.pad_token = tokenizer.eos_token +PEFT_MODEL = "PrincySinghal991/falcon-7b-sharded-bf16-finetuned-html-code-generation" +# PEFT_MODEL = "kasperius/falcon-7b-sharded-bf16-finetuned-html-code-generation-the-css-only" + +peft_model = AutoModelForCausalLM.from_pretrained( + PEFT_MODEL, + quantization_config=bnb_config, + device_map="auto", # Let the transformers library handle device placement + trust_remote_code=True, + torch_dtype=torch.float16, # Use mixed precision to reduce memory usage + low_cpu_mem_usage=True +) - outputs = MODEL.generate( - input_ids=input_ids, - max_length=max_length, - num_return_sequences=num_return_sequences, - pad_token_id=MODEL.config.eos_token_id, - do_sample=True +# Load tokenizer +peft_tokenizer = AutoTokenizer.from_pretrained(PEFT_MODEL, trust_remote_code=True) +peft_tokenizer.pad_token = peft_tokenizer.eos_token + +def generate_html_from_text(prompt): + # Tokenize and generate with the PEFT model + peft_encoding = peft_tokenizer(prompt, return_tensors="pt") + peft_outputs = peft_model.generate( + input_ids=peft_encoding["input_ids"].to(peft_model.device), + attention_mask=peft_encoding["attention_mask"].to(peft_model.device), + max_length=2048, + pad_token_id=peft_tokenizer.eos_token_id, + eos_token_id=peft_tokenizer.eos_token_id ) + peft_model_html = peft_tokenizer.decode(peft_outputs[0], skip_special_tokens=True) + return peft_model_html[len(prompt):] - response = TOKENIZER.decode(outputs[0], skip_special_tokens=True) - return response - if __name__ == "__main__": - print(generate_html_from_text("Write a simple HTML page with a header, a paragraph, and a footer.")) \ No newline at end of file + + # Example usage + prompt="create a simple login page with html and css" + print("=================") + html = generate_html_from_text(prompt) + print("=================") + print(html) + From 7ee25c2865eda60b78d61a5713e629fa86311647 Mon Sep 17 00:00:00 2001 From: donbusha Date: Thu, 19 Dec 2024 11:17:12 +0000 Subject: [PATCH 104/129] chore: added time measure --- neurons/miners/hf_models/falcon7b.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/neurons/miners/hf_models/falcon7b.py b/neurons/miners/hf_models/falcon7b.py index 9df0b024..7e47e25d 100644 --- a/neurons/miners/hf_models/falcon7b.py +++ b/neurons/miners/hf_models/falcon7b.py @@ -54,7 +54,12 @@ def generate_html_from_text(prompt): # Example usage prompt="create a simple login page with html and css" print("=================") + import time + start_time = time.time() + print(f"Prompt: {prompt}") html = generate_html_from_text(prompt) + end_time = time.time() + print(f"Time taken: {end_time - start_time} seconds") print("=================") print(html) From 84182ab7d00b05dbbabd3ab5854ec4f0cdeeb1a4 Mon Sep 17 00:00:00 2001 From: donbusha Date: Thu, 19 Dec 2024 12:21:40 +0000 Subject: [PATCH 105/129] feat: checked gpu --- neurons/miners/hf_miner.py | 20 +++++++++++++++++--- webgenie/utils/gpus.py | 13 +++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 webgenie/utils/gpus.py diff --git a/neurons/miners/hf_miner.py b/neurons/miners/hf_miner.py index f29e7f45..bbc3f5cd 100644 --- a/neurons/miners/hf_miner.py +++ b/neurons/miners/hf_miner.py @@ -5,16 +5,30 @@ from webgenie.protocol import WebgenieTextSynapse, WebgenieImageSynapse from webgenie.helpers.images import base64_to_image -from neurons.miners.hf_models.falcon7b import generate_html_from_text +from webgenie.utils.gpus import get_gpu_info +total_memory_mb, _, _ = get_gpu_info() + +if total_memory_mb is None: + raise ValueError("No GPU detected. HfMiner requires a GPU.") + +if total_memory_mb < 1024 * 25: + raise ValueError("Insufficient GPU memory. HfMiner requires at least 25GB of GPU memory.") + from neurons.miners.hf_models.websight_finetuned import generate_html_from_image +if total_memory_mb > 1024 * 50: + from neurons.miners.hf_models.falcon7b import generate_html_from_text + class HfMiner: def __init__(self, neuron: BaseNeuron): self.neuron = neuron - + async def forward_text(self, synapse: WebgenieTextSynapse) -> WebgenieTextSynapse: try: - synapse.html = generate_html_from_text(synapse.prompt) + if total_memory_mb > 1024 * 50: + synapse.html = generate_html_from_text(synapse.prompt) + else: + synapse.html = "you don't have enough memory to generate html from text" return synapse except Exception as e: bt.logging.error(f"Error in HfMiner forward_text: {e}") diff --git a/webgenie/utils/gpus.py b/webgenie/utils/gpus.py new file mode 100644 index 00000000..8d615a30 --- /dev/null +++ b/webgenie/utils/gpus.py @@ -0,0 +1,13 @@ +import torch + +def get_gpu_info(): + if torch.cuda.is_available(): + total_memory = torch.cuda.get_device_properties(0).total_memory + allocated_memory = torch.cuda.memory_allocated(0) + cached_memory = torch.cuda.memory_reserved(0) + total_memory_mb = total_memory / (1024 ** 2) + allocated_memory_mb = allocated_memory / (1024 ** 2) + cached_memory_mb = cached_memory / (1024 ** 2) + return total_memory_mb, allocated_memory_mb, cached_memory_mb + else: + return None, None, None \ No newline at end of file From 612d783227fb9097b2770ef24f10e90dbc337806 Mon Sep 17 00:00:00 2001 From: donbusha Date: Thu, 19 Dec 2024 13:53:57 +0000 Subject: [PATCH 106/129] chore: added logs --- neurons/validators/genie_validator.py | 2 +- webgenie/datasets/huggingface_dataset.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 74a08fa8..36af4c0e 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -60,7 +60,7 @@ async def forward(self): if not solutions: bt.logging.warning(f"No valid solutions received") return - + bt.logging.info(f"Received {len(solutions)} solutions") self.synthetic_history.append((task, solutions)) except Exception as e: bt.logging.error(f"Error in forward: {e}") diff --git a/webgenie/datasets/huggingface_dataset.py b/webgenie/datasets/huggingface_dataset.py index 832fa1e4..cfbb1dbb 100644 --- a/webgenie/datasets/huggingface_dataset.py +++ b/webgenie/datasets/huggingface_dataset.py @@ -29,6 +29,7 @@ def __init__(self , dataset_name: str, split: str, html_field: str): self.output_parser = JsonOutputParser(pydantic_object=HTMLResponse) async def _make_html_complex(self, html: str)->str: + bt.logging.info("Making HTML complex") prompt = ChatPromptTemplate.from_messages([ ("system", PROMPT_MAKE_HTML_COMPLEX), ]) @@ -41,9 +42,9 @@ async def _make_html_complex(self, html: str)->str: async def generate_context(self)->DatasetEntry: try: + bt.logging.info("Generating context") random_index = random.randint(0, len(self.dataset) - 1) html = self.dataset[random_index][self.html_field] - bt.logging.debug(f"HTML: {html}") complex_html = await self._make_html_complex(html) return DatasetEntry( src="huggingface", From 7b1573eae939dfb11c1ae8a7d954b4aa1ce244b1 Mon Sep 17 00:00:00 2001 From: donbusha Date: Thu, 19 Dec 2024 13:59:19 +0000 Subject: [PATCH 107/129] chore: added logs --- webgenie/datasets/huggingface_dataset.py | 2 +- webgenie/datasets/synthetic_dataset.py | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/webgenie/datasets/huggingface_dataset.py b/webgenie/datasets/huggingface_dataset.py index cfbb1dbb..6ef1fb80 100644 --- a/webgenie/datasets/huggingface_dataset.py +++ b/webgenie/datasets/huggingface_dataset.py @@ -42,7 +42,7 @@ async def _make_html_complex(self, html: str)->str: async def generate_context(self)->DatasetEntry: try: - bt.logging.info("Generating context") + bt.logging.info("Generating Huggingface context") random_index = random.randint(0, len(self.dataset) - 1) html = self.dataset[random_index][self.html_field] complex_html = await self._make_html_complex(html) diff --git a/webgenie/datasets/synthetic_dataset.py b/webgenie/datasets/synthetic_dataset.py index aec88df1..58e57124 100644 --- a/webgenie/datasets/synthetic_dataset.py +++ b/webgenie/datasets/synthetic_dataset.py @@ -37,6 +37,7 @@ def __init__(self, has_ground_truth_html: bool = True): self.concepts = [] async def _generate_concepts(self): + bt.logging.info("Generating concepts") prompt = ChatPromptTemplate.from_messages([ ("system", PROMPT_GEN_CONCEPT), ]) @@ -47,6 +48,7 @@ async def _generate_concepts(self): return response["concepts"] async def _generate_html(self, concept: str): + bt.logging.info("Generating HTML from concept") prompt = ChatPromptTemplate.from_messages([ ("system", PROMPT_GEN_HTML), ]) @@ -58,6 +60,7 @@ async def _generate_html(self, concept: str): return response["html"] async def generate_context(self)->DatasetEntry: + bt.logging.info("Generating Synthetic context") if not self.concepts: self.concepts = await self._generate_concepts() From 1552a2322a08536414c45f57a0d21148d33b6715 Mon Sep 17 00:00:00 2001 From: donbusha Date: Thu, 19 Dec 2024 14:04:57 +0000 Subject: [PATCH 108/129] chore: added logs --- neurons/miners/hf_miner.py | 4 +++- webgenie/tasks/image_task_generator.py | 1 + webgenie/tasks/text_task_generator.py | 1 + 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/neurons/miners/hf_miner.py b/neurons/miners/hf_miner.py index bbc3f5cd..93ee3ab7 100644 --- a/neurons/miners/hf_miner.py +++ b/neurons/miners/hf_miner.py @@ -11,7 +11,9 @@ if total_memory_mb is None: raise ValueError("No GPU detected. HfMiner requires a GPU.") -if total_memory_mb < 1024 * 25: +bt.logging.info(f"Total memory: {total_memory_mb}") + +if total_memory_mb < 1024 * 23: raise ValueError("Insufficient GPU memory. HfMiner requires at least 25GB of GPU memory.") from neurons.miners.hf_models.websight_finetuned import generate_html_from_image diff --git a/webgenie/tasks/image_task_generator.py b/webgenie/tasks/image_task_generator.py index c7dc3613..9e5507ba 100644 --- a/webgenie/tasks/image_task_generator.py +++ b/webgenie/tasks/image_task_generator.py @@ -26,6 +26,7 @@ def __init__(self): ] async def generate_task(self) -> Tuple[Task, bt.Synapse]: + bt.logging.info("Generating Image task") dataset_entry = await random.choice(self.datasets).generate_context() ground_truth_html = preprocess_html(dataset_entry.ground_truth_html) if not ground_truth_html : diff --git a/webgenie/tasks/text_task_generator.py b/webgenie/tasks/text_task_generator.py index 0d06cfcf..9387b83a 100644 --- a/webgenie/tasks/text_task_generator.py +++ b/webgenie/tasks/text_task_generator.py @@ -36,6 +36,7 @@ def __init__(self, has_ground_truth_html: bool = True): ] async def generate_task(self) -> Tuple[Task, bt.Synapse]: + bt.logging.info("Generating Text task") dataset_entry = await random.choice(self.datasets).generate_context() return TextTask( prompt=dataset_entry.prompt, From 5dca432b0bfc0585dfbdb4e6ab3ad9e265280a6f Mon Sep 17 00:00:00 2001 From: donbusha Date: Thu, 19 Dec 2024 14:43:14 +0000 Subject: [PATCH 109/129] chore: added logs --- neurons/validators/genie_validator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 36af4c0e..4a4bb175 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -40,7 +40,7 @@ async def forward(self): if not self.synthetic_tasks: return - + bt.logging.info("Popping synthetic task and sending it to miners") task, synapse = self.synthetic_tasks.pop(0) miner_uids = get_random_uids(self.neuron, k=self.config.neuron.sample_size) bt.logging.debug(f"Selected miner uids: {miner_uids}") From fd0108f3f0aea0480069f3a6f520635b60e2988d Mon Sep 17 00:00:00 2001 From: donbusha Date: Thu, 19 Dec 2024 10:43:20 -0600 Subject: [PATCH 110/129] build(bittensor): upgraded version --- neurons/miners/miner.py | 4 ++-- neurons/validators/genie_validator.py | 2 +- requirements.txt | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/neurons/miners/miner.py b/neurons/miners/miner.py index 57795ee7..61d17ec6 100644 --- a/neurons/miners/miner.py +++ b/neurons/miners/miner.py @@ -28,7 +28,7 @@ from webgenie.helpers.weights import init_wandb from webgenie.protocol import WebgenieTextSynapse, WebgenieImageSynapse -from neurons.miners.hf_miner import HfMiner +from neurons.miners.openai_miner import OpenaiMiner class Miner(BaseMinerNeuron): """ @@ -54,7 +54,7 @@ def __init__(self, config=None): priority_fn=self.priority_image, ) - self.genie_miner = HfMiner(self) + self.genie_miner = OpenaiMiner(self) init_wandb(self) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 4a4bb175..14f67f2d 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -105,7 +105,7 @@ async def organic_forward(self, synapse: Union[WebgenieTextSynapse, WebgenieImag else: bt.logging.debug(f"Organic image forward: {synapse.base64_image[:MAX_DEBUG_IMAGE_STRING_LENGTH]}...") - best_miner_uid = 1 + best_miner_uid = 3 try: axon = self.neuron.metagraph.axons[best_miner_uid] async with bt.dendrite(wallet=self.neuron.wallet) as dendrite: diff --git a/requirements.txt b/requirements.txt index c3a133ef..151b2c3e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,13 +1,12 @@ ansible-vault==2.1.0 beautifulsoup4==4.12.3 bert-score==0.3.13 -bittensor==7.4.0 +bittensor colormath==3.0.0 datasets==3.2.0 ddt==1.6.0 datasets einops -flash-attn langchain==0.3.11 langchain-openai==0.2.12 lxml==5.3.0 From 2dffbd44e98d0140d40ce89b976e36b6689bc4ff Mon Sep 17 00:00:00 2001 From: donbusha Date: Thu, 19 Dec 2024 12:26:16 -0600 Subject: [PATCH 111/129] chore: added default llm config --- .env.validator.example | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.env.validator.example b/.env.validator.example index 8fac36d4..810ecffc 100644 --- a/.env.validator.example +++ b/.env.validator.example @@ -2,5 +2,5 @@ OPENAI_API_KEY = your_openai_api_key WANDB_API_KEY = your_wandb_api_key WANDB_ENTITY_NAME = your_wandb_entity_name -LLM_MODEL_ID = gpt-4o-mini -LLM_MODEL_URL = \ No newline at end of file +LLM_MODEL_ID = gpt-3.5-turbo +LLM_MODEL_URL = https://api.openai.com/v1/ \ No newline at end of file From 136691753c57f6cf2f798c877b6231b75b65020a Mon Sep 17 00:00:00 2001 From: donbusha Date: Thu, 19 Dec 2024 18:18:54 -0600 Subject: [PATCH 112/129] fix: fixed clearing state --- neurons/validators/validator.py | 5 ----- webgenie/base/validator.py | 28 ++++++++++++++++++++------- webgenie/rewards/incentive_rewards.py | 5 ++++- webgenie/rewards/visual_reward.py | 1 + 4 files changed, 26 insertions(+), 13 deletions(-) diff --git a/neurons/validators/validator.py b/neurons/validators/validator.py index acd0a573..a3cbe5f6 100644 --- a/neurons/validators/validator.py +++ b/neurons/validators/validator.py @@ -12,7 +12,6 @@ from webgenie.base.validator import BaseValidatorNeuron from webgenie.constants import API_HOTKEY -from webgenie.helpers.weights import init_wandb from webgenie.protocol import WebgenieTextSynapse, WebgenieImageSynapse from neurons.validators.genie_validator import GenieValidator @@ -27,10 +26,6 @@ class Validator(BaseValidatorNeuron): def __init__(self, config=None): super(Validator, self).__init__(config=config) - bt.logging.info("load_state()") - self.load_state() - init_wandb(self) - if not self.config.axon_off: self.serve_axon() diff --git a/webgenie/base/validator.py b/webgenie/base/validator.py index 89af5768..b7bc27be 100644 --- a/webgenie/base/validator.py +++ b/webgenie/base/validator.py @@ -32,10 +32,10 @@ process_weights_for_netuid, convert_weights_and_uids_for_emit, ) # TODO: Replace when bittensor switches to numpy +from webgenie.helpers.weights import init_wandb from webgenie.mock import MockDendrite from webgenie.utils.config import add_validator_args - class BaseValidatorNeuron(BaseNeuron): """ Base class for Bittensor validators. Your validator should inherit from this class. @@ -49,7 +49,8 @@ def add_args(cls, parser: argparse.ArgumentParser): add_validator_args(cls, parser) def __init__(self, config=None): - super().__init__(config=config) + super().__init__(config=config) + init_wandb(self) # Save a copy of the hotkeys to local memory. self.hotkeys = copy.deepcopy(self.metagraph.hotkeys) @@ -64,7 +65,10 @@ def __init__(self, config=None): # Set up initial scoring weights for validation bt.logging.info("Building validation weights.") self.scores = np.zeros(self.metagraph.n, dtype=np.float32) - + + bt.logging.info("load_state()") + self.load_state() + # Init sync with the network. Updates the metagraph. self.sync() @@ -228,7 +232,7 @@ def update_scores(self, rewards: np.ndarray, uids: List[int]): def save_state(self): """Saves the state of the validator to a file.""" - #bt.logging.info("Saving validator state.") + bt.logging.info("Saving validator state.") # Save the state of the validator to file. np.savez( @@ -237,6 +241,8 @@ def save_state(self): scores=self.scores, hotkeys=self.hotkeys, ) + + bt.logging.debug(f"Saved state: step={self.step}, scores={self.scores}, hotkeys={self.hotkeys}") def load_state(self): """Loads the state of the validator from a file.""" @@ -244,6 +250,14 @@ def load_state(self): # Load the state of the validator from file. state = np.load(self.config.neuron.full_path + "/state.npz") - self.step = state["step"] - self.scores = state["scores"] - self.hotkeys = state["hotkeys"] + if "step" in state: + self.step = state["step"] + self.scores = state["scores"] + self.hotkeys = state["hotkeys"] + else: + bt.logging.warning("No state found. Initializing with default values.") + self.step = 0 + self.scores = np.zeros(self.metagraph.n, dtype=np.float32) + self.hotkeys = copy.deepcopy(self.metagraph.hotkeys) + + bt.logging.debug(f"Loaded state: step={self.step}, scores={self.scores}, hotkeys={self.hotkeys}") diff --git a/webgenie/rewards/incentive_rewards.py b/webgenie/rewards/incentive_rewards.py index f048d06b..1da56501 100644 --- a/webgenie/rewards/incentive_rewards.py +++ b/webgenie/rewards/incentive_rewards.py @@ -1,3 +1,4 @@ +import bittensor as bt import numpy as np def get_incentive_rewards(scores: np.ndarray, base_reward=100, alpha=1.5) -> np.ndarray: @@ -13,6 +14,7 @@ def get_incentive_rewards(scores: np.ndarray, base_reward=100, alpha=1.5) -> np. Returns: - rewards: NumPy array of rewards corresponding to the original order of scores. """ + bt.logging.debug(f"Scores: {scores}") threshold = scores.shape[0] // 2 # Ensure input is a NumPy array @@ -35,7 +37,8 @@ def get_incentive_rewards(scores: np.ndarray, base_reward=100, alpha=1.5) -> np. reward = base_reward * (alpha ** (rank - threshold)) # Exponential scaling rewards[idx] = reward # Assign reward to the corresponding index - + + bt.logging.debug(f"Rewards: {rewards}") return rewards diff --git a/webgenie/rewards/visual_reward.py b/webgenie/rewards/visual_reward.py index 68824984..14c94be0 100644 --- a/webgenie/rewards/visual_reward.py +++ b/webgenie/rewards/visual_reward.py @@ -33,4 +33,5 @@ async def reward(self, task: Task, solutions: List[Solution]) -> np.ndarray: miner_html_paths.append(path) visual_scores = visual_eval_v3_multi([miner_html_paths, original_html_path]) + bt.logging.debug(f"Visual scores: {visual_scores}") return np.array([score[1] for score in visual_scores]) From 1569d5c4b4b164fc90c2d778e95a5f2c64be0ccf Mon Sep 17 00:00:00 2001 From: donbusha Date: Thu, 19 Dec 2024 20:02:51 -0600 Subject: [PATCH 113/129] feat: updated s_bert --- requirements.txt | 1 + webgenie/base/validator.py | 4 ++-- webgenie/rewards/metrics/s_bert.py | 25 +++++++++++++++++++++++++ webgenie/rewards/rtc_reward.py | 11 ++++++++--- 4 files changed, 36 insertions(+), 5 deletions(-) create mode 100644 webgenie/rewards/metrics/s_bert.py diff --git a/requirements.txt b/requirements.txt index 151b2c3e..396d10d2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -17,6 +17,7 @@ pip-chill==1.0.3 playwright==1.49.1 python-dotenv==1.0.1 scikit-learn==1.6.0 +sentence-transformers shtab==1.6.5 tinycss2==1.4.0 wandb==0.19.0 diff --git a/webgenie/base/validator.py b/webgenie/base/validator.py index b7bc27be..2033134e 100644 --- a/webgenie/base/validator.py +++ b/webgenie/base/validator.py @@ -242,7 +242,7 @@ def save_state(self): hotkeys=self.hotkeys, ) - bt.logging.debug(f"Saved state: step={self.step}, scores={self.scores}, hotkeys={self.hotkeys}") + bt.logging.debug(f"Saved state: step={self.step}, scores={self.scores}") def load_state(self): """Loads the state of the validator from a file.""" @@ -260,4 +260,4 @@ def load_state(self): self.scores = np.zeros(self.metagraph.n, dtype=np.float32) self.hotkeys = copy.deepcopy(self.metagraph.hotkeys) - bt.logging.debug(f"Loaded state: step={self.step}, scores={self.scores}, hotkeys={self.hotkeys}") + bt.logging.debug(f"Loaded state: step={self.step}, scores={self.scores}") diff --git a/webgenie/rewards/metrics/s_bert.py b/webgenie/rewards/metrics/s_bert.py new file mode 100644 index 00000000..e58152f6 --- /dev/null +++ b/webgenie/rewards/metrics/s_bert.py @@ -0,0 +1,25 @@ +from sentence_transformers import SentenceTransformer +from sklearn.metrics.pairwise import cosine_similarity + +model = SentenceTransformer('all-MiniLM-L6-v2') + +def score(sentences1, sentences2): + embeddings1 = model.encode(sentences1) + embeddings2 = model.encode(sentences2) + similarities = cosine_similarity(embeddings1, embeddings2) + # Scale similarities to be between 0 and 1 + scores = [(float(similarity[0]) + 1) / 2 for similarity in similarities] + return scores + +if __name__ == "__main__": + # Define a list of sentence pairs + sentence_pairs = [ + ("The cat is on the mat.", "A cat sits on a rug."), + ("I am going to the store.", "I will head to the shop."), + ("The weather is great today.", "It is sunny outside."), + ("She loves playing the piano.", "She enjoys playing the guitar.") + ] + sentences1 = [pair[0] for pair in sentence_pairs] + sentences2 = [pair[1] for pair in sentence_pairs] + # Call the function + print(score(sentences1, sentences2)) diff --git a/webgenie/rewards/rtc_reward.py b/webgenie/rewards/rtc_reward.py index e4b75f0b..424cef58 100644 --- a/webgenie/rewards/rtc_reward.py +++ b/webgenie/rewards/rtc_reward.py @@ -14,9 +14,11 @@ from webgenie.prompts import PROMPT_RTC from webgenie.rewards import Reward +from webgenie.rewards.metrics import s_bert from webgenie.tasks.task import Task from webgenie.tasks.solution import Solution + class PromptResponse(BaseModel): prompt: str = Field(default="", description="The prompt that generates the given html code") @@ -29,7 +31,7 @@ def __init__(self): ) self.prompt_response_parser = JsonOutputParser(pydantic_object=PromptResponse) - + async def _get_prompt(self, task: Task, solutions: List[Solution]) -> str: prompt = ChatPromptTemplate.from_messages([ SystemMessagePromptTemplate.from_template(PROMPT_RTC) @@ -48,8 +50,11 @@ async def reward(self, task: Task, solutions: List[Solution]) -> np.ndarray: bt.logging.debug(f"Rewarding task in rtc reward") original_prompts = [task.prompt for _ in solutions] miner_prompts = [await self._get_prompt(task, solution) for solution in solutions] - P, R, F1 = bert_score.score(original_prompts, miner_prompts, lang='en') - return np.array(R) + + #P, R, F1 = bert_score.score(original_prompts, miner_prompts, lang='en') + scores = s_bert.score(original_prompts, miner_prompts) + return np.array(scores) + From 7df5a6244efeb6f28e2435dc12980b3d88a19273 Mon Sep 17 00:00:00 2001 From: donbusha Date: Thu, 2 Jan 2025 02:25:44 -0600 Subject: [PATCH 114/129] feat: implemented batch scoring --- neurons/validators/genie_validator.py | 32 ++++++++++++++++++++------- neurons/validators/validator.py | 3 +-- webgenie/base/validator.py | 7 +++--- webgenie/constants.py | 3 +++ webgenie/rewards/metrics/s_bert.py | 2 +- webgenie/tasks/task_generator.py | 3 +-- 6 files changed, 33 insertions(+), 17 deletions(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 14f67f2d..ee3912f5 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -1,11 +1,20 @@ +import os import bittensor as bt +import numpy as np import random -from typing import Union +from typing import Union, List from webgenie.base.neuron import BaseNeuron -from webgenie.constants import MAX_SYNTHETIC_HISTORY_SIZE, MAX_SYNTHETIC_TASK_SIZE, MAX_DEBUG_IMAGE_STRING_LENGTH +from webgenie.constants import ( + MAX_SYNTHETIC_HISTORY_SIZE, + MAX_SYNTHETIC_TASK_SIZE, + MAX_DEBUG_IMAGE_STRING_LENGTH, + UPDATE_SCORES_STEP, + WORK_DIR +) from webgenie.helpers.htmls import preprocess_html from webgenie.protocol import WebgenieImageSynapse, WebgenieTextSynapse +from webgenie.rewards.incentive_rewards import get_incentive_rewards from webgenie.tasks.solution import Solution from webgenie.tasks.image_task_generator import ImageTaskGenerator from webgenie.tasks.text_task_generator import TextTaskGenerator @@ -26,9 +35,6 @@ def __init__(self, neuron: BaseNeuron): self.make_work_dir() def make_work_dir(self): - import os - from webgenie.constants import WORK_DIR - if not os.path.exists(WORK_DIR): os.makedirs(WORK_DIR) bt.logging.info(f"Created work directory at {WORK_DIR}") @@ -65,7 +71,18 @@ async def forward(self): except Exception as e: bt.logging.error(f"Error in forward: {e}") raise e - + + def update_raw_scores(self, rewards: np.ndarray, uids: List[int]): + rewards = np.asarray(rewards) + uids = np.asarray(uids) + + self.neuron.raw_scores[uids] += rewards + self.neuron.step += 1 + + if self.neuron.step % UPDATE_SCORES_STEP == 0: + incentive_rewards = get_incentive_rewards(self.neuron.raw_scores) + self.neuron.update_scores(incentive_rewards, [i for i in range(self.neuron.metagraph.n)]) + async def score(self): if not self.synthetic_history: return @@ -79,8 +96,7 @@ async def score(self): rewards = await task_generator.reward(task, solutions) bt.logging.debug(f"Incentive rewards: {rewards}") - self.neuron.update_scores(rewards, miner_uids) - self.neuron.sync() + self.update_raw_scores(rewards, miner_uids) async def synthensize_task(self): try: diff --git a/neurons/validators/validator.py b/neurons/validators/validator.py index a3cbe5f6..a7a61bb2 100644 --- a/neurons/validators/validator.py +++ b/neurons/validators/validator.py @@ -92,10 +92,8 @@ async def forward_loop(self): bt.logging.info(f"Validator starting at block: {self.block}") while True: try: - bt.logging.info(f"step({self.step}) block({self.block})") self.loop.run_until_complete(self.concurrent_forward()) self.sync() - self.step += 1 except Exception as e: bt.logging.error(f"Error during forward loop: {str(e)}") await asyncio.sleep(1) @@ -105,6 +103,7 @@ async def scoring_loop(self): while True: try: await self.genie_validator.score() + self.sync() except Exception as e: bt.logging.error(f"Error during scoring: {str(e)}") await asyncio.sleep(1) diff --git a/webgenie/base/validator.py b/webgenie/base/validator.py index 2033134e..137a47a1 100644 --- a/webgenie/base/validator.py +++ b/webgenie/base/validator.py @@ -61,10 +61,6 @@ def __init__(self, config=None): else: 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) bt.logging.info("load_state()") self.load_state() @@ -238,6 +234,7 @@ def save_state(self): np.savez( self.config.neuron.full_path + "/state.npz", step=self.step, + raw_scores=self.raw_scores, scores=self.scores, hotkeys=self.hotkeys, ) @@ -252,11 +249,13 @@ def load_state(self): state = np.load(self.config.neuron.full_path + "/state.npz") if "step" in state: self.step = state["step"] + self.raw_scores = state["raw_scores"] self.scores = state["scores"] self.hotkeys = state["hotkeys"] else: bt.logging.warning("No state found. Initializing with default values.") self.step = 0 + self.raw_scores = np.zeros(self.metagraph.n, dtype=np.float32) self.scores = np.zeros(self.metagraph.n, dtype=np.float32) self.hotkeys = copy.deepcopy(self.metagraph.hotkeys) diff --git a/webgenie/constants.py b/webgenie/constants.py index df5f284d..d4155f92 100644 --- a/webgenie/constants.py +++ b/webgenie/constants.py @@ -10,6 +10,9 @@ # max debug image string length MAX_DEBUG_IMAGE_STRING_LENGTH = 20 +# update scores step +UPDATE_SCORES_STEP = 10 + # place holder image url PLACE_HOLDER_IMAGE_URL = "https://picsum.photos/seed/picsum/800/600" diff --git a/webgenie/rewards/metrics/s_bert.py b/webgenie/rewards/metrics/s_bert.py index e58152f6..264cb55c 100644 --- a/webgenie/rewards/metrics/s_bert.py +++ b/webgenie/rewards/metrics/s_bert.py @@ -9,7 +9,7 @@ def score(sentences1, sentences2): similarities = cosine_similarity(embeddings1, embeddings2) # Scale similarities to be between 0 and 1 scores = [(float(similarity[0]) + 1) / 2 for similarity in similarities] - return scores + return [similarity[0] for similarity in similarities] if __name__ == "__main__": # Define a list of sentence pairs diff --git a/webgenie/tasks/task_generator.py b/webgenie/tasks/task_generator.py index 6a546498..562f66dc 100644 --- a/webgenie/tasks/task_generator.py +++ b/webgenie/tasks/task_generator.py @@ -21,6 +21,5 @@ async def reward(self, task: Task, solutions: List[Solution]) -> np.ndarray: for reward, weight in self.rewards: reward_scores = await reward.reward(task, solutions) scores += weight * np.array(reward_scores) - rewards = get_incentive_rewards(scores) - return rewards + return scores From 73165c16bf0da04a3a56954259d050aee5612cb7 Mon Sep 17 00:00:00 2001 From: donbusha Date: Thu, 2 Jan 2025 02:31:34 -0600 Subject: [PATCH 115/129] feat: updated incentive reward --- webgenie/rewards/incentive_rewards.py | 34 ++++----------------------- 1 file changed, 5 insertions(+), 29 deletions(-) diff --git a/webgenie/rewards/incentive_rewards.py b/webgenie/rewards/incentive_rewards.py index 1da56501..e82efaf5 100644 --- a/webgenie/rewards/incentive_rewards.py +++ b/webgenie/rewards/incentive_rewards.py @@ -1,44 +1,20 @@ -import bittensor as bt import numpy as np def get_incentive_rewards(scores: np.ndarray, base_reward=100, alpha=1.5) -> np.ndarray: - """ - Calculate rewards based on the piecewise linear with exponential growth method, - preserving the original order of the scores, and returning rewards as a NumPy array. - - Parameters: - - scores: NumPy array of raw scores. - - base_reward: The minimum reward for the highest rank (rank 1). - - alpha: The exponential scaling factor for ranks above the threshold. - - Returns: - - rewards: NumPy array of rewards corresponding to the original order of scores. - """ - bt.logging.debug(f"Scores: {scores}") threshold = scores.shape[0] // 2 - - # Ensure input is a NumPy array scores = np.array(scores) - - # Rank players based on scores (highest score gets better rank) - sorted_scores = np.sort(scores) # Sort in ascending order - score_to_rank = {score: idx + 1 for idx, score in enumerate(sorted_scores)} # Map each score to its rank - # Calculate rewards for each score based on its rank - rewards = np.zeros_like(scores, dtype=float) # Initialize the rewards array + sorted_scores = np.sort(scores) + score_to_rank = {score: idx + 1 for idx, score in enumerate(sorted_scores)} + rewards = np.zeros_like(scores, dtype=float) for idx, score in enumerate(scores): rank = score_to_rank[score] - if rank <= threshold: - # Linear reward scaling for ranks <= threshold - reward = base_reward + (rank - 1) * (base_reward / 2) # Linear scaling + reward = (rank - 1) * (base_reward / 2) # Linear scaling else: - # Exponential reward scaling for ranks > threshold reward = base_reward * (alpha ** (rank - threshold)) # Exponential scaling - - rewards[idx] = reward # Assign reward to the corresponding index + rewards[idx] = reward - bt.logging.debug(f"Rewards: {rewards}") return rewards From a8aa6aaf093f550b87e6f5f818990a5d623b4ee1 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 3 Jan 2025 07:43:26 -0600 Subject: [PATCH 116/129] feat: added code quality reward --- webgenie/base/miner.py | 4 -- webgenie/prompts.py | 12 ++++++ webgenie/rewards/quality_reward.py | 58 ++++++++++++++++++++++++++ webgenie/tasks/image_task_generator.py | 4 +- webgenie/tasks/text_task_generator.py | 9 ++-- 5 files changed, 79 insertions(+), 8 deletions(-) create mode 100644 webgenie/rewards/quality_reward.py diff --git a/webgenie/base/miner.py b/webgenie/base/miner.py index 0f567652..4f449b0d 100644 --- a/webgenie/base/miner.py +++ b/webgenie/base/miner.py @@ -180,8 +180,4 @@ def __exit__(self, exc_type, exc_value, traceback): def resync_metagraph(self): """Resyncs the metagraph and updates the hotkeys and moving averages based on the new metagraph.""" - #TODO: Implement this - #bt.logging.info("resync_metagraph()") - - # Sync the metagraph. self.metagraph.sync(subtensor=self.subtensor) diff --git a/webgenie/prompts.py b/webgenie/prompts.py index c4b89048..6caa4394 100644 --- a/webgenie/prompts.py +++ b/webgenie/prompts.py @@ -40,3 +40,15 @@ {instructions} """ + +PROMPT_QUALITY = """ +You are an HTML, CSS expert. I have an HTML code. +I want you to evaluate the html code on the following criteria and give a score from 0 to 100. +1. The html code is in the professional style. +2. The html code is not using redundant code. + +The following is the given html code: +{html} + +{instructions} +""" \ No newline at end of file diff --git a/webgenie/rewards/quality_reward.py b/webgenie/rewards/quality_reward.py new file mode 100644 index 00000000..26b7bca3 --- /dev/null +++ b/webgenie/rewards/quality_reward.py @@ -0,0 +1,58 @@ +# The paper [Unsupervised Evaluation of Code LLMs with Round-Trip Correctness] +# (https://arxiv.org/pdf/2402.08699#page=11&zoom=100,384,458) is our inspiration for this reward. + +import bittensor as bt +import bert_score +import os +import numpy as np +from typing import List + +from langchain_openai import ChatOpenAI +from langchain.prompts import ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate +from langchain_core.output_parsers import JsonOutputParser +from langchain_core.pydantic_v1 import BaseModel, Field + +from webgenie.prompts import PROMPT_QUALITY +from webgenie.rewards import Reward +from webgenie.rewards.metrics import s_bert +from webgenie.tasks.task import Task +from webgenie.tasks.solution import Solution + + +class ScoreResponse(BaseModel): + score: float = Field(default=0, description="The score of the html code") + +class QualityReward(Reward): + def __init__(self): + self.model = ChatOpenAI( + api_key= os.getenv("OPENAI_API_KEY"), + model_name=os.getenv("LLM_MODEL_ID"), + base_url=os.getenv("LLM_MODEL_URL"), + ) + + self.prompt_response_parser = JsonOutputParser(pydantic_object=ScoreResponse) + + async def _get_score(self, solution: Solution) -> float: + prompt = ChatPromptTemplate.from_messages([ + SystemMessagePromptTemplate.from_template(PROMPT_QUALITY) + ]) + + chain = prompt | self.model | self.prompt_response_parser + prompt_response = await chain.ainvoke({ + "html": solution.html, + "instructions": self.prompt_response_parser.get_format_instructions() + }) + + return float(prompt_response["score"]) / 100 + + async def reward(self, task: Task, solutions: List[Solution]) -> np.ndarray: + bt.logging.debug(f"Rewarding task in quality reward") + scores = [] + for solution in solutions: + score = await self._get_score(solution) + scores.append(score) + return np.array(scores) + + + + \ No newline at end of file diff --git a/webgenie/tasks/image_task_generator.py b/webgenie/tasks/image_task_generator.py index 9e5507ba..cf25584d 100644 --- a/webgenie/tasks/image_task_generator.py +++ b/webgenie/tasks/image_task_generator.py @@ -8,6 +8,7 @@ from webgenie.tasks.solution import Solution from webgenie.tasks.task import Task, ImageTask from webgenie.tasks.task_generator import TaskGenerator +from webgenie.rewards.quality_reward import QualityReward from webgenie.rewards.visual_reward import VisualReward from webgenie.datasets.mockup_dataset import MockUpDataset from webgenie.datasets.synthetic_dataset import SyntheticDataset @@ -17,7 +18,8 @@ class ImageTaskGenerator(TaskGenerator): def __init__(self): super().__init__() self.rewards = [ - (VisualReward(), 1.0) + (VisualReward(), 0.9), + (QualityReward(), 0.1) ] self.datasets = [ # MockUpDataset(), diff --git a/webgenie/tasks/text_task_generator.py b/webgenie/tasks/text_task_generator.py index 9387b83a..272e8ecf 100644 --- a/webgenie/tasks/text_task_generator.py +++ b/webgenie/tasks/text_task_generator.py @@ -8,6 +8,7 @@ ) from webgenie.protocol import WebgenieTextSynapse from webgenie.rewards.bert_reward import BertReward +from webgenie.rewards.quality_reward import QualityReward from webgenie.rewards.rtc_reward import RtcReward from webgenie.tasks.task import Task, TextTask from webgenie.tasks.task_generator import TaskGenerator @@ -17,8 +18,9 @@ def __init__(self, has_ground_truth_html: bool = True): super().__init__() if has_ground_truth_html: self.rewards = [ - (BertReward(), 0.5), - (RtcReward(), 0.5) + (BertReward(), 0.4), + (RtcReward(), 0.5), + (QualityReward(), 0.1) ] self.datasets = [ @@ -27,7 +29,8 @@ def __init__(self, has_ground_truth_html: bool = True): ] else: self.rewards = [ - (RtcReward(), 1.0) + (RtcReward(), 0.9), + (QualityReward(), 0.1) ] self.datasets = [ From b862df3d8c027d95b1c07d9576af3093c91c6d6b Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Fri, 3 Jan 2025 07:49:58 -0600 Subject: [PATCH 117/129] chore: added code quality criteria --- webgenie/prompts.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/webgenie/prompts.py b/webgenie/prompts.py index 6caa4394..a6771b2a 100644 --- a/webgenie/prompts.py +++ b/webgenie/prompts.py @@ -45,7 +45,8 @@ You are an HTML, CSS expert. I have an HTML code. I want you to evaluate the html code on the following criteria and give a score from 0 to 100. 1. The html code is in the professional style. -2. The html code is not using redundant code. +2. The html code is using SEO-friendly practices. +3. The html code is not using redundant code. The following is the given html code: {html} From 975ff6ec56df04730363e8a25e3784f0ba301d06 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sun, 5 Jan 2025 00:18:04 -0600 Subject: [PATCH 118/129] chore: implemented stressing miners --- .env.miner.example | 2 +- .env.validator.example | 2 +- neurons/miners/openai_miner.py | 2 +- neurons/validators/genie_validator.py | 36 +++++++++++++----------- neurons/validators/validator.py | 31 +++++++++++--------- webgenie/base/neuron.py | 8 ------ webgenie/base/validator.py | 7 ----- webgenie/constants.py | 7 ++--- webgenie/datasets/huggingface_dataset.py | 2 +- webgenie/datasets/synthetic_dataset.py | 2 +- webgenie/helpers/llm_calls.py | 0 webgenie/rewards/quality_reward.py | 2 +- webgenie/rewards/rtc_reward.py | 2 +- 13 files changed, 45 insertions(+), 58 deletions(-) create mode 100644 webgenie/helpers/llm_calls.py diff --git a/.env.miner.example b/.env.miner.example index 4a124b0b..a974dd68 100644 --- a/.env.miner.example +++ b/.env.miner.example @@ -1,4 +1,4 @@ -OPENAI_API_KEY = your_openai_api_key +LLM_API_KEY = your_openai_api_key WANDB_API_KEY = your_wandb_api_key WANDB_ENTITY_NAME = your_wandb_entity_name diff --git a/.env.validator.example b/.env.validator.example index 810ecffc..c47a6452 100644 --- a/.env.validator.example +++ b/.env.validator.example @@ -1,4 +1,4 @@ -OPENAI_API_KEY = your_openai_api_key +LLM_API_KEY = your_openai_api_key WANDB_API_KEY = your_wandb_api_key WANDB_ENTITY_NAME = your_wandb_entity_name diff --git a/neurons/miners/openai_miner.py b/neurons/miners/openai_miner.py index 7bcda60d..1a2bde79 100644 --- a/neurons/miners/openai_miner.py +++ b/neurons/miners/openai_miner.py @@ -18,7 +18,7 @@ def __init__(self, neuron: BaseNeuron): self.neuron = neuron self.model = ChatOpenAI( - api_key= os.getenv("OPENAI_API_KEY"), + api_key= os.getenv("LLM_API_KEY"), model_name="gpt-4o", ) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index ee3912f5..db2b8fa9 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -26,6 +26,7 @@ def __init__(self, neuron: BaseNeuron): self.config = neuron.config self.synthetic_history = [] self.synthetic_tasks = [] + self.un_responsed_count = [0] * self.neuron.metagraph.n self.task_generators = [ (TextTaskGenerator(), 0.1), @@ -39,15 +40,17 @@ def make_work_dir(self): os.makedirs(WORK_DIR) bt.logging.info(f"Created work directory at {WORK_DIR}") - async def forward(self): + async def query_miners(self): try: if len(self.synthetic_history) > MAX_SYNTHETIC_HISTORY_SIZE: return if not self.synthetic_tasks: return - bt.logging.info("Popping synthetic task and sending it to miners") + task, synapse = self.synthetic_tasks.pop(0) + bt.logging.info("Popping synthetic task and sending it to miners") + miner_uids = get_random_uids(self.neuron, k=self.config.neuron.sample_size) bt.logging.debug(f"Selected miner uids: {miner_uids}") @@ -58,45 +61,44 @@ async def forward(self): ) solutions = [] + for synapse, miner_uid in zip(all_synapse_results, miner_uids): processed_synapse = await self.process_synapse(synapse) if processed_synapse is not None: solutions.append(Solution(html = processed_synapse.html, miner_uid = miner_uid, process_time = processed_synapse.dendrite.process_time)) - + else: + self.un_responsed_count[miner_uid] += 1 + if not solutions: bt.logging.warning(f"No valid solutions received") return bt.logging.info(f"Received {len(solutions)} solutions") + self.synthetic_history.append((task, solutions)) except Exception as e: bt.logging.error(f"Error in forward: {e}") raise e - - def update_raw_scores(self, rewards: np.ndarray, uids: List[int]): - rewards = np.asarray(rewards) - uids = np.asarray(uids) - - self.neuron.raw_scores[uids] += rewards - self.neuron.step += 1 - - if self.neuron.step % UPDATE_SCORES_STEP == 0: - incentive_rewards = get_incentive_rewards(self.neuron.raw_scores) - self.neuron.update_scores(incentive_rewards, [i for i in range(self.neuron.metagraph.n)]) async def score(self): if not self.synthetic_history: return - task, solutions = self.synthetic_history.pop(0) + task, solutions = random.choice(self.synthetic_history) + self.synthetic_history = [] + task_generator = task.generator miner_uids = [solution.miner_uid for solution in solutions] bt.logging.debug(f"Miner uids: {miner_uids}") rewards = await task_generator.reward(task, solutions) - bt.logging.debug(f"Incentive rewards: {rewards}") - self.update_raw_scores(rewards, miner_uids) + for i in range(len(miner_uids)): + responsed_ratio = 1 - self.un_responsed_count[miner_uids[i]] / len(self.synthetic_history) + rewards[i] = rewards[i] * responsed_ratio * responsed_ratio + + bt.logging.debug(f"Incentive rewards: {rewards}") + self.neuron.update_scores(rewards, miner_uids) async def synthensize_task(self): try: diff --git a/neurons/validators/validator.py b/neurons/validators/validator.py index a7a61bb2..c833de85 100644 --- a/neurons/validators/validator.py +++ b/neurons/validators/validator.py @@ -77,28 +77,28 @@ def serve_axon(self): bt.logging.error(f"Failed to serve Axon with exception: {e}") pass - async def forward(self): - return await self.genie_validator.forward() + async def query_miners(self): + return await self.genie_validator.query_miners() - async def concurrent_forward(self): + async def concurrent_query(self): coroutines = [ - self.forward() + self.query_miners() for _ in range(self.config.neuron.num_concurrent_forwards) ] await asyncio.gather(*coroutines) - async def forward_loop(self): - self.sync() + async def query_miners_loop(self): bt.logging.info(f"Validator starting at block: {self.block}") + self.sync() while True: try: - self.loop.run_until_complete(self.concurrent_forward()) + self.loop.run_until_complete(self.concurrent_query()) self.sync() except Exception as e: bt.logging.error(f"Error during forward loop: {str(e)}") await asyncio.sleep(1) - async def scoring_loop(self): + async def score_loop(self): bt.logging.info(f"Scoring loop starting") while True: try: @@ -119,17 +119,20 @@ async def synthensize_task_loop(self): async def __aenter__(self): self.loop.create_task(self.synthensize_task_loop()) - self.loop.create_task(self.forward_loop()) - self.loop.create_task(self.scoring_loop()) + self.loop.create_task(self.query_miners_loop()) + self.loop.create_task(self.score_loop()) self.is_running = True + bt.logging.debug("Starting validator in background thread") return self async def __aexit__(self, exc_type, exc_value, traceback): - if self.is_running: - self.should_exit = True - self.is_running = False - bt.logging.debug("Stopping validator in background thread") + if not self.is_running: + return + + self.should_exit = True + self.is_running = False + bt.logging.debug("Stopping validator in background thread") async def main(): async with Validator() as validator: diff --git a/webgenie/base/neuron.py b/webgenie/base/neuron.py index 1eed9a77..3fffe10b 100644 --- a/webgenie/base/neuron.py +++ b/webgenie/base/neuron.py @@ -108,14 +108,6 @@ def __init__(self, config=None): ) self.step = 0 - @abstractmethod - async def forward(self, synapse: bt.Synapse) -> bt.Synapse: - ... - - @abstractmethod - def run(self): - ... - def sync(self): """ Wrapper for synchronizing the state of the network for the given miner or validator. diff --git a/webgenie/base/validator.py b/webgenie/base/validator.py index 137a47a1..bfc9d8e7 100644 --- a/webgenie/base/validator.py +++ b/webgenie/base/validator.py @@ -76,10 +76,6 @@ def __init__(self, config=None): self.is_running: bool = False self.thread: Union[threading.Thread, None] = None self.lock = asyncio.Lock() - - - def run(self): - pass def set_weights(self): """ @@ -234,7 +230,6 @@ def save_state(self): np.savez( self.config.neuron.full_path + "/state.npz", step=self.step, - raw_scores=self.raw_scores, scores=self.scores, hotkeys=self.hotkeys, ) @@ -249,13 +244,11 @@ def load_state(self): state = np.load(self.config.neuron.full_path + "/state.npz") if "step" in state: self.step = state["step"] - self.raw_scores = state["raw_scores"] self.scores = state["scores"] self.hotkeys = state["hotkeys"] else: bt.logging.warning("No state found. Initializing with default values.") self.step = 0 - self.raw_scores = np.zeros(self.metagraph.n, dtype=np.float32) self.scores = np.zeros(self.metagraph.n, dtype=np.float32) self.hotkeys = copy.deepcopy(self.metagraph.hotkeys) diff --git a/webgenie/constants.py b/webgenie/constants.py index d4155f92..0cc3725c 100644 --- a/webgenie/constants.py +++ b/webgenie/constants.py @@ -2,17 +2,14 @@ API_HOTKEY = "5DXDCYTuPfLqQXbxfvvnarG31SdTDtaubqpQrzjrcMgoP9dp" # max synthetic history size -MAX_SYNTHETIC_HISTORY_SIZE = 3 +MAX_SYNTHETIC_HISTORY_SIZE = 30 # max synthensize task size -MAX_SYNTHETIC_TASK_SIZE = 3 +MAX_SYNTHETIC_TASK_SIZE = 30 # max debug image string length MAX_DEBUG_IMAGE_STRING_LENGTH = 20 -# update scores step -UPDATE_SCORES_STEP = 10 - # place holder image url PLACE_HOLDER_IMAGE_URL = "https://picsum.photos/seed/picsum/800/600" diff --git a/webgenie/datasets/huggingface_dataset.py b/webgenie/datasets/huggingface_dataset.py index 6ef1fb80..1ae7c7b7 100644 --- a/webgenie/datasets/huggingface_dataset.py +++ b/webgenie/datasets/huggingface_dataset.py @@ -24,7 +24,7 @@ def __init__(self , dataset_name: str, split: str, html_field: str): self.model = ChatOpenAI( base_url=os.getenv("LLM_MODEL_URL"), model=os.getenv("LLM_MODEL_ID"), - api_key=os.getenv("OPENAI_API_KEY") + api_key=os.getenv("LLM_API_KEY") ) self.output_parser = JsonOutputParser(pydantic_object=HTMLResponse) diff --git a/webgenie/datasets/synthetic_dataset.py b/webgenie/datasets/synthetic_dataset.py index 58e57124..0c8b3100 100644 --- a/webgenie/datasets/synthetic_dataset.py +++ b/webgenie/datasets/synthetic_dataset.py @@ -26,7 +26,7 @@ def __init__(self, has_ground_truth_html: bool = True): self.has_ground_truth_html = has_ground_truth_html self.model = ChatOpenAI( - api_key= os.getenv("OPENAI_API_KEY"), + api_key= os.getenv("LLM_API_KEY"), model_name=os.getenv("LLM_MODEL_ID"), base_url=os.getenv("LLM_MODEL_URL"), temperature=0.6, diff --git a/webgenie/helpers/llm_calls.py b/webgenie/helpers/llm_calls.py new file mode 100644 index 00000000..e69de29b diff --git a/webgenie/rewards/quality_reward.py b/webgenie/rewards/quality_reward.py index 26b7bca3..35fc3ea9 100644 --- a/webgenie/rewards/quality_reward.py +++ b/webgenie/rewards/quality_reward.py @@ -25,7 +25,7 @@ class ScoreResponse(BaseModel): class QualityReward(Reward): def __init__(self): self.model = ChatOpenAI( - api_key= os.getenv("OPENAI_API_KEY"), + api_key= os.getenv("LLM_API_KEY"), model_name=os.getenv("LLM_MODEL_ID"), base_url=os.getenv("LLM_MODEL_URL"), ) diff --git a/webgenie/rewards/rtc_reward.py b/webgenie/rewards/rtc_reward.py index 424cef58..36f6e8c4 100644 --- a/webgenie/rewards/rtc_reward.py +++ b/webgenie/rewards/rtc_reward.py @@ -25,7 +25,7 @@ class PromptResponse(BaseModel): class RtcReward(Reward): def __init__(self): self.model = ChatOpenAI( - api_key= os.getenv("OPENAI_API_KEY"), + api_key= os.getenv("LLM_API_KEY"), model_name=os.getenv("LLM_MODEL_ID"), base_url=os.getenv("LLM_MODEL_URL"), ) From 6fd2e2fffefb185d5792fcec529054710b29dbca Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sun, 5 Jan 2025 02:44:45 -0600 Subject: [PATCH 119/129] feat: implemented stressing miners --- neurons/miners/miner.py | 1 - neurons/miners/openai_miner.py | 33 ++- neurons/validators/genie_validator.py | 10 +- run_miners.sh | 2 + run_validator copy.sh | 3 + webgenie/base/validator.py | 3 - webgenie/constants.py | 9 + webgenie/datasets/__init__.py | 1 - webgenie/datasets/huggingface_dataset.py | 32 ++- webgenie/datasets/mockup_dataset.py | 264 ----------------------- webgenie/datasets/synthetic_dataset.py | 38 ++-- webgenie/helpers/llm_calls.py | 0 webgenie/helpers/llms.py | 26 +++ webgenie/rewards/__init__.py | 4 +- webgenie/rewards/quality_reward.py | 32 +-- webgenie/rewards/rtc_reward.py | 32 +-- webgenie/tasks/image_task_generator.py | 12 +- webgenie/tasks/text_task_generator.py | 36 +--- webgenie/utils/config.py | 2 +- 19 files changed, 131 insertions(+), 409 deletions(-) create mode 100644 run_miners.sh create mode 100644 run_validator copy.sh delete mode 100644 webgenie/datasets/mockup_dataset.py delete mode 100644 webgenie/helpers/llm_calls.py create mode 100644 webgenie/helpers/llms.py diff --git a/neurons/miners/miner.py b/neurons/miners/miner.py index 61d17ec6..acc01d50 100644 --- a/neurons/miners/miner.py +++ b/neurons/miners/miner.py @@ -168,7 +168,6 @@ async def priority(self, synapse: bt.Synapse) -> float: bt.logging.warning("Received a request without a dendrite or hotkey.") return 0.0 - # TODO(developer): Define how miners should prioritize requests. caller_uid = self.metagraph.hotkeys.index( synapse.dendrite.hotkey ) # Get the caller index. diff --git a/neurons/miners/openai_miner.py b/neurons/miners/openai_miner.py index 1a2bde79..daaff6ea 100644 --- a/neurons/miners/openai_miner.py +++ b/neurons/miners/openai_miner.py @@ -7,6 +7,7 @@ from langchain_core.pydantic_v1 import BaseModel, Field from webgenie.base.neuron import BaseNeuron +from webgenie.helpers.llms import call_llm from webgenie.protocol import WebgenieTextSynapse, WebgenieImageSynapse from webgenie.tasks.solution import Solution @@ -17,16 +18,11 @@ class OpenaiMiner: def __init__(self, neuron: BaseNeuron): self.neuron = neuron - self.model = ChatOpenAI( - api_key= os.getenv("LLM_API_KEY"), - model_name="gpt-4o", - ) - self.html_response_parser = JsonOutputParser(pydantic_object=HTMLResponse) async def forward_text(self, synapse: WebgenieTextSynapse) -> WebgenieTextSynapse: try: - prompt = ChatPromptTemplate.from_messages([ + template = [ ("system", """You are an expert web developer who specializes in HTML and CSS. A user will provide you with the webpage requirements. You need to return a single html file that uses HTML and CSS to satisfy the requirements. Include all CSS code in the HTML file itself. If it involves any images, use "rick.jpg" as the placeholder. @@ -35,13 +31,13 @@ async def forward_text(self, synapse: WebgenieTextSynapse) -> WebgenieTextSynaps Respond with the content of the HTML+CSS file: {instructions}"""), ("user", "{query}"), - ]) + ] - chain = prompt | self.model | self.html_response_parser - html_response = await chain.ainvoke({ - "query": synapse.prompt, - "instructions": self.html_response_parser.get_format_instructions() - }) + html_response = await call_llm( + template=template, + params={"query": synapse.prompt, "instructions": self.html_response_parser.get_format_instructions()}, + output_parser=self.html_response_parser + ) synapse.html = html_response["html"] return synapse @@ -71,14 +67,11 @@ async def forward_image(self, synapse: WebgenieImageSynapse) -> WebgenieImageSyn ) ] - prompt = ChatPromptTemplate(messages=prompt_messages) - - chain = prompt | self.model | self.html_response_parser - - html_response = chain.invoke({ - "instructions": self.html_response_parser.get_format_instructions(), - "image_url": f"data:image/jpeg;base64,{synapse.base64_image}", - }) + html_response = await call_llm( + template=prompt_messages, + params={"instructions": self.html_response_parser.get_format_instructions(), "image_url": f"data:image/jpeg;base64,{synapse.base64_image}"}, + output_parser=self.html_response_parser + ) synapse.html = html_response["html"] return synapse diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index db2b8fa9..2e743948 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -9,12 +9,11 @@ MAX_SYNTHETIC_HISTORY_SIZE, MAX_SYNTHETIC_TASK_SIZE, MAX_DEBUG_IMAGE_STRING_LENGTH, - UPDATE_SCORES_STEP, + MIN_SYNTHETIC_HISTORY_SIZE_TO_SCORE, WORK_DIR ) from webgenie.helpers.htmls import preprocess_html from webgenie.protocol import WebgenieImageSynapse, WebgenieTextSynapse -from webgenie.rewards.incentive_rewards import get_incentive_rewards from webgenie.tasks.solution import Solution from webgenie.tasks.image_task_generator import ImageTaskGenerator from webgenie.tasks.text_task_generator import TextTaskGenerator @@ -26,7 +25,9 @@ def __init__(self, neuron: BaseNeuron): self.config = neuron.config self.synthetic_history = [] self.synthetic_tasks = [] - self.un_responsed_count = [0] * self.neuron.metagraph.n + bt.logging.info(f"GenieValidator initialized with neuron: {self.neuron.metagraph.n}") + self.un_responsed_count = np.zeros(self.neuron.metagraph.n, dtype=np.float32) + bt.logging.info(f"Un responsed count: {self.un_responsed_count}") self.task_generators = [ (TextTaskGenerator(), 0.1), @@ -80,7 +81,7 @@ async def query_miners(self): raise e async def score(self): - if not self.synthetic_history: + if len(self.synthetic_history) < MIN_SYNTHETIC_HISTORY_SIZE_TO_SCORE: return task, solutions = random.choice(self.synthetic_history) @@ -99,6 +100,7 @@ async def score(self): bt.logging.debug(f"Incentive rewards: {rewards}") self.neuron.update_scores(rewards, miner_uids) + self.neuron.step += 1 async def synthensize_task(self): try: diff --git a/run_miners.sh b/run_miners.sh new file mode 100644 index 00000000..140c643a --- /dev/null +++ b/run_miners.sh @@ -0,0 +1,2 @@ +export PYTHONPATH=. +pm2 start neurons/miners/miner.py --name "webgenie_miner" --interpreter python -- --netuid 214 --subtensor.network test --wallet.name s-miner --wallet.hotkey miner2 --logging.debug --axon.port 8090 \ No newline at end of file diff --git a/run_validator copy.sh b/run_validator copy.sh new file mode 100644 index 00000000..1e3606ac --- /dev/null +++ b/run_validator copy.sh @@ -0,0 +1,3 @@ +export PYTHONPATH=. + +pm2 start neurons/validators/validator.py --name "webgenie_validator" --interpreter python -- --netuid 214 --subtensor.network test --wallet.name sc-val1 --wallet.hotkey sh-val1 --logging.debug --neuron.axon_port 8091 \ No newline at end of file diff --git a/webgenie/base/validator.py b/webgenie/base/validator.py index bfc9d8e7..944bb3aa 100644 --- a/webgenie/base/validator.py +++ b/webgenie/base/validator.py @@ -224,7 +224,6 @@ def update_scores(self, rewards: np.ndarray, uids: List[int]): def save_state(self): """Saves the state of the validator to a file.""" - bt.logging.info("Saving validator state.") # Save the state of the validator to file. np.savez( @@ -234,8 +233,6 @@ def save_state(self): hotkeys=self.hotkeys, ) - bt.logging.debug(f"Saved state: step={self.step}, scores={self.scores}") - def load_state(self): """Loads the state of the validator from a file.""" bt.logging.info("Loading validator state.") diff --git a/webgenie/constants.py b/webgenie/constants.py index 0cc3725c..7bc7361b 100644 --- a/webgenie/constants.py +++ b/webgenie/constants.py @@ -1,9 +1,18 @@ # backend api hotkey API_HOTKEY = "5DXDCYTuPfLqQXbxfvvnarG31SdTDtaubqpQrzjrcMgoP9dp" +# image task timeout +IMAGE_TASK_TIMEOUT = 100 + +# text task timeout +TEXT_TASK_TIMEOUT = 100 + # max synthetic history size MAX_SYNTHETIC_HISTORY_SIZE = 30 +# min synthetic history size to score +MIN_SYNTHETIC_HISTORY_SIZE_TO_SCORE = 5 + # max synthensize task size MAX_SYNTHETIC_TASK_SIZE = 30 diff --git a/webgenie/datasets/__init__.py b/webgenie/datasets/__init__.py index 4e1069f2..0e4c0f25 100644 --- a/webgenie/datasets/__init__.py +++ b/webgenie/datasets/__init__.py @@ -1,3 +1,2 @@ from .dataset import Dataset, DatasetEntry from .synthetic_dataset import SyntheticDataset -from .mockup_dataset import MockUpDataset, MockUpPromptDataset diff --git a/webgenie/datasets/huggingface_dataset.py b/webgenie/datasets/huggingface_dataset.py index 1ae7c7b7..febd94f7 100644 --- a/webgenie/datasets/huggingface_dataset.py +++ b/webgenie/datasets/huggingface_dataset.py @@ -11,40 +11,38 @@ from langchain_core.pydantic_v1 import BaseModel, Field from webgenie.datasets.dataset import Dataset, DatasetEntry +from webgenie.helpers.llms import call_llm from webgenie.prompts import PROMPT_MAKE_HTML_COMPLEX - class HTMLResponse(BaseModel): complex_html: str = Field(description="the complex html code") class HuggingfaceDataset(Dataset): - def __init__(self , dataset_name: str, split: str, html_field: str): + def __init__(self , **kwargs): + dataset_name = kwargs["dataset_name"] + html_column = kwargs["html_column"] + split = kwargs["split"] + self.dataset = load_dataset(dataset_name, split=split) - self.html_field = html_field - self.model = ChatOpenAI( - base_url=os.getenv("LLM_MODEL_URL"), - model=os.getenv("LLM_MODEL_ID"), - api_key=os.getenv("LLM_API_KEY") - ) + self.html_column = html_column self.output_parser = JsonOutputParser(pydantic_object=HTMLResponse) async def _make_html_complex(self, html: str)->str: bt.logging.info("Making HTML complex") - prompt = ChatPromptTemplate.from_messages([ - ("system", PROMPT_MAKE_HTML_COMPLEX), - ]) - chain = prompt | self.model | self.output_parser - response = await chain.ainvoke({ - "html": html, - "instructions": self.output_parser.get_format_instructions() - }) + response = await call_llm( + template=[ + ("system", PROMPT_MAKE_HTML_COMPLEX), + ], + params={"html": html, "instructions": self.output_parser.get_format_instructions()}, + output_parser=self.output_parser + ) return response["complex_html"] async def generate_context(self)->DatasetEntry: try: bt.logging.info("Generating Huggingface context") random_index = random.randint(0, len(self.dataset) - 1) - html = self.dataset[random_index][self.html_field] + html = self.dataset[random_index][self.html_column] complex_html = await self._make_html_complex(html) return DatasetEntry( src="huggingface", diff --git a/webgenie/datasets/mockup_dataset.py b/webgenie/datasets/mockup_dataset.py deleted file mode 100644 index f480a504..00000000 --- a/webgenie/datasets/mockup_dataset.py +++ /dev/null @@ -1,264 +0,0 @@ -from webgenie.datasets.dataset import Dataset, DatasetEntry - -class MockUpDataset(Dataset): - async def generate_context(self)->DatasetEntry: - html = """ - - - - - -Tech Company - - - - -
-
- - -
-
-
-Hero Image -
-
-

Welcome to Tech Company

-

At Tech Company, we are dedicated to providing the best technology solutions for your needs. Our team of experts is always ready to help you with any questions or problems you may have.

-
- - - - """ - return DatasetEntry( - src="mockup", - topic="tech company", - ground_truth_html=html, - prompt="", - base64_image="" - ) - -class MockUpPromptDataset(Dataset): - async def generate_context(self)->DatasetEntry: - html = """ - - - - - - Coming Soon - - - - - -
- -
- - -
-

Coming Soon!

-

We're working hard to launch something amazing. Stay tuned!

- -
- - - - - - - -""" - return DatasetEntry( - src="mockup", - topic="tech company", - ground_truth_html=html, - prompt="CommingSoon Page with goback button, navHeader, and footer", - base64_image="" - ) \ No newline at end of file diff --git a/webgenie/datasets/synthetic_dataset.py b/webgenie/datasets/synthetic_dataset.py index 0c8b3100..5de18660 100644 --- a/webgenie/datasets/synthetic_dataset.py +++ b/webgenie/datasets/synthetic_dataset.py @@ -13,6 +13,7 @@ from langchain_core.pydantic_v1 import BaseModel, Field from webgenie.datasets.dataset import Dataset, DatasetEntry +from webgenie.helpers.llms import call_llm from webgenie.prompts import PROMPT_GEN_CONCEPT, PROMPT_GEN_HTML class ConceptResponse(BaseModel): @@ -24,39 +25,30 @@ class HTMLResponse(BaseModel): class SyntheticDataset(Dataset): def __init__(self, has_ground_truth_html: bool = True): self.has_ground_truth_html = has_ground_truth_html - - self.model = ChatOpenAI( - api_key= os.getenv("LLM_API_KEY"), - model_name=os.getenv("LLM_MODEL_ID"), - base_url=os.getenv("LLM_MODEL_URL"), - temperature=0.6, - ) - self.concept_parser = JsonOutputParser(pydantic_object=ConceptResponse) self.html_parser = JsonOutputParser(pydantic_object=HTMLResponse) self.concepts = [] async def _generate_concepts(self): bt.logging.info("Generating concepts") - prompt = ChatPromptTemplate.from_messages([ - ("system", PROMPT_GEN_CONCEPT), - ]) - chain = prompt | self.model | self.concept_parser - response = await chain.ainvoke({ - "instructions": self.concept_parser.get_format_instructions() - }) + response = await call_llm( + template=[ + ("system", PROMPT_GEN_CONCEPT), + ], + params={"instructions": self.concept_parser.get_format_instructions()}, + output_parser=self.concept_parser + ) return response["concepts"] async def _generate_html(self, concept: str): bt.logging.info("Generating HTML from concept") - prompt = ChatPromptTemplate.from_messages([ - ("system", PROMPT_GEN_HTML), - ]) - chain = prompt | self.model | self.html_parser - response = await chain.ainvoke({ - "concept": concept, - "instructions": self.html_parser.get_format_instructions() - }) + response = await call_llm( + template=[ + ("system", PROMPT_GEN_HTML), + ], + params={"concept": concept, "instructions": self.html_parser.get_format_instructions()}, + output_parser=self.html_parser + ) return response["html"] async def generate_context(self)->DatasetEntry: diff --git a/webgenie/helpers/llm_calls.py b/webgenie/helpers/llm_calls.py deleted file mode 100644 index e69de29b..00000000 diff --git a/webgenie/helpers/llms.py b/webgenie/helpers/llms.py new file mode 100644 index 00000000..2a8cefe4 --- /dev/null +++ b/webgenie/helpers/llms.py @@ -0,0 +1,26 @@ +import os +import bittensor as bt + +from langchain_core.prompts import ChatPromptTemplate +from langchain_openai import ChatOpenAI + +LLM = ChatOpenAI( + base_url=os.getenv("LLM_MODEL_URL"), + model=os.getenv("LLM_MODEL_ID"), + api_key=os.getenv("LLM_API_KEY"), + temperature=0.7 +) + +async def call_llm(template, params, output_parser, retries=3): + if not os.getenv("LLM_API_KEY"): + raise Exception("LLM_API_KEY is not set") + + for _ in range(retries): + try: + prompt = ChatPromptTemplate.from_messages(template) + chain = prompt | LLM | output_parser + return await chain.ainvoke(params) + except Exception as e: + bt.logging.error(f"Error calling LLM: {e}") + continue + raise Exception("Failed to call LLM") \ No newline at end of file diff --git a/webgenie/rewards/__init__.py b/webgenie/rewards/__init__.py index a4073de3..9624c634 100644 --- a/webgenie/rewards/__init__.py +++ b/webgenie/rewards/__init__.py @@ -1,2 +1,4 @@ from .reward import Reward -from .visual_reward import VisualReward \ No newline at end of file +from .visual_reward import VisualReward +from .quality_reward import QualityReward +from .rtc_reward import RtcReward \ No newline at end of file diff --git a/webgenie/rewards/quality_reward.py b/webgenie/rewards/quality_reward.py index 35fc3ea9..4173f73f 100644 --- a/webgenie/rewards/quality_reward.py +++ b/webgenie/rewards/quality_reward.py @@ -7,43 +7,31 @@ import numpy as np from typing import List -from langchain_openai import ChatOpenAI -from langchain.prompts import ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate from langchain_core.output_parsers import JsonOutputParser from langchain_core.pydantic_v1 import BaseModel, Field +from webgenie.helpers.llms import call_llm from webgenie.prompts import PROMPT_QUALITY -from webgenie.rewards import Reward -from webgenie.rewards.metrics import s_bert +from webgenie.rewards.reward import Reward from webgenie.tasks.task import Task from webgenie.tasks.solution import Solution - class ScoreResponse(BaseModel): score: float = Field(default=0, description="The score of the html code") class QualityReward(Reward): def __init__(self): - self.model = ChatOpenAI( - api_key= os.getenv("LLM_API_KEY"), - model_name=os.getenv("LLM_MODEL_ID"), - base_url=os.getenv("LLM_MODEL_URL"), - ) - self.prompt_response_parser = JsonOutputParser(pydantic_object=ScoreResponse) async def _get_score(self, solution: Solution) -> float: - prompt = ChatPromptTemplate.from_messages([ - SystemMessagePromptTemplate.from_template(PROMPT_QUALITY) - ]) - - chain = prompt | self.model | self.prompt_response_parser - prompt_response = await chain.ainvoke({ - "html": solution.html, - "instructions": self.prompt_response_parser.get_format_instructions() - }) - - return float(prompt_response["score"]) / 100 + response = await call_llm( + template=[ + ("system", PROMPT_QUALITY), + ], + params={"html": solution.html, "instructions": self.prompt_response_parser.get_format_instructions()}, + output_parser=self.prompt_response_parser + ) + return float(response["score"]) / 100 async def reward(self, task: Task, solutions: List[Solution]) -> np.ndarray: bt.logging.debug(f"Rewarding task in quality reward") diff --git a/webgenie/rewards/rtc_reward.py b/webgenie/rewards/rtc_reward.py index 36f6e8c4..49653bcf 100644 --- a/webgenie/rewards/rtc_reward.py +++ b/webgenie/rewards/rtc_reward.py @@ -7,13 +7,12 @@ import numpy as np from typing import List -from langchain_openai import ChatOpenAI -from langchain.prompts import ChatPromptTemplate, HumanMessagePromptTemplate, SystemMessagePromptTemplate from langchain_core.output_parsers import JsonOutputParser from langchain_core.pydantic_v1 import BaseModel, Field +from webgenie.helpers.llms import call_llm from webgenie.prompts import PROMPT_RTC -from webgenie.rewards import Reward +from webgenie.rewards.reward import Reward from webgenie.rewards.metrics import s_bert from webgenie.tasks.task import Task from webgenie.tasks.solution import Solution @@ -24,27 +23,18 @@ class PromptResponse(BaseModel): class RtcReward(Reward): def __init__(self): - self.model = ChatOpenAI( - api_key= os.getenv("LLM_API_KEY"), - model_name=os.getenv("LLM_MODEL_ID"), - base_url=os.getenv("LLM_MODEL_URL"), - ) - self.prompt_response_parser = JsonOutputParser(pydantic_object=PromptResponse) async def _get_prompt(self, task: Task, solutions: List[Solution]) -> str: - prompt = ChatPromptTemplate.from_messages([ - SystemMessagePromptTemplate.from_template(PROMPT_RTC) - ]) - - chain = prompt | self.model | self.prompt_response_parser - prompt_response = await chain.ainvoke({ - "html": task.ground_truth_html, - "prompt": task.prompt, - "instructions": self.prompt_response_parser.get_format_instructions() - }) - - return prompt_response["prompt"] + response = await call_llm( + template=[ + ("system", PROMPT_RTC), + ], + params={"html": task.ground_truth_html, "prompt": task.prompt, "instructions": self.prompt_response_parser.get_format_instructions()}, + output_parser=self.prompt_response_parser + ) + + return response["prompt"] async def reward(self, task: Task, solutions: List[Solution]) -> np.ndarray: bt.logging.debug(f"Rewarding task in rtc reward") diff --git a/webgenie/tasks/image_task_generator.py b/webgenie/tasks/image_task_generator.py index cf25584d..ca7ac7ed 100644 --- a/webgenie/tasks/image_task_generator.py +++ b/webgenie/tasks/image_task_generator.py @@ -3,6 +3,7 @@ import random from typing import List, Tuple +from webgenie.constants import IMAGE_TASK_TIMEOUT from webgenie.helpers.htmls import html_to_screenshot, preprocess_html, is_empty_html from webgenie.protocol import WebgenieImageSynapse from webgenie.tasks.solution import Solution @@ -10,10 +11,9 @@ from webgenie.tasks.task_generator import TaskGenerator from webgenie.rewards.quality_reward import QualityReward from webgenie.rewards.visual_reward import VisualReward -from webgenie.datasets.mockup_dataset import MockUpDataset from webgenie.datasets.synthetic_dataset import SyntheticDataset -from webgenie.datasets.huggingface_dataset import HuggingfaceDataset - +from webgenie.datasets.huggingface_dataset import HuggingfaceDataset + class ImageTaskGenerator(TaskGenerator): def __init__(self): super().__init__() @@ -22,9 +22,8 @@ def __init__(self): (QualityReward(), 0.1) ] self.datasets = [ - # MockUpDataset(), SyntheticDataset(), - HuggingfaceDataset("SALT-NLP/Design2Code-hf", "train", "text"), + HuggingfaceDataset(dataset_name="SALT-NLP/Design2Code-hf", split="train", html_column="text"), ] async def generate_task(self) -> Tuple[Task, bt.Synapse]: @@ -41,7 +40,8 @@ async def generate_task(self) -> Tuple[Task, bt.Synapse]: return ImageTask( base64_image=base64_image, ground_truth_html=ground_truth_html, - timeout=250, + timeout=IMAGE_TASK_TIMEOUT, generator=self, ), WebgenieImageSynapse(base64_image=base64_image) + diff --git a/webgenie/tasks/text_task_generator.py b/webgenie/tasks/text_task_generator.py index 272e8ecf..d9d4abd5 100644 --- a/webgenie/tasks/text_task_generator.py +++ b/webgenie/tasks/text_task_generator.py @@ -3,11 +3,10 @@ import random from typing import List, Tuple from webgenie.datasets import ( - MockUpPromptDataset, SyntheticDataset, ) +from webgenie.constants import TEXT_TASK_TIMEOUT from webgenie.protocol import WebgenieTextSynapse -from webgenie.rewards.bert_reward import BertReward from webgenie.rewards.quality_reward import QualityReward from webgenie.rewards.rtc_reward import RtcReward from webgenie.tasks.task import Task, TextTask @@ -16,34 +15,21 @@ class TextTaskGenerator(TaskGenerator): def __init__(self, has_ground_truth_html: bool = True): super().__init__() - if has_ground_truth_html: - self.rewards = [ - (BertReward(), 0.4), - (RtcReward(), 0.5), - (QualityReward(), 0.1) - ] - - self.datasets = [ - MockUpPromptDataset(), - SyntheticDataset(has_ground_truth_html = True) - ] - else: - self.rewards = [ - (RtcReward(), 0.9), - (QualityReward(), 0.1) - ] - - self.datasets = [ - MockUpPromptDataset(), - SyntheticDataset(has_ground_truth_html = False) - ] - + self.rewards = [ + (RtcReward(), 0.9), + (QualityReward(), 0.1) + ] + + self.datasets = [ + SyntheticDataset(has_ground_truth_html = True) + ] + async def generate_task(self) -> Tuple[Task, bt.Synapse]: bt.logging.info("Generating Text task") dataset_entry = await random.choice(self.datasets).generate_context() return TextTask( prompt=dataset_entry.prompt, ground_truth_html=dataset_entry.ground_truth_html, - timeout=50, + timeout=TEXT_TASK_TIMEOUT, generator=self ), WebgenieTextSynapse(prompt=dataset_entry.prompt) diff --git a/webgenie/utils/config.py b/webgenie/utils/config.py index 059fac05..ab2c2609 100644 --- a/webgenie/utils/config.py +++ b/webgenie/utils/config.py @@ -194,7 +194,7 @@ def add_validator_args(cls, parser): "--neuron.num_concurrent_forwards", type=int, help="The number of concurrent forwards running at any time.", - default=1, + default=3, ) parser.add_argument( From fa03ba0056ebc520a7821cf442f70bf7bc66a95a Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sun, 5 Jan 2025 02:46:43 -0600 Subject: [PATCH 120/129] chore: removed logs --- neurons/validators/genie_validator.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 2e743948..3928499a 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -25,9 +25,7 @@ def __init__(self, neuron: BaseNeuron): self.config = neuron.config self.synthetic_history = [] self.synthetic_tasks = [] - bt.logging.info(f"GenieValidator initialized with neuron: {self.neuron.metagraph.n}") self.un_responsed_count = np.zeros(self.neuron.metagraph.n, dtype=np.float32) - bt.logging.info(f"Un responsed count: {self.un_responsed_count}") self.task_generators = [ (TextTaskGenerator(), 0.1), From b05a0aa540411d6321fd83a5958900bdf729031f Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sun, 5 Jan 2025 02:54:16 -0600 Subject: [PATCH 121/129] chore: edited .env files --- .env.miner.example | 7 +++++-- .env.validator.example | 6 +++--- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/.env.miner.example b/.env.miner.example index a974dd68..e9fd95e7 100644 --- a/.env.miner.example +++ b/.env.miner.example @@ -1,5 +1,8 @@ -LLM_API_KEY = your_openai_api_key WANDB_API_KEY = your_wandb_api_key WANDB_ENTITY_NAME = your_wandb_entity_name -HF_TOKEN = your_hf_token \ No newline at end of file +HF_TOKEN = your_huggingface_token + +LLM_API_KEY = your_openai_api_key +LLM_MODEL_ID = your_openai_model_id +LLM_MODEL_URL = your_openai_model_url \ No newline at end of file diff --git a/.env.validator.example b/.env.validator.example index c47a6452..3e0b781f 100644 --- a/.env.validator.example +++ b/.env.validator.example @@ -1,6 +1,6 @@ -LLM_API_KEY = your_openai_api_key WANDB_API_KEY = your_wandb_api_key WANDB_ENTITY_NAME = your_wandb_entity_name -LLM_MODEL_ID = gpt-3.5-turbo -LLM_MODEL_URL = https://api.openai.com/v1/ \ No newline at end of file +LLM_API_KEY = your_openai_api_key +LLM_MODEL_ID = your_openai_model_id +LLM_MODEL_URL = your_openai_model_url \ No newline at end of file From eea7232a93f6a1b0255ffa9097bb5d20d4bb5ccc Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sun, 5 Jan 2025 03:01:43 -0600 Subject: [PATCH 122/129] hotfix: fixed division by zero --- neurons/validators/genie_validator.py | 10 +++------- webgenie/utils/config.py | 2 +- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 3928499a..46b83d9f 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -81,22 +81,18 @@ async def query_miners(self): async def score(self): if len(self.synthetic_history) < MIN_SYNTHETIC_HISTORY_SIZE_TO_SCORE: return - + history_size = len(self.synthetic_history) task, solutions = random.choice(self.synthetic_history) - self.synthetic_history = [] - task_generator = task.generator - miner_uids = [solution.miner_uid for solution in solutions] - bt.logging.debug(f"Miner uids: {miner_uids}") rewards = await task_generator.reward(task, solutions) for i in range(len(miner_uids)): - responsed_ratio = 1 - self.un_responsed_count[miner_uids[i]] / len(self.synthetic_history) + responsed_ratio = 1 - self.un_responsed_count[miner_uids[i]] / history_size rewards[i] = rewards[i] * responsed_ratio * responsed_ratio - bt.logging.debug(f"Incentive rewards: {rewards}") + bt.logging.success(f"Incentive rewards for {miner_uids}: {rewards}") self.neuron.update_scores(rewards, miner_uids) self.neuron.step += 1 diff --git a/webgenie/utils/config.py b/webgenie/utils/config.py index ab2c2609..1d02aa14 100644 --- a/webgenie/utils/config.py +++ b/webgenie/utils/config.py @@ -194,7 +194,7 @@ def add_validator_args(cls, parser): "--neuron.num_concurrent_forwards", type=int, help="The number of concurrent forwards running at any time.", - default=3, + default=5, ) parser.add_argument( From 7942716f83e59b7be7e9bfbf5fd5fb8d80e078fa Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sun, 5 Jan 2025 03:05:11 -0600 Subject: [PATCH 123/129] chore: created new dendrite when query to miners --- neurons/validators/genie_validator.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 46b83d9f..1e06a6dd 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -53,11 +53,12 @@ async def query_miners(self): miner_uids = get_random_uids(self.neuron, k=self.config.neuron.sample_size) bt.logging.debug(f"Selected miner uids: {miner_uids}") - all_synapse_results = await self.neuron.dendrite( - axons = [self.neuron.metagraph.axons[uid] for uid in miner_uids], - synapse=synapse, - timeout=task.timeout - ) + async with bt.dendrite(wallet=self.neuron.wallet) as dendrite: + all_synapse_results = await dendrite( + axons = [self.neuron.metagraph.axons[uid] for uid in miner_uids], + synapse=synapse, + timeout=task.timeout + ) solutions = [] From 00a432a12a736b46972ea20db3f123dc6aa5f739 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sun, 5 Jan 2025 04:11:43 -0600 Subject: [PATCH 124/129] hotfix: fixed bugs when new miners register --- neurons/validators/genie_validator.py | 29 ++++++++++++++++++--------- webgenie/base/validator.py | 28 ++++++++++++-------------- webgenie/constants.py | 5 ++++- 3 files changed, 36 insertions(+), 26 deletions(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 1e06a6dd..b450b245 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -10,6 +10,7 @@ MAX_SYNTHETIC_TASK_SIZE, MAX_DEBUG_IMAGE_STRING_LENGTH, MIN_SYNTHETIC_HISTORY_SIZE_TO_SCORE, + UPDATE_SCORE_STEPS, WORK_DIR ) from webgenie.helpers.htmls import preprocess_html @@ -78,24 +79,32 @@ async def query_miners(self): except Exception as e: bt.logging.error(f"Error in forward: {e}") raise e - + + def update_raw_scores(self, rewards: List[float], miner_uids: List[int]): + for i in range(len(miner_uids)): + self.neuron.raw_scores[miner_uids[i]] += rewards[i] + self.neuron.step += 1 + + if self.neuron.step % UPDATE_SCORE_STEPS == 0: + rewards_array = np.zeros(self.neuron.metagraph.n, dtype=np.float32) + rewards_array[:] = self.neuron.raw_scores[:] ** 3 + + bt.logging.success(f"Blockchain rewards: {rewards_array}") + self.neuron.update_scores(rewards_array, range(self.neuron.metagraph.n)) + self.neuron.raw_scores[:] = 0 + async def score(self): if len(self.synthetic_history) < MIN_SYNTHETIC_HISTORY_SIZE_TO_SCORE: return - history_size = len(self.synthetic_history) + task, solutions = random.choice(self.synthetic_history) task_generator = task.generator miner_uids = [solution.miner_uid for solution in solutions] rewards = await task_generator.reward(task, solutions) - - for i in range(len(miner_uids)): - responsed_ratio = 1 - self.un_responsed_count[miner_uids[i]] / history_size - rewards[i] = rewards[i] * responsed_ratio * responsed_ratio - - bt.logging.success(f"Incentive rewards for {miner_uids}: {rewards}") - self.neuron.update_scores(rewards, miner_uids) - self.neuron.step += 1 + bt.logging.success(f"Rewards for {miner_uids}: {rewards}") + self.update_raw_scores(rewards, miner_uids) + self.synthetic_history = [] async def synthensize_task(self): try: diff --git a/webgenie/base/validator.py b/webgenie/base/validator.py index 944bb3aa..9a452ca6 100644 --- a/webgenie/base/validator.py +++ b/webgenie/base/validator.py @@ -51,17 +51,14 @@ def add_args(cls, parser: argparse.ArgumentParser): def __init__(self, config=None): super().__init__(config=config) init_wandb(self) - - # 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. if self.config.mock: self.dendrite = MockDendrite(wallet=self.wallet) else: self.dendrite = bt.dendrite(wallet=self.wallet) bt.logging.info(f"Dendrite: {self.dendrite}") - + bt.logging.info("load_state()") self.load_state() @@ -143,10 +140,6 @@ def set_weights(self): def resync_metagraph(self): """Resyncs the metagraph and updates the hotkeys and moving averages based on the new metagraph.""" - #TODO: Implement this - - #bt.logging.info("resync_metagraph()") - # Copies state of metagraph before syncing. previous_metagraph = copy.deepcopy(self.metagraph) @@ -164,16 +157,20 @@ def resync_metagraph(self): for uid, hotkey in enumerate(self.hotkeys): if hotkey != self.metagraph.hotkeys[uid]: self.scores[uid] = 0 # hotkey has been replaced + self.raw_scores[uid] = 0 # 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 + new_raw_scores = np.zeros(self.metagraph.n, dtype=np.float32) + new_raw_scores[:min_len] = self.raw_scores[:min_len] + self.raw_scores = new_raw_scores + # Update the hotkeys. self.hotkeys = copy.deepcopy(self.metagraph.hotkeys) @@ -183,7 +180,6 @@ def update_scores(self, rewards: np.ndarray, uids: List[int]): # Check if rewards contains NaN values. if np.isnan(rewards).any(): bt.logging.warning(f"NaN values detected in rewards: {rewards}") - # Replace any NaN values in rewards with 0. rewards = np.nan_to_num(rewards, nan=0) # Ensure rewards is a numpy array. @@ -229,6 +225,7 @@ def save_state(self): np.savez( self.config.neuron.full_path + "/state.npz", step=self.step, + raw_scores=self.raw_scores, scores=self.scores, hotkeys=self.hotkeys, ) @@ -238,14 +235,15 @@ def load_state(self): bt.logging.info("Loading validator state.") # Load the state of the validator from file. - state = np.load(self.config.neuron.full_path + "/state.npz") - if "step" in state: + try: + state = np.load(self.config.neuron.full_path + "/state.npz") self.step = state["step"] self.scores = state["scores"] + self.raw_scores = state["raw_scores"] self.hotkeys = state["hotkeys"] - else: - bt.logging.warning("No state found. Initializing with default values.") + except Exception as e: self.step = 0 + self.raw_scores = np.zeros(self.metagraph.n, dtype=np.float32) self.scores = np.zeros(self.metagraph.n, dtype=np.float32) self.hotkeys = copy.deepcopy(self.metagraph.hotkeys) diff --git a/webgenie/constants.py b/webgenie/constants.py index 7bc7361b..5f7bb60b 100644 --- a/webgenie/constants.py +++ b/webgenie/constants.py @@ -11,7 +11,10 @@ MAX_SYNTHETIC_HISTORY_SIZE = 30 # min synthetic history size to score -MIN_SYNTHETIC_HISTORY_SIZE_TO_SCORE = 5 +MIN_SYNTHETIC_HISTORY_SIZE_TO_SCORE = 3 + +# update score interval +UPDATE_SCORE_STEPS = 10 # max synthensize task size MAX_SYNTHETIC_TASK_SIZE = 30 From 531387eb84aebba405aedca62ee1ae9e1740fbe8 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sun, 5 Jan 2025 05:32:28 -0600 Subject: [PATCH 125/129] chore: removed temp files --- run_miners.sh | 2 -- run_validator copy.sh | 3 --- 2 files changed, 5 deletions(-) delete mode 100644 run_miners.sh delete mode 100644 run_validator copy.sh diff --git a/run_miners.sh b/run_miners.sh deleted file mode 100644 index 140c643a..00000000 --- a/run_miners.sh +++ /dev/null @@ -1,2 +0,0 @@ -export PYTHONPATH=. -pm2 start neurons/miners/miner.py --name "webgenie_miner" --interpreter python -- --netuid 214 --subtensor.network test --wallet.name s-miner --wallet.hotkey miner2 --logging.debug --axon.port 8090 \ No newline at end of file diff --git a/run_validator copy.sh b/run_validator copy.sh deleted file mode 100644 index 1e3606ac..00000000 --- a/run_validator copy.sh +++ /dev/null @@ -1,3 +0,0 @@ -export PYTHONPATH=. - -pm2 start neurons/validators/validator.py --name "webgenie_validator" --interpreter python -- --netuid 214 --subtensor.network test --wallet.name sc-val1 --wallet.hotkey sh-val1 --logging.debug --neuron.axon_port 8091 \ No newline at end of file From b26bac54a79b8c1d73efa3b24cbff3e85b939658 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sun, 5 Jan 2025 06:50:14 -0600 Subject: [PATCH 126/129] chore: updated reward system --- neurons/validators/genie_validator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index b450b245..9e243b34 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -103,7 +103,7 @@ async def score(self): rewards = await task_generator.reward(task, solutions) bt.logging.success(f"Rewards for {miner_uids}: {rewards}") - self.update_raw_scores(rewards, miner_uids) + self.update_raw_scores(rewards * len(self.synthetic_history), miner_uids) self.synthetic_history = [] async def synthensize_task(self): From 53a8830aed992f73e9686f47ef0eaa3da066d068 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Sun, 5 Jan 2025 18:35:00 -0600 Subject: [PATCH 127/129] feat: updated code quality prompt --- webgenie/prompts.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/webgenie/prompts.py b/webgenie/prompts.py index a6771b2a..8e88b35b 100644 --- a/webgenie/prompts.py +++ b/webgenie/prompts.py @@ -44,9 +44,20 @@ PROMPT_QUALITY = """ You are an HTML, CSS expert. I have an HTML code. I want you to evaluate the html code on the following criteria and give a score from 0 to 100. -1. The html code is in the professional style. -2. The html code is using SEO-friendly practices. -3. The html code is not using redundant code. + +The following criteria: +1. Semantic HTML: Use appropriate HTML tags to convey meaning. weight: 20 +2. Accessibility: Ensure content is usable for all, including those with disabilities. weight: 15 +3. Clean and Readable Code: Maintain consistent formatting and meaningful naming conventions. weight: 10 +4. Responsive Design: Implement designs that adapt to various screen sizes. weight: 12 +5. Performance Optimization: Minimize file sizes and optimize selectors for faster loading. weight: 10 +6. Cross-Browser Compatibility: Ensure consistent rendering across different browsers. weight: 8 +7. Validation: Use W3C validators to check for errors and deprecated elements. weight: 7 +8. Maintainability: Structure code for easy updates and modifications. weight: 8 +9. Use of Best Practices: Avoid anti-patterns like excessive specificity and inline styles. weight: 5 +10. Documentation: Provide clear documentation for styles and design choices. weight: 5 + +If the html/css code is not following each criteria, reduce score by its weight. The following is the given html code: {html} From 7175af84ea8d219c23c576a3e806d61c29d1bee0 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Mon, 6 Jan 2025 07:43:18 -0600 Subject: [PATCH 128/129] feat: updated incentive mechanize to avoid uid pressure --- neurons/validators/genie_validator.py | 98 ++++++++++++++------------- webgenie/base/validator.py | 8 --- webgenie/constants.py | 9 +-- webgenie/utils/config.py | 2 +- 4 files changed, 55 insertions(+), 62 deletions(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 9e243b34..2e06c2fd 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -1,16 +1,16 @@ import os import bittensor as bt +import asyncio import numpy as np import random from typing import Union, List from webgenie.base.neuron import BaseNeuron from webgenie.constants import ( + NUM_CONCURRENT_QUERIES, MAX_SYNTHETIC_HISTORY_SIZE, MAX_SYNTHETIC_TASK_SIZE, MAX_DEBUG_IMAGE_STRING_LENGTH, - MIN_SYNTHETIC_HISTORY_SIZE_TO_SCORE, - UPDATE_SCORE_STEPS, WORK_DIR ) from webgenie.helpers.htmls import preprocess_html @@ -26,7 +26,6 @@ def __init__(self, neuron: BaseNeuron): self.config = neuron.config self.synthetic_history = [] self.synthetic_tasks = [] - self.un_responsed_count = np.zeros(self.neuron.metagraph.n, dtype=np.float32) self.task_generators = [ (TextTaskGenerator(), 0.1), @@ -40,20 +39,8 @@ def make_work_dir(self): os.makedirs(WORK_DIR) bt.logging.info(f"Created work directory at {WORK_DIR}") - async def query_miners(self): - try: - if len(self.synthetic_history) > MAX_SYNTHETIC_HISTORY_SIZE: - return - - if not self.synthetic_tasks: - return - - task, synapse = self.synthetic_tasks.pop(0) - bt.logging.info("Popping synthetic task and sending it to miners") - - miner_uids = get_random_uids(self.neuron, k=self.config.neuron.sample_size) - bt.logging.debug(f"Selected miner uids: {miner_uids}") - + async def query_one_task(self, task, synapse, miner_uids): + try: async with bt.dendrite(wallet=self.neuron.wallet) as dendrite: all_synapse_results = await dendrite( axons = [self.neuron.metagraph.axons[uid] for uid in miner_uids], @@ -67,44 +54,61 @@ async def query_miners(self): processed_synapse = await self.process_synapse(synapse) if processed_synapse is not None: solutions.append(Solution(html = processed_synapse.html, miner_uid = miner_uid, process_time = processed_synapse.dendrite.process_time)) - else: - self.un_responsed_count[miner_uid] += 1 - - if not solutions: - bt.logging.warning(f"No valid solutions received") + + return task, solutions + except Exception as e: + bt.logging.error(f"Error in query_one_task: {e}") + raise e + + async def query_miners(self): + try: + if len(self.synthetic_history) > MAX_SYNTHETIC_HISTORY_SIZE: return - bt.logging.info(f"Received {len(solutions)} solutions") - self.synthetic_history.append((task, solutions)) + if len(self.synthetic_tasks) < NUM_CONCURRENT_QUERIES: + return + + bt.logging.info("querying miners") + + miner_uids = get_random_uids(self.neuron, k=self.config.neuron.sample_size) + bt.logging.debug(f"Selected miner uids: {miner_uids}") + + query_coroutines = [self.query_one_task(task, synapse, miner_uids) for task, synapse in self.synthetic_tasks] + + self.synthetic_tasks = [] + + results = await asyncio.gather(*query_coroutines, return_exceptions=True) + self.synthetic_history.append(results) + except Exception as e: - bt.logging.error(f"Error in forward: {e}") + bt.logging.error(f"Error in query_miners: {e}") raise e - def update_raw_scores(self, rewards: List[float], miner_uids: List[int]): - for i in range(len(miner_uids)): - self.neuron.raw_scores[miner_uids[i]] += rewards[i] - self.neuron.step += 1 + async def score(self): + if not self.synthetic_history: + return - if self.neuron.step % UPDATE_SCORE_STEPS == 0: - rewards_array = np.zeros(self.neuron.metagraph.n, dtype=np.float32) - rewards_array[:] = self.neuron.raw_scores[:] ** 3 + results = self.synthetic_history.pop(0) + raw_scores = np.zeros(self.neuron.metagraph.n, dtype=np.float32) + for result in results: + if isinstance(result, Exception): + continue + + task, solutions = result + if not solutions: + continue + + task_generator = task.generator + miner_uids = [solution.miner_uid for solution in solutions] + rewards = await task_generator.reward(task, solutions) + bt.logging.success(f"Rewards for {miner_uids}: {rewards}") - bt.logging.success(f"Blockchain rewards: {rewards_array}") - self.neuron.update_scores(rewards_array, range(self.neuron.metagraph.n)) - self.neuron.raw_scores[:] = 0 + for i in range(len(miner_uids)): + raw_scores[miner_uids[i]] += rewards[i] - async def score(self): - if len(self.synthetic_history) < MIN_SYNTHETIC_HISTORY_SIZE_TO_SCORE: - return - - task, solutions = random.choice(self.synthetic_history) - task_generator = task.generator - miner_uids = [solution.miner_uid for solution in solutions] - - rewards = await task_generator.reward(task, solutions) - bt.logging.success(f"Rewards for {miner_uids}: {rewards}") - self.update_raw_scores(rewards * len(self.synthetic_history), miner_uids) - self.synthetic_history = [] + raw_scores[:] = raw_scores[:] ** 3 + self.neuron.update_scores(raw_scores, range(self.neuron.metagraph.n)) + self.neuron.step += 1 async def synthensize_task(self): try: diff --git a/webgenie/base/validator.py b/webgenie/base/validator.py index 9a452ca6..89f13319 100644 --- a/webgenie/base/validator.py +++ b/webgenie/base/validator.py @@ -157,7 +157,6 @@ def resync_metagraph(self): for uid, hotkey in enumerate(self.hotkeys): if hotkey != self.metagraph.hotkeys[uid]: self.scores[uid] = 0 # hotkey has been replaced - self.raw_scores[uid] = 0 # Check to see if the metagraph has changed size. # If so, we need to add new hotkeys and moving averages. @@ -167,10 +166,6 @@ def resync_metagraph(self): new_moving_average[:min_len] = self.scores[:min_len] self.scores = new_moving_average - new_raw_scores = np.zeros(self.metagraph.n, dtype=np.float32) - new_raw_scores[:min_len] = self.raw_scores[:min_len] - self.raw_scores = new_raw_scores - # Update the hotkeys. self.hotkeys = copy.deepcopy(self.metagraph.hotkeys) @@ -225,7 +220,6 @@ def save_state(self): np.savez( self.config.neuron.full_path + "/state.npz", step=self.step, - raw_scores=self.raw_scores, scores=self.scores, hotkeys=self.hotkeys, ) @@ -239,11 +233,9 @@ def load_state(self): state = np.load(self.config.neuron.full_path + "/state.npz") self.step = state["step"] self.scores = state["scores"] - self.raw_scores = state["raw_scores"] self.hotkeys = state["hotkeys"] except Exception as e: self.step = 0 - self.raw_scores = np.zeros(self.metagraph.n, dtype=np.float32) self.scores = np.zeros(self.metagraph.n, dtype=np.float32) self.hotkeys = copy.deepcopy(self.metagraph.hotkeys) diff --git a/webgenie/constants.py b/webgenie/constants.py index 5f7bb60b..0eb9312a 100644 --- a/webgenie/constants.py +++ b/webgenie/constants.py @@ -10,15 +10,12 @@ # max synthetic history size MAX_SYNTHETIC_HISTORY_SIZE = 30 -# min synthetic history size to score -MIN_SYNTHETIC_HISTORY_SIZE_TO_SCORE = 3 - -# update score interval -UPDATE_SCORE_STEPS = 10 - # max synthensize task size MAX_SYNTHETIC_TASK_SIZE = 30 +# the number of concurrent queries +NUM_CONCURRENT_QUERIES = 10 + # max debug image string length MAX_DEBUG_IMAGE_STRING_LENGTH = 20 diff --git a/webgenie/utils/config.py b/webgenie/utils/config.py index 1d02aa14..059fac05 100644 --- a/webgenie/utils/config.py +++ b/webgenie/utils/config.py @@ -194,7 +194,7 @@ def add_validator_args(cls, parser): "--neuron.num_concurrent_forwards", type=int, help="The number of concurrent forwards running at any time.", - default=5, + default=1, ) parser.add_argument( From b0b40936eb7cd1f8072e3cfab47ee099faabee02 Mon Sep 17 00:00:00 2001 From: pycorn0729 Date: Mon, 6 Jan 2025 07:50:57 -0600 Subject: [PATCH 129/129] chore: renamed the var name --- neurons/validators/genie_validator.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/neurons/validators/genie_validator.py b/neurons/validators/genie_validator.py index 2e06c2fd..d8724956 100644 --- a/neurons/validators/genie_validator.py +++ b/neurons/validators/genie_validator.py @@ -89,7 +89,7 @@ async def score(self): return results = self.synthetic_history.pop(0) - raw_scores = np.zeros(self.neuron.metagraph.n, dtype=np.float32) + tatal_scores = np.zeros(self.neuron.metagraph.n, dtype=np.float32) for result in results: if isinstance(result, Exception): continue @@ -104,10 +104,10 @@ async def score(self): bt.logging.success(f"Rewards for {miner_uids}: {rewards}") for i in range(len(miner_uids)): - raw_scores[miner_uids[i]] += rewards[i] + tatal_scores[miner_uids[i]] += rewards[i] - raw_scores[:] = raw_scores[:] ** 3 - self.neuron.update_scores(raw_scores, range(self.neuron.metagraph.n)) + tatal_scores[:] = tatal_scores[:] ** 3 + self.neuron.update_scores(tatal_scores, range(self.neuron.metagraph.n)) self.neuron.step += 1 async def synthensize_task(self):