From d4984a0f6f059b824ab8168431a3b47030214bec Mon Sep 17 00:00:00 2001 From: latentvector Date: Wed, 6 Nov 2024 15:43:27 -0800 Subject: [PATCH] agent --- commune/cli.py | 11 +- commune/module.py | 18 ++- commune/modules/agent/agent.py | 109 +++++++++++++ commune/network/network.py | 9 +- commune/network/subspace/subspace.py | 18 ++- commune/server.py | 5 +- commune/vali.py | 169 +++++++------------- {commune/tests => tests}/test_key.py | 0 {commune/tests => tests}/test_serializer.py | 0 {commune/tests => tests}/test_server.py | 0 {commune/tests => tests}/test_subspace.py | 0 {commune/tests => tests}/test_validator.py | 0 12 files changed, 202 insertions(+), 137 deletions(-) create mode 100644 commune/modules/agent/agent.py rename {commune/tests => tests}/test_key.py (100%) rename {commune/tests => tests}/test_serializer.py (100%) rename {commune/tests => tests}/test_server.py (100%) rename {commune/tests => tests}/test_subspace.py (100%) rename {commune/tests => tests}/test_validator.py (100%) diff --git a/commune/cli.py b/commune/cli.py index 51eddd710..170facb81 100644 --- a/commune/cli.py +++ b/commune/cli.py @@ -11,7 +11,12 @@ def __init__(self, argv = None, base = 'module', fn_splitters = [':', '/', '//', '::'], - helper_fns = ['code', 'schema', 'fn_schema', 'help', 'fn_info', 'fn_hash'], + helper_fns = ['code', + 'schema', + 'fn_schema', + 'help', + 'fn_info', + 'fn_hash'], sep = '--' ): @@ -34,9 +39,9 @@ def forward(self, argv=None): if arg.startswith(self.sep): key = arg[len(self.sep):].split('=')[0] if key in self.helper_fns: + # is it a helper function return self.forward([key , argv[0]]) else: - value = arg.split('=')[-1] if '=' in arg else True argv.remove(arg) init_kwargs[key] = self.determine_type(value) @@ -67,7 +72,7 @@ def forward(self, argv=None): fn2module = module.fn2module() if not fn in fn2module: functions = c.get_functions(module) - return c.print(f'Function {fn} not found in {module}. Available functions are {functions}') + return c.print(f'FN({fn}) not found {module}', color='red') module = c.module(fn2module[fn]) diff --git a/commune/module.py b/commune/module.py index 004b8e960..3f2c525b6 100755 --- a/commune/module.py +++ b/commune/module.py @@ -49,7 +49,7 @@ class c: lib_path = libpath = os.path.dirname(root_path) # the path to the library repo_path = repopath = os.path.dirname(root_path) # the path to the repo modules_path = os.path.dirname(__file__) + '/modules' - tests_path = f'{root_path}/tests' + cache = {} # cache for module objects home = os.path.expanduser('~') # the home directory @@ -306,9 +306,10 @@ def from_json(cls, json_str:str) -> 'Module': @classmethod def test_fns(cls, *args, **kwargs): return [f for f in cls.functions(*args, **kwargs) if f.startswith('test_')] - + tests_path = f'{libpath}/tests' @classmethod def pytest(cls, *args, **kwargs): + return c.cmd(f'pytest {c.tests_path}', stream=1, *args, **kwargs) @classmethod @@ -451,6 +452,11 @@ def utils(cls, search=None): utils = [u for u in utils if search in u] return sorted(utils) + @classmethod + def num_utils(cls, search=None): + return len(cls.utils(search)) + + cache = {} @classmethod def util2code(cls, search=None): utils = cls.utils() @@ -1417,7 +1423,7 @@ def get_fn(cls, fn:str, init_kwargs = None, splitters=splitters): return getattr(module, fn_name) if callable(fn): return fn - raise ValueError(f'{fn} is not a function') + return fn @classmethod def self_functions(cls, search = None): @@ -1882,9 +1888,9 @@ def lib_tree(cls, depth=10, **kwargs): return c.get_tree(c.libpath, depth=depth, **kwargs) @classmethod - def core_tree(cls, depth=10, **kwargs): - tree = c.get_tree(c.libpath, depth=depth, **kwargs) - return {k:v for k,v in tree.items() if '.modules.' not in v} + def core_tree(cls, **kwargs): + tree = c.get_tree(c.libpath, **kwargs) + return {k:v for k,v in tree.items() if 'modules.' not in v} @classmethod def local_tree(cls , depth=4, **kwargs): return c.get_tree(c.pwd(), depth=depth, **kwargs) diff --git a/commune/modules/agent/agent.py b/commune/modules/agent/agent.py new file mode 100644 index 000000000..a8ebb7c5b --- /dev/null +++ b/commune/modules/agent/agent.py @@ -0,0 +1,109 @@ +import commune as c +import json +import os +class Agent: + anchor="OUTPUT" + + def ask(self, *args, path='./'): + text = self.args2text(args) + context = self.summarize(query=text, path=path) + prompt = f""" + {context} + AD START FINISH THE OUTPUT WITH THE ANCHOR TAGS + if you write a file so i can easily process it back + <{self.anchor}(path=filepath)> + you are totally fine using ./ if you are refering to the pwd for brevity + """ + return c.ask(prompt) + + def args2text(self, args): + return ' '.join(list(map(str, args))) + + def get_context(self, + path='./', + query='what are the required packages', + instruction= "Which files are relevant to you?", + output_format="DICT(files:list[str])"): + + c.print('FINDING RELEVANT FILES IN THE PATH {}'.format(path), color='green') + files = c.files(path) + prompt = f""" + QUERY \n {query} \n INSTRUCTION \n {instruction} \n CONTEXT {files} {query } + OUTPUT FORMAT + USE THE FOLLOWING FORMAT FOR OUTPUT WITH A JSON STRING IN THE CENTER + <{self.anchor}>{output_format}) + + """ + + output = '' + for ch in c.ask(prompt): + print(ch, end='') + output += ch + if ch == f'': + break + + files = json.loads(output.split('<' +self.anchor + '>')[1].split('')[0])['files'] + file2text = {c.get_text(f) for f in files} + return file2text + + def score(self, *args, path='./'): + text = self.args2text(args) + context = self.get_context(text, path=path) + return c.ask(self.prompt.format(context=context, text=text)) + + + def summary(self, path='./', + query = "get all of the important objects and a description", + anchor = 'OUTPUT', + max_ = 100000, + ): + + self.batch_context(path=path) + context = c.file2text(path) + + prompt = f""" + INSTRUCTION + SUMMARIZE the info as a black hole condenses info + ensure to include all necessary info and discard + useless info. do it as such. use the query to condition + the tuples listed. + + [head, relation, tail] + + QUERY + {query} + OUTPUT FORMAT + USE THE FOLLOWING FORMAT FOR OUTPUT WITH A JSON STRING IN THE CENTER + store it in a relations + <{anchor}> + DICT(relations:list[str]) + + CONTEXT + {context} + """ + output = '' + for ch in c.ask(prompt): + print(ch, end='') + output += ch + if ch == f'': + break + return json.loads(output.split('<' +self.anchor + '>')[1].split('')[0]) + + + def batch_context(self, path='./', batch_size=20000): + + file2text = c.file2text(path) + file2size = {k:len(v) for k,v in file2text.items()} + current_size = 0 + batch_list = [] + files_batch = {} + for f, s in file2size.items(): + if (current_size + s) > batch_size: + batch_list += [files_batch] + files_batch = {} + current_size = 0 + current_size += s + files_batch[f] = c.get_text(path + f ) + return batch_list + + \ No newline at end of file diff --git a/commune/network/network.py b/commune/network/network.py index ec7de3f0e..278448c5f 100644 --- a/commune/network/network.py +++ b/commune/network/network.py @@ -221,8 +221,13 @@ def registration_signature(self, name='agi', address='0.0.0.0:8888', key=None): def infos(self, timeout=10): return c.wait([c.submit(c.call, [s + '/info']) for s in c.servers()], timeout=timeout) - def keys(self, timeout=10): - return c.wait([c.submit(c.call, [s + '/key_address']) for s in c.servers()], timeout=timeout) + def keys(self, max_age=60, update=False, timeout=10): + path = 'network_keys' + keys = c.get(path, max_age=max_age, update=update) + if keys == None: + keys = c.wait([c.submit(c.call, [s + '/key_address']) for s in c.servers()], timeout=timeout) + c.put(path, keys) + return keys def infos(self, timeout=10): return c.wait([c.submit(c.call, [s + '/info']) for s in c.servers()], timeout=timeout) diff --git a/commune/network/subspace/subspace.py b/commune/network/subspace/subspace.py index 1b5ee9441..929543338 100644 --- a/commune/network/subspace/subspace.py +++ b/commune/network/subspace/subspace.py @@ -1429,23 +1429,21 @@ def update_subnet( ChainTransactionError: If the transaction fails. """ netuid = self.resolve_netuid(subnet) - subnet_params = self.subnet_params(netuid=netuid) + subnet_params = self.subnet_params(netuid=netuid, update=True) + # get subnet_key address2key = c.address2key() assert subnet_params['founder'] in address2key, f'No key found for {subnet_params["founder"]}' key = c.get_key(address2key[subnet_params['founder']]) - params = params or {} - params.update(extra_params) + params = {**(params or {}), **extra_params} if 'founder' in params: params['founder'] = self.resolve_key_address(params['founder']) + params = {**subnet_params, **params} assert any([k in subnet_params for k in params.keys()]), f'Invalid params {params.keys()}' params["netuid"] = netuid - governance_config = params.pop('governance_configuration', None) - params['vote_mode'] = governance_config['vote_mode'] + params['vote_mode'] = params.pop('governance_configuration')['vote_mode'] params["metadata"] = params.pop("metadata", None) - response = self.compose_call(fn="update_subnet",params=params,key=key) - return response - + return self.compose_call(fn="update_subnet",params=params,key=key) def metadata(self) -> str: netuids = self.netuids() @@ -2719,6 +2717,7 @@ def subnet_params(self, ("MaxAllowedValidators", params), ("ModuleBurnConfig", params), ("SubnetMetadata", params), + ("TrustRatio", params) ], "GovernanceModule": [ ("SubnetGovernanceConfig", params), @@ -2749,6 +2748,7 @@ def subnet_params(self, "min_validator_stake": bulk_query.get("MinValidatorStake", {}), "max_allowed_validators": bulk_query.get("MaxAllowedValidators", {}), "module_burn_config": bulk_query.get("ModuleBurnConfig", {}), + 'trust_ratio': bulk_query.get("TrustRatio", {}), "metadata": bulk_query.get("SubnetMetadata", {}), } @@ -2834,6 +2834,8 @@ def global_params(self, max_age=60, update=False) -> NetworkParams: self.put(path, result) return result + params = subnet_params + def clean_feature_name(self, x): new_x = '' for i, ch in enumerate(x): diff --git a/commune/server.py b/commune/server.py index e7f32cb57..487ca9974 100644 --- a/commune/server.py +++ b/commune/server.py @@ -93,7 +93,7 @@ def __init__( module.key_address = module.key.ss58_address module.fn2cost = fn2cost or {} module.schema = self.get_schema(module) - module.functions = module.server_functions = functions or list(set(helper_functions + list(module.schema.keys()))) + module.functions = module.fns = module.server_functions = functions or list(set(helper_functions + list(module.schema.keys()))) module.info = module.server_info = self.get_info(module) module.network_path = self.resolve_path(f'{self.network}/state.json') module.users_path = users_path or self.resolve_path(f'{name}/users') @@ -707,9 +707,6 @@ def processes(cls, search=None, **kwargs) -> List[str]: for line in output_string.split('\n')[3:]: if line.count('│') > 2: name = line.split('│')[2].strip() - if 'errored' in line: - self.kill(name, verbose=True) - continue module_list += [name] if search != None: module_list = [m for m in module_list if search in m] diff --git a/commune/vali.py b/commune/vali.py index acf6d1613..134825e23 100644 --- a/commune/vali.py +++ b/commune/vali.py @@ -5,10 +5,10 @@ from typing import * class Vali(c.Module): - whitelist = ['eval_module', 'score', 'eval', 'leaderboard'] + endpoints = ['eval_module', 'score', 'eval', 'leaderboard'] voting_networks = ['bittensor', 'commune', 'subspace'] networks = ['local'] + voting_networks - + subnet_splitters = ['.', '/', ':'] def __init__(self, network= 'local', # for local subspace:test or test # for testnet subspace:main or main # for mainnet @@ -17,18 +17,15 @@ def __init__(self, verbose= True, # the verbose mode for the worker # EPOCH batch_size= 16, max_workers= None , - info_function = 'info', # the function to get the info of the module - info_timeout = 2, # the timeout for the info function score = None, # score function path= None, # the storage path for the module eval, if not null then the module eval is stored in this directory alpha= 1.0, # alpha for score min_score= 0, # the minimum weight of the leaderboard - tempo = 10, # the interval for the run loop to run run_loop= True, # This is the key that we need to change to false test= False, # the test mode for the validator module = None, + info_fn = 'info', max_age= 120, # the maximum age of the network - max_sample_age= 120, # the maximum age of the sample timeout= 2, # timeout per evaluation of the module update=False, key = None, @@ -49,16 +46,11 @@ def score(self, module): return 'name' in module.info() def set_score(self, score: Union[Callable, str] ): - """ - Set the score function for the validator - """ - if score == None: - score = self.score - elif isinstance(score, str): + score = score or self.score + if isinstance(score, str): if hasattr(self, score): score = getattr(self, score) else: - assert c.object_exists(score), f'{score} does not exist' score = c.obj(score) assert callable(score), f'{score} is not callable' setattr(self, 'score', score ) @@ -67,23 +59,13 @@ def set_score(self, score: Union[Callable, str] ): @property def is_voting_network(self): return any([v in self.config.network for v in self.voting_networks]) - - + def run_loop(self): - """ - The run loop is a backgroun loop that runs to do two checks - - network: check the staleness of the network to resync it - - workers: check the staleness of the last success to restart the workers - - voting: check the staleness of the last vote to vote (if it is a voting network) - """ - # start the workers - while True: from tqdm import tqdm - print('RUNNING LOOP') time_to_wait = int(max(0,self.epoch_start_time - c.time() + self.config.tempo)) desc = f'Waiting Next Epoch ({self.epochs}) with Tempo {self.config.tempo}' - print(f'WAITING FOR {time_to_wait} SECONDS') + print(f'Time Until Next Epoch --> {time_to_wait}') [ c.sleep(1) for _ in tqdm(range(time_to_wait), desc=desc)] try: self.epoch() @@ -118,10 +100,6 @@ def get_next_result(self, futures): epoch2results = {} - @classmethod - def run_epoch(cls, run_loop=False, **kwargs): - return cls( run_loop=run_loop, **kwargs).epoch(df=1) - def epoch(self, network=None, df=True, **kwargs): futures = [] self.epoch_start_time = c.time() @@ -152,77 +130,70 @@ def network_staleness(self) -> int: The staleness of the network """ return c.time() - self.network_time + - def filter_module(self, module:str, search=None): - search = search or self.config.search + def filter_module(self, module:str): + search = self.config.search if ',' in str(search): search_list = search.split(',') else: search_list = [search] return all([s == None or s in module for s in search_list ]) - def set_network(self, - network:str=None, - subnet:int=None, - search = None, - update = False, - max_age=None, - tempo=None): - if not hasattr(self, 'network_time'): - self.network_time = 0 + def set_network(self, network:str=None, update = False,tempo=None): config = self.config - tempo = tempo or config.tempo network = network or config.network - if '.' in network: - network, subnet = network.split('.') - subnet = subnet or config.subnet - self.network_path = self.get_storage_path() + '/network_state' - if subnet != None or network not in self.networks: - network = 'subspace' - search = search or config.search - max_age = max_age if max_age != None else config.max_age - if update: - max_age = 0 - if self.network_staleness < max_age: - return {'msg': 'Alredy Synced network Within Interval', } + for subnet_splitter in self.subnet_splitters: + if subnet_splitter in network: + network, subnet = network.split(subnet_splitter) + break + subnet = config.subnet + path = self.config.path + if path == None: + path = network if subnet == None else f'{network}/{subnet}' + path = self.resolve_path(path) + network_path = path + '/network_state' + max_age = config.max_age + network_state = c.get(network_path, max_age=max_age, update=update) + self.path = path + # RESOLVE THE VOTING NETWORKS has_network_module = bool(hasattr(self, 'network_module') and network == self.config.network) network_module = c.module(network)() if not has_network_module else self.network_module if 'local' in network: # local network does not need to be updated as it is atomically updated - namespace = network_module.namespace(max_age=max_age, search=search) + namespace = network_module.namespace(max_age=max_age) params = network_module.params() elif 'subspace' in network: # the network is a voting network subnet2netuid = network_module.subnet2netuid() - config.netuid = subnet2netuid.get(subnet, None) - config.subnet = subnet namespace = network_module.namespace(subnet=subnet, max_age=max_age) params = network_module.subnet_params(subnet=subnet, max_age=max_age) + config.netuid = subnet2netuid.get(subnet, None) + config.subnet = subnet config.tempo = params.get('tempo', self.config.tempo) else: raise ValueError(f'Network {network} is not a valid network') - - self.network_module = network_module - self.n = len(namespace) - c.print(f'Network(network={config.network}, subnet={config.subnet} n={self.n})') - self.namespace = {k: v for k, v in namespace.items() if self.filter_module(k)} - self.namespace = namespace - self.network = config.network = network - self.network_time = c.time() - self.network_state = { + n = len(namespace) + network_state = { 'network': network, 'subnet': subnet, - 'n': self.n, - 'search': search, + 'n': n, + 'time': c.time(), 'params': params, 'namespace': namespace, - } + + self.network_module = network_module + self.n = len(namespace) + self.namespace = {k: v for k, v in namespace.items() if self.filter_module(k)} + self.network = config.network = network self.config = config - self.put_json(self.network_path, self.network_state) - return + c.put(network_path, network_state) + c.print(f'Network(network={config.network}, subnet={config.subnet} n={self.n})') + + return network_state module2last_update = {} @@ -230,39 +201,25 @@ def check_info(self, info:dict) -> bool: return bool(isinstance(info, dict) and all([k in info for k in ['score', 'address', 'name', 'key']])) def eval_module(self, module:str, **kwargs): - """ - The following evaluates a module sver - """ - info = {} - # RESOLVE THE NAME OF THE ADDRESS IF IT IS NOT A NAME address = self.namespace.get(module, module) - path = self.get_storage_path() +'/'+ address + path = self.path +'/'+ address module_client = c.connect(address, key=self.key) info = self.get_json(path, {}) if not self.check_info(info): - info = module_client.info(timeout=self.config.info_timeout) - info['timestamp'] = c.timestamp() # the timestamp + info = getattr(module_client, self.config.info_fn)() + info['timestamp'] = c.time() # the timestamp last_score = info.get('score', 0) response = self.score(module_client, **kwargs) response = {'score': float(response)} if type(response) in [int, float, bool] else response - info['latency'] = c.round(c.time() - info['timestamp'], 3) info.update(response) - alpha = self.config.alpha - info['score'] = info['score'] * alpha + last_score * (1 - alpha) + info['score'] = info['score'] * self.config.alpha + last_score * (1 - self.config.alpha) if response['score'] > self.config.min_score: self.put_json(path, info) return info eval = eval_module - - def get_storage_path(self): - # the set storage path in config.path is set, then the modules are saved in that directory - if self.config.path == None: - path = f'{self.config.network}/{self.config.subnet}' if self.config.subnet != None else self.config.network - self.config.path = self.resolve_path(path) - return self.config.path - + def votes(self, **kwargs): votes = {'modules': [], 'weights': []} for module in self.leaderboard().to_records(): @@ -273,7 +230,7 @@ def votes(self, **kwargs): @property def votes_path(self): - return self.get_storage_path() + f'/votes' + return self.path + f'/votes' def vote(self, update=False, **kwargs): if not self.is_voting_network : @@ -322,7 +279,6 @@ def leaderboard(self, self.rm(path) df = c.df(df) - if len(df) > 0: if isinstance(by, str): by = [by] @@ -332,7 +288,6 @@ def leaderboard(self, df = df[page*n:(page+1)*n] else: df = df[:n] - # if to_dict is true, we return the dataframe as a list of dictionaries if to_dict: return df.to_dict(orient='records') @@ -340,35 +295,20 @@ def leaderboard(self, return df def paths(self): - paths = self.ls(self.get_storage_path()) + paths = self.ls(self.path) return paths def refresh_leaderboard(self): - path = self.get_storage_path() + path = self.path c.rm(path) df = self.leaderboard() assert len(df) == 0, f'Leaderboard not removed {df}' return {'success': True, 'msg': 'Leaderboard removed', 'path': path} - refresh = refresh_leaderboard + refresh = refresh_leaderboard - @property - def vote_staleness(self): - try: - if 'subspace' in self.config.network: - return self.network_module.block - self.module_info()['last_update'] - except Exception as e: - pass - return 0 - - @staticmethod - def test( n=2, - tag = 'vali_test_net', - miner='module', - vali='vali', - storage_path = '/tmp/commune/vali_test', - network='local'): + def test( n=2, tag = 'vali_test_net', miner='module', vali='vali', path = '/tmp/commune/vali_test',network='local'): test_miners = [f'{miner}::{tag}_{i}' for i in range(n)] modules = test_miners @@ -380,7 +320,7 @@ def test( n=2, namespace = c.namespace(search=search) while len(namespace) < n: namespace = c.namespace(search=search) - leaderboard = Vali.run_epoch(network=network, search=search, path=storage_path) + leaderboard = Vali.run_epoch(network=network, search=search, path=path) assert len(leaderboard) == n, f'Leaderboard not updated {leaderboard}' for miner in modules: c.print(c.kill(miner)) @@ -390,5 +330,6 @@ def __del__(self): c.print('Cancelling futures') return {'success': True, 'msg': 'Cancelling futures'} -if __name__ == '__main__': - Vali.run() + @classmethod + def run_epoch(cls, run_loop=False, **kwargs): + return cls( run_loop=run_loop, **kwargs).epoch(df=1) diff --git a/commune/tests/test_key.py b/tests/test_key.py similarity index 100% rename from commune/tests/test_key.py rename to tests/test_key.py diff --git a/commune/tests/test_serializer.py b/tests/test_serializer.py similarity index 100% rename from commune/tests/test_serializer.py rename to tests/test_serializer.py diff --git a/commune/tests/test_server.py b/tests/test_server.py similarity index 100% rename from commune/tests/test_server.py rename to tests/test_server.py diff --git a/commune/tests/test_subspace.py b/tests/test_subspace.py similarity index 100% rename from commune/tests/test_subspace.py rename to tests/test_subspace.py diff --git a/commune/tests/test_validator.py b/tests/test_validator.py similarity index 100% rename from commune/tests/test_validator.py rename to tests/test_validator.py