diff --git a/examples/ollama_dvm/README.md b/examples/ollama_dvm/README.md new file mode 100644 index 0000000..2709125 --- /dev/null +++ b/examples/ollama_dvm/README.md @@ -0,0 +1,26 @@ +# NostrAI: Nostr NIP90 Data Vending Machine Framework Example + +Projects in this folder contain ready-to-use DVMs. To tun the DVM following the next steps: + +## To get started: +- Install Python 3.10 + + +Create a new venv in this directory by opening the terminal here, or navigate to this directory and type: `"python -m venv venv"` + - Place .env file (based on .env_example) in this folder. + - Recommended but optional: + - Create a `LNbits` account on an accessible instance of your choice, enter one account's id and admin key (this account will create other accounts for the dvms) Open the .env file and enter this info to `LNBITS_ADMIN_KEY`, `LNBITS_ADMIN_ID`, `LNBITS_HOST`. + - If you are running an own instance of `Nostdress` enter `NOSTDRESS_DOMAIN` or use the default one. + - Activate the venv with + - MacOS/Linux: source ./venv/bin/activate + - Windows: .\venv\Scripts\activate + - Type: `pip install nostr-dvm` + - Run `python3 main.py` (or python main.py) + - The framework will then automatically create keys, nip89 tags and zapable NIP57 `lightning addresses` for your dvms in this file. + - Check the .env file if these values look correct. + - Check the `main.py` file. You can update the image/description/name of your DVM before announcing it. + - You can then in main.py set `admin_config.REBROADCAST_NIP89` and + `admin_config.UPDATE_PROFILE` to `True` to announce the NIP89 info and update the npubs profile automatically. + - After this was successful you can set these back to False until the next time you want to update the NIP89 or profile. + +You are now running your own DVM. \ No newline at end of file diff --git a/examples/ollama_dvm/main.py b/examples/ollama_dvm/main.py new file mode 100644 index 0000000..6e0fb91 --- /dev/null +++ b/examples/ollama_dvm/main.py @@ -0,0 +1,54 @@ +import json +from pathlib import Path +import dotenv + +from nostr_dvm.tasks.textgeneration_llmlite import TextGenerationLLMLite +from nostr_dvm.utils.admin_utils import AdminConfig +from nostr_dvm.utils.dvmconfig import build_default_config +from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag + + +def main(): + identifier = "llama2" + name = "Ollama" + + dvm_config = build_default_config(identifier) + admin_config = AdminConfig() + admin_config.REBROADCAST_NIP89 = False + admin_config.UPDATE_PROFILE = False + admin_config.LUD16 = dvm_config.LN_ADDRESS + + options = {'default_model': "ollama/llama2", 'server': "http://localhost:11434"} + + nip89info = { + "name": name, + "image": "https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg", + "about": "I use a LLM connected via OLLAMA", + "encryptionSupported": True, + "cashuAccepted": True, + "nip90Params": { + + } + } + + nip89config = NIP89Config() + nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, nip89info["image"]) + nip89config.CONTENT = json.dumps(nip89info) + + ollama = TextGenerationLLMLite(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config, + options=options) + ollama.run() + + +if __name__ == '__main__': + env_path = Path('.env') + if not env_path.is_file(): + with open('.env', 'w') as f: + print("Writing new .env file") + f.write('') + if env_path.is_file(): + print(f'loading environment from {env_path.resolve()}') + dotenv.load_dotenv(env_path, verbose=True, override=True) + else: + raise FileNotFoundError(f'.env file not found at {env_path} ') + main() diff --git a/examples/ollama_dvm/test_client.py b/examples/ollama_dvm/test_client.py new file mode 100644 index 0000000..05c3752 --- /dev/null +++ b/examples/ollama_dvm/test_client.py @@ -0,0 +1,94 @@ +import json +import time +from pathlib import Path +from threading import Thread + +import dotenv +from nostr_sdk import Keys, Client, Tag, EventBuilder, Filter, HandleNotification, Timestamp, nip04_decrypt + +from nostr_dvm.utils.dvmconfig import DVMConfig +from nostr_dvm.utils.nostr_utils import send_event, check_and_set_private_key +from nostr_dvm.utils.definitions import EventDefinitions + + +def nostr_client_test_llm(prompt): + keys = Keys.from_sk_str(check_and_set_private_key("test_client")) + + iTag = Tag.parse(["i", prompt, "text"]) + relaysTag = Tag.parse(['relays', "wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org", + "wss://nostr-pub.wellorder.net"]) + alttag = Tag.parse(["alt", "This is a NIP90 DVM AI task to generate TTSt"]) + event = EventBuilder(EventDefinitions.KIND_NIP90_GENERATE_TEXT, str("Generate an Audio File."), + [iTag, relaysTag, alttag]).to_event(keys) + + relay_list = ["wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org", + "wss://nostr-pub.wellorder.net"] + + client = Client(keys) + for relay in relay_list: + client.add_relay(relay) + client.connect() + config = DVMConfig + send_event(event, client=client, dvm_config=config) + return event.as_json() + +def nostr_client(): + keys = Keys.from_sk_str(check_and_set_private_key("test_client")) + sk = keys.secret_key() + pk = keys.public_key() + print(f"Nostr Test Client public key: {pk.to_bech32()}, Hex: {pk.to_hex()} ") + client = Client(keys) + dvmconfig = DVMConfig() + for relay in dvmconfig.RELAY_LIST: + client.add_relay(relay) + client.connect() + + dm_zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM, + EventDefinitions.KIND_ZAP]).since( + Timestamp.now()) # events to us specific + dvm_filter = (Filter().kinds([EventDefinitions.KIND_NIP90_RESULT_GENERATE_TEXT, + EventDefinitions.KIND_FEEDBACK]).since(Timestamp.now())) # public events + client.subscribe([dm_zap_filter, dvm_filter]) + + + nostr_client_test_llm("Tell me a joke about a purple Ostrich!") + print("Sending Job Request") + + + #nostr_client_test_image_private("a beautiful ostrich watching the sunset") + class NotificationHandler(HandleNotification): + def handle(self, relay_url, event): + print(f"Received new event from {relay_url}: {event.as_json()}") + if event.kind() == 7000: + print("[Nostr Client]: " + event.as_json()) + elif 6000 < event.kind() < 6999: + print("[Nostr Client]: " + event.as_json()) + print("[Nostr Client]: " + event.content()) + + elif event.kind() == 4: + dec_text = nip04_decrypt(sk, event.pubkey(), event.content()) + print("[Nostr Client]: " + f"Received new msg: {dec_text}") + + elif event.kind() == 9735: + print("[Nostr Client]: " + f"Received new zap:") + print(event.as_json()) + + def handle_msg(self, relay_url, msg): + return + + client.handle_notifications(NotificationHandler()) + while True: + time.sleep(5.0) + + +if __name__ == '__main__': + + env_path = Path('.env') + if env_path.is_file(): + print(f'loading environment from {env_path.resolve()}') + dotenv.load_dotenv(env_path, verbose=True, override=True) + else: + raise FileNotFoundError(f'.env file not found at {env_path} ') + + nostr_dvm_thread = Thread(target=nostr_client()) + nostr_dvm_thread.start() diff --git a/examples/tts_dvm/README.md b/examples/tts_dvm/README.md new file mode 100644 index 0000000..2709125 --- /dev/null +++ b/examples/tts_dvm/README.md @@ -0,0 +1,26 @@ +# NostrAI: Nostr NIP90 Data Vending Machine Framework Example + +Projects in this folder contain ready-to-use DVMs. To tun the DVM following the next steps: + +## To get started: +- Install Python 3.10 + + +Create a new venv in this directory by opening the terminal here, or navigate to this directory and type: `"python -m venv venv"` + - Place .env file (based on .env_example) in this folder. + - Recommended but optional: + - Create a `LNbits` account on an accessible instance of your choice, enter one account's id and admin key (this account will create other accounts for the dvms) Open the .env file and enter this info to `LNBITS_ADMIN_KEY`, `LNBITS_ADMIN_ID`, `LNBITS_HOST`. + - If you are running an own instance of `Nostdress` enter `NOSTDRESS_DOMAIN` or use the default one. + - Activate the venv with + - MacOS/Linux: source ./venv/bin/activate + - Windows: .\venv\Scripts\activate + - Type: `pip install nostr-dvm` + - Run `python3 main.py` (or python main.py) + - The framework will then automatically create keys, nip89 tags and zapable NIP57 `lightning addresses` for your dvms in this file. + - Check the .env file if these values look correct. + - Check the `main.py` file. You can update the image/description/name of your DVM before announcing it. + - You can then in main.py set `admin_config.REBROADCAST_NIP89` and + `admin_config.UPDATE_PROFILE` to `True` to announce the NIP89 info and update the npubs profile automatically. + - After this was successful you can set these back to False until the next time you want to update the NIP89 or profile. + +You are now running your own DVM. \ No newline at end of file diff --git a/examples/tts_dvm/main.py b/examples/tts_dvm/main.py new file mode 100644 index 0000000..2ce8d46 --- /dev/null +++ b/examples/tts_dvm/main.py @@ -0,0 +1,60 @@ +import json +from pathlib import Path +import dotenv +from nostr_dvm.tasks.texttospeech import TextToSpeech +from nostr_dvm.utils.admin_utils import AdminConfig +from nostr_dvm.utils.dvmconfig import build_default_config +from nostr_dvm.utils.nip89_utils import NIP89Config, check_and_set_d_tag + + +def main(): + identifier = "tts" + name = "Guy Swann Clone" + + dvm_config = build_default_config(identifier) + admin_config = AdminConfig() + admin_config.REBROADCAST_NIP89 = False + admin_config.UPDATE_PROFILE = False + admin_config.LUD16 = dvm_config.LN_ADDRESS + + # Use default file if paramter is empty, else overwrite with any local wav file + options = {'input_file': ""} + + nip89info = { + "name": name, + "image": "https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg", + "about": "I Generate Speech from Text", + "encryptionSupported": True, + "cashuAccepted": True, + "nip90Params": { + "language": { + "required": False, + "values": [] + } + } + } + + nip89config = NIP89Config() + nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, nip89info["image"]) + nip89config.CONTENT = json.dumps(nip89info) + + tts = TextToSpeech(name=name, + dvm_config=dvm_config, + nip89config=nip89config, + admin_config=admin_config, + options=options) + tts.run() + + +if __name__ == '__main__': + env_path = Path('.env') + if not env_path.is_file(): + with open('.env', 'w') as f: + print("Writing new .env file") + f.write('') + if env_path.is_file(): + print(f'loading environment from {env_path.resolve()}') + dotenv.load_dotenv(env_path, verbose=True, override=True) + else: + raise FileNotFoundError(f'.env file not found at {env_path} ') + main() diff --git a/examples/tts_dvm/test_client.py b/examples/tts_dvm/test_client.py new file mode 100644 index 0000000..47cc76e --- /dev/null +++ b/examples/tts_dvm/test_client.py @@ -0,0 +1,98 @@ +import json +import time +from pathlib import Path +from threading import Thread + +import dotenv +from nostr_sdk import Keys, Client, Tag, EventBuilder, Filter, HandleNotification, Timestamp, nip04_decrypt + +from nostr_dvm.utils.dvmconfig import DVMConfig +from nostr_dvm.utils.nostr_utils import send_event, check_and_set_private_key +from nostr_dvm.utils.definitions import EventDefinitions + + +def nostr_client_test_tts(prompt): + keys = Keys.from_sk_str(check_and_set_private_key("test_client")) + + iTag = Tag.parse(["i", prompt, "text"]) + paramTag1 = Tag.parse(["param", "language", "en"]) + + + bidTag = Tag.parse(['bid', str(1000 * 1000), str(1000 * 1000)]) + relaysTag = Tag.parse(['relays', "wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org", + "wss://nostr-pub.wellorder.net"]) + alttag = Tag.parse(["alt", "This is a NIP90 DVM AI task to generate TTSt"]) + event = EventBuilder(EventDefinitions.KIND_NIP90_TEXT_TO_SPEECH, str("Generate an Audio File."), + [iTag, paramTag1, bidTag, relaysTag, alttag]).to_event(keys) + + relay_list = ["wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org", + "wss://nostr-pub.wellorder.net"] + + client = Client(keys) + for relay in relay_list: + client.add_relay(relay) + client.connect() + config = DVMConfig + send_event(event, client=client, dvm_config=config) + return event.as_json() + +def nostr_client(): + keys = Keys.from_sk_str(check_and_set_private_key("test_client")) + sk = keys.secret_key() + pk = keys.public_key() + print(f"Nostr Test Client public key: {pk.to_bech32()}, Hex: {pk.to_hex()} ") + client = Client(keys) + dvmconfig = DVMConfig() + for relay in dvmconfig.RELAY_LIST: + client.add_relay(relay) + client.connect() + + dm_zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM, + EventDefinitions.KIND_ZAP]).since( + Timestamp.now()) # events to us specific + dvm_filter = (Filter().kinds([EventDefinitions.KIND_NIP90_RESULT_TEXT_TO_SPEECH, + EventDefinitions.KIND_FEEDBACK]).since(Timestamp.now())) # public events + client.subscribe([dm_zap_filter, dvm_filter]) + + + nostr_client_test_tts("Hello, this is a test. Mic check one, two.") + print("Sending Job Request") + + + #nostr_client_test_image_private("a beautiful ostrich watching the sunset") + class NotificationHandler(HandleNotification): + def handle(self, relay_url, event): + print(f"Received new event from {relay_url}: {event.as_json()}") + if event.kind() == 7000: + print("[Nostr Client]: " + event.as_json()) + elif 6000 < event.kind() < 6999: + print("[Nostr Client]: " + event.as_json()) + print("[Nostr Client]: " + event.content()) + + elif event.kind() == 4: + dec_text = nip04_decrypt(sk, event.pubkey(), event.content()) + print("[Nostr Client]: " + f"Received new msg: {dec_text}") + + elif event.kind() == 9735: + print("[Nostr Client]: " + f"Received new zap:") + print(event.as_json()) + + def handle_msg(self, relay_url, msg): + return + + client.handle_notifications(NotificationHandler()) + while True: + time.sleep(5.0) + + +if __name__ == '__main__': + + env_path = Path('.env') + if env_path.is_file(): + print(f'loading environment from {env_path.resolve()}') + dotenv.load_dotenv(env_path, verbose=True, override=True) + else: + raise FileNotFoundError(f'.env file not found at {env_path} ') + + nostr_dvm_thread = Thread(target=nostr_client()) + nostr_dvm_thread.start() diff --git a/main.py b/main.py index 6a2bf38..2d71129 100644 --- a/main.py +++ b/main.py @@ -148,7 +148,7 @@ if __name__ == '__main__': env_path = Path('.env') if not env_path.is_file(): with open('.env', 'w') as f: - print("Wrting new .env file") + print("Writing new .env file") f.write('') if env_path.is_file(): print(f'loading environment from {env_path.resolve()}') diff --git a/nostr_dvm/dvm.py b/nostr_dvm/dvm.py index 32f899f..44febf7 100644 --- a/nostr_dvm/dvm.py +++ b/nostr_dvm/dvm.py @@ -484,9 +484,11 @@ class DVM: print("Finished processing, loading data..") with open(os.path.abspath('output.txt')) as f: - result = f.readlines()[0] - print(result) - #f.close() + resultall = f.readlines() + result = "" + for line in resultall: + if line != '\n': + result += line os.remove(os.path.abspath('output.txt')) else: #Some components might have issues with running code in otuside venv. # We install locally in these cases for now diff --git a/nostr_dvm/tasks/textgeneration_llmlite.py b/nostr_dvm/tasks/textgeneration_llmlite.py index 9bee4c1..ab2bd0e 100644 --- a/nostr_dvm/tasks/textgeneration_llmlite.py +++ b/nostr_dvm/tasks/textgeneration_llmlite.py @@ -20,7 +20,7 @@ Outputs: Generated text """ -class TextGenerationOLLAMA(DVMTaskInterface): +class TextGenerationLLMLite(DVMTaskInterface): KIND: int = EventDefinitions.KIND_NIP90_GENERATE_TEXT TASK: str = "text-to-text" FIX_COST: float = 0 @@ -80,7 +80,8 @@ class TextGenerationOLLAMA(DVMTaskInterface): response = completion( model=options["model"], messages=[{"content": options["prompt"], "role": "user"}], - api_base=options["server"] + api_base=options["server"], + stream=False ) print(response.choices[0].message.content) return response.choices[0].message.content @@ -112,25 +113,20 @@ def build_example(name, identifier, admin_config): "about": "I use a LLM connected via OLLAMA", "encryptionSupported": True, "cashuAccepted": True, - "nip90Params": { - "size": { - "required": False, - "values": ["1024:1024", "1024x1792", "1792x1024"] - } - } + "nip90Params": {} } nip89config = NIP89Config() nip89config.DTAG = check_and_set_d_tag(identifier, name, dvm_config.PRIVATE_KEY, nip89info["image"]) nip89config.CONTENT = json.dumps(nip89info) - return TextGenerationOLLAMA(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config, options=options) + return TextGenerationLLMLite(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config, options=options) def process_venv(): args = DVMTaskInterface.process_args() dvm_config = build_default_config(args.identifier) - dvm = TextGenerationOLLAMA(name="", dvm_config=dvm_config, nip89config=NIP89Config(), admin_config=None) + dvm = TextGenerationLLMLite(name="", dvm_config=dvm_config, nip89config=NIP89Config(), admin_config=None) result = dvm.process(json.loads(args.request)) DVMTaskInterface.write_output(result, args.output) diff --git a/nostr_dvm/utils/definitions.py b/nostr_dvm/utils/definitions.py index 5191ff0..99d1df9 100644 --- a/nostr_dvm/utils/definitions.py +++ b/nostr_dvm/utils/definitions.py @@ -15,7 +15,7 @@ class EventDefinitions: KIND_NIP90_TRANSLATE_TEXT = 5002 KIND_NIP90_RESULT_TRANSLATE_TEXT = KIND_NIP90_TRANSLATE_TEXT + 1000 KIND_NIP90_TEXT_TO_SPEECH = 5005 - KIND_NIP90_TEXT_TO_SPEECH_RESULT = KIND_NIP90_TEXT_TO_SPEECH + 1000 + KIND_NIP90_RESULT_TEXT_TO_SPEECH = KIND_NIP90_TEXT_TO_SPEECH + 1000 KIND_NIP90_GENERATE_TEXT = 5050 KIND_NIP90_RESULT_GENERATE_TEXT = KIND_NIP90_GENERATE_TEXT + 1000 KIND_NIP90_GENERATE_IMAGE = 5100 diff --git a/setup.py b/setup.py index abdd287..628e364 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,6 @@ from setuptools import setup, find_packages -VERSION = '0.0.8' +VERSION = '0.0.9' DESCRIPTION = 'A framework to build and run Nostr NIP90 Data Vending Machines' LONG_DESCRIPTION = ('A framework to build and run Nostr NIP90 Data Vending Machines. ' 'This is an early stage release. Interfaces might change/brick')