diff --git a/.env.example b/.env.example index 6ed4aef..26425e0 100644 --- a/.env.example +++ b/.env.example @@ -5,6 +5,9 @@ export VALIDATOR_CADENCE='500' # Solana wallet for sample queries, should have at least 0.1 SOL export SOLANA_WALLET='5HHSqMvTCvgtzdqFb5BbtYjB8cEiJjf8UZ6p5rQczagL' +# Gemini API key for question generation +export GEMINI_API_KEY='' + # MINER CONFIGS FOR BITQUANT AGENT # REPO: https://github.com/OpenGradient/BitQuant-Subnet diff --git a/neurons/validator.py b/neurons/validator.py index 7e36957..3417844 100644 --- a/neurons/validator.py +++ b/neurons/validator.py @@ -17,7 +17,7 @@ import time -import sys +import os # Bittensor import bittensor as bt @@ -31,6 +31,8 @@ # Import the shared Quant agent server module from neurons import quant_agent_server +from quant.utils.questioner import Questioner + 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. @@ -46,6 +48,11 @@ def __init__(self, config=None): bt.logging.info("load_state()") self.load_state() + api_key = os.getenv("GEMINI_API_KEY", "") + if not api_key: + bt.logging.warning("No GEMINI_API_KEY found in environment variables!") + + self.questioner = Questioner(api_key=api_key) # TODO(developer): Anything specific to your use case you can do here async def forward(self): diff --git a/quant/utils/questioner.py b/quant/utils/questioner.py new file mode 100644 index 0000000..5c4b3bf --- /dev/null +++ b/quant/utils/questioner.py @@ -0,0 +1,80 @@ +from datetime import timedelta, datetime +from typing import Optional +import random +import json + +import google.genai as genai +from pydantic import BaseModel, Field +import bittensor as bt + + +GENERATION_PROMPT = """Create a list of {question_count} questions that sound like they're being asked by a real person with varying levels of knowledge. The questions should be about common crypto topics. Some should be professionally formatted and others lazy (e.g., no capitalization, missing punctuation). The questions should cover the following topics, with an example for each: +* Portfolio Analytics: (e.g., "how can i evaluate my portfolio risk") +* Yield & Earning: (e.g., "what's the best way to earn yield on my ETH") +* Trading & Popularity: (e.g., "what are the trading tokens on solana?") +* Investment Advice: (e.g., "should i buy BONK?") +* Token Risk & Security: (e.g., "who are the top holders of pepe?")""" + + +class Questioner: + + def __init__( + self, + api_key: str, + question_lifetime: timedelta = timedelta(days=7), + initial_questions: Optional[list[str]] = None, + question_count: int = 42, + llm_model: str = "gemini-2.5-flash-lite", + temperature: float = 1.0, + ): + self.question_lifetime = question_lifetime + self.questions: list[str] = initial_questions or [] + self.last_question_update = datetime.now() + + self.genai_client = genai.Client(api_key=api_key) + self.llm_model: str = llm_model + self.question_count: int = question_count + self.temperature: float = temperature + + def choose_question(self) -> str: + self.maybe_update_questions() + return random.choice(self.questions) + + def maybe_update_questions(self): + if not self.questions or self.is_update_time(): + self.questions = self.generate_exactly_n_questions(n=self.question_count) + self.last_question_update = datetime.now() + + def is_update_time(self) -> bool: + return datetime.now() - self.last_question_update > self.question_lifetime + + def generate_exactly_n_questions(self, n: int) -> list[str]: + new_questions = [] + while len(new_questions) < n: + qs = self.generate_about_n_questions(n=n - len(new_questions)) + print(f"{len(qs) = }") + new_questions.extend(qs) + return new_questions[:n] + + def generate_about_n_questions(self, n: int) -> list[str]: + class Questions(BaseModel): + questions: list[str] = Field(..., description="List of crypto questions") + + prompt = GENERATION_PROMPT.format(question_count=n) + + response = self.genai_client.models.generate_content( + model=self.llm_model, + contents=prompt, + config=genai.types.GenerateContentConfig( + response_mime_type="application/json", + response_schema=Questions, + temperature=self.temperature, + ), + ) + + if response_text := response.text: + data = json.loads(response_text) + return data["questions"] + else: + bt.logging.warning("No questions were generated!") + return [] diff --git a/quant/validator/forward.py b/quant/validator/forward.py index 9421a4f..37add07 100644 --- a/quant/validator/forward.py +++ b/quant/validator/forward.py @@ -17,13 +17,11 @@ import os import time -import random import bittensor as bt from quant.protocol import QuantQuery, QuantSynapse from quant.validator.reward import get_rewards from quant.utils.uids import get_random_uids -from quant.utils.questions import questions async def forward(self): @@ -44,9 +42,12 @@ async def forward(self): if not wallet_address: bt.logging.error("SOLANA_WALLET environment variable is not set. Using a default value.") wallet_address = "5HHSqMvTCvgtzdqFb5BbtYjB8cEiJjf8UZ6p5rQczagL" + + question = self.questioner.choose_question() + bt.logging.info(f"Choosen question: '{question}'") query = QuantQuery( - query=random.choice(questions), + query=question, userID=wallet_address, metadata={ "Create_Proof": "True",