From 5a3f5606df8804c01c932b7d51597344cb995f9c Mon Sep 17 00:00:00 2001 From: Believethehype Date: Mon, 20 Nov 2023 19:17:10 +0100 Subject: [PATCH 01/18] added generate image via nserver, refactor --- .env_example | 17 ++- README.md | 2 +- backends/README.md | 10 ++ backends/nova_server.py | 105 +++++++++++++++ dvm.py | 67 ++++++---- interfaces/dvmtaskinterface.py | 2 +- main.py | 80 +++++++++--- requirements.txt | 2 + tasks/README.md | 13 ++ tasks/imagegenerationsdxl.py | 120 ++++++++++++++++++ ...textractionPDF.py => textextractionpdf.py} | 28 ++-- tasks/translation.py | 21 ++- test_client.py | 59 ++++++--- utils/backend_utils.py | 14 +- utils/env.py | 1 + utils/output_utils.py | 52 +++++++- 16 files changed, 493 insertions(+), 100 deletions(-) create mode 100644 backends/README.md create mode 100644 backends/nova_server.py create mode 100644 tasks/README.md create mode 100644 tasks/imagegenerationsdxl.py rename tasks/{textextractionPDF.py => textextractionpdf.py} (72%) diff --git a/.env_example b/.env_example index ea271f8..a5718bf 100644 --- a/.env_example +++ b/.env_example @@ -1,9 +1,14 @@ -NOSTR_PRIVATE_KEY = nostrSecretkeyinhex -NOSTR_TEST_CLIENT_PRIVATE_KEY = nostrSecretkeyinhex_forthetestclient -USER_DB_PATH = nostrzaps.db +NOSTR_PRIVATE_KEY = "nostrSecretkeyinhex" +NOSTR_TEST_CLIENT_PRIVATE_KEY = "nostrSecretkeyinhex_forthetestclient" +USER_DB_PATH = "nostrzaps.db" -LNBITS_INVOICE_KEY = lnbitswalletinvoicekey -LNBITS_HOST = https://lnbits.com +# Optional LNBITS options to create invoices (if empty, it will use the lud16 from profile) +LNBITS_INVOICE_KEY = "" +LNBITS_HOST = "https://lnbits.com" TASK_TEXTEXTRACTION_NIP89_DTAG = "asdd" -TASK_TRANSLATION_NIP89_DTAG = abcded +TASK_TRANSLATION_NIP89_DTAG = "abcded" +TASK_IMAGEGENERATION_NIP89_DTAG = "fgdfgdf" + +#Backend Specific Options for tasks that require them +NOVA_SERVER = "127.0.0.1:37318" \ No newline at end of file diff --git a/README.md b/README.md index 637e042..db4f1e6 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Nostr AI Data Vending Machine +# NostrAI Data Vending Machine This example DVM implementation in Python currently supports simple translations using Google translate, as well as extraction of text from links with pdf files. diff --git a/backends/README.md b/backends/README.md new file mode 100644 index 0000000..c7a2bac --- /dev/null +++ b/backends/README.md @@ -0,0 +1,10 @@ +# NostrAI Data Vending Machine Backends + +Each DVM task might either run locally or use a specific backend. +Especially for GPU tasks it might make sense to outsource some tasks on other machines. +Backends can also be API calls to (paid) services. This directory contains basic calling functions to such backends. +Modules in the folder "tasks" might use these functions to call a specific backend. + +Using backends might require some extra work like running/hosting a server or acquiring an API key. + + diff --git a/backends/nova_server.py b/backends/nova_server.py new file mode 100644 index 0000000..c631241 --- /dev/null +++ b/backends/nova_server.py @@ -0,0 +1,105 @@ +import io +import json +import os +import time +import zipfile +import pandas as pd +import requests +import PIL.Image as Image + +from utils.output_utils import uploadMediaToHoster + +""" +This file contains basic calling functions for ML tasks that are outsourced to nova-server +(https://github.com/hcmlab/nova-server). nova-server is an Open-Source backend that enables running models locally, by +accepting a request form. Modules are deployed in in separate virtual environments so dependencies won't conflict. + +Setup nova-server: +https://hcmlab.github.io/nova-server/docbuild/html/tutorials/introduction.html + +""" + +""" +send_request_to_nova_server(request_form) +Function to send a request_form to the server, containing all the information we parsed from the Nostr event and added +in the module that is calling the server + +""" + + +def send_request_to_nova_server(request_form, address): + print("Sending job to NOVA-Server") + url = ('http://' + address + '/' + str(request_form["mode"]).lower()) + headers = {'Content-type': 'application/x-www-form-urlencoded'} + response = requests.post(url, headers=headers, data=request_form) + return response.content + + +""" +check_nova_server_status(request_form) +Function that requests the status of the current process with the jobID (we use the Nostr event as jobID). +When the Job is successfully finished we grab the result and depending on the type return the output +We throw an exception on error +""" + + +def check_nova_server_status(jobID, address): + headers = {'Content-type': 'application/x-www-form-urlencoded'} + url_status = 'http://' + address + '/job_status' + url_log = 'http://' + address + '/log' + + print("Sending Status Request to NOVA-Server") + data = {"jobID": jobID} + + status = 0 + length = 0 + while status != 2 and status != 3: + response_status = requests.post(url_status, headers=headers, data=data) + response_log = requests.post(url_log, headers=headers, data=data) + status = int(json.loads(response_status.text)['status']) + + log = str(response_log.content)[length:] + length = len(str(response_log.content)) + if log != "": + print(log + " Status: " + str(status)) + # WAITING = 0, RUNNING = 1, FINISHED = 2, ERROR = 3 + time.sleep(1.0) + + if status == 2: + try: + result = "" + url_fetch = 'http://' + address + '/fetch_result' + print("Fetching Results from NOVA-Server...") + data = {"jobID": jobID} + response = requests.post(url_fetch, headers=headers, data=data) + content_type = response.headers['content-type'] + print(content_type) + if content_type == "image/jpeg": + image = Image.open(io.BytesIO(response.content)) + image.save("./outputs/image.jpg") + result = uploadMediaToHoster("./outputs/image.jpg") + os.remove("./outputs/image.jpg") + elif content_type == 'text/plain; charset=utf-8': + result = response.content.decode('utf-8') + elif content_type == "zip": + zf = zipfile.ZipFile(io.BytesIO(response.content), "r") + + for fileinfo in zf.infolist(): + if fileinfo.filename.endswith(".annotation~"): + try: + anno_string = zf.read(fileinfo).decode('utf-8', errors='replace') + columns = ['from', 'to', 'name', 'conf'] + result = pd.DataFrame([row.split(';') for row in anno_string.split('\n')], + columns=columns) + print(result) + with open("response.zip", "wb") as f: + f.write(response.content) + except: + zf.extractall() + + return result + except Exception as e: + print("Couldn't fetch result: " + str(e)) + + elif status == 3: + return "error" diff --git a/dvm.py b/dvm.py index c712ff1..535be35 100644 --- a/dvm.py +++ b/dvm.py @@ -22,13 +22,14 @@ jobs_on_hold_list = [] dvm_config = DVMConfig() -def dvm(config): +def DVM(config): dvm_config = config keys = Keys.from_sk_str(dvm_config.PRIVATE_KEY) pk = keys.public_key() print(f"Nostr DVM public key: {pk.to_bech32()}, Hex: {pk.to_hex()} ") - print('Supported DVM tasks: ' + ', '.join(p.TASK for p in dvm_config.SUPPORTED_TASKS)) + print('Supported DVM tasks: ' + ', '.join(p.NAME + ":" + p.TASK for p in dvm_config.SUPPORTED_TASKS)) + client = Client(keys) for relay in dvm_config.RELAY_LIST: @@ -207,6 +208,7 @@ def dvm(config): or job_event.kind() == EventDefinitions.KIND_DM): task = get_task(job_event, client=client, dvmconfig=dvm_config) + result = "" for dvm in dvm_config.SUPPORTED_TASKS: try: if task == dvm.TASK: @@ -215,7 +217,11 @@ def dvm(config): check_and_return_event(result, str(job_event.as_json()), dvm_key=dvm_config.PRIVATE_KEY) except Exception as e: + print(e) respond_to_error(e, job_event.as_json(), is_from_bot, dvm_config.PRIVATE_KEY) + return + + def check_event_has_not_unfinished_job_input(nevent, append, client, dvmconfig): task_supported, task, duration = check_task_is_supported(nevent, client, False, config=dvmconfig) @@ -246,44 +252,43 @@ def dvm(config): def send_job_status_reaction(original_event, status, is_paid=True, amount=0, client=None, content=None, config=None, key=None): dvmconfig = config - altdesc = "This is a reaction to a NIP90 DVM AI task. " + alt_description = "This is a reaction to a NIP90 DVM AI task. " task = get_task(original_event, client=client, dvmconfig=dvmconfig) if status == "processing": - altdesc = "NIP90 DVM AI task " + task + " started processing. " - reaction = altdesc + emoji.emojize(":thumbs_up:") + alt_description = "NIP90 DVM AI task " + task + " started processing. " + reaction = alt_description + emoji.emojize(":thumbs_up:") elif status == "success": - altdesc = "NIP90 DVM AI task " + task + " finished successfully. " - reaction = altdesc + emoji.emojize(":call_me_hand:") + alt_description = "NIP90 DVM AI task " + task + " finished successfully. " + reaction = alt_description + emoji.emojize(":call_me_hand:") elif status == "chain-scheduled": - altdesc = "NIP90 DVM AI task " + task + " Chain Task scheduled" - reaction = altdesc + emoji.emojize(":thumbs_up:") + alt_description = "NIP90 DVM AI task " + task + " Chain Task scheduled" + reaction = alt_description + emoji.emojize(":thumbs_up:") elif status == "error": - altdesc = "NIP90 DVM AI task " + task + " had an error. " + alt_description = "NIP90 DVM AI task " + task + " had an error. " if content is None: - reaction = altdesc + emoji.emojize(":thumbs_down:") + reaction = alt_description + emoji.emojize(":thumbs_down:") else: - reaction = altdesc + emoji.emojize(":thumbs_down:") + content + reaction = alt_description + emoji.emojize(":thumbs_down:") + content elif status == "payment-required": - altdesc = "NIP90 DVM AI task " + task + " requires payment of min " + str(amount) + " Sats. " - reaction = altdesc + emoji.emojize(":orange_heart:") + alt_description = "NIP90 DVM AI task " + task + " requires payment of min " + str(amount) + " Sats. " + reaction = alt_description + emoji.emojize(":orange_heart:") elif status == "payment-rejected": - altdesc = "NIP90 DVM AI task " + task + " payment is below required amount of " + str(amount) + " Sats. " - reaction = altdesc + emoji.emojize(":thumbs_down:") + alt_description = "NIP90 DVM AI task " + task + " payment is below required amount of " + str(amount) + " Sats. " + reaction = alt_description + emoji.emojize(":thumbs_down:") elif status == "user-blocked-from-service": - - altdesc = "NIP90 DVM AI task " + task + " can't be performed. User has been blocked from Service. " - reaction = altdesc + emoji.emojize(":thumbs_down:") + alt_description = "NIP90 DVM AI task " + task + " can't be performed. User has been blocked from Service. " + reaction = alt_description + emoji.emojize(":thumbs_down:") else: reaction = emoji.emojize(":thumbs_down:") - etag = Tag.parse(["e", original_event.id().to_hex()]) - ptag = Tag.parse(["p", original_event.pubkey().to_hex()]) - alttag = Tag.parse(["alt", altdesc]) - statustag = Tag.parse(["status", status]) - tags = [etag, ptag, alttag, statustag] + e_tag = Tag.parse(["e", original_event.id().to_hex()]) + p_tag = Tag.parse(["p", original_event.pubkey().to_hex()]) + alt_tag = Tag.parse(["alt", alt_description]) + status_tag = Tag.parse(["status", status]) + tags = [e_tag, p_tag, alt_tag, status_tag] if status == "success" or status == "error": # for x in job_list: @@ -354,8 +359,13 @@ def dvm(config): send_nostr_reply_event(data, original_event_str, key=keys) break - post_processed_content = post_process_result(data, original_event) - send_nostr_reply_event(post_processed_content, original_event_str, key=keys) + + try: + post_processed_content = post_process_result(data, original_event) + send_nostr_reply_event(post_processed_content, original_event_str, key=keys) + except Exception as e: + respond_to_error(e, original_event_str, False, dvm_config.PRIVATE_KEY) + def send_nostr_reply_event(content, original_event_as_str, key=None): originalevent = Event.from_json(original_event_as_str) @@ -395,7 +405,7 @@ def dvm(config): sender = "" task = "" if not is_from_bot: - send_job_status_reaction(original_event, "error", content=content, key=dvm_key) + send_job_status_reaction(original_event, "error", content=str(content), key=dvm_key) # TODO Send Zap back else: for tag in original_event.tags(): @@ -407,7 +417,8 @@ def dvm(config): user = get_from_sql_table(sender) if not user.iswhitelisted: amount = int(user.balance) + get_amount_per_task(task, dvm_config) - update_sql_table(sender, amount, user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16, user.name, + update_sql_table(sender, amount, user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16, + user.name, Timestamp.now().as_secs()) message = "There was the following error : " + content + ". Credits have been reimbursed" else: diff --git a/interfaces/dvmtaskinterface.py b/interfaces/dvmtaskinterface.py index 25316c0..d08c79e 100644 --- a/interfaces/dvmtaskinterface.py +++ b/interfaces/dvmtaskinterface.py @@ -3,7 +3,7 @@ class DVMTaskInterface: TASK: str COST: int - def NIP89_announcement(self): + def NIP89_announcement(self, d_tag, content): """Define the NIP89 Announcement""" pass diff --git a/main.py b/main.py index b8a12f1..afcab5e 100644 --- a/main.py +++ b/main.py @@ -4,36 +4,74 @@ from threading import Thread import dotenv import utils.env as env -from tasks.textextractionPDF import TextExtractionPDF +from tasks.imagegenerationsdxl import ImageGenerationSDXL +from tasks.textextractionpdf import TextExtractionPDF from tasks.translation import Translation -from utils.definitions import EventDefinitions - def run_nostr_dvm_with_local_config(): - from dvm import dvm, DVMConfig + from dvm import DVM, DVMConfig - dvmconfig = DVMConfig() - dvmconfig.PRIVATE_KEY = os.getenv(env.NOSTR_PRIVATE_KEY) + dvm_config = DVMConfig() + dvm_config.PRIVATE_KEY = os.getenv(env.NOSTR_PRIVATE_KEY) - #Spawn two DVMs - PDFextactor = TextExtractionPDF("PDF Extractor", env.NOSTR_PRIVATE_KEY) - Translator = Translation("Translator", env.NOSTR_PRIVATE_KEY) + # Spawn the DVMs + # Add NIP89 events for each DVM (set rebroad_cast = True for the next start in admin_utils) + # Add the dtag here or in your .env file, so you can update your dvm later and change the content as needed. + # Get a dtag and the content at vendata.io - #Add the 2 DVMS to the config - dvmconfig.SUPPORTED_TASKS = [PDFextactor, Translator] + # Spawn DVM1 Kind 5000 Text Ectractor from PDFs + pdfextactor = TextExtractionPDF("PDF Extractor", os.getenv(env.NOSTR_PRIVATE_KEY)) + d_tag = os.getenv(env.TASK_TEXTEXTRACTION_NIP89_DTAG) + content = "{\"name\":\"" + pdfextactor.NAME + ("\",\"image\":\"https://image.nostr.build" + "/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669" + ".jpg\",\"about\":\"I extract Text from pdf documents\"," + "\"nip90Params\":{}}") + dvm_config.NIP89s.append(pdfextactor.NIP89_announcement(d_tag, content)) - # Add NIP89 events for both DVMs (set rebroad_cast = True in admin_utils) - # Add the dtag in your .env file so you can update your dvm later and change the content in the module file as needed. - # Get a dtag at vendata.io - dvmconfig.NIP89s.append(PDFextactor.NIP89_announcement()) - dvmconfig.NIP89s.append(Translator.NIP89_announcement()) + # Spawn DVM2 Kind 5002 Text Translation + translator = Translation("Translator", os.getenv(env.NOSTR_PRIVATE_KEY)) + d_tag = os.getenv(env.TASK_TRANSLATION_NIP89_DTAG) + content = "{\"name\":\"" + translator.NAME + ("\",\"image\":\"https://image.nostr.build" + "/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669" + ".jpg\",\"about\":\"I translate text from given text/event/job, " + "currently using Google Translation Services into language defined " + "in param. \",\"nip90Params\":{\"language\":{\"required\":true," + "\"values\":[\"af\",\"am\",\"ar\",\"az\",\"be\",\"bg\",\"bn\"," + "\"bs\",\"ca\",\"ceb\",\"co\",\"cs\",\"cy\",\"da\",\"de\",\"el\"," + "\"eo\",\"es\",\"et\",\"eu\",\"fa\",\"fi\",\"fr\",\"fy\",\"ga\"," + "\"gd\",\"gl\",\"gu\",\"ha\",\"haw\",\"hi\",\"hmn\",\"hr\",\"ht\"," + "\"hu\",\"hy\",\"id\",\"ig\",\"is\",\"it\",\"he\",\"ja\",\"jv\"," + "\"ka\",\"kk\",\"km\",\"kn\",\"ko\",\"ku\",\"ky\",\"la\",\"lb\"," + "\"lo\",\"lt\",\"lv\",\"mg\",\"mi\",\"mk\",\"ml\",\"mn\",\"mr\"," + "\"ms\",\"mt\",\"my\",\"ne\",\"nl\",\"no\",\"ny\",\"or\",\"pa\"," + "\"pl\",\"ps\",\"pt\",\"ro\",\"ru\",\"sd\",\"si\",\"sk\",\"sl\"," + "\"sm\",\"sn\",\"so\",\"sq\",\"sr\",\"st\",\"su\",\"sv\",\"sw\"," + "\"ta\",\"te\",\"tg\",\"th\",\"tl\",\"tr\",\"ug\",\"uk\",\"ur\"," + "\"uz\",\"vi\",\"xh\",\"yi\",\"yo\",\"zh\",\"zu\"]}}}") + dvm_config.NIP89s.append(translator.NIP89_announcement(d_tag, content)) - #SET Lnbits Invoice Key and Server if DVM should provide invoices directly, else make sure you have a lnaddress on the profile - dvmconfig.LNBITS_INVOICE_KEY = os.getenv(env.LNBITS_INVOICE_KEY) - dvmconfig.LNBITS_URL = os.getenv(env.LNBITS_HOST) + # Spawn DVM3 Kind 5100 Image Generation This one uses a specific backend called nova-server. If you want to use + # it see the instructions in backends/nova_server + artist = ImageGenerationSDXL("Unstable Diffusion", os.getenv(env.NOSTR_PRIVATE_KEY)) + d_tag = os.getenv(env.TASK_IMAGEGENERATION_NIP89_DTAG) + content = "{\"name\":\"" + artist.NAME + ("\",\"image\":\"https://image.nostr.build" + "/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg" + "\",\"about\":\"I draw images based on a prompt with Stable Diffusion " + "XL 1.0.\",\"nip90Params\":{}}") + dvm_config.NIP89s.append(artist.NIP89_announcement(d_tag, content)) - #Start the DVM - nostr_dvm_thread = Thread(target=dvm, args=[dvmconfig]) + + + # Add the DVMS you want to use to the config + dvm_config.SUPPORTED_TASKS = [pdfextactor, translator, artist] + + # SET Lnbits Invoice Key and Server if DVM should provide invoices directly, else make sure you have a lnaddress + # on the profile + dvm_config.LNBITS_INVOICE_KEY = os.getenv(env.LNBITS_INVOICE_KEY) + dvm_config.LNBITS_URL = os.getenv(env.LNBITS_HOST) + + # Start the Server + nostr_dvm_thread = Thread(target=DVM, args=[dvm_config]) nostr_dvm_thread.start() diff --git a/requirements.txt b/requirements.txt index 16bad4f..a1df8ea 100644 --- a/requirements.txt +++ b/requirements.txt @@ -21,6 +21,7 @@ python-dateutil==2.8.2 python-dotenv==1.0.0 python-editor==1.0.4 pytz==2023.3.post1 +PyUpload~=0.1.4 pyuseragents==1.0.5 readchar==4.0.5 requests==2.31.0 @@ -31,3 +32,4 @@ translatepy==2.3 tzdata==2023.3 urllib3==2.1.0 wcwidth==0.2.10 + diff --git a/tasks/README.md b/tasks/README.md new file mode 100644 index 0000000..7dcf575 --- /dev/null +++ b/tasks/README.md @@ -0,0 +1,13 @@ +# NostrAI Data Vending Machine Tasks + +Here Tasks can be defined. Tasks need to follow the DVMTaskInterface as defined in interfaces. +Tasks can either happen locally (especially if they are fast) or they can call an alternative backend. +Reusable backend functions can be defined in backends (e.g. API calls) + +Current List of Tasks: + +| Module | Kind | Description | Backend | +|---------------------|------|-------------------------------------------|---------------------------| +| Translation | 5002 | Translates Inputs to another language | Local, calling Google API | +| TextExtractionPDF | 5001 | Extracts Text from a PDF file | Local | +| ImageGenerationSDXL | 5100 | Generates an Image with StableDiffusionXL | nova-server | \ No newline at end of file diff --git a/tasks/imagegenerationsdxl.py b/tasks/imagegenerationsdxl.py new file mode 100644 index 0000000..58ecb40 --- /dev/null +++ b/tasks/imagegenerationsdxl.py @@ -0,0 +1,120 @@ +import os +from multiprocessing.pool import ThreadPool +from backends.nova_server import check_nova_server_status, send_request_to_nova_server +from interfaces.dvmtaskinterface import DVMTaskInterface +from utils.definitions import EventDefinitions +from utils.nip89_utils import NIP89Announcement + + +""" +This File contains a Module to transform Text input on NOVA-Server and receive results back. + +Accepted Inputs: Prompt (text) +Outputs: An url to an Image +""" + + +class ImageGenerationSDXL(DVMTaskInterface): + KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE + TASK: str = "text-to-image" + COST: int = 0 + + def __init__(self, name, pk): + self.NAME = name + self.PK = pk + + def NIP89_announcement(self, d_tag, content): + nip89 = NIP89Announcement() + nip89.kind = self.KIND + nip89.pk = self.PK + nip89.dtag = d_tag + nip89.content = content + return nip89 + + def is_input_supported(self, input_type, input_content): + if input_type != "text": + return False + return True + + def create_request_form_from_nostr_event(self, event, client=None, dvm_config=None): + request_form = {"jobID": event.id().to_hex() + "_"+ self.NAME.replace(" ", "")} + request_form["mode"] = "PROCESS" + request_form["trainerFilePath"] = 'modules\\stablediffusionxl\\stablediffusionxl.trainer' + + prompt = "" + negative_prompt = "" + #model = "stabilityai/stable-diffusion-xl-base-1.0" + model = "unstable" + # models: juggernautXL, dynavisionXL, colossusProjectXL, newrealityXL, unstable + ratio_width = "1" + ratio_height = "1" + width = "" + height = "" + lora = "" + lora_weight = "" + strength = "" + guidance_scale = "" + for tag in event.tags(): + if tag.as_vec()[0] == 'i': + input_type = tag.as_vec()[2] + if input_type == "text": + prompt = tag.as_vec()[1] + + elif tag.as_vec()[0] == 'param': + print(tag.as_vec()[2]) + if tag.as_vec()[1] == "negative_prompt": + negative_prompt = tag.as_vec()[2] + elif tag.as_vec()[1] == "lora": + lora = tag.as_vec()[2] + elif tag.as_vec()[1] == "lora_weight": + lora_weight = tag.as_vec()[2] + elif tag.as_vec()[1] == "strength": + strength = tag.as_vec()[2] + elif tag.as_vec()[1] == "guidance_scale": + guidance_scale = tag.as_vec()[2] + elif tag.as_vec()[1] == "ratio": + if len(tag.as_vec()) > 3: + ratio_width = (tag.as_vec()[2]) + ratio_height = (tag.as_vec()[3]) + elif len(tag.as_vec()) == 3: + split = tag.as_vec()[2].split(":") + ratio_width = split[0] + ratio_height = split[1] + #if size is set it will overwrite ratio. + elif tag.as_vec()[1] == "size": + + if len(tag.as_vec()) > 3: + width = (tag.as_vec()[2]) + height = (tag.as_vec()[3]) + elif len(tag.as_vec()) == 3: + split = tag.as_vec()[2].split("x") + if len(split) > 1: + width = split[0] + height = split[1] + print(width) + print(height) + elif tag.as_vec()[1] == "model": + model = tag.as_vec()[2] + + prompt = prompt.replace(";", ",") + request_form['data'] = '[{"id":"input_prompt","type":"input","src":"request:text","data":"' + prompt + '","active":"True"},{"id":"negative_prompt","type":"input","src":"request:text","data":"' + negative_prompt + '","active":"True"},{"id":"output_image","type":"output","src":"request:image","active":"True"}]' + request_form["optStr"] = ('model=' + model + ';ratio=' + str(ratio_width) + '-' + str(ratio_height) + ';size=' + + str(width) + '-' + str(height) + ';strength=' + str(strength) + ';guidance_scale=' + + str(guidance_scale) + ';lora=' + lora + ';lora_weight=' + lora_weight) + + return request_form + + def process(self, request_form): + try: + # Call the process route of NOVA-Server with our request form. + success = send_request_to_nova_server(request_form, os.environ["NOVA_SERVER"]) + print(success) + + pool = ThreadPool(processes=1) + thread = pool.apply_async(check_nova_server_status, (request_form['jobID'], os.environ["NOVA_SERVER"])) + print("Wait for results of NOVA-Server...") + result = thread.get() + return str(result) + + except Exception as e: + raise Exception(e) diff --git a/tasks/textextractionPDF.py b/tasks/textextractionpdf.py similarity index 72% rename from tasks/textextractionPDF.py rename to tasks/textextractionpdf.py index 05ac8ce..26f8c45 100644 --- a/tasks/textextractionPDF.py +++ b/tasks/textextractionpdf.py @@ -2,12 +2,16 @@ import os import re from interfaces.dvmtaskinterface import DVMTaskInterface -from utils import env from utils.definitions import EventDefinitions from utils.nip89_utils import NIP89Announcement -from utils.nostr_utils import get_event_by_id, get_referenced_event_by_id +from utils.nostr_utils import get_event_by_id +""" +This File contains a Module to extract Text from a PDF file locally on the DVM Machine +Accepted Inputs: Url to pdf file, Event containing an URL to a PDF file +Outputs: Text containing the extracted contents of the PDF file +""" class TextExtractionPDF(DVMTaskInterface): KIND: int = EventDefinitions.KIND_NIP90_EXTRACT_TEXT TASK: str = "pdf-to-text" @@ -17,16 +21,16 @@ class TextExtractionPDF(DVMTaskInterface): self.NAME = name self.PK = pk - def NIP89_announcement(self): + def NIP89_announcement(self, d_tag, content): nip89 = NIP89Announcement() nip89.kind = self.KIND nip89.pk = self.PK - nip89.dtag = os.getenv(env.TASK_TEXTEXTRACTION_NIP89_DTAG) - nip89.content = "{\"name\":\"" + self.NAME + "\",\"image\":\"https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg\",\"about\":\"I extract Text from pdf documents\",\"nip90Params\":{}}" + nip89.dtag = d_tag + nip89.content = content return nip89 def is_input_supported(self, input_type, input_content): - if input_type != "url": + if input_type != "url" and input_type != "event": return False return True @@ -45,23 +49,21 @@ class TextExtractionPDF(DVMTaskInterface): if input_type == "url": url = input_content + # if event contains url to pdf, we checked for a pdf link before elif input_type == "event": evt = get_event_by_id(input_content, config=dvm_config) url = re.search("(?Phttps?://[^\s]+)", evt.content()).group("url") - elif input_type == "job": - evt = get_referenced_event_by_id(input_content, [EventDefinitions.KIND_NIP90_RESULT_GENERATE_IMAGE], - client, config=dvm_config) - - url = re.search("(?Phttps?://[^\s]+)", evt.content()).group("url") request_form["optStr"] = 'url=' + url return request_form def process(self, request_form): - options = DVMTaskInterface.setOptions(request_form) from pypdf import PdfReader from pathlib import Path import requests + + options = DVMTaskInterface.setOptions(request_form) + try: file_path = Path('temp.pdf') response = requests.get(options["url"]) @@ -76,4 +78,4 @@ class TextExtractionPDF(DVMTaskInterface): os.remove('temp.pdf') return text except Exception as e: - raise Exception(e) \ No newline at end of file + raise Exception(e) diff --git a/tasks/translation.py b/tasks/translation.py index 01e99ff..362aee8 100644 --- a/tasks/translation.py +++ b/tasks/translation.py @@ -1,12 +1,19 @@ import os from interfaces.dvmtaskinterface import DVMTaskInterface -from utils import env from utils.definitions import EventDefinitions from utils.nip89_utils import NIP89Announcement from utils.nostr_utils import get_referenced_event_by_id, get_event_by_id +""" +This File contains a Module to call Google Translate Services locally on the DVM Machine + +Accepted Inputs: Text, Events, Jobs (Text Extraction, Summary, Translation) +Outputs: Text containing the Translation in the desired language. +""" + + class Translation(DVMTaskInterface): KIND: int = EventDefinitions.KIND_NIP90_TRANSLATE_TEXT TASK: str = "translation" @@ -16,12 +23,12 @@ class Translation(DVMTaskInterface): self.NAME = name self.PK = pk - def NIP89_announcement(self): + def NIP89_announcement(self, d_tag, content): nip89 = NIP89Announcement() nip89.kind = self.KIND nip89.pk = self.PK - nip89.dtag = os.getenv(env.TASK_TRANSLATION_NIP89_DTAG) - nip89.content = "{\"name\":\"" + self.NAME + "\",\"image\":\"https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg\",\"about\":\"I translate text from given text/event/job, currently using Google Translation Services into language defined in param. \",\"nip90Params\":{\"language\":{\"required\":true,\"values\":[\"af\",\"am\",\"ar\",\"az\",\"be\",\"bg\",\"bn\",\"bs\",\"ca\",\"ceb\",\"co\",\"cs\",\"cy\",\"da\",\"de\",\"el\",\"eo\",\"es\",\"et\",\"eu\",\"fa\",\"fi\",\"fr\",\"fy\",\"ga\",\"gd\",\"gl\",\"gu\",\"ha\",\"haw\",\"hi\",\"hmn\",\"hr\",\"ht\",\"hu\",\"hy\",\"id\",\"ig\",\"is\",\"it\",\"he\",\"ja\",\"jv\",\"ka\",\"kk\",\"km\",\"kn\",\"ko\",\"ku\",\"ky\",\"la\",\"lb\",\"lo\",\"lt\",\"lv\",\"mg\",\"mi\",\"mk\",\"ml\",\"mn\",\"mr\",\"ms\",\"mt\",\"my\",\"ne\",\"nl\",\"no\",\"ny\",\"or\",\"pa\",\"pl\",\"ps\",\"pt\",\"ro\",\"ru\",\"sd\",\"si\",\"sk\",\"sl\",\"sm\",\"sn\",\"so\",\"sq\",\"sr\",\"st\",\"su\",\"sv\",\"sw\",\"ta\",\"te\",\"tg\",\"th\",\"tl\",\"tr\",\"ug\",\"uk\",\"ur\",\"uz\",\"vi\",\"xh\",\"yi\",\"yo\",\"zh\",\"zu\"]}}}" + nip89.dtag = d_tag + nip89.content = content return nip89 def is_input_supported(self, input_type, input_content): @@ -65,7 +72,8 @@ class Translation(DVMTaskInterface): if tag.as_vec()[0] == 'i': evt = get_referenced_event_by_id(tag.as_vec()[1], [EventDefinitions.KIND_NIP90_RESULT_EXTRACT_TEXT, - EventDefinitions.KIND_NIP90_RESULT_SUMMARIZE_TEXT], + EventDefinitions.KIND_NIP90_RESULT_SUMMARIZE_TEXT, + EventDefinitions.KIND_NIP90_RESULT_TRANSLATE_TEXT], client, config=dvm_config) text = evt.content() @@ -77,8 +85,9 @@ class Translation(DVMTaskInterface): return request_form def process(self, request_form): - options = DVMTaskInterface.setOptions(request_form) from translatepy.translators.google import GoogleTranslate + + options = DVMTaskInterface.setOptions(request_form) gtranslate = GoogleTranslate() length = len(options["text"]) diff --git a/test_client.py b/test_client.py index 4e7a4bf..29f3d97 100644 --- a/test_client.py +++ b/test_client.py @@ -1,4 +1,3 @@ - import os import time import datetime as datetime @@ -12,7 +11,9 @@ from utils.nostr_utils import send_event from utils.definitions import EventDefinitions, RELAY_LIST import utils.env as env -#TODO HINT: Only use this path with a preiously whitelisted privkey, as zapping events is not implemented in the lib/code + + +# TODO HINT: Best use this path with a previously whitelisted privkey, as zapping events is not implemented in the lib/code def nostr_client_test_translation(input, kind, lang, sats, satsmax): keys = Keys.from_sk_str(os.getenv(env.NOSTR_TEST_CLIENT_PRIVATE_KEY)) if kind == "text": @@ -22,19 +23,46 @@ def nostr_client_test_translation(input, kind, lang, sats, satsmax): paramTag1 = Tag.parse(["param", "language", lang]) bidTag = Tag.parse(['bid', str(sats * 1000), str(satsmax * 1000)]) - relaysTag = Tag.parse(['relays', "wss://relay.damus.io", "wss://blastr.f7z.xyz", "wss://relayable.org", "wss://nostr-pub.wellorder.net"]) + 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 translate a given Input"]) - event = EventBuilder(EventDefinitions.KIND_NIP90_TRANSLATE_TEXT, str("Translate the given input."), [iTag, paramTag1, bidTag, relaysTag, alttag]).to_event(keys) + event = EventBuilder(EventDefinitions.KIND_NIP90_TRANSLATE_TEXT, str("Translate the given input."), + [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() - send_event(event, client, keys) + send_event(event, client, keys) + return event.as_json() + + +def nostr_client_test_image(prompt): + keys = Keys.from_sk_str(os.getenv(env.NOSTR_TEST_CLIENT_PRIVATE_KEY)) + + iTag = Tag.parse(["i", prompt, "text"]) + outTag = Tag.parse(["output", "image/png;format=url"]) + paramTag1 = Tag.parse(["param", "size", "1024x1024"]) + tTag = Tag.parse(["t", "bitcoin"]) + + 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 translate a given Input"]) + event = EventBuilder(EventDefinitions.KIND_NIP90_GENERATE_IMAGE, str("Generate an Image."), + [iTag, outTag, tTag, 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() + send_event(event, client, keys) return event.as_json() def nostr_client(): @@ -48,17 +76,19 @@ def nostr_client(): client.connect() dm_zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM, - EventDefinitions.KIND_ZAP]).since(Timestamp.now()) # events to us specific + EventDefinitions.KIND_ZAP]).since( + Timestamp.now()) # events to us specific dvm_filter = (Filter().kinds([EventDefinitions.KIND_NIP90_RESULT_TRANSLATE_TEXT, - EventDefinitions.KIND_FEEDBACK]).since(Timestamp.now())) # public events + EventDefinitions.KIND_FEEDBACK]).since(Timestamp.now())) # public events client.subscribe([dm_zap_filter, dvm_filter]) + # nostr_client_test_translation("This is the result of the DVM in spanish", "text", "es", 20, 20) + #nostr_client_test_translation("44a0a8b395ade39d46b9d20038b3f0c8a11168e67c442e3ece95e4a1703e2beb", "event", "es", 20, + # 20) - #nostr_client_test_translation("This is the result of the DVM in spanish", "text", "es", 20, 20) - nostr_client_test_translation("44a0a8b395ade39d46b9d20038b3f0c8a11168e67c442e3ece95e4a1703e2beb", "event", "fr", 20, 20) + # nostr_client_test_translation("9c5d6d054e1b7a34a6a4b26ac59469c96e77f7cba003a30456fa6a57974ea86d", "event", "zh", 20, 20) - - #nostr_client_test_image(sats=50, satsmax=10) + nostr_client_test_image("a beautiful purple ostrich watching the sunset") class NotificationHandler(HandleNotification): def handle(self, relay_url, event): print(f"Received new event from {relay_url}: {event.as_json()}") @@ -76,16 +106,14 @@ def nostr_client(): print("[Nostr Client]: " + f"Received new zap:") print(event.as_json()) - def handle_msg(self, relay_url, msg): - None + return client.handle_notifications(NotificationHandler()) while True: time.sleep(5.0) - if __name__ == '__main__': env_path = Path('.env') @@ -95,6 +123,5 @@ if __name__ == '__main__': 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/utils/backend_utils.py b/utils/backend_utils.py index e0a0132..adcc9e1 100644 --- a/utils/backend_utils.py +++ b/utils/backend_utils.py @@ -1,10 +1,10 @@ import requests - -from tasks.textextractionPDF import TextExtractionPDF from utils.definitions import EventDefinitions from utils.nostr_utils import get_event_by_id +from tasks.textextractionpdf import TextExtractionPDF from tasks.translation import Translation +from tasks.imagegenerationsdxl import ImageGenerationSDXL def get_task(event, client, dvmconfig): @@ -35,9 +35,9 @@ def get_task(event, client, dvmconfig): evt = get_event_by_id(tag.as_vec()[1], config=dvmconfig) if evt is not None: if evt.kind() == 1063: - for tag in evt.tags(): - if tag.as_vec()[0] == 'url': - file_type = check_url_is_readable(tag.as_vec()[1]) + for tg in evt.tags(): + if tg.as_vec()[0] == 'url': + file_type = check_url_is_readable(tg.as_vec()[1]) if file_type == "pdf": return "pdf-to-text" else: @@ -45,9 +45,10 @@ def get_task(event, client, dvmconfig): else: return "unknown type" - elif event.kind() == EventDefinitions.KIND_NIP90_TRANSLATE_TEXT: return Translation.TASK + elif event.kind() == EventDefinitions.KIND_NIP90_GENERATE_IMAGE: + return ImageGenerationSDXL.TASK else: return "unknown type" @@ -121,7 +122,6 @@ def check_url_is_readable(url): def get_amount_per_task(task, dvm_config, duration=1): - print(dvm_config.SUPPORTED_TASKS) for dvm in dvm_config.SUPPORTED_TASKS: if dvm.TASK == task: amount = dvm.COST * duration diff --git a/utils/env.py b/utils/env.py index 1fef1d1..3d0bd1a 100644 --- a/utils/env.py +++ b/utils/env.py @@ -8,5 +8,6 @@ LNBITS_HOST = "LNBITS_HOST" TASK_TRANSLATION_NIP89_DTAG = "TASK_TRANSLATION_NIP89_DTAG" TASK_TEXTEXTRACTION_NIP89_DTAG = "TASK_TEXTEXTRACTION_NIP89_DTAG" +TASK_IMAGEGENERATION_NIP89_DTAG = "TASK_IMAGEGENERATION_NIP89_DTAG" diff --git a/utils/output_utils.py b/utils/output_utils.py index 06536f6..6f8c0f3 100644 --- a/utils/output_utils.py +++ b/utils/output_utils.py @@ -1,10 +1,15 @@ import json import datetime as datetime +import os from types import NoneType +import requests +from pyupload.uploader import CatboxUploader import pandas - +''' +Post process results to either given output format or a Nostr readable plain text. +''' def post_process_result(anno, original_event): print("post-processing...") if isinstance(anno, pandas.DataFrame): # if input is an anno we parse it to required output format @@ -84,7 +89,52 @@ def post_process_result(anno, original_event): return result +''' +Convenience function to replace words like Noster with Nostr +''' def replace_broken_words(text): result = (text.replace("Noster", "Nostr").replace("Nostra", "Nostr").replace("no stir", "Nostr"). replace("Nostro", "Nostr").replace("Impub", "npub").replace("sets", "Sats")) return result + + +''' +Function to upload to Nostr.build and if it fails to Nostrfiles.dev +Larger files than these hosters allow and fallback is catbox currently. +Will probably need to switch to another system in the future. +''' +def uploadMediaToHoster(filepath): + print("Uploading image: " + filepath) + try: + files = {'file': open(filepath, 'rb')} + file_stats = os.stat(filepath) + sizeinmb = file_stats.st_size / (1024*1024) + print("Filesize of Uploaded media: " + str(sizeinmb) + " Mb.") + if sizeinmb > 25: + uploader = CatboxUploader(filepath) + result = uploader.execute() + return result + else: + url = 'https://nostr.build/api/v2/upload/files' + response = requests.post(url, files=files) + json_object = json.loads(response.text) + result = json_object["data"][0]["url"] + return result + except: + try: + file = {'file': open(filepath, 'rb')} + url = 'https://nostrfiles.dev/upload_image' + response = requests.post(url, files=file) + json_object = json.loads(response.text) + print(json_object["url"]) + return json_object["url"] + # fallback filehoster + except: + + try: + uploader = CatboxUploader(filepath) + result = uploader.execute() + print(result) + return result + except: + return "Upload not possible, all hosters didn't work" From 13bffaea9643a7ecad55fafc7e6b4b702e6b1dd9 Mon Sep 17 00:00:00 2001 From: Believethehype Date: Mon, 20 Nov 2023 22:09:38 +0100 Subject: [PATCH 02/18] rebuild so each dvm runs on its own. --- .env_example | 1 + backends/nova_server.py | 2 +- dvm.py | 823 +++++++++++++++++---------------- interfaces/dvmtaskinterface.py | 12 +- main.py | 62 ++- tasks/imagegenerationsdxl.py | 38 +- tasks/textextractionpdf.py | 22 +- tasks/translation.py | 19 +- utils/admin_utils.py | 14 +- utils/backend_utils.py | 27 +- utils/definitions.py | 5 +- utils/env.py | 1 + utils/zap_utils.py | 3 +- 13 files changed, 547 insertions(+), 482 deletions(-) diff --git a/.env_example b/.env_example index a5718bf..31ca32a 100644 --- a/.env_example +++ b/.env_example @@ -9,6 +9,7 @@ LNBITS_HOST = "https://lnbits.com" TASK_TEXTEXTRACTION_NIP89_DTAG = "asdd" TASK_TRANSLATION_NIP89_DTAG = "abcded" TASK_IMAGEGENERATION_NIP89_DTAG = "fgdfgdf" +TASK_IMAGEGENERATION_NIP89_DTAG2 = "fgdfgdf" #Backend Specific Options for tasks that require them NOVA_SERVER = "127.0.0.1:37318" \ No newline at end of file diff --git a/backends/nova_server.py b/backends/nova_server.py index c631241..46ce83d 100644 --- a/backends/nova_server.py +++ b/backends/nova_server.py @@ -29,7 +29,7 @@ in the module that is calling the server def send_request_to_nova_server(request_form, address): print("Sending job to NOVA-Server") - url = ('http://' + address + '/' + str(request_form["mode"]).lower()) + url = ('http://' + address + '/process') headers = {'Content-type': 'application/x-www-form-urlencoded'} response = requests.post(url, headers=headers, data=request_form) return response.content diff --git a/dvm.py b/dvm.py index 535be35..48d3f17 100644 --- a/dvm.py +++ b/dvm.py @@ -19,438 +19,473 @@ if use_logger: job_list = [] jobs_on_hold_list = [] -dvm_config = DVMConfig() -def DVM(config): - dvm_config = config - keys = Keys.from_sk_str(dvm_config.PRIVATE_KEY) - pk = keys.public_key() +class DVM: + dvm_config: DVMConfig + keys: Keys + client: Client - print(f"Nostr DVM public key: {pk.to_bech32()}, Hex: {pk.to_hex()} ") - print('Supported DVM tasks: ' + ', '.join(p.NAME + ":" + p.TASK for p in dvm_config.SUPPORTED_TASKS)) + def __init__(self, config): + self.dvm_config = config + self.keys = Keys.from_sk_str(config.PRIVATE_KEY) + self.client = Client(self.keys) + pk = self.keys.public_key() - client = Client(keys) - for relay in dvm_config.RELAY_LIST: - client.add_relay(relay) - client.connect() + print("Nostr DVM public key: " + str(pk.to_bech32()) + "Hex: " + str(pk.to_hex()) + " Supported DVM tasks: " + + ', '.join(p.NAME + ":" + p.TASK for p in self.dvm_config.SUPPORTED_TASKS) + "\n") - dm_zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_ZAP]).since(Timestamp.now()) + for relay in self.dvm_config.RELAY_LIST: + self.client.add_relay(relay) + self.client.connect() - kinds = [EventDefinitions.KIND_NIP90_GENERIC] - for dvm in dvm_config.SUPPORTED_TASKS: - if dvm.KIND not in kinds: - kinds.append(dvm.KIND) - dvm_filter = (Filter().kinds(kinds).since(Timestamp.now())) - client.subscribe([dm_zap_filter, dvm_filter]) + dm_zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_ZAP]).since(Timestamp.now()) - create_sql_table() - admin_make_database_updates(config=dvm_config, client=client) + kinds = [EventDefinitions.KIND_NIP90_GENERIC] + for dvm in self.dvm_config.SUPPORTED_TASKS: + if dvm.KIND not in kinds: + kinds.append(dvm.KIND) + dvm_filter = (Filter().kinds(kinds).since(Timestamp.now())) + self.client.subscribe([dm_zap_filter, dvm_filter]) - class NotificationHandler(HandleNotification): - def handle(self, relay_url, nostr_event): - if EventDefinitions.KIND_NIP90_EXTRACT_TEXT <= nostr_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC: + create_sql_table() + admin_make_database_updates(config=self.dvm_config, client=self.client) + + class NotificationHandler(HandleNotification): + client = self.client + dvm_config = self.dvm_config + keys = self.keys + + def handle(self, relay_url, nostr_event): print(f"[Nostr] Received new NIP90 Job Request from {relay_url}: {nostr_event.as_json()}") - handle_nip90_job_event(nostr_event, dvm_config) - elif nostr_event.kind() == EventDefinitions.KIND_ZAP: - handle_zap(nostr_event, dvm_config) + if EventDefinitions.KIND_NIP90_EXTRACT_TEXT <= nostr_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC: + self.handle_nip90_job_event(nostr_event) + elif nostr_event.kind() == EventDefinitions.KIND_ZAP: + self.handle_zap(nostr_event) - def handle_msg(self, relay_url, msg): - return - - def handle_nip90_job_event(event, dvm_config): - user = get_or_add_user(event.pubkey().to_hex()) - task_supported, task, duration = check_task_is_supported(event, client=client, - get_duration=(not user.iswhitelisted), - config=dvm_config) - print(task) - - if user.isblacklisted: - send_job_status_reaction(event, "error", client=client, config=dvm_config) - print("[Nostr] Request by blacklisted user, skipped") - - elif task_supported: - print("Received new Task: " + task) - print(duration) - amount = get_amount_per_task(task, dvm_config, duration) - if amount is None: + def handle_msg(self, relay_url, msg): return - task_is_free = False - for dvm in dvm_config.SUPPORTED_TASKS: - if dvm.TASK == task and dvm.COST == 0: - task_is_free = True + def handle_nip90_job_event(self, nip90_event): + user = get_or_add_user(nip90_event.pubkey().to_hex()) + task_supported, task, duration = check_task_is_supported(nip90_event, client=self.client, + get_duration=(not user.iswhitelisted), + config=self.dvm_config) + print(task) - if user.iswhitelisted or task_is_free: - print("[Nostr] Free or Whitelisted for task " + task + ". Starting processing..") - send_job_status_reaction(event, "processing", True, 0, client=client, config=dvm_config) - do_work(event, is_from_bot=False) - # otherwise send payment request - else: - bid = 0 - for tag in event.tags(): - if tag.as_vec()[0] == 'bid': - bid = int(tag.as_vec()[1]) + if user.isblacklisted: + send_job_status_reaction(nip90_event, "error", client=self.client, config=self.dvm_config) + print("[Nostr] Request by blacklisted user, skipped") - print("[Nostr][Payment required] New Nostr " + task + " Job event: " + event.as_json()) - if bid > 0: - bid_offer = int(bid / 1000) - if bid_offer >= amount: - send_job_status_reaction(event, "payment-required", False, - amount, # bid_offer - client=client, config=dvm_config) + elif task_supported: + print("Received new Task: " + task) + amount = get_amount_per_task(task, self.dvm_config, duration) + if amount is None: + return - else: # If there is no bid, just request server rate from user - print("[Nostr] Requesting payment for Event: " + event.id().to_hex()) - send_job_status_reaction(event, "payment-required", - False, amount, client=client, config=dvm_config) - else: - print("Task not supported on this DVM, skipping..") + task_is_free = False + for dvm in self.dvm_config.SUPPORTED_TASKS: + if dvm.TASK == task and dvm.COST == 0: + task_is_free = True - def handle_zap(event, dvm_config): - zapped_event = None - invoice_amount = 0 - anon = False - sender = event.pubkey() + if user.iswhitelisted or task_is_free: + print("[Nostr] Free or Whitelisted for task " + task + ". Starting processing..") + send_job_status_reaction(nip90_event, "processing", True, 0, client=self.client, + config=self.dvm_config) + do_work(nip90_event, is_from_bot=False) + # otherwise send payment request + else: + bid = 0 + for tag in nip90_event.tags(): + if tag.as_vec()[0] == 'bid': + bid = int(tag.as_vec()[1]) - try: - for tag in event.tags(): - if tag.as_vec()[0] == 'bolt11': - invoice_amount = parse_bolt11_invoice(tag.as_vec()[1]) - elif tag.as_vec()[0] == 'e': - zapped_event = get_event_by_id(tag.as_vec()[1], config=dvm_config) - elif tag.as_vec()[0] == 'description': - zap_request_event = Event.from_json(tag.as_vec()[1]) - sender = check_for_zapplepay(zap_request_event.pubkey().to_hex(), - zap_request_event.content()) - for ztag in zap_request_event.tags(): - if ztag.as_vec()[0] == 'anon': - if len(ztag.as_vec()) > 1: - print("Private Zap received.") - decrypted_content = decrypt_private_zap_message(ztag.as_vec()[1], - keys.secret_key(), - zap_request_event.pubkey()) - decrypted_private_event = Event.from_json(decrypted_content) - if decrypted_private_event.kind() == 9733: - sender = decrypted_private_event.pubkey().to_hex() - message = decrypted_private_event.content() - if message != "": - print("Zap Message: " + message) - else: - anon = True - print("Anonymous Zap received. Unlucky, I don't know from whom, and never will") - user = get_or_add_user(sender) - print(str(user)) + print("[Nostr][Payment required] New Nostr " + task + " Job event: " + nip90_event.as_json()) + if bid > 0: + bid_offer = int(bid / 1000) + if bid_offer >= amount: + send_job_status_reaction(nip90_event, "payment-required", False, + amount, # bid_offer + client=self.client, config=self.dvm_config) - if zapped_event is not None: - if zapped_event.kind() == EventDefinitions.KIND_FEEDBACK: # if a reaction by us got zapped - if not dvm_config.IS_BOT: - print("Zap received for NIP90 task: " + str(invoice_amount) + " Sats from " + str( - user.name)) - amount = 0 - job_event = None - for tag in zapped_event.tags(): - if tag.as_vec()[0] == 'amount': - amount = int(float(tag.as_vec()[1]) / 1000) - elif tag.as_vec()[0] == 'e': - job_event = get_event_by_id(tag.as_vec()[1], config=dvm_config) - - task_supported, task, duration = check_task_is_supported(job_event, client=client, - get_duration=False, config=dvm_config) - if job_event is not None and task_supported: - if amount <= invoice_amount: - print("[Nostr] Payment-request fulfilled...") - send_job_status_reaction(job_event, "processing", client=client, - config=dvm_config) - indices = [i for i, x in enumerate(job_list) if - x.event_id == job_event.id().to_hex()] - index = -1 - if len(indices) > 0: - index = indices[0] - if index > -1: - if job_list[index].is_processed: # If payment-required appears a processing - job_list[index].is_paid = True - check_and_return_event(job_list[index].result, str(job_event.as_json()), - dvm_key=dvm_config.PRIVATE_KEY) - elif not (job_list[index]).is_processed: - # If payment-required appears before processing - job_list.pop(index) - print("Starting work...") - do_work(job_event, is_from_bot=False) - else: - print("Job not in List, but starting work...") - do_work(job_event, is_from_bot=False) - - else: - send_job_status_reaction(job_event, "payment-rejected", - False, invoice_amount, client=client, config=dvm_config) - print("[Nostr] Invoice was not paid sufficiently") - - elif zapped_event.kind() in EventDefinitions.ANY_RESULT: - print("Someone zapped the result of an exisiting Task. Nice") - elif not anon and not dvm_config.PASSIVE_MODE: - print("Note Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( - user.name)) - update_user_balance(sender, invoice_amount, config=dvm_config) - - # a regular note - elif not anon and not dvm_config.PASSIVE_MODE: - print("Profile Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( - user.name)) - update_user_balance(sender, invoice_amount, config=dvm_config) - - except Exception as e: - print(f"Error during content decryption: {e}") - - def do_work(job_event, is_from_bot=False): - if ((EventDefinitions.KIND_NIP90_EXTRACT_TEXT <= job_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC) - or job_event.kind() == EventDefinitions.KIND_DM): - - task = get_task(job_event, client=client, dvmconfig=dvm_config) - result = "" - for dvm in dvm_config.SUPPORTED_TASKS: - try: - if task == dvm.TASK: - request_form = dvm.create_request_form_from_nostr_event(job_event, client, dvm_config) - result = dvm.process(request_form) - check_and_return_event(result, str(job_event.as_json()), dvm_key=dvm_config.PRIVATE_KEY) - - except Exception as e: - print(e) - respond_to_error(e, job_event.as_json(), is_from_bot, dvm_config.PRIVATE_KEY) - return - - - - def check_event_has_not_unfinished_job_input(nevent, append, client, dvmconfig): - task_supported, task, duration = check_task_is_supported(nevent, client, False, config=dvmconfig) - if not task_supported: - return False - - for tag in nevent.tags(): - if tag.as_vec()[0] == 'i': - if len(tag.as_vec()) < 3: - print("Job Event missing/malformed i tag, skipping..") - return False + else: # If there is no bid, just request server rate from user + print("[Nostr] Requesting payment for Event: " + nip90_event.id().to_hex()) + send_job_status_reaction(nip90_event, "payment-required", + False, amount, client=self.client, config=self.dvm_config) else: - input = tag.as_vec()[1] - input_type = tag.as_vec()[2] - if input_type == "job": - evt = get_referenced_event_by_id(input, EventDefinitions.ANY_RESULT, client, config=dvmconfig) - if evt is None: - if append: - job = RequiredJobToWatch(event=nevent, timestamp=Timestamp.now().as_secs()) - jobs_on_hold_list.append(job) - send_job_status_reaction(nevent, "chain-scheduled", True, 0, client=client, - config=dvmconfig) + print("Task not supported on this DVM, skipping..") - return False - else: - return True + def handle_zap(self, event): + zapped_event = None + invoice_amount = 0 + anon = False + sender = event.pubkey() - def send_job_status_reaction(original_event, status, is_paid=True, amount=0, client=None, content=None, config=None, - key=None): - dvmconfig = config - alt_description = "This is a reaction to a NIP90 DVM AI task. " - task = get_task(original_event, client=client, dvmconfig=dvmconfig) - if status == "processing": - alt_description = "NIP90 DVM AI task " + task + " started processing. " - reaction = alt_description + emoji.emojize(":thumbs_up:") - elif status == "success": - alt_description = "NIP90 DVM AI task " + task + " finished successfully. " - reaction = alt_description + emoji.emojize(":call_me_hand:") - elif status == "chain-scheduled": - alt_description = "NIP90 DVM AI task " + task + " Chain Task scheduled" - reaction = alt_description + emoji.emojize(":thumbs_up:") - elif status == "error": - alt_description = "NIP90 DVM AI task " + task + " had an error. " - if content is None: - reaction = alt_description + emoji.emojize(":thumbs_down:") - else: - reaction = alt_description + emoji.emojize(":thumbs_down:") + content - - elif status == "payment-required": - - alt_description = "NIP90 DVM AI task " + task + " requires payment of min " + str(amount) + " Sats. " - reaction = alt_description + emoji.emojize(":orange_heart:") - - elif status == "payment-rejected": - alt_description = "NIP90 DVM AI task " + task + " payment is below required amount of " + str(amount) + " Sats. " - reaction = alt_description + emoji.emojize(":thumbs_down:") - elif status == "user-blocked-from-service": - alt_description = "NIP90 DVM AI task " + task + " can't be performed. User has been blocked from Service. " - reaction = alt_description + emoji.emojize(":thumbs_down:") - else: - reaction = emoji.emojize(":thumbs_down:") - - e_tag = Tag.parse(["e", original_event.id().to_hex()]) - p_tag = Tag.parse(["p", original_event.pubkey().to_hex()]) - alt_tag = Tag.parse(["alt", alt_description]) - status_tag = Tag.parse(["status", status]) - tags = [e_tag, p_tag, alt_tag, status_tag] - - if status == "success" or status == "error": # - for x in job_list: - if x.event_id == original_event.id(): - is_paid = x.is_paid - amount = x.amount - break - - bolt11 = "" - payment_hash = "" - expires = original_event.created_at().as_secs() + (60 * 60 * 24) - if status == "payment-required" or (status == "processing" and not is_paid): - if dvmconfig.LNBITS_INVOICE_KEY != "": try: - bolt11, payment_hash = create_bolt11_ln_bits(amount, dvmconfig) + for tag in event.tags(): + if tag.as_vec()[0] == 'bolt11': + invoice_amount = parse_bolt11_invoice(tag.as_vec()[1]) + elif tag.as_vec()[0] == 'e': + zapped_event = get_event_by_id(tag.as_vec()[1], config=self.dvm_config) + elif tag.as_vec()[0] == 'description': + zap_request_event = Event.from_json(tag.as_vec()[1]) + sender = check_for_zapplepay(zap_request_event.pubkey().to_hex(), + zap_request_event.content()) + for ztag in zap_request_event.tags(): + if ztag.as_vec()[0] == 'anon': + if len(ztag.as_vec()) > 1: + print("Private Zap received.") + decrypted_content = decrypt_private_zap_message(ztag.as_vec()[1], + self.keys.secret_key(), + zap_request_event.pubkey()) + decrypted_private_event = Event.from_json(decrypted_content) + if decrypted_private_event.kind() == 9733: + sender = decrypted_private_event.pubkey().to_hex() + message = decrypted_private_event.content() + if message != "": + print("Zap Message: " + message) + else: + anon = True + print("Anonymous Zap received. Unlucky, I don't know from whom, and never will") + user = get_or_add_user(sender) + print(str(user)) + + if zapped_event is not None: + if zapped_event.kind() == EventDefinitions.KIND_FEEDBACK: # if a reaction by us got zapped + if not self.dvm_config.IS_BOT: + print("Zap received for NIP90 task: " + str(invoice_amount) + " Sats from " + str( + user.name)) + amount = 0 + job_event = None + for tag in zapped_event.tags(): + if tag.as_vec()[0] == 'amount': + amount = int(float(tag.as_vec()[1]) / 1000) + elif tag.as_vec()[0] == 'e': + job_event = get_event_by_id(tag.as_vec()[1], config=self.dvm_config) + + task_supported, task, duration = check_task_is_supported(job_event, client=self.client, + get_duration=False, + config=self.dvm_config) + if job_event is not None and task_supported: + if amount <= invoice_amount: + print("[Nostr] Payment-request fulfilled...") + send_job_status_reaction(job_event, "processing", client=self.client, + config=self.dvm_config) + indices = [i for i, x in enumerate(job_list) if + x.event_id == job_event.id().to_hex()] + index = -1 + if len(indices) > 0: + index = indices[0] + if index > -1: + if job_list[index].is_processed: # If payment-required appears a processing + job_list[index].is_paid = True + check_and_return_event(job_list[index].result, + str(job_event.as_json()), + dvm_key=self.dvm_config.PRIVATE_KEY) + elif not (job_list[index]).is_processed: + # If payment-required appears before processing + job_list.pop(index) + print("Starting work...") + do_work(job_event, is_from_bot=False) + else: + print("Job not in List, but starting work...") + do_work(job_event, is_from_bot=False) + + else: + send_job_status_reaction(job_event, "payment-rejected", + False, invoice_amount, client=self.client, + config=self.dvm_config) + print("[Nostr] Invoice was not paid sufficiently") + + elif zapped_event.kind() in EventDefinitions.ANY_RESULT: + print("Someone zapped the result of an exisiting Task. Nice") + elif not anon and not self.dvm_config.PASSIVE_MODE: + print("Note Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( + user.name)) + update_user_balance(sender, invoice_amount, config=self.dvm_config) + + # a regular note + elif not anon and not self.dvm_config.PASSIVE_MODE: + print("Profile Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( + user.name)) + update_user_balance(sender, invoice_amount, config=self.dvm_config) + except Exception as e: - print(e) + print(f"Error during content decryption: {e}") - if not any(x.event_id == original_event.id().to_hex() for x in job_list): - job_list.append( - JobToWatch(event_id=original_event.id().to_hex(), timestamp=original_event.created_at().as_secs(), - amount=amount, - is_paid=is_paid, - status=status, result="", is_processed=False, bolt11=bolt11, payment_hash=payment_hash, - expires=expires, from_bot=False)) - print(str(job_list)) - if status == "payment-required" or status == "payment-rejected" or (status == "processing" and not is_paid) or ( - status == "success" and not is_paid): - if dvmconfig.LNBITS_INVOICE_KEY != "": - amount_tag = Tag.parse(["amount", str(amount * 1000), bolt11]) + + def check_event_has_not_unfinished_job_input(nevent, append, client, dvmconfig): + task_supported, task, duration = check_task_is_supported(nevent, client, False, config=dvmconfig) + if not task_supported: + return False + + for tag in nevent.tags(): + if tag.as_vec()[0] == 'i': + if len(tag.as_vec()) < 3: + print("Job Event missing/malformed i tag, skipping..") + return False + else: + input = tag.as_vec()[1] + input_type = tag.as_vec()[2] + if input_type == "job": + evt = get_referenced_event_by_id(input, EventDefinitions.ANY_RESULT, client, + config=dvmconfig) + if evt is None: + if append: + job = RequiredJobToWatch(event=nevent, timestamp=Timestamp.now().as_secs()) + jobs_on_hold_list.append(job) + send_job_status_reaction(nevent, "chain-scheduled", True, 0, client=client, + config=dvmconfig) + + return False else: - amount_tag = Tag.parse(["amount", str(amount * 1000)]) # to millisats - tags.append(amount_tag) - if key is not None: - keys = Keys.from_sk_str(key) - else: - keys = Keys.from_sk_str(dvmconfig.PRIVATE_KEY) - event = EventBuilder(EventDefinitions.KIND_FEEDBACK, reaction, tags).to_event(keys) - - send_event(event, key=keys) - print( - "[Nostr] Sent Kind " + str(EventDefinitions.KIND_FEEDBACK) + " Reaction: " + status + " " + event.as_json()) - return event.as_json() - - def check_and_return_event(data, original_event_str: str, dvm_key=""): - original_event = Event.from_json(original_event_str) - keys = Keys.from_sk_str(dvm_key) - - for x in job_list: - if x.event_id == original_event.id().to_hex(): - is_paid = x.is_paid - amount = x.amount - x.result = data - x.is_processed = True - if dvm_config.SHOWRESULTBEFOREPAYMENT and not is_paid: - send_nostr_reply_event(data, original_event_str, key=keys) - send_job_status_reaction(original_event, "success", amount, - config=dvm_config) # or payment-required, or both? - elif not dvm_config.SHOWRESULTBEFOREPAYMENT and not is_paid: - send_job_status_reaction(original_event, "success", amount, - config=dvm_config) # or payment-required, or both? - - if dvm_config.SHOWRESULTBEFOREPAYMENT and is_paid: - job_list.remove(x) - elif not dvm_config.SHOWRESULTBEFOREPAYMENT and is_paid: - job_list.remove(x) - send_nostr_reply_event(data, original_event_str, key=keys) - break + return True - try: - post_processed_content = post_process_result(data, original_event) - send_nostr_reply_event(post_processed_content, original_event_str, key=keys) - except Exception as e: - respond_to_error(e, original_event_str, False, dvm_config.PRIVATE_KEY) - - def send_nostr_reply_event(content, original_event_as_str, key=None): - originalevent = Event.from_json(original_event_as_str) - requesttag = Tag.parse(["request", original_event_as_str.replace("\\", "")]) - etag = Tag.parse(["e", originalevent.id().to_hex()]) - ptag = Tag.parse(["p", originalevent.pubkey().to_hex()]) - alttag = Tag.parse(["alt", "This is the result of a NIP90 DVM AI task with kind " + str( - originalevent.kind()) + ". The task was: " + originalevent.content()]) - statustag = Tag.parse(["status", "success"]) - replytags = [requesttag, etag, ptag, alttag, statustag] - for tag in originalevent.tags(): - if tag.as_vec()[0] == "i": - icontent = tag.as_vec()[1] - ikind = tag.as_vec()[2] - itag = Tag.parse(["i", icontent, ikind]) - replytags.append(itag) - - if key is None: - key = Keys.from_sk_str(dvm_config.PRIVATE_KEY) - - response_kind = originalevent.kind() + 1000 - event = EventBuilder(response_kind, str(content), replytags).to_event(key) - send_event(event, key=key) - print("[Nostr] " + str(response_kind) + " Job Response event sent: " + event.as_json()) - return event.as_json() - - client.handle_notifications(NotificationHandler()) - - def respond_to_error(content, originaleventstr, is_from_bot=False, dvm_key=None): - print("ERROR") - if dvm_key is None: - keys = Keys.from_sk_str(dvm_config.PRIVATE_KEY) - else: + def check_and_return_event(data, original_event_str: str, dvm_key=""): + original_event = Event.from_json(original_event_str) keys = Keys.from_sk_str(dvm_key) - original_event = Event.from_json(originaleventstr) - sender = "" - task = "" - if not is_from_bot: - send_job_status_reaction(original_event, "error", content=str(content), key=dvm_key) - # TODO Send Zap back - else: - for tag in original_event.tags(): - if tag.as_vec()[0] == "p": - sender = tag.as_vec()[1] - elif tag.as_vec()[0] == "i": - task = tag.as_vec()[1] + for x in job_list: + if x.event_id == original_event.id().to_hex(): + is_paid = x.is_paid + amount = x.amount + x.result = data + x.is_processed = True + if self.dvm_config.SHOWRESULTBEFOREPAYMENT and not is_paid: + send_nostr_reply_event(data, original_event_str, key=keys) + send_job_status_reaction(original_event, "success", amount, + config=self.dvm_config) # or payment-required, or both? + elif not self.dvm_config.SHOWRESULTBEFOREPAYMENT and not is_paid: + send_job_status_reaction(original_event, "success", amount, + config=self.dvm_config) # or payment-required, or both? - user = get_from_sql_table(sender) - if not user.iswhitelisted: - amount = int(user.balance) + get_amount_per_task(task, dvm_config) - update_sql_table(sender, amount, user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16, - user.name, - Timestamp.now().as_secs()) - message = "There was the following error : " + content + ". Credits have been reimbursed" + if self.dvm_config.SHOWRESULTBEFOREPAYMENT and is_paid: + job_list.remove(x) + elif not self.dvm_config.SHOWRESULTBEFOREPAYMENT and is_paid: + job_list.remove(x) + send_nostr_reply_event(data, original_event_str, key=keys) + break + + try: + post_processed_content = post_process_result(data, original_event) + send_nostr_reply_event(post_processed_content, original_event_str, key=keys) + except Exception as e: + respond_to_error(e, original_event_str, False, self.dvm_config.PRIVATE_KEY) + + def send_nostr_reply_event(content, original_event_as_str, key=None): + originalevent = Event.from_json(original_event_as_str) + requesttag = Tag.parse(["request", original_event_as_str.replace("\\", "")]) + etag = Tag.parse(["e", originalevent.id().to_hex()]) + ptag = Tag.parse(["p", originalevent.pubkey().to_hex()]) + alttag = Tag.parse(["alt", "This is the result of a NIP90 DVM AI task with kind " + str( + originalevent.kind()) + ". The task was: " + originalevent.content()]) + statustag = Tag.parse(["status", "success"]) + replytags = [requesttag, etag, ptag, alttag, statustag] + for tag in originalevent.tags(): + if tag.as_vec()[0] == "i": + icontent = tag.as_vec()[1] + ikind = tag.as_vec()[2] + itag = Tag.parse(["i", icontent, ikind]) + replytags.append(itag) + + if key is None: + key = Keys.from_sk_str(self.dvm_config.PRIVATE_KEY) + + response_kind = originalevent.kind() + 1000 + event = EventBuilder(response_kind, str(content), replytags).to_event(key) + send_event(event, key=key) + print("[Nostr] " + str(response_kind) + " Job Response event sent: " + event.as_json()) + return event.as_json() + + def respond_to_error(content, originaleventstr, is_from_bot=False, dvm_key=None): + print("ERROR") + if dvm_key is None: + keys = Keys.from_sk_str(self.dvm_config.PRIVATE_KEY) else: - # User didn't pay, so no reimbursement - message = "There was the following error : " + content + keys = Keys.from_sk_str(dvm_key) - evt = EventBuilder.new_encrypted_direct_msg(keys, PublicKey.from_hex(sender), message, None).to_event(keys) - send_event(evt, key=keys) + original_event = Event.from_json(originaleventstr) + sender = "" + task = "" + if not is_from_bot: + send_job_status_reaction(original_event, "error", content=str(content), key=dvm_key) + # TODO Send Zap back + else: + for tag in original_event.tags(): + if tag.as_vec()[0] == "p": + sender = tag.as_vec()[1] + elif tag.as_vec()[0] == "i": + task = tag.as_vec()[1] - while True: - for job in job_list: - if job.bolt11 != "" and job.payment_hash != "" and not job.is_paid: - if str(check_bolt11_ln_bits_is_paid(job.payment_hash, dvm_config)) == "True": - job.is_paid = True - event = get_event_by_id(job.event_id, config=dvm_config) - if event != None: - send_job_status_reaction(event, "processing", True, 0, client=client, config=dvm_config) - print("do work from joblist") + user = get_from_sql_table(sender) + if not user.iswhitelisted: + amount = int(user.balance) + get_amount_per_task(task, self.dvm_config) + update_sql_table(sender, amount, user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16, + user.name, + Timestamp.now().as_secs()) + message = "There was the following error : " + content + ". Credits have been reimbursed" + else: + # User didn't pay, so no reimbursement + message = "There was the following error : " + content - do_work(event, is_from_bot=False) - elif check_bolt11_ln_bits_is_paid(job.payment_hash, dvm_config) is None: # invoice expired - job_list.remove(job) + evt = EventBuilder.new_encrypted_direct_msg(keys, PublicKey.from_hex(sender), message, + None).to_event(keys) + send_event(evt, key=keys) - if Timestamp.now().as_secs() > job.expires: - job_list.remove(job) + def send_job_status_reaction(original_event, status, is_paid=True, amount=0, client=None, + content=None, + config=None, + key=None): + dvmconfig = config + alt_description = "This is a reaction to a NIP90 DVM AI task. " + task = get_task(original_event, client=client, dvmconfig=dvmconfig) + if status == "processing": + alt_description = "NIP90 DVM AI task " + task + " started processing. " + reaction = alt_description + emoji.emojize(":thumbs_up:") + elif status == "success": + alt_description = "NIP90 DVM AI task " + task + " finished successfully. " + reaction = alt_description + emoji.emojize(":call_me_hand:") + elif status == "chain-scheduled": + alt_description = "NIP90 DVM AI task " + task + " Chain Task scheduled" + reaction = alt_description + emoji.emojize(":thumbs_up:") + elif status == "error": + alt_description = "NIP90 DVM AI task " + task + " had an error. " + if content is None: + reaction = alt_description + emoji.emojize(":thumbs_down:") + else: + reaction = alt_description + emoji.emojize(":thumbs_down:") + content - for job in jobs_on_hold_list: - if check_event_has_not_unfinished_job_input(job.event, False, client=client, dvmconfig=dvm_config): - handle_nip90_job_event(job.event) - jobs_on_hold_list.remove(job) + elif status == "payment-required": - if Timestamp.now().as_secs() > job.timestamp + 60 * 20: # remove jobs to look for after 20 minutes.. - jobs_on_hold_list.remove(job) + alt_description = "NIP90 DVM AI task " + task + " requires payment of min " + str( + amount) + " Sats. " + reaction = alt_description + emoji.emojize(":orange_heart:") - time.sleep(1.0) + elif status == "payment-rejected": + alt_description = "NIP90 DVM AI task " + task + " payment is below required amount of " + str( + amount) + " Sats. " + reaction = alt_description + emoji.emojize(":thumbs_down:") + elif status == "user-blocked-from-service": + alt_description = "NIP90 DVM AI task " + task + " can't be performed. User has been blocked from Service. " + reaction = alt_description + emoji.emojize(":thumbs_down:") + else: + reaction = emoji.emojize(":thumbs_down:") + + e_tag = Tag.parse(["e", original_event.id().to_hex()]) + p_tag = Tag.parse(["p", original_event.pubkey().to_hex()]) + alt_tag = Tag.parse(["alt", alt_description]) + status_tag = Tag.parse(["status", status]) + tags = [e_tag, p_tag, alt_tag, status_tag] + + if status == "success" or status == "error": # + for x in job_list: + if x.event_id == original_event.id(): + is_paid = x.is_paid + amount = x.amount + break + + bolt11 = "" + payment_hash = "" + expires = original_event.created_at().as_secs() + (60 * 60 * 24) + if status == "payment-required" or (status == "processing" and not is_paid): + if dvmconfig.LNBITS_INVOICE_KEY != "": + try: + bolt11, payment_hash = create_bolt11_ln_bits(amount, dvmconfig) + except Exception as e: + print(e) + + if not any(x.event_id == original_event.id().to_hex() for x in job_list): + job_list.append( + JobToWatch(event_id=original_event.id().to_hex(), + timestamp=original_event.created_at().as_secs(), + amount=amount, + is_paid=is_paid, + status=status, result="", is_processed=False, bolt11=bolt11, + payment_hash=payment_hash, + expires=expires, from_bot=False)) + print(str(job_list)) + if status == "payment-required" or status == "payment-rejected" or ( + status == "processing" and not is_paid) or ( + status == "success" and not is_paid): + + if dvmconfig.LNBITS_INVOICE_KEY != "": + amount_tag = Tag.parse(["amount", str(amount * 1000), bolt11]) + else: + amount_tag = Tag.parse(["amount", str(amount * 1000)]) # to millisats + tags.append(amount_tag) + if key is not None: + keys = Keys.from_sk_str(key) + else: + keys = Keys.from_sk_str(dvmconfig.PRIVATE_KEY) + event = EventBuilder(EventDefinitions.KIND_FEEDBACK, reaction, tags).to_event(keys) + + send_event(event, key=keys) + print( + "[Nostr] Sent Kind " + str( + EventDefinitions.KIND_FEEDBACK) + " Reaction: " + status + " " + event.as_json()) + return event.as_json() + def do_work(job_event, is_from_bot=False): + if (( + EventDefinitions.KIND_NIP90_EXTRACT_TEXT <= job_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC) + or job_event.kind() == EventDefinitions.KIND_DM): + + task = get_task(job_event, client=self.client, dvmconfig=self.dvm_config) + result = "" + for dvm in self.dvm_config.SUPPORTED_TASKS: + try: + if task == dvm.TASK: + request_form = dvm.create_request_form_from_nostr_event(job_event, self.client, + self.dvm_config) + result = dvm.process(request_form) + check_and_return_event(result, str(job_event.as_json()), + dvm_key=self.dvm_config.PRIVATE_KEY) + + except Exception as e: + print(e) + respond_to_error(e, job_event.as_json(), is_from_bot, self.dvm_config.PRIVATE_KEY) + return + + self.client.handle_notifications(NotificationHandler()) + while True: + for job in job_list: + if job.bolt11 != "" and job.payment_hash != "" and not job.is_paid: + if str(check_bolt11_ln_bits_is_paid(job.payment_hash, self.dvm_config)) == "True": + job.is_paid = True + event = get_event_by_id(job.event_id, config=self.dvm_config) + if event is not None: + send_job_status_reaction(event, "processing", True, 0, + client=self.client, + config=self.dvm_config) + print("do work from joblist") + + do_work(event, is_from_bot=False) + elif check_bolt11_ln_bits_is_paid(job.payment_hash, self.dvm_config) is None: # invoice expired + try: + job_list.remove(job) + except: + continue + + if Timestamp.now().as_secs() > job.expires: + try: + job_list.remove(job) + except: + continue + + for job in jobs_on_hold_list: + if check_event_has_not_unfinished_job_input(job.event, False, client=self.client, + dvmconfig=self.dvm_config): + # handle_nip90_job_event(event=job.event) + try: + jobs_on_hold_list.remove(job) + except: + continue + + + if Timestamp.now().as_secs() > job.timestamp + 60 * 20: # remove jobs to look for after 20 minutes.. + jobs_on_hold_list.remove(job) + + time.sleep(1.0) diff --git a/interfaces/dvmtaskinterface.py b/interfaces/dvmtaskinterface.py index d08c79e..e48df54 100644 --- a/interfaces/dvmtaskinterface.py +++ b/interfaces/dvmtaskinterface.py @@ -1,11 +1,19 @@ +from utils.nip89_utils import NIP89Announcement + + class DVMTaskInterface: KIND: int TASK: str COST: int + PK: str def NIP89_announcement(self, d_tag, content): - """Define the NIP89 Announcement""" - pass + nip89 = NIP89Announcement() + nip89.kind = self.KIND + nip89.pk = self.PK + nip89.dtag = d_tag + nip89.content = content + return nip89 def is_input_supported(self, input_type, input_content) -> bool: """Check if input is supported for current Task.""" diff --git a/main.py b/main.py index afcab5e..d110401 100644 --- a/main.py +++ b/main.py @@ -8,19 +8,22 @@ from tasks.imagegenerationsdxl import ImageGenerationSDXL from tasks.textextractionpdf import TextExtractionPDF from tasks.translation import Translation + def run_nostr_dvm_with_local_config(): from dvm import DVM, DVMConfig - dvm_config = DVMConfig() - dvm_config.PRIVATE_KEY = os.getenv(env.NOSTR_PRIVATE_KEY) - # Spawn the DVMs - # Add NIP89 events for each DVM (set rebroad_cast = True for the next start in admin_utils) + # Add NIP89 events for each DVM (set rebroadcast = True for the next start in admin_utils) # Add the dtag here or in your .env file, so you can update your dvm later and change the content as needed. # Get a dtag and the content at vendata.io # Spawn DVM1 Kind 5000 Text Ectractor from PDFs - pdfextactor = TextExtractionPDF("PDF Extractor", os.getenv(env.NOSTR_PRIVATE_KEY)) + dvm_config = DVMConfig() + dvm_config.PRIVATE_KEY = os.getenv(env.NOSTR_PRIVATE_KEY) + dvm_config.LNBITS_INVOICE_KEY = os.getenv(env.LNBITS_INVOICE_KEY) + dvm_config.LNBITS_URL = os.getenv(env.LNBITS_HOST) + + pdfextactor = TextExtractionPDF("PDF Extractor", dvm_config) d_tag = os.getenv(env.TASK_TEXTEXTRACTION_NIP89_DTAG) content = "{\"name\":\"" + pdfextactor.NAME + ("\",\"image\":\"https://image.nostr.build" "/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669" @@ -29,7 +32,13 @@ def run_nostr_dvm_with_local_config(): dvm_config.NIP89s.append(pdfextactor.NIP89_announcement(d_tag, content)) # Spawn DVM2 Kind 5002 Text Translation - translator = Translation("Translator", os.getenv(env.NOSTR_PRIVATE_KEY)) + dvm_config = DVMConfig() + dvm_config.PRIVATE_KEY = os.getenv(env.NOSTR_PRIVATE_KEY) + dvm_config.LNBITS_INVOICE_KEY = os.getenv(env.LNBITS_INVOICE_KEY) + dvm_config.LNBITS_URL = os.getenv(env.LNBITS_HOST) + + translator = Translation("Translator", dvm_config) + d_tag = os.getenv(env.TASK_TRANSLATION_NIP89_DTAG) content = "{\"name\":\"" + translator.NAME + ("\",\"image\":\"https://image.nostr.build" "/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669" @@ -48,31 +57,36 @@ def run_nostr_dvm_with_local_config(): "\"sm\",\"sn\",\"so\",\"sq\",\"sr\",\"st\",\"su\",\"sv\",\"sw\"," "\"ta\",\"te\",\"tg\",\"th\",\"tl\",\"tr\",\"ug\",\"uk\",\"ur\"," "\"uz\",\"vi\",\"xh\",\"yi\",\"yo\",\"zh\",\"zu\"]}}}") + dvm_config.NIP89s.append(translator.NIP89_announcement(d_tag, content)) # Spawn DVM3 Kind 5100 Image Generation This one uses a specific backend called nova-server. If you want to use # it see the instructions in backends/nova_server - artist = ImageGenerationSDXL("Unstable Diffusion", os.getenv(env.NOSTR_PRIVATE_KEY)) - d_tag = os.getenv(env.TASK_IMAGEGENERATION_NIP89_DTAG) - content = "{\"name\":\"" + artist.NAME + ("\",\"image\":\"https://image.nostr.build" - "/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg" - "\",\"about\":\"I draw images based on a prompt with Stable Diffusion " - "XL 1.0.\",\"nip90Params\":{}}") - dvm_config.NIP89s.append(artist.NIP89_announcement(d_tag, content)) - - - - # Add the DVMS you want to use to the config - dvm_config.SUPPORTED_TASKS = [pdfextactor, translator, artist] - - # SET Lnbits Invoice Key and Server if DVM should provide invoices directly, else make sure you have a lnaddress - # on the profile + dvm_config = DVMConfig() + dvm_config.PRIVATE_KEY = os.getenv(env.NOSTR_PRIVATE_KEY) dvm_config.LNBITS_INVOICE_KEY = os.getenv(env.LNBITS_INVOICE_KEY) dvm_config.LNBITS_URL = os.getenv(env.LNBITS_HOST) - # Start the Server - nostr_dvm_thread = Thread(target=DVM, args=[dvm_config]) - nostr_dvm_thread.start() + unstableartist = ImageGenerationSDXL("Unstable Diffusion", dvm_config, "unstable") + d_tag = os.getenv(env.TASK_IMAGEGENERATION_NIP89_DTAG) + content = "{\"name\":\"" + unstableartist.NAME + ("\",\"image\":\"https://image.nostr.build" + "/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg" + "\",\"about\":\"I draw images based on a prompt with a Model called unstable diffusion.\",\"nip90Params\":{}}") + dvm_config.NIP89s.append(unstableartist.NIP89_announcement(d_tag, content)) + + dvm_config = DVMConfig() + dvm_config.PRIVATE_KEY = "73b262d31edc6ea1316dffcc7daa772651d661e6475761b7b78291482c1bf5cb" + dvm_config.LNBITS_INVOICE_KEY = os.getenv(env.LNBITS_INVOICE_KEY) + dvm_config.LNBITS_URL = os.getenv(env.LNBITS_HOST) + + # Spawn another Instance of text-to-image but use a different model and lora this time. + + sketcher = ImageGenerationSDXL("Sketcher", dvm_config, "mohawk", "timburton") + d_tag = os.getenv(env.TASK_IMAGEGENERATION_NIP89_DTAG2) + content = "{\"name\":\"" + sketcher.NAME + ("\",\"image\":\"https://image.nostr.build" + "/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg" + "\",\"about\":\"I draw images based on a prompt in kind of Tim Burton style\",\"nip90Params\":{}}") + dvm_config.NIP89s.append(sketcher.NIP89_announcement(d_tag, content)) if __name__ == '__main__': diff --git a/tasks/imagegenerationsdxl.py b/tasks/imagegenerationsdxl.py index 58ecb40..0db36f9 100644 --- a/tasks/imagegenerationsdxl.py +++ b/tasks/imagegenerationsdxl.py @@ -1,6 +1,9 @@ import os from multiprocessing.pool import ThreadPool +from threading import Thread + from backends.nova_server import check_nova_server_status, send_request_to_nova_server +from dvm import DVM from interfaces.dvmtaskinterface import DVMTaskInterface from utils.definitions import EventDefinitions from utils.nip89_utils import NIP89Announcement @@ -15,21 +18,23 @@ Outputs: An url to an Image class ImageGenerationSDXL(DVMTaskInterface): + NAME: str KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE TASK: str = "text-to-image" - COST: int = 0 + COST: int = 50 + PK: str - def __init__(self, name, pk): + def __init__(self, name, dvm_config, default_model=None, default_lora=None): self.NAME = name - self.PK = pk + dvm_config.SUPPORTED_TASKS = [self] + self.PK = dvm_config.PRIVATE_KEY + self.default_model = default_model + self.default_lora = default_lora + + dvm = DVM + nostr_dvm_thread = Thread(target=dvm, args=[dvm_config]) + nostr_dvm_thread.start() - def NIP89_announcement(self, d_tag, content): - nip89 = NIP89Announcement() - nip89.kind = self.KIND - nip89.pk = self.PK - nip89.dtag = d_tag - nip89.content = content - return nip89 def is_input_supported(self, input_type, input_content): if input_type != "text": @@ -38,19 +43,24 @@ class ImageGenerationSDXL(DVMTaskInterface): def create_request_form_from_nostr_event(self, event, client=None, dvm_config=None): request_form = {"jobID": event.id().to_hex() + "_"+ self.NAME.replace(" ", "")} - request_form["mode"] = "PROCESS" request_form["trainerFilePath"] = 'modules\\stablediffusionxl\\stablediffusionxl.trainer' prompt = "" negative_prompt = "" - #model = "stabilityai/stable-diffusion-xl-base-1.0" - model = "unstable" + if self.default_model is None: + model = "stabilityai/stable-diffusion-xl-base-1.0" + else: + model = self.default_model + # models: juggernautXL, dynavisionXL, colossusProjectXL, newrealityXL, unstable ratio_width = "1" ratio_height = "1" width = "" height = "" - lora = "" + if self.default_lora == None: + lora = "" + else: + lora = self.default_lora lora_weight = "" strength = "" guidance_scale = "" diff --git a/tasks/textextractionpdf.py b/tasks/textextractionpdf.py index 26f8c45..598fcbd 100644 --- a/tasks/textextractionpdf.py +++ b/tasks/textextractionpdf.py @@ -1,6 +1,8 @@ import os import re +from threading import Thread +from dvm import DVM from interfaces.dvmtaskinterface import DVMTaskInterface from utils.definitions import EventDefinitions from utils.nip89_utils import NIP89Announcement @@ -13,21 +15,23 @@ Accepted Inputs: Url to pdf file, Event containing an URL to a PDF file Outputs: Text containing the extracted contents of the PDF file """ class TextExtractionPDF(DVMTaskInterface): + NAME: str KIND: int = EventDefinitions.KIND_NIP90_EXTRACT_TEXT TASK: str = "pdf-to-text" COST: int = 20 + PK: str - def __init__(self, name, pk): + + def __init__(self, name, dvm_config): self.NAME = name - self.PK = pk + dvm_config.SUPPORTED_TASKS = [self] + self.PK = dvm_config.PRIVATE_KEY + + dvm = DVM + nostr_dvm_thread = Thread(target=dvm, args=[dvm_config]) + nostr_dvm_thread.start() + - def NIP89_announcement(self, d_tag, content): - nip89 = NIP89Announcement() - nip89.kind = self.KIND - nip89.pk = self.PK - nip89.dtag = d_tag - nip89.content = content - return nip89 def is_input_supported(self, input_type, input_content): if input_type != "url" and input_type != "event": diff --git a/tasks/translation.py b/tasks/translation.py index 362aee8..912bd0b 100644 --- a/tasks/translation.py +++ b/tasks/translation.py @@ -1,5 +1,7 @@ import os +from threading import Thread +from dvm import DVM from interfaces.dvmtaskinterface import DVMTaskInterface from utils.definitions import EventDefinitions from utils.nip89_utils import NIP89Announcement @@ -15,21 +17,20 @@ Outputs: Text containing the Translation in the desired language. class Translation(DVMTaskInterface): + NAME: str KIND: int = EventDefinitions.KIND_NIP90_TRANSLATE_TEXT TASK: str = "translation" COST: int = 0 + PK: str - def __init__(self, name, pk): + def __init__(self, name, dvm_config): self.NAME = name - self.PK = pk + dvm_config.SUPPORTED_TASKS = [self] + self.PK = dvm_config.PRIVATE_KEY - def NIP89_announcement(self, d_tag, content): - nip89 = NIP89Announcement() - nip89.kind = self.KIND - nip89.pk = self.PK - nip89.dtag = d_tag - nip89.content = content - return nip89 + dvm = DVM + nostr_dvm_thread = Thread(target=dvm, args=[dvm_config]) + nostr_dvm_thread.start() def is_input_supported(self, input_type, input_content): if input_type != "event" and input_type != "job" and input_type != "text": diff --git a/utils/admin_utils.py b/utils/admin_utils.py index 743b4bc..f7429c1 100644 --- a/utils/admin_utils.py +++ b/utils/admin_utils.py @@ -20,8 +20,7 @@ def admin_make_database_updates(config=None, client=None): whitelistuser = False unwhitelistuser = False blacklistuser = False - addbalance = False - additional_balance = 50 + # publickey = PublicKey.from_bech32("npub1...").to_hex() # use this if you have the npub @@ -42,17 +41,6 @@ def admin_make_database_updates(config=None, client=None): user = get_from_sql_table(publickey) update_sql_table(user.npub, user.balance, False, True, user.nip05, user.lud16, user.name, user.lastactive) - if addbalance: - user = get_from_sql_table(publickey) - update_sql_table(user[0], (int(user.balance) + additional_balance), user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16, user.name, user.lastactive) - time.sleep(1.0) - message = str(additional_balance) + " Sats have been added to your balance. Your new balance is " + str( - (int(user.balance) + additional_balance)) + " Sats." - keys = Keys.from_sk_str(config.PRIVATE_KEY) - evt = EventBuilder.new_encrypted_direct_msg(keys, PublicKey.from_hex(publickey), message, - None).to_event(keys) - send_event(evt, key=keys) - if deleteuser: delete_from_sql_table(publickey) diff --git a/utils/backend_utils.py b/utils/backend_utils.py index adcc9e1..b56fdde 100644 --- a/utils/backend_utils.py +++ b/utils/backend_utils.py @@ -2,10 +2,6 @@ import requests from utils.definitions import EventDefinitions from utils.nostr_utils import get_event_by_id -from tasks.textextractionpdf import TextExtractionPDF -from tasks.translation import Translation -from tasks.imagegenerationsdxl import ImageGenerationSDXL - def get_task(event, client, dvmconfig): if event.kind() == EventDefinitions.KIND_NIP90_GENERIC: # use this for events that have no id yet @@ -28,7 +24,7 @@ def get_task(event, client, dvmconfig): if tag.as_vec()[2] == "url": file_type = check_url_is_readable(tag.as_vec()[1]) if file_type == "pdf": - return TextExtractionPDF.TASK + return "pdf-to-text" else: return "unknown job" elif tag.as_vec()[2] == "event": @@ -46,9 +42,9 @@ def get_task(event, client, dvmconfig): return "unknown type" elif event.kind() == EventDefinitions.KIND_NIP90_TRANSLATE_TEXT: - return Translation.TASK + return "translation" elif event.kind() == EventDefinitions.KIND_NIP90_GENERATE_IMAGE: - return ImageGenerationSDXL.TASK + return "text-to-image" else: return "unknown type" @@ -60,6 +56,8 @@ def check_task_is_supported(event, client, get_duration=False, config=None): input_type = "" duration = 1 + + for tag in event.tags(): if tag.as_vec()[0] == 'i': if len(tag.as_vec()) < 3: @@ -82,6 +80,10 @@ def check_task_is_supported(event, client, get_duration=False, config=None): return False, "", 0 task = get_task(event, client=client, dvmconfig=dvm_config) + for dvm in dvm_config.SUPPORTED_TASKS: + if dvm.TASK == task: + if not dvm.is_input_supported(input_type, event.content()): + return False, task, duration if input_type == 'url' and check_url_is_readable(input_value) is None: print("url not readable") @@ -90,10 +92,7 @@ def check_task_is_supported(event, client, get_duration=False, config=None): if task not in (x.TASK for x in dvm_config.SUPPORTED_TASKS): return False, task, duration - for dvm in dvm_config.SUPPORTED_TASKS: - if dvm.TASK == task: - if not dvm.is_input_supported(input_type, event.content()): - return False, task, duration + return True, task, duration @@ -107,9 +106,9 @@ def check_url_is_readable(url): if content_type == 'audio/x-wav' or str(url).endswith(".wav") or content_type == 'audio/mpeg' or str(url).endswith( ".mp3") or content_type == 'audio/ogg' or str(url).endswith(".ogg"): return "audio" - elif content_type == 'image/png' or str(url).endswith(".png") or content_type == 'image/jpg' or str(url).endswith( - ".jpg") or content_type == 'image/jpeg' or str(url).endswith(".jpeg") or content_type == 'image/png' or str( - url).endswith(".png"): + elif (content_type == 'image/png' or str(url).endswith(".png") or content_type == 'image/jpg' or str(url).endswith( + ".jpg") or content_type == 'image/jpeg' or str(url).endswith(".jpeg") or content_type == 'image/png' or + str(url).endswith(".png")): return "image" elif content_type == 'video/mp4' or str(url).endswith(".mp4") or content_type == 'video/avi' or str(url).endswith( ".avi") or content_type == 'video/mov' or str(url).endswith(".mov"): diff --git a/utils/definitions.py b/utils/definitions.py index 04f5c8b..0344493 100644 --- a/utils/definitions.py +++ b/utils/definitions.py @@ -1,7 +1,10 @@ +import os from dataclasses import dataclass from nostr_sdk import Event +from utils import env + NEW_USER_BALANCE: int = 250 # Free credits for new users RELAY_LIST = ["wss://relay.damus.io", "wss://nostr-pub.wellorder.net", "wss://nos.lol", "wss://nostr.wine", @@ -42,7 +45,7 @@ class EventDefinitions: class DVMConfig: SUPPORTED_TASKS = [] - PRIVATE_KEY: str + PRIVATE_KEY: str = os.getenv(env.NOSTR_PRIVATE_KEY) RELAY_LIST = ["wss://relay.damus.io", "wss://nostr-pub.wellorder.net", "wss://nos.lol", "wss://nostr.wine", "wss://relay.nostfiles.dev", "wss://nostr.mom", "wss://nostr.oxtr.dev", "wss://relay.nostr.bg", diff --git a/utils/env.py b/utils/env.py index 3d0bd1a..6945dfd 100644 --- a/utils/env.py +++ b/utils/env.py @@ -9,5 +9,6 @@ LNBITS_HOST = "LNBITS_HOST" TASK_TRANSLATION_NIP89_DTAG = "TASK_TRANSLATION_NIP89_DTAG" TASK_TEXTEXTRACTION_NIP89_DTAG = "TASK_TEXTEXTRACTION_NIP89_DTAG" TASK_IMAGEGENERATION_NIP89_DTAG = "TASK_IMAGEGENERATION_NIP89_DTAG" +TASK_IMAGEGENERATION_NIP89_DTAG2 = "TASK_IMAGEGENERATION_NIP89_DTAG2" diff --git a/utils/zap_utils.py b/utils/zap_utils.py index b81a627..8e7fa31 100644 --- a/utils/zap_utils.py +++ b/utils/zap_utils.py @@ -33,6 +33,7 @@ def parse_bolt11_invoice(invoice): return int(number) + def create_bolt11_ln_bits(sats, config): url = config.LNBITS_URL + "/api/v1/payments" data = {'out': False, 'amount': sats, 'memo': "Nostr-DVM"} @@ -45,6 +46,7 @@ def create_bolt11_ln_bits(sats, config): print(e) return None + def check_bolt11_ln_bits_is_paid(payment_hash, config): url = config.LNBITS_URL + "/api/v1/payments/" + payment_hash headers = {'X-API-Key': config.LNBITS_INVOICE_KEY, 'Content-Type': 'application/json', 'charset': 'UTF-8'} @@ -92,4 +94,3 @@ def decrypt_private_zap_message(msg, privkey, pubkey): return decoded except Exception as ex: return str(ex) - From 2e1920a940904680b417b9b6ed805ea4eeab65f4 Mon Sep 17 00:00:00 2001 From: Believethehype Date: Mon, 20 Nov 2023 23:18:05 +0100 Subject: [PATCH 03/18] some fixes, updated readme --- README.md | 8 + dvm.py | 452 +++++++++++++++++------------------ main.py | 16 +- tasks/imagegenerationsdxl.py | 2 +- utils/output_utils.py | 10 +- utils/zap_utils.py | 2 +- 6 files changed, 249 insertions(+), 241 deletions(-) diff --git a/README.md b/README.md index db4f1e6..441366a 100644 --- a/README.md +++ b/README.md @@ -10,3 +10,11 @@ Place .env file (based on .env_example) in main folder, install requirements.txt Use vendata.io to create a nip89 announcement of your dvm and save the dtag in your .env config. A tutorial on how to add additional tasks, as well as the larger server backend will be added soon. + +Known Issues: +- After refactoring DVMs work independent from each other for the most part. + - They currently still share a joblist and might act weird together (TODO rework joblist) + - Some functions might work easier than they did before (need some refactoring) +- Bot currently not implemented +- Some basic functionality is still missing, e.g. handling various mediasources +- Interface might still change a lot and brick things. diff --git a/dvm.py b/dvm.py index 48d3f17..9919332 100644 --- a/dvm.py +++ b/dvm.py @@ -60,161 +60,159 @@ class DVM: def handle(self, relay_url, nostr_event): print(f"[Nostr] Received new NIP90 Job Request from {relay_url}: {nostr_event.as_json()}") if EventDefinitions.KIND_NIP90_EXTRACT_TEXT <= nostr_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC: - self.handle_nip90_job_event(nostr_event) + handle_nip90_job_event(nostr_event) elif nostr_event.kind() == EventDefinitions.KIND_ZAP: - self.handle_zap(nostr_event) + handle_zap(nostr_event) def handle_msg(self, relay_url, msg): return - def handle_nip90_job_event(self, nip90_event): - user = get_or_add_user(nip90_event.pubkey().to_hex()) - task_supported, task, duration = check_task_is_supported(nip90_event, client=self.client, - get_duration=(not user.iswhitelisted), - config=self.dvm_config) - print(task) + def handle_nip90_job_event(nip90_event): + user = get_or_add_user(nip90_event.pubkey().to_hex()) + task_supported, task, duration = check_task_is_supported(nip90_event, client=self.client, + get_duration=(not user.iswhitelisted), + config=self.dvm_config) + print(task) - if user.isblacklisted: - send_job_status_reaction(nip90_event, "error", client=self.client, config=self.dvm_config) - print("[Nostr] Request by blacklisted user, skipped") + if user.isblacklisted: + send_job_status_reaction(nip90_event, "error", client=self.client, config=self.dvm_config) + print("[Nostr] Request by blacklisted user, skipped") - elif task_supported: - print("Received new Task: " + task) - amount = get_amount_per_task(task, self.dvm_config, duration) - if amount is None: - return + elif task_supported: + print("Received new Task: " + task) + amount = get_amount_per_task(task, self.dvm_config, duration) + if amount is None: + return - task_is_free = False - for dvm in self.dvm_config.SUPPORTED_TASKS: - if dvm.TASK == task and dvm.COST == 0: - task_is_free = True + task_is_free = False + for dvm in self.dvm_config.SUPPORTED_TASKS: + if dvm.TASK == task and dvm.COST == 0: + task_is_free = True - if user.iswhitelisted or task_is_free: - print("[Nostr] Free or Whitelisted for task " + task + ". Starting processing..") - send_job_status_reaction(nip90_event, "processing", True, 0, client=self.client, - config=self.dvm_config) - do_work(nip90_event, is_from_bot=False) - # otherwise send payment request - else: - bid = 0 - for tag in nip90_event.tags(): - if tag.as_vec()[0] == 'bid': - bid = int(tag.as_vec()[1]) - - print("[Nostr][Payment required] New Nostr " + task + " Job event: " + nip90_event.as_json()) - if bid > 0: - bid_offer = int(bid / 1000) - if bid_offer >= amount: - send_job_status_reaction(nip90_event, "payment-required", False, - amount, # bid_offer - client=self.client, config=self.dvm_config) - - else: # If there is no bid, just request server rate from user - print("[Nostr] Requesting payment for Event: " + nip90_event.id().to_hex()) - send_job_status_reaction(nip90_event, "payment-required", - False, amount, client=self.client, config=self.dvm_config) + if user.iswhitelisted or task_is_free: + print("[Nostr] Free or Whitelisted for task " + task + ". Starting processing..") + send_job_status_reaction(nip90_event, "processing", True, 0, client=self.client, + config=self.dvm_config) + do_work(nip90_event, is_from_bot=False) + # otherwise send payment request else: - print("Task not supported on this DVM, skipping..") + bid = 0 + for tag in nip90_event.tags(): + if tag.as_vec()[0] == 'bid': + bid = int(tag.as_vec()[1]) - def handle_zap(self, event): - zapped_event = None - invoice_amount = 0 - anon = False - sender = event.pubkey() + print("[Nostr][Payment required] New Nostr " + task + " Job event: " + nip90_event.as_json()) + if bid > 0: + bid_offer = int(bid / 1000) + if bid_offer >= amount: + send_job_status_reaction(nip90_event, "payment-required", False, + amount, # bid_offer + client=self.client, config=self.dvm_config) - try: - for tag in event.tags(): - if tag.as_vec()[0] == 'bolt11': - invoice_amount = parse_bolt11_invoice(tag.as_vec()[1]) - elif tag.as_vec()[0] == 'e': - zapped_event = get_event_by_id(tag.as_vec()[1], config=self.dvm_config) - elif tag.as_vec()[0] == 'description': - zap_request_event = Event.from_json(tag.as_vec()[1]) - sender = check_for_zapplepay(zap_request_event.pubkey().to_hex(), - zap_request_event.content()) - for ztag in zap_request_event.tags(): - if ztag.as_vec()[0] == 'anon': - if len(ztag.as_vec()) > 1: - print("Private Zap received.") - decrypted_content = decrypt_private_zap_message(ztag.as_vec()[1], - self.keys.secret_key(), - zap_request_event.pubkey()) - decrypted_private_event = Event.from_json(decrypted_content) - if decrypted_private_event.kind() == 9733: - sender = decrypted_private_event.pubkey().to_hex() - message = decrypted_private_event.content() - if message != "": - print("Zap Message: " + message) - else: - anon = True - print("Anonymous Zap received. Unlucky, I don't know from whom, and never will") - user = get_or_add_user(sender) - print(str(user)) + else: # If there is no bid, just request server rate from user + print("[Nostr] Requesting payment for Event: " + nip90_event.id().to_hex()) + send_job_status_reaction(nip90_event, "payment-required", + False, amount, client=self.client, config=self.dvm_config) + else: + print("Task not supported on this DVM, skipping..") - if zapped_event is not None: - if zapped_event.kind() == EventDefinitions.KIND_FEEDBACK: # if a reaction by us got zapped - if not self.dvm_config.IS_BOT: - print("Zap received for NIP90 task: " + str(invoice_amount) + " Sats from " + str( - user.name)) - amount = 0 - job_event = None - for tag in zapped_event.tags(): - if tag.as_vec()[0] == 'amount': - amount = int(float(tag.as_vec()[1]) / 1000) - elif tag.as_vec()[0] == 'e': - job_event = get_event_by_id(tag.as_vec()[1], config=self.dvm_config) + def handle_zap(event): + zapped_event = None + invoice_amount = 0 + anon = False + sender = event.pubkey() + print("Zap received") - task_supported, task, duration = check_task_is_supported(job_event, client=self.client, - get_duration=False, - config=self.dvm_config) - if job_event is not None and task_supported: - if amount <= invoice_amount: - print("[Nostr] Payment-request fulfilled...") - send_job_status_reaction(job_event, "processing", client=self.client, - config=self.dvm_config) - indices = [i for i, x in enumerate(job_list) if - x.event_id == job_event.id().to_hex()] - index = -1 - if len(indices) > 0: - index = indices[0] - if index > -1: - if job_list[index].is_processed: # If payment-required appears a processing - job_list[index].is_paid = True - check_and_return_event(job_list[index].result, - str(job_event.as_json()), - dvm_key=self.dvm_config.PRIVATE_KEY) - elif not (job_list[index]).is_processed: - # If payment-required appears before processing - job_list.pop(index) - print("Starting work...") - do_work(job_event, is_from_bot=False) - else: - print("Job not in List, but starting work...") - do_work(job_event, is_from_bot=False) + try: + for tag in event.tags(): + if tag.as_vec()[0] == 'bolt11': + invoice_amount = parse_bolt11_invoice(tag.as_vec()[1]) + elif tag.as_vec()[0] == 'e': + zapped_event = get_event_by_id(tag.as_vec()[1], config=self.dvm_config) + elif tag.as_vec()[0] == 'description': + zap_request_event = Event.from_json(tag.as_vec()[1]) + sender = check_for_zapplepay(zap_request_event.pubkey().to_hex(), + zap_request_event.content()) + for ztag in zap_request_event.tags(): + if ztag.as_vec()[0] == 'anon': + if len(ztag.as_vec()) > 1: + print("Private Zap received.") + decrypted_content = decrypt_private_zap_message(ztag.as_vec()[1], + self.keys.secret_key(), + zap_request_event.pubkey()) + decrypted_private_event = Event.from_json(decrypted_content) + if decrypted_private_event.kind() == 9733: + sender = decrypted_private_event.pubkey().to_hex() + message = decrypted_private_event.content() + if message != "": + print("Zap Message: " + message) + else: + anon = True + print("Anonymous Zap received. Unlucky, I don't know from whom, and never will") + user = get_or_add_user(sender) + print(str(user)) - else: - send_job_status_reaction(job_event, "payment-rejected", - False, invoice_amount, client=self.client, - config=self.dvm_config) - print("[Nostr] Invoice was not paid sufficiently") + if zapped_event is not None: + if zapped_event.kind() == EventDefinitions.KIND_FEEDBACK: # if a reaction by us got zapped + print("Zap received for NIP90 task: " + str(invoice_amount) + " Sats from " + str( + user.name)) + amount = 0 + job_event = None + for tag in zapped_event.tags(): + if tag.as_vec()[0] == 'amount': + amount = int(float(tag.as_vec()[1]) / 1000) + elif tag.as_vec()[0] == 'e': + job_event = get_event_by_id(tag.as_vec()[1], config=self.dvm_config) - elif zapped_event.kind() in EventDefinitions.ANY_RESULT: - print("Someone zapped the result of an exisiting Task. Nice") - elif not anon and not self.dvm_config.PASSIVE_MODE: - print("Note Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( - user.name)) - update_user_balance(sender, invoice_amount, config=self.dvm_config) + task_supported, task, duration = check_task_is_supported(job_event, client=self.client, + get_duration=False, + config=self.dvm_config) + if job_event is not None and task_supported: + if amount <= invoice_amount: + print("[Nostr] Payment-request fulfilled...") + send_job_status_reaction(job_event, "processing", client=self.client, + config=self.dvm_config) + indices = [i for i, x in enumerate(job_list) if + x.event_id == job_event.id().to_hex()] + index = -1 + if len(indices) > 0: + index = indices[0] + if index > -1: + if job_list[index].is_processed: # If payment-required appears a processing + job_list[index].is_paid = True + check_and_return_event(job_list[index].result, + str(job_event.as_json()), + dvm_key=self.dvm_config.PRIVATE_KEY) + elif not (job_list[index]).is_processed: + # If payment-required appears before processing + job_list.pop(index) + print("Starting work...") + do_work(job_event, is_from_bot=False) + else: + print("Job not in List, but starting work...") + do_work(job_event, is_from_bot=False) - # a regular note - elif not anon and not self.dvm_config.PASSIVE_MODE: - print("Profile Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( + else: + send_job_status_reaction(job_event, "payment-rejected", + False, invoice_amount, client=self.client, + config=self.dvm_config) + print("[Nostr] Invoice was not paid sufficiently") + + elif zapped_event.kind() in EventDefinitions.ANY_RESULT: + print("Someone zapped the result of an exisiting Task. Nice") + elif not anon: + print("Note Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( user.name)) update_user_balance(sender, invoice_amount, config=self.dvm_config) - except Exception as e: - print(f"Error during content decryption: {e}") - + # a regular note + elif not anon: + print("Profile Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( + user.name)) + update_user_balance(sender, invoice_amount, config=self.dvm_config) + except Exception as e: + print(f"Error during content decryption: {e}") def check_event_has_not_unfinished_job_input(nevent, append, client, dvmconfig): task_supported, task, duration = check_task_is_supported(nevent, client, False, config=dvmconfig) @@ -237,14 +235,12 @@ class DVM: job = RequiredJobToWatch(event=nevent, timestamp=Timestamp.now().as_secs()) jobs_on_hold_list.append(job) send_job_status_reaction(nevent, "chain-scheduled", True, 0, client=client, - config=dvmconfig) + config=dvmconfig) return False else: return True - - def check_and_return_event(data, original_event_str: str, dvm_key=""): original_event = Event.from_json(original_event_str) keys = Keys.from_sk_str(dvm_key) @@ -258,10 +254,10 @@ class DVM: if self.dvm_config.SHOWRESULTBEFOREPAYMENT and not is_paid: send_nostr_reply_event(data, original_event_str, key=keys) send_job_status_reaction(original_event, "success", amount, - config=self.dvm_config) # or payment-required, or both? + config=self.dvm_config) # or payment-required, or both? elif not self.dvm_config.SHOWRESULTBEFOREPAYMENT and not is_paid: send_job_status_reaction(original_event, "success", amount, - config=self.dvm_config) # or payment-required, or both? + config=self.dvm_config) # or payment-required, or both? if self.dvm_config.SHOWRESULTBEFOREPAYMENT and is_paid: job_list.remove(x) @@ -337,97 +333,98 @@ class DVM: send_event(evt, key=keys) def send_job_status_reaction(original_event, status, is_paid=True, amount=0, client=None, - content=None, - config=None, - key=None): - dvmconfig = config - alt_description = "This is a reaction to a NIP90 DVM AI task. " - task = get_task(original_event, client=client, dvmconfig=dvmconfig) - if status == "processing": - alt_description = "NIP90 DVM AI task " + task + " started processing. " - reaction = alt_description + emoji.emojize(":thumbs_up:") - elif status == "success": - alt_description = "NIP90 DVM AI task " + task + " finished successfully. " - reaction = alt_description + emoji.emojize(":call_me_hand:") - elif status == "chain-scheduled": - alt_description = "NIP90 DVM AI task " + task + " Chain Task scheduled" - reaction = alt_description + emoji.emojize(":thumbs_up:") - elif status == "error": - alt_description = "NIP90 DVM AI task " + task + " had an error. " - if content is None: - reaction = alt_description + emoji.emojize(":thumbs_down:") - else: - reaction = alt_description + emoji.emojize(":thumbs_down:") + content - - elif status == "payment-required": - - alt_description = "NIP90 DVM AI task " + task + " requires payment of min " + str( - amount) + " Sats. " - reaction = alt_description + emoji.emojize(":orange_heart:") - - elif status == "payment-rejected": - alt_description = "NIP90 DVM AI task " + task + " payment is below required amount of " + str( - amount) + " Sats. " - reaction = alt_description + emoji.emojize(":thumbs_down:") - elif status == "user-blocked-from-service": - alt_description = "NIP90 DVM AI task " + task + " can't be performed. User has been blocked from Service. " + content=None, + config=None, + key=None): + dvmconfig = config + alt_description = "This is a reaction to a NIP90 DVM AI task. " + task = get_task(original_event, client=client, dvmconfig=dvmconfig) + if status == "processing": + alt_description = "NIP90 DVM AI task " + task + " started processing. " + reaction = alt_description + emoji.emojize(":thumbs_up:") + elif status == "success": + alt_description = "NIP90 DVM AI task " + task + " finished successfully. " + reaction = alt_description + emoji.emojize(":call_me_hand:") + elif status == "chain-scheduled": + alt_description = "NIP90 DVM AI task " + task + " Chain Task scheduled" + reaction = alt_description + emoji.emojize(":thumbs_up:") + elif status == "error": + alt_description = "NIP90 DVM AI task " + task + " had an error. " + if content is None: reaction = alt_description + emoji.emojize(":thumbs_down:") else: - reaction = emoji.emojize(":thumbs_down:") + reaction = alt_description + emoji.emojize(":thumbs_down:") + content - e_tag = Tag.parse(["e", original_event.id().to_hex()]) - p_tag = Tag.parse(["p", original_event.pubkey().to_hex()]) - alt_tag = Tag.parse(["alt", alt_description]) - status_tag = Tag.parse(["status", status]) - tags = [e_tag, p_tag, alt_tag, status_tag] + elif status == "payment-required": - if status == "success" or status == "error": # - for x in job_list: - if x.event_id == original_event.id(): - is_paid = x.is_paid - amount = x.amount - break + alt_description = "NIP90 DVM AI task " + task + " requires payment of min " + str( + amount) + " Sats. " + reaction = alt_description + emoji.emojize(":orange_heart:") - bolt11 = "" - payment_hash = "" - expires = original_event.created_at().as_secs() + (60 * 60 * 24) - if status == "payment-required" or (status == "processing" and not is_paid): - if dvmconfig.LNBITS_INVOICE_KEY != "": - try: - bolt11, payment_hash = create_bolt11_ln_bits(amount, dvmconfig) - except Exception as e: - print(e) + elif status == "payment-rejected": + alt_description = "NIP90 DVM AI task " + task + " payment is below required amount of " + str( + amount) + " Sats. " + reaction = alt_description + emoji.emojize(":thumbs_down:") + elif status == "user-blocked-from-service": + alt_description = "NIP90 DVM AI task " + task + " can't be performed. User has been blocked from Service. " + reaction = alt_description + emoji.emojize(":thumbs_down:") + else: + reaction = emoji.emojize(":thumbs_down:") - if not any(x.event_id == original_event.id().to_hex() for x in job_list): - job_list.append( - JobToWatch(event_id=original_event.id().to_hex(), - timestamp=original_event.created_at().as_secs(), - amount=amount, - is_paid=is_paid, - status=status, result="", is_processed=False, bolt11=bolt11, - payment_hash=payment_hash, - expires=expires, from_bot=False)) - print(str(job_list)) - if status == "payment-required" or status == "payment-rejected" or ( - status == "processing" and not is_paid) or ( - status == "success" and not is_paid): + e_tag = Tag.parse(["e", original_event.id().to_hex()]) + p_tag = Tag.parse(["p", original_event.pubkey().to_hex()]) + alt_tag = Tag.parse(["alt", alt_description]) + status_tag = Tag.parse(["status", status]) + tags = [e_tag, p_tag, alt_tag, status_tag] - if dvmconfig.LNBITS_INVOICE_KEY != "": - amount_tag = Tag.parse(["amount", str(amount * 1000), bolt11]) - else: - amount_tag = Tag.parse(["amount", str(amount * 1000)]) # to millisats - tags.append(amount_tag) - if key is not None: - keys = Keys.from_sk_str(key) + if status == "success" or status == "error": # + for x in job_list: + if x.event_id == original_event.id(): + is_paid = x.is_paid + amount = x.amount + break + + bolt11 = "" + payment_hash = "" + expires = original_event.created_at().as_secs() + (60 * 60 * 24) + if status == "payment-required" or (status == "processing" and not is_paid): + if dvmconfig.LNBITS_INVOICE_KEY != "": + try: + bolt11, payment_hash = create_bolt11_ln_bits(amount, dvmconfig) + except Exception as e: + print(e) + + if not any(x.event_id == original_event.id().to_hex() for x in job_list): + job_list.append( + JobToWatch(event_id=original_event.id().to_hex(), + timestamp=original_event.created_at().as_secs(), + amount=amount, + is_paid=is_paid, + status=status, result="", is_processed=False, bolt11=bolt11, + payment_hash=payment_hash, + expires=expires, from_bot=False)) + print(str(job_list)) + if status == "payment-required" or status == "payment-rejected" or ( + status == "processing" and not is_paid) or ( + status == "success" and not is_paid): + + if dvmconfig.LNBITS_INVOICE_KEY != "": + amount_tag = Tag.parse(["amount", str(amount * 1000), bolt11]) else: - keys = Keys.from_sk_str(dvmconfig.PRIVATE_KEY) - event = EventBuilder(EventDefinitions.KIND_FEEDBACK, reaction, tags).to_event(keys) + amount_tag = Tag.parse(["amount", str(amount * 1000)]) # to millisats + tags.append(amount_tag) + if key is not None: + keys = Keys.from_sk_str(key) + else: + keys = Keys.from_sk_str(dvmconfig.PRIVATE_KEY) + event = EventBuilder(EventDefinitions.KIND_FEEDBACK, reaction, tags).to_event(keys) + + send_event(event, key=keys) + print( + "[Nostr] Sent Kind " + str( + EventDefinitions.KIND_FEEDBACK) + " Reaction: " + status + " " + event.as_json()) + return event.as_json() - send_event(event, key=keys) - print( - "[Nostr] Sent Kind " + str( - EventDefinitions.KIND_FEEDBACK) + " Reaction: " + status + " " + event.as_json()) - return event.as_json() def do_work(job_event, is_from_bot=False): if (( EventDefinitions.KIND_NIP90_EXTRACT_TEXT <= job_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC) @@ -442,7 +439,7 @@ class DVM: self.dvm_config) result = dvm.process(request_form) check_and_return_event(result, str(job_event.as_json()), - dvm_key=self.dvm_config.PRIVATE_KEY) + dvm_key=self.dvm_config.PRIVATE_KEY) except Exception as e: print(e) @@ -458,8 +455,8 @@ class DVM: event = get_event_by_id(job.event_id, config=self.dvm_config) if event is not None: send_job_status_reaction(event, "processing", True, 0, - client=self.client, - config=self.dvm_config) + client=self.client, + config=self.dvm_config) print("do work from joblist") do_work(event, is_from_bot=False) @@ -477,15 +474,14 @@ class DVM: for job in jobs_on_hold_list: if check_event_has_not_unfinished_job_input(job.event, False, client=self.client, - dvmconfig=self.dvm_config): - # handle_nip90_job_event(event=job.event) + dvmconfig=self.dvm_config): + handle_nip90_job_event(nip90_event=job.event) try: jobs_on_hold_list.remove(job) except: continue - if Timestamp.now().as_secs() > job.timestamp + 60 * 20: # remove jobs to look for after 20 minutes.. jobs_on_hold_list.remove(job) - time.sleep(1.0) + time.sleep(2.0) diff --git a/main.py b/main.py index d110401..eda1580 100644 --- a/main.py +++ b/main.py @@ -67,20 +67,18 @@ def run_nostr_dvm_with_local_config(): dvm_config.LNBITS_INVOICE_KEY = os.getenv(env.LNBITS_INVOICE_KEY) dvm_config.LNBITS_URL = os.getenv(env.LNBITS_HOST) - unstableartist = ImageGenerationSDXL("Unstable Diffusion", dvm_config, "unstable") - d_tag = os.getenv(env.TASK_IMAGEGENERATION_NIP89_DTAG) - content = "{\"name\":\"" + unstableartist.NAME + ("\",\"image\":\"https://image.nostr.build" - "/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg" - "\",\"about\":\"I draw images based on a prompt with a Model called unstable diffusion.\",\"nip90Params\":{}}") - dvm_config.NIP89s.append(unstableartist.NIP89_announcement(d_tag, content)) + #unstableartist = ImageGenerationSDXL("Unstable Diffusion", dvm_config, "unstable") + #d_tag = os.getenv(env.TASK_IMAGEGENERATION_NIP89_DTAG) + #content = "{\"name\":\"" + unstableartist.NAME + ("\",\"image\":\"https://image.nostr.build" + # "/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg" + # "\",\"about\":\"I draw images based on a prompt with a Model called unstable diffusion.\",\"nip90Params\":{}}") + #dvm_config.NIP89s.append(unstableartist.NIP89_announcement(d_tag, content)) + # Spawn another Instance of text-to-image but use a different model and lora this time. dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = "73b262d31edc6ea1316dffcc7daa772651d661e6475761b7b78291482c1bf5cb" dvm_config.LNBITS_INVOICE_KEY = os.getenv(env.LNBITS_INVOICE_KEY) dvm_config.LNBITS_URL = os.getenv(env.LNBITS_HOST) - - # Spawn another Instance of text-to-image but use a different model and lora this time. - sketcher = ImageGenerationSDXL("Sketcher", dvm_config, "mohawk", "timburton") d_tag = os.getenv(env.TASK_IMAGEGENERATION_NIP89_DTAG2) content = "{\"name\":\"" + sketcher.NAME + ("\",\"image\":\"https://image.nostr.build" diff --git a/tasks/imagegenerationsdxl.py b/tasks/imagegenerationsdxl.py index 0db36f9..e9fe8c6 100644 --- a/tasks/imagegenerationsdxl.py +++ b/tasks/imagegenerationsdxl.py @@ -21,7 +21,7 @@ class ImageGenerationSDXL(DVMTaskInterface): NAME: str KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE TASK: str = "text-to-image" - COST: int = 50 + COST: int = 5 PK: str def __init__(self, name, dvm_config, default_model=None, default_lora=None): diff --git a/utils/output_utils.py b/utils/output_utils.py index 6f8c0f3..aea0e60 100644 --- a/utils/output_utils.py +++ b/utils/output_utils.py @@ -10,6 +10,8 @@ import pandas ''' Post process results to either given output format or a Nostr readable plain text. ''' + + def post_process_result(anno, original_event): print("post-processing...") if isinstance(anno, pandas.DataFrame): # if input is an anno we parse it to required output format @@ -85,13 +87,15 @@ def post_process_result(anno, original_event): elif isinstance(anno, NoneType): return "An error occurred" else: - result = replace_broken_words(anno) #TODO + result = replace_broken_words(anno) # TODO return result ''' Convenience function to replace words like Noster with Nostr ''' + + def replace_broken_words(text): result = (text.replace("Noster", "Nostr").replace("Nostra", "Nostr").replace("no stir", "Nostr"). replace("Nostro", "Nostr").replace("Impub", "npub").replace("sets", "Sats")) @@ -103,12 +107,14 @@ Function to upload to Nostr.build and if it fails to Nostrfiles.dev Larger files than these hosters allow and fallback is catbox currently. Will probably need to switch to another system in the future. ''' + + def uploadMediaToHoster(filepath): print("Uploading image: " + filepath) try: files = {'file': open(filepath, 'rb')} file_stats = os.stat(filepath) - sizeinmb = file_stats.st_size / (1024*1024) + sizeinmb = file_stats.st_size / (1024 * 1024) print("Filesize of Uploaded media: " + str(sizeinmb) + " Mb.") if sizeinmb > 25: uploader = CatboxUploader(filepath) diff --git a/utils/zap_utils.py b/utils/zap_utils.py index 8e7fa31..4eca69c 100644 --- a/utils/zap_utils.py +++ b/utils/zap_utils.py @@ -43,7 +43,7 @@ def create_bolt11_ln_bits(sats, config): obj = json.loads(res.text) return obj["payment_request"], obj["payment_hash"] except Exception as e: - print(e) + print("LNBITS: " + str(e)) return None From 86dafcc32059de456196953fbd2b5d9fb7b8f5ec Mon Sep 17 00:00:00 2001 From: Believethehype Date: Mon, 20 Nov 2023 23:18:42 +0100 Subject: [PATCH 04/18] Update README.md --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 441366a..bad9941 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,8 @@ A tutorial on how to add additional tasks, as well as the larger server backend Known Issues: - After refactoring DVMs work independent from each other for the most part. - They currently still share a joblist and might act weird together (TODO rework joblist) + + - Probably therefore jobs might sometimes not start (need investigation) - Some functions might work easier than they did before (need some refactoring) - Bot currently not implemented - Some basic functionality is still missing, e.g. handling various mediasources From 50f407641629349163d672e3b604505cae1bdb9f Mon Sep 17 00:00:00 2001 From: Believethehype Date: Tue, 21 Nov 2023 10:03:04 +0100 Subject: [PATCH 05/18] separate dbs per DVM, fix for shared joblists --- .gitignore | 1 + dvm.py | 66 ++++++++++++---------- main.py | 15 +++-- tasks/imagegenerationsdxl.py | 2 +- tasks/textextractionpdf.py | 7 +-- tasks/translation.py | 4 +- utils/admin_utils.py | 23 ++++---- utils/database_utils.py | 103 ++++++++++++++++++----------------- utils/definitions.py | 16 ------ utils/dvmconfig.py | 22 ++++++++ 10 files changed, 138 insertions(+), 121 deletions(-) create mode 100644 utils/dvmconfig.py diff --git a/.gitignore b/.gitignore index 8bbe727..279dbf3 100644 --- a/.gitignore +++ b/.gitignore @@ -160,3 +160,4 @@ cython_debug/ #.idea/ nostrzaps.db .DS_Store +*.db diff --git a/dvm.py b/dvm.py index 9919332..9c28ac2 100644 --- a/dvm.py +++ b/dvm.py @@ -3,7 +3,8 @@ from nostr_sdk import PublicKey, Keys, Client, Tag, Event, EventBuilder, Filter, import time import emoji -from utils.definitions import EventDefinitions, DVMConfig, RequiredJobToWatch, JobToWatch +from utils.definitions import EventDefinitions, RequiredJobToWatch, JobToWatch +from utils.dvmconfig import DVMConfig from utils.admin_utils import admin_make_database_updates from utils.backend_utils import get_amount_per_task, check_task_is_supported, get_task from utils.database_utils import update_sql_table, get_from_sql_table, \ @@ -17,19 +18,22 @@ use_logger = False if use_logger: init_logger(LogLevel.DEBUG) -job_list = [] -jobs_on_hold_list = [] + class DVM: dvm_config: DVMConfig keys: Keys client: Client + job_list: list + jobs_on_hold_list: list def __init__(self, config): self.dvm_config = config self.keys = Keys.from_sk_str(config.PRIVATE_KEY) self.client = Client(self.keys) + self.job_list = [] + self.jobs_on_hold_list = [] pk = self.keys.public_key() @@ -49,7 +53,7 @@ class DVM: dvm_filter = (Filter().kinds(kinds).since(Timestamp.now())) self.client.subscribe([dm_zap_filter, dvm_filter]) - create_sql_table() + create_sql_table(self.dvm_config.DB) admin_make_database_updates(config=self.dvm_config, client=self.client) class NotificationHandler(HandleNotification): @@ -68,7 +72,9 @@ class DVM: return def handle_nip90_job_event(nip90_event): - user = get_or_add_user(nip90_event.pubkey().to_hex()) + print(str(self.dvm_config.DB)) + user = get_or_add_user(self.dvm_config.DB, nip90_event.pubkey().to_hex()) + print("got user") task_supported, task, duration = check_task_is_supported(nip90_event, client=self.client, get_duration=(not user.iswhitelisted), config=self.dvm_config) @@ -149,7 +155,7 @@ class DVM: else: anon = True print("Anonymous Zap received. Unlucky, I don't know from whom, and never will") - user = get_or_add_user(sender) + user = get_or_add_user(self.dvm_config.DB, sender) print(str(user)) if zapped_event is not None: @@ -172,20 +178,20 @@ class DVM: print("[Nostr] Payment-request fulfilled...") send_job_status_reaction(job_event, "processing", client=self.client, config=self.dvm_config) - indices = [i for i, x in enumerate(job_list) if + indices = [i for i, x in enumerate(self.job_list) if x.event_id == job_event.id().to_hex()] index = -1 if len(indices) > 0: index = indices[0] if index > -1: - if job_list[index].is_processed: # If payment-required appears a processing - job_list[index].is_paid = True - check_and_return_event(job_list[index].result, + if self.job_list[index].is_processed: # If payment-required appears a processing + self.job_list[index].is_paid = True + check_and_return_event(self.job_list[index].result, str(job_event.as_json()), dvm_key=self.dvm_config.PRIVATE_KEY) - elif not (job_list[index]).is_processed: + elif not (self.job_list[index]).is_processed: # If payment-required appears before processing - job_list.pop(index) + self.job_list.pop(index) print("Starting work...") do_work(job_event, is_from_bot=False) else: @@ -203,13 +209,13 @@ class DVM: elif not anon: print("Note Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( user.name)) - update_user_balance(sender, invoice_amount, config=self.dvm_config) + update_user_balance(self.dvm_config.DB, sender, invoice_amount, config=self.dvm_config) # a regular note elif not anon: print("Profile Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( user.name)) - update_user_balance(sender, invoice_amount, config=self.dvm_config) + update_user_balance(self.dvm_config.DB, sender, invoice_amount, config=self.dvm_config) except Exception as e: print(f"Error during content decryption: {e}") @@ -233,7 +239,7 @@ class DVM: if evt is None: if append: job = RequiredJobToWatch(event=nevent, timestamp=Timestamp.now().as_secs()) - jobs_on_hold_list.append(job) + self.jobs_on_hold_list.append(job) send_job_status_reaction(nevent, "chain-scheduled", True, 0, client=client, config=dvmconfig) @@ -245,7 +251,7 @@ class DVM: original_event = Event.from_json(original_event_str) keys = Keys.from_sk_str(dvm_key) - for x in job_list: + for x in self.job_list: if x.event_id == original_event.id().to_hex(): is_paid = x.is_paid amount = x.amount @@ -260,9 +266,9 @@ class DVM: config=self.dvm_config) # or payment-required, or both? if self.dvm_config.SHOWRESULTBEFOREPAYMENT and is_paid: - job_list.remove(x) + self.job_list.remove(x) elif not self.dvm_config.SHOWRESULTBEFOREPAYMENT and is_paid: - job_list.remove(x) + self.job_list.remove(x) send_nostr_reply_event(data, original_event_str, key=keys) break @@ -317,10 +323,10 @@ class DVM: elif tag.as_vec()[0] == "i": task = tag.as_vec()[1] - user = get_from_sql_table(sender) + user = get_from_sql_table(self.dvm_config.DB, sender) if not user.iswhitelisted: amount = int(user.balance) + get_amount_per_task(task, self.dvm_config) - update_sql_table(sender, amount, user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16, + update_sql_table(self.dvm_config.DB, sender, amount, user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16, user.name, Timestamp.now().as_secs()) message = "There was the following error : " + content + ". Credits have been reimbursed" @@ -378,7 +384,7 @@ class DVM: tags = [e_tag, p_tag, alt_tag, status_tag] if status == "success" or status == "error": # - for x in job_list: + for x in self.job_list: if x.event_id == original_event.id(): is_paid = x.is_paid amount = x.amount @@ -394,8 +400,8 @@ class DVM: except Exception as e: print(e) - if not any(x.event_id == original_event.id().to_hex() for x in job_list): - job_list.append( + if not any(x.event_id == original_event.id().to_hex() for x in self.job_list): + self.job_list.append( JobToWatch(event_id=original_event.id().to_hex(), timestamp=original_event.created_at().as_secs(), amount=amount, @@ -403,7 +409,7 @@ class DVM: status=status, result="", is_processed=False, bolt11=bolt11, payment_hash=payment_hash, expires=expires, from_bot=False)) - print(str(job_list)) + print(str(self.job_list)) if status == "payment-required" or status == "payment-rejected" or ( status == "processing" and not is_paid) or ( status == "success" and not is_paid): @@ -448,7 +454,7 @@ class DVM: self.client.handle_notifications(NotificationHandler()) while True: - for job in job_list: + for job in self.job_list: if job.bolt11 != "" and job.payment_hash != "" and not job.is_paid: if str(check_bolt11_ln_bits_is_paid(job.payment_hash, self.dvm_config)) == "True": job.is_paid = True @@ -462,26 +468,26 @@ class DVM: do_work(event, is_from_bot=False) elif check_bolt11_ln_bits_is_paid(job.payment_hash, self.dvm_config) is None: # invoice expired try: - job_list.remove(job) + self.job_list.remove(job) except: continue if Timestamp.now().as_secs() > job.expires: try: - job_list.remove(job) + self.job_list.remove(job) except: continue - for job in jobs_on_hold_list: + for job in self.jobs_on_hold_list: if check_event_has_not_unfinished_job_input(job.event, False, client=self.client, dvmconfig=self.dvm_config): handle_nip90_job_event(nip90_event=job.event) try: - jobs_on_hold_list.remove(job) + self.jobs_on_hold_list.remove(job) except: continue if Timestamp.now().as_secs() > job.timestamp + 60 * 20: # remove jobs to look for after 20 minutes.. - jobs_on_hold_list.remove(job) + self.jobs_on_hold_list.remove(job) time.sleep(2.0) diff --git a/main.py b/main.py index eda1580..7e65190 100644 --- a/main.py +++ b/main.py @@ -1,6 +1,5 @@ import os from pathlib import Path -from threading import Thread import dotenv import utils.env as env @@ -10,7 +9,7 @@ from tasks.translation import Translation def run_nostr_dvm_with_local_config(): - from dvm import DVM, DVMConfig + from utils.dvmconfig import DVMConfig # Spawn the DVMs # Add NIP89 events for each DVM (set rebroadcast = True for the next start in admin_utils) @@ -67,12 +66,12 @@ def run_nostr_dvm_with_local_config(): dvm_config.LNBITS_INVOICE_KEY = os.getenv(env.LNBITS_INVOICE_KEY) dvm_config.LNBITS_URL = os.getenv(env.LNBITS_HOST) - #unstableartist = ImageGenerationSDXL("Unstable Diffusion", dvm_config, "unstable") - #d_tag = os.getenv(env.TASK_IMAGEGENERATION_NIP89_DTAG) - #content = "{\"name\":\"" + unstableartist.NAME + ("\",\"image\":\"https://image.nostr.build" - # "/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg" - # "\",\"about\":\"I draw images based on a prompt with a Model called unstable diffusion.\",\"nip90Params\":{}}") - #dvm_config.NIP89s.append(unstableartist.NIP89_announcement(d_tag, content)) + unstableartist = ImageGenerationSDXL("Unstable Diffusion", dvm_config, "unstable") + d_tag = os.getenv(env.TASK_IMAGEGENERATION_NIP89_DTAG) + content = "{\"name\":\"" + unstableartist.NAME + ("\",\"image\":\"https://image.nostr.build" + "/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg" + "\",\"about\":\"I draw images based on a prompt with a Model called unstable diffusion.\",\"nip90Params\":{}}") + dvm_config.NIP89s.append(unstableartist.NIP89_announcement(d_tag, content)) # Spawn another Instance of text-to-image but use a different model and lora this time. dvm_config = DVMConfig() diff --git a/tasks/imagegenerationsdxl.py b/tasks/imagegenerationsdxl.py index e9fe8c6..24bb134 100644 --- a/tasks/imagegenerationsdxl.py +++ b/tasks/imagegenerationsdxl.py @@ -6,7 +6,6 @@ from backends.nova_server import check_nova_server_status, send_request_to_nova_ from dvm import DVM from interfaces.dvmtaskinterface import DVMTaskInterface from utils.definitions import EventDefinitions -from utils.nip89_utils import NIP89Announcement """ @@ -27,6 +26,7 @@ class ImageGenerationSDXL(DVMTaskInterface): def __init__(self, name, dvm_config, default_model=None, default_lora=None): self.NAME = name dvm_config.SUPPORTED_TASKS = [self] + dvm_config.DB = "db/" + self.NAME + ".db" self.PK = dvm_config.PRIVATE_KEY self.default_model = default_model self.default_lora = default_lora diff --git a/tasks/textextractionpdf.py b/tasks/textextractionpdf.py index 598fcbd..27ddc45 100644 --- a/tasks/textextractionpdf.py +++ b/tasks/textextractionpdf.py @@ -5,7 +5,6 @@ from threading import Thread from dvm import DVM from interfaces.dvmtaskinterface import DVMTaskInterface from utils.definitions import EventDefinitions -from utils.nip89_utils import NIP89Announcement from utils.nostr_utils import get_event_by_id """ @@ -14,6 +13,8 @@ This File contains a Module to extract Text from a PDF file locally on the DVM M Accepted Inputs: Url to pdf file, Event containing an URL to a PDF file Outputs: Text containing the extracted contents of the PDF file """ + + class TextExtractionPDF(DVMTaskInterface): NAME: str KIND: int = EventDefinitions.KIND_NIP90_EXTRACT_TEXT @@ -21,18 +22,16 @@ class TextExtractionPDF(DVMTaskInterface): COST: int = 20 PK: str - def __init__(self, name, dvm_config): self.NAME = name dvm_config.SUPPORTED_TASKS = [self] + dvm_config.DB = "db/" + self.NAME + ".db" self.PK = dvm_config.PRIVATE_KEY dvm = DVM nostr_dvm_thread = Thread(target=dvm, args=[dvm_config]) nostr_dvm_thread.start() - - def is_input_supported(self, input_type, input_content): if input_type != "url" and input_type != "event": return False diff --git a/tasks/translation.py b/tasks/translation.py index 912bd0b..3e25722 100644 --- a/tasks/translation.py +++ b/tasks/translation.py @@ -1,13 +1,10 @@ -import os from threading import Thread from dvm import DVM from interfaces.dvmtaskinterface import DVMTaskInterface from utils.definitions import EventDefinitions -from utils.nip89_utils import NIP89Announcement from utils.nostr_utils import get_referenced_event_by_id, get_event_by_id - """ This File contains a Module to call Google Translate Services locally on the DVM Machine @@ -26,6 +23,7 @@ class Translation(DVMTaskInterface): def __init__(self, name, dvm_config): self.NAME = name dvm_config.SUPPORTED_TASKS = [self] + dvm_config.DB = "db/" + self.NAME + ".db" self.PK = dvm_config.PRIVATE_KEY dvm = DVM diff --git a/utils/admin_utils.py b/utils/admin_utils.py index f7429c1..feea351 100644 --- a/utils/admin_utils.py +++ b/utils/admin_utils.py @@ -8,10 +8,13 @@ from utils.database_utils import get_from_sql_table, list_db, delete_from_sql_ta from utils.nip89_utils import nip89_announce_tasks from utils.nostr_utils import send_event +class AdminConfig: + REBROADCASTNIP89: bool = False def admin_make_database_updates(config=None, client=None): # This is called on start of Server, Admin function to manually whitelist/blacklist/add balance/delete users dvmconfig = config + db = config.DB rebroadcast_nip89 = False cleandb = False @@ -28,27 +31,27 @@ def admin_make_database_updates(config=None, client=None): #use this if you have hex if whitelistuser: - user = get_or_add_user(publickey) - update_sql_table(user.npub, user.balance, True, False, user.nip05, user.lud16, user.name, user.lastactive) - user = get_from_sql_table(publickey) + user = get_or_add_user(db, publickey) + update_sql_table(db, user.npub, user.balance, True, False, user.nip05, user.lud16, user.name, user.lastactive) + user = get_from_sql_table(db, publickey) print(str(user.name) + " is whitelisted: " + str(user.iswhitelisted)) if unwhitelistuser: - user = get_from_sql_table(publickey) - update_sql_table(user.npub, user.balance, False, False, user.nip05, user.lud16, user.name, user.lastactive) + user = get_from_sql_table(db, publickey) + update_sql_table(db, user.npub, user.balance, False, False, user.nip05, user.lud16, user.name, user.lastactive) if blacklistuser: - user = get_from_sql_table(publickey) - update_sql_table(user.npub, user.balance, False, True, user.nip05, user.lud16, user.name, user.lastactive) + user = get_from_sql_table(db, publickey) + update_sql_table(db, user.npub, user.balance, False, True, user.nip05, user.lud16, user.name, user.lastactive) if deleteuser: - delete_from_sql_table(publickey) + delete_from_sql_table(db, publickey) if cleandb: - clean_db() + clean_db(db) if listdatabase: - list_db() + list_db(db) if rebroadcast_nip89: nip89_announce_tasks(dvmconfig) diff --git a/utils/database_utils.py b/utils/database_utils.py index 856dcec..2bab825 100644 --- a/utils/database_utils.py +++ b/utils/database_utils.py @@ -1,5 +1,4 @@ # DATABASE LOGIC -import os import sqlite3 import time @@ -10,10 +9,10 @@ from logging import Filter from nostr_sdk import Timestamp, Keys, PublicKey, EventBuilder, Metadata, Filter -from utils import env from utils.definitions import NEW_USER_BALANCE from utils.nostr_utils import send_event + @dataclass class User: npub: str @@ -26,10 +25,9 @@ class User: lastactive: int - -def create_sql_table(): +def create_sql_table(db): try: - con = sqlite3.connect(os.getenv(env.USER_DB_PATH)) + con = sqlite3.connect(db) cur = con.cursor() cur.execute(""" CREATE TABLE IF NOT EXISTS users ( npub text PRIMARY KEY, @@ -48,9 +46,9 @@ def create_sql_table(): print(e) -def add_sql_table_column(): +def add_sql_table_column(db): try: - con = sqlite3.connect(os.getenv(env.USER_DB_PATH)) + con = sqlite3.connect(db) cur = con.cursor() cur.execute(""" ALTER TABLE users ADD COLUMN lastactive 'integer' """) con.close() @@ -58,9 +56,11 @@ def add_sql_table_column(): print(e) -def add_to_sql_table(npub, sats, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive): +def add_to_sql_table(db, npub, sats, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive): try: - con = sqlite3.connect(os.getenv(env.USER_DB_PATH)) + print("ADD: " + str(db)) + con = sqlite3.connect(db) + print("Connected") cur = con.cursor() data = (npub, sats, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive) cur.execute("INSERT or IGNORE INTO users VALUES(?, ?, ?, ?, ?, ?, ?, ?)", data) @@ -70,9 +70,9 @@ def add_to_sql_table(npub, sats, iswhitelisted, isblacklisted, nip05, lud16, nam print(e) -def update_sql_table(npub, sats, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive): +def update_sql_table(db, npub, sats, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive): try: - con = sqlite3.connect(os.getenv(env.USER_DB_PATH)) + con = sqlite3.connect(db) cur = con.cursor() data = (sats, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive, npub) @@ -91,32 +91,39 @@ def update_sql_table(npub, sats, iswhitelisted, isblacklisted, nip05, lud16, nam print(e) -def get_from_sql_table(npub): +def get_from_sql_table(db, npub): try: - con = sqlite3.connect(os.getenv(env.USER_DB_PATH)) + con = sqlite3.connect(db) + print("Connecting to DB") cur = con.cursor() cur.execute("SELECT * FROM users WHERE npub=?", (npub,)) row = cur.fetchone() con.close() - user = User - user.npub = row[0] - user.balance = row[1] - user.iswhitelisted = row[2] - user.isblacklisted = row[3] - user.nip05 = row[4] - user.lud16 = row[5] - user.name = row[6] - user.lastactive = row[7] + print(row) + if row is None: + user = None + print("returning None") + return user + else: + user = User + user.npub = row[0] + user.balance = row[1] + user.iswhitelisted = row[2] + user.isblacklisted = row[3] + user.nip05 = row[4] + user.lud16 = row[5] + user.name = row[6] + user.lastactive = row[7] - return user + return user except Error as e: print(e) -def delete_from_sql_table(npub): +def delete_from_sql_table(db, npub): try: - con = sqlite3.connect(os.getenv(env.USER_DB_PATH)) + con = sqlite3.connect(db) cur = con.cursor() cur.execute("DELETE FROM users WHERE npub=?", (npub,)) con.commit() @@ -125,24 +132,24 @@ def delete_from_sql_table(npub): print(e) -def clean_db(): +def clean_db(db): try: - con = sqlite3.connect(os.getenv(env.USER_DB_PATH)) + con = sqlite3.connect(db) cur = con.cursor() cur.execute("SELECT * FROM users WHERE npub IS NULL OR npub = '' ") rows = cur.fetchall() for row in rows: print(row) - delete_from_sql_table(row[0]) + delete_from_sql_table(db, row[0]) con.close() return rows except Error as e: print(e) -def list_db(): +def list_db(db): try: - con = sqlite3.connect(os.getenv(env.USER_DB_PATH)) + con = sqlite3.connect(db) cur = con.cursor() cur.execute("SELECT * FROM users ORDER BY sats DESC") rows = cur.fetchall() @@ -153,17 +160,16 @@ def list_db(): print(e) -def update_user_balance(sender, sats, config=None): - user = get_from_sql_table(sender) +def update_user_balance(db, sender, sats, config=None): + user = get_from_sql_table(db, sender) if user is None: - add_to_sql_table(sender, (int(sats) + NEW_USER_BALANCE), False, False, + add_to_sql_table(db, sender, (int(sats) + NEW_USER_BALANCE), False, False, "", "", "", Timestamp.now().as_secs()) print("NEW USER: " + sender + " Zap amount: " + str(sats) + " Sats.") else: - user = get_from_sql_table(sender) + user = get_from_sql_table(db, sender) print(str(sats)) - if user.nip05 is None: user.nip05 = "" if user.lud16 is None: @@ -172,36 +178,36 @@ def update_user_balance(sender, sats, config=None): user.name = "" new_balance = int(user.balance) + int(sats) - update_sql_table(sender, new_balance, user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16, user.name, + update_sql_table(db, sender, new_balance, user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16, + user.name, Timestamp.now().as_secs()) print("UPDATE USER BALANCE: " + str(user.name) + " Zap amount: " + str(sats) + " Sats.") - if config is not None: keys = Keys.from_sk_str(config.PRIVATE_KEY) time.sleep(1.0) - message = ("Added "+ str(sats) + " Sats to balance. New balance is " + str(new_balance) + " Sats. " ) - + message = ("Added " + str(sats) + " Sats to balance. New balance is " + str(new_balance) + " Sats. ") evt = EventBuilder.new_encrypted_direct_msg(keys, PublicKey.from_hex(sender), message, None).to_event(keys) send_event(evt, key=keys) -def get_or_add_user(sender): - user = get_from_sql_table(sender) - +def get_or_add_user(db, sender): + user = get_from_sql_table(db, sender) if user is None: - add_to_sql_table(sender, NEW_USER_BALANCE, False, False, None, + print("Adding User") + add_to_sql_table(db, sender, NEW_USER_BALANCE, False, False, None, None, None, Timestamp.now().as_secs()) - user = get_from_sql_table(sender) + user = get_from_sql_table(db, sender) print(user) return user -def update_user_metadata(sender, client): - user = get_from_sql_table(sender) + +def update_user_metadata(db, sender, client): + user = get_from_sql_table(db, sender) try: profile_filter = Filter().kind(0).author(sender).limit(1) events = client.get_events_of([profile_filter], timedelta(seconds=3)) @@ -215,8 +221,7 @@ def update_user_metadata(sender, client): user.lud16 = metadata.get_lud16() except: print("Couldn't get meta information") - update_sql_table(user.npub, user.balance, user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16, + update_sql_table(db, user.npub, user.balance, user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16, user.name, Timestamp.now().as_secs()) - user = get_from_sql_table(user.npub) + user = get_from_sql_table(db, user.npub) return user - diff --git a/utils/definitions.py b/utils/definitions.py index 0344493..2616015 100644 --- a/utils/definitions.py +++ b/utils/definitions.py @@ -43,22 +43,6 @@ class EventDefinitions: KIND_NIP90_RESULT_GENERIC] -class DVMConfig: - SUPPORTED_TASKS = [] - PRIVATE_KEY: str = os.getenv(env.NOSTR_PRIVATE_KEY) - - RELAY_LIST = ["wss://relay.damus.io", "wss://nostr-pub.wellorder.net", "wss://nos.lol", "wss://nostr.wine", - "wss://relay.nostfiles.dev", "wss://nostr.mom", "wss://nostr.oxtr.dev", "wss://relay.nostr.bg", - "wss://relay.f7z.io"] - RELAY_TIMEOUT = 5 - LNBITS_INVOICE_KEY = '' - LNBITS_URL = 'https://lnbits.com' - REQUIRES_NIP05: bool = False - - SHOWRESULTBEFOREPAYMENT: bool = True # if this is true show results even when not paid right after autoprocess - - - NIP89s: list = [] @dataclass diff --git a/utils/dvmconfig.py b/utils/dvmconfig.py new file mode 100644 index 0000000..e5804b2 --- /dev/null +++ b/utils/dvmconfig.py @@ -0,0 +1,22 @@ +import os + +from utils import env + + +class DVMConfig: + SUPPORTED_TASKS = [] + PRIVATE_KEY: str = os.getenv(env.NOSTR_PRIVATE_KEY) + + RELAY_LIST = ["wss://relay.damus.io", "wss://nostr-pub.wellorder.net", "wss://nos.lol", "wss://nostr.wine", + "wss://relay.nostfiles.dev", "wss://nostr.mom", "wss://nostr.oxtr.dev", "wss://relay.nostr.bg", + "wss://relay.f7z.io"] + RELAY_TIMEOUT = 5 + LNBITS_INVOICE_KEY = '' + LNBITS_URL = 'https://lnbits.com' + REQUIRES_NIP05: bool = False + DB: str + + SHOWRESULTBEFOREPAYMENT: bool = True # if this is true show results even when not paid right after autoprocess + + + NIP89s: list = [] From 27aa132ec945005d0e30ba788d71a86662277f7b Mon Sep 17 00:00:00 2001 From: Believethehype Date: Tue, 21 Nov 2023 11:11:12 +0100 Subject: [PATCH 06/18] add adminconfig per dvm --- dvm.py | 12 +++++---- interfaces/dvmtaskinterface.py | 2 ++ main.py | 28 ++++++++++++-------- tasks/imagegenerationsdxl.py | 7 ++--- tasks/textextractionpdf.py | 6 +++-- tasks/translation.py | 6 +++-- utils/admin_utils.py | 47 ++++++++++++++++++++++++---------- utils/dvmconfig.py | 6 +++-- utils/nip89_utils.py | 17 ++++++------ 9 files changed, 83 insertions(+), 48 deletions(-) diff --git a/dvm.py b/dvm.py index 9c28ac2..91071f9 100644 --- a/dvm.py +++ b/dvm.py @@ -5,7 +5,7 @@ import emoji from utils.definitions import EventDefinitions, RequiredJobToWatch, JobToWatch from utils.dvmconfig import DVMConfig -from utils.admin_utils import admin_make_database_updates +from utils.admin_utils import admin_make_database_updates, AdminConfig from utils.backend_utils import get_amount_per_task, check_task_is_supported, get_task from utils.database_utils import update_sql_table, get_from_sql_table, \ create_sql_table, get_or_add_user, update_user_balance @@ -23,14 +23,16 @@ if use_logger: class DVM: dvm_config: DVMConfig + admin_config: AdminConfig keys: Keys client: Client job_list: list jobs_on_hold_list: list - def __init__(self, config): - self.dvm_config = config - self.keys = Keys.from_sk_str(config.PRIVATE_KEY) + def __init__(self, dvmconfig, adminconfig = None): + self.dvm_config = dvmconfig + self.admin_config = adminconfig + self.keys = Keys.from_sk_str(dvmconfig.PRIVATE_KEY) self.client = Client(self.keys) self.job_list = [] self.jobs_on_hold_list = [] @@ -54,7 +56,7 @@ class DVM: self.client.subscribe([dm_zap_filter, dvm_filter]) create_sql_table(self.dvm_config.DB) - admin_make_database_updates(config=self.dvm_config, client=self.client) + admin_make_database_updates(adminconfig=self.admin_config, dvmconfig=self.dvm_config, client=self.client) class NotificationHandler(HandleNotification): client = self.client diff --git a/interfaces/dvmtaskinterface.py b/interfaces/dvmtaskinterface.py index e48df54..e5c02db 100644 --- a/interfaces/dvmtaskinterface.py +++ b/interfaces/dvmtaskinterface.py @@ -2,6 +2,7 @@ from utils.nip89_utils import NIP89Announcement class DVMTaskInterface: + NAME: str KIND: int TASK: str COST: int @@ -9,6 +10,7 @@ class DVMTaskInterface: def NIP89_announcement(self, d_tag, content): nip89 = NIP89Announcement() + nip89.name = self.NAME nip89.kind = self.KIND nip89.pk = self.PK nip89.dtag = d_tag diff --git a/main.py b/main.py index 7e65190..864a803 100644 --- a/main.py +++ b/main.py @@ -6,17 +6,23 @@ import utils.env as env from tasks.imagegenerationsdxl import ImageGenerationSDXL from tasks.textextractionpdf import TextExtractionPDF from tasks.translation import Translation +from utils.admin_utils import AdminConfig +from utils.dvmconfig import DVMConfig def run_nostr_dvm_with_local_config(): - from utils.dvmconfig import DVMConfig + #Generate a optional Admin Config, in this case, whenever we give our DVMS this config, they will (re)broadcast + # their NIP89 announcement + admin_config = AdminConfig() + admin_config.REBROADCASTNIP89 = True # Spawn the DVMs - # Add NIP89 events for each DVM (set rebroadcast = True for the next start in admin_utils) + # Add NIP89 events for each DVM # Add the dtag here or in your .env file, so you can update your dvm later and change the content as needed. # Get a dtag and the content at vendata.io - # Spawn DVM1 Kind 5000 Text Ectractor from PDFs + # Spawn DVM1 Kind 5000 Text Extractor from PDFs + dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = os.getenv(env.NOSTR_PRIVATE_KEY) dvm_config.LNBITS_INVOICE_KEY = os.getenv(env.LNBITS_INVOICE_KEY) @@ -28,7 +34,7 @@ def run_nostr_dvm_with_local_config(): "/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669" ".jpg\",\"about\":\"I extract Text from pdf documents\"," "\"nip90Params\":{}}") - dvm_config.NIP89s.append(pdfextactor.NIP89_announcement(d_tag, content)) + dvm_config.NIP89 = pdfextactor.NIP89_announcement(d_tag, content) # Spawn DVM2 Kind 5002 Text Translation dvm_config = DVMConfig() @@ -37,7 +43,6 @@ def run_nostr_dvm_with_local_config(): dvm_config.LNBITS_URL = os.getenv(env.LNBITS_HOST) translator = Translation("Translator", dvm_config) - d_tag = os.getenv(env.TASK_TRANSLATION_NIP89_DTAG) content = "{\"name\":\"" + translator.NAME + ("\",\"image\":\"https://image.nostr.build" "/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669" @@ -57,7 +62,7 @@ def run_nostr_dvm_with_local_config(): "\"ta\",\"te\",\"tg\",\"th\",\"tl\",\"tr\",\"ug\",\"uk\",\"ur\"," "\"uz\",\"vi\",\"xh\",\"yi\",\"yo\",\"zh\",\"zu\"]}}}") - dvm_config.NIP89s.append(translator.NIP89_announcement(d_tag, content)) + dvm_config.NIP89 = translator.NIP89_announcement(d_tag, content) # Spawn DVM3 Kind 5100 Image Generation This one uses a specific backend called nova-server. If you want to use # it see the instructions in backends/nova_server @@ -65,25 +70,26 @@ def run_nostr_dvm_with_local_config(): dvm_config.PRIVATE_KEY = os.getenv(env.NOSTR_PRIVATE_KEY) dvm_config.LNBITS_INVOICE_KEY = os.getenv(env.LNBITS_INVOICE_KEY) dvm_config.LNBITS_URL = os.getenv(env.LNBITS_HOST) - unstableartist = ImageGenerationSDXL("Unstable Diffusion", dvm_config, "unstable") d_tag = os.getenv(env.TASK_IMAGEGENERATION_NIP89_DTAG) content = "{\"name\":\"" + unstableartist.NAME + ("\",\"image\":\"https://image.nostr.build" "/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg" "\",\"about\":\"I draw images based on a prompt with a Model called unstable diffusion.\",\"nip90Params\":{}}") - dvm_config.NIP89s.append(unstableartist.NIP89_announcement(d_tag, content)) + dvm_config.NIP89 = unstableartist.NIP89_announcement(d_tag, content) - # Spawn another Instance of text-to-image but use a different model and lora this time. + + # Spawn another Instance of text-to-image but use a different privatekey, model and lora this time. dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = "73b262d31edc6ea1316dffcc7daa772651d661e6475761b7b78291482c1bf5cb" dvm_config.LNBITS_INVOICE_KEY = os.getenv(env.LNBITS_INVOICE_KEY) dvm_config.LNBITS_URL = os.getenv(env.LNBITS_HOST) - sketcher = ImageGenerationSDXL("Sketcher", dvm_config, "mohawk", "timburton") + #We add an optional AdminConfig for this one, and tell the dvm to rebroadcast its NIP89 + sketcher = ImageGenerationSDXL("Sketcher", dvm_config, admin_config, default_model="mohawk", default_lora="timburton") d_tag = os.getenv(env.TASK_IMAGEGENERATION_NIP89_DTAG2) content = "{\"name\":\"" + sketcher.NAME + ("\",\"image\":\"https://image.nostr.build" "/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg" "\",\"about\":\"I draw images based on a prompt in kind of Tim Burton style\",\"nip90Params\":{}}") - dvm_config.NIP89s.append(sketcher.NIP89_announcement(d_tag, content)) + dvm_config.NIP89 = sketcher.NIP89_announcement(d_tag, content) if __name__ == '__main__': diff --git a/tasks/imagegenerationsdxl.py b/tasks/imagegenerationsdxl.py index 24bb134..1e5199a 100644 --- a/tasks/imagegenerationsdxl.py +++ b/tasks/imagegenerationsdxl.py @@ -5,8 +5,9 @@ from threading import Thread from backends.nova_server import check_nova_server_status, send_request_to_nova_server from dvm import DVM from interfaces.dvmtaskinterface import DVMTaskInterface +from utils.admin_utils import AdminConfig from utils.definitions import EventDefinitions - +from utils.dvmconfig import DVMConfig """ This File contains a Module to transform Text input on NOVA-Server and receive results back. @@ -23,7 +24,7 @@ class ImageGenerationSDXL(DVMTaskInterface): COST: int = 5 PK: str - def __init__(self, name, dvm_config, default_model=None, default_lora=None): + def __init__(self, name, dvm_config: DVMConfig, admin_config: AdminConfig = None, default_model=None, default_lora=None): self.NAME = name dvm_config.SUPPORTED_TASKS = [self] dvm_config.DB = "db/" + self.NAME + ".db" @@ -32,7 +33,7 @@ class ImageGenerationSDXL(DVMTaskInterface): self.default_lora = default_lora dvm = DVM - nostr_dvm_thread = Thread(target=dvm, args=[dvm_config]) + nostr_dvm_thread = Thread(target=dvm, args=[dvm_config, admin_config]) nostr_dvm_thread.start() diff --git a/tasks/textextractionpdf.py b/tasks/textextractionpdf.py index 27ddc45..ba5bd94 100644 --- a/tasks/textextractionpdf.py +++ b/tasks/textextractionpdf.py @@ -4,7 +4,9 @@ from threading import Thread from dvm import DVM from interfaces.dvmtaskinterface import DVMTaskInterface +from utils.admin_utils import AdminConfig from utils.definitions import EventDefinitions +from utils.dvmconfig import DVMConfig from utils.nostr_utils import get_event_by_id """ @@ -22,14 +24,14 @@ class TextExtractionPDF(DVMTaskInterface): COST: int = 20 PK: str - def __init__(self, name, dvm_config): + def __init__(self, name, dvm_config: DVMConfig, admin_config: AdminConfig = None): self.NAME = name dvm_config.SUPPORTED_TASKS = [self] dvm_config.DB = "db/" + self.NAME + ".db" self.PK = dvm_config.PRIVATE_KEY dvm = DVM - nostr_dvm_thread = Thread(target=dvm, args=[dvm_config]) + nostr_dvm_thread = Thread(target=dvm, args=[dvm_config, admin_config]) nostr_dvm_thread.start() def is_input_supported(self, input_type, input_content): diff --git a/tasks/translation.py b/tasks/translation.py index 3e25722..afe02d1 100644 --- a/tasks/translation.py +++ b/tasks/translation.py @@ -2,7 +2,9 @@ from threading import Thread from dvm import DVM from interfaces.dvmtaskinterface import DVMTaskInterface +from utils.admin_utils import AdminConfig from utils.definitions import EventDefinitions +from utils.dvmconfig import DVMConfig from utils.nostr_utils import get_referenced_event_by_id, get_event_by_id """ @@ -20,14 +22,14 @@ class Translation(DVMTaskInterface): COST: int = 0 PK: str - def __init__(self, name, dvm_config): + def __init__(self, name, dvm_config: DVMConfig, admin_config: AdminConfig = None): self.NAME = name dvm_config.SUPPORTED_TASKS = [self] dvm_config.DB = "db/" + self.NAME + ".db" self.PK = dvm_config.PRIVATE_KEY dvm = DVM - nostr_dvm_thread = Thread(target=dvm, args=[dvm_config]) + nostr_dvm_thread = Thread(target=dvm, args=[dvm_config, admin_config]) nostr_dvm_thread.start() def is_input_supported(self, input_type, input_content): diff --git a/utils/admin_utils.py b/utils/admin_utils.py index feea351..a178daa 100644 --- a/utils/admin_utils.py +++ b/utils/admin_utils.py @@ -5,30 +5,49 @@ from nostr_sdk import Keys, EventBuilder, PublicKey from utils.database_utils import get_from_sql_table, list_db, delete_from_sql_table, update_sql_table, \ get_or_add_user, clean_db +from utils.dvmconfig import DVMConfig from utils.nip89_utils import nip89_announce_tasks from utils.nostr_utils import send_event class AdminConfig: REBROADCASTNIP89: bool = False + WHITELISTUSER: bool = False + UNWHITELISTUSER: bool = False + BLACKLISTUSER: bool = False + DELETEUSER: bool = False + LISTDATABASE: bool = False + ClEANDB: bool = False + USERNPUB: str = "" -def admin_make_database_updates(config=None, client=None): +def admin_make_database_updates(adminconfig: AdminConfig = None, dvmconfig: DVMConfig = None, client=None): # This is called on start of Server, Admin function to manually whitelist/blacklist/add balance/delete users - dvmconfig = config - db = config.DB + if adminconfig is None or dvmconfig is None: + return - rebroadcast_nip89 = False - cleandb = False - listdatabase = False - deleteuser = False - whitelistuser = False - unwhitelistuser = False - blacklistuser = False + if not isinstance(adminconfig, AdminConfig): + return + + if ((adminconfig.WHITELISTUSER is True or adminconfig.UNWHITELISTUSER is True or adminconfig.BLACKLISTUSER is True or adminconfig.DELETEUSER is True) + and adminconfig.USERNPUB == ""): + return - # publickey = PublicKey.from_bech32("npub1...").to_hex() - # use this if you have the npub - publickey = "asd123" - #use this if you have hex + db = dvmconfig.DB + + rebroadcast_nip89 = adminconfig.REBROADCASTNIP89 + cleandb = adminconfig.ClEANDB + listdatabase = adminconfig.LISTDATABASE + deleteuser = adminconfig.DELETEUSER + whitelistuser = adminconfig.WHITELISTUSER + unwhitelistuser = adminconfig.UNWHITELISTUSER + blacklistuser = adminconfig.BLACKLISTUSER + + if adminconfig.USERNPUB != "": + if str(adminconfig.USERNPUB).startswith("npub"): + publickey = PublicKey.from_bech32(adminconfig.USERNPUB).to_hex() + else: + publickey = adminconfig.USERNPUB + if whitelistuser: user = get_or_add_user(db, publickey) diff --git a/utils/dvmconfig.py b/utils/dvmconfig.py index e5804b2..3de7079 100644 --- a/utils/dvmconfig.py +++ b/utils/dvmconfig.py @@ -1,6 +1,7 @@ import os from utils import env +from utils.nip89_utils import NIP89Announcement class DVMConfig: @@ -13,10 +14,11 @@ class DVMConfig: RELAY_TIMEOUT = 5 LNBITS_INVOICE_KEY = '' LNBITS_URL = 'https://lnbits.com' - REQUIRES_NIP05: bool = False DB: str + NIP89: NIP89Announcement + REQUIRES_NIP05: bool = False SHOWRESULTBEFOREPAYMENT: bool = True # if this is true show results even when not paid right after autoprocess - NIP89s: list = [] + diff --git a/utils/nip89_utils.py b/utils/nip89_utils.py index 220e53c..65be301 100644 --- a/utils/nip89_utils.py +++ b/utils/nip89_utils.py @@ -2,18 +2,17 @@ from nostr_sdk import Tag, Keys, EventBuilder from utils.nostr_utils import send_event class NIP89Announcement: + name: str kind: int dtag: str pk: str content: str def nip89_announce_tasks(dvmconfig): - for nip89 in dvmconfig.NIP89s: - k_tag = Tag.parse(["k", str(nip89.kind)]) - d_tag = Tag.parse(["d", nip89.dtag]) - keys = Keys.from_sk_str(nip89.pk) - content = nip89.content - event = EventBuilder(31990, content, [k_tag, d_tag]).to_event(keys) - send_event(event, key=keys) - - print("Announced NIP 89") \ No newline at end of file + k_tag = Tag.parse(["k", str(dvmconfig.NIP89.kind)]) + d_tag = Tag.parse(["d", dvmconfig.NIP89.dtag]) + keys = Keys.from_sk_str(dvmconfig.NIP89.pk) + content = dvmconfig.NIP89.content + event = EventBuilder(31990, content, [k_tag, d_tag]).to_event(keys) + send_event(event, key=keys) + print("Announced NIP 89 for " + dvmconfig.NIP89.name) \ No newline at end of file From 645fff6284a0e56fe731f7e23b632d5d27b611a6 Mon Sep 17 00:00:00 2001 From: Believethehype Date: Tue, 21 Nov 2023 11:39:36 +0100 Subject: [PATCH 07/18] updates --- README.md | 3 --- backends/nova_server.py | 9 +++++---- main.py | 32 +++++++++++++++++--------------- 3 files changed, 22 insertions(+), 22 deletions(-) diff --git a/README.md b/README.md index bad9941..9a6ba7e 100644 --- a/README.md +++ b/README.md @@ -13,9 +13,6 @@ A tutorial on how to add additional tasks, as well as the larger server backend Known Issues: - After refactoring DVMs work independent from each other for the most part. - - They currently still share a joblist and might act weird together (TODO rework joblist) - - - Probably therefore jobs might sometimes not start (need investigation) - Some functions might work easier than they did before (need some refactoring) - Bot currently not implemented - Some basic functionality is still missing, e.g. handling various mediasources diff --git a/backends/nova_server.py b/backends/nova_server.py index 46ce83d..7df6d8c 100644 --- a/backends/nova_server.py +++ b/backends/nova_server.py @@ -11,8 +11,9 @@ from utils.output_utils import uploadMediaToHoster """ This file contains basic calling functions for ML tasks that are outsourced to nova-server -(https://github.com/hcmlab/nova-server). nova-server is an Open-Source backend that enables running models locally, by -accepting a request form. Modules are deployed in in separate virtual environments so dependencies won't conflict. +(https://github.com/hcmlab/nova-server). nova-server is an Open-Source backend that enables running models locally + based on preefined modules (nova-server-modules), by accepting a request form. + Modules are deployed in in separate virtual environments so dependencies won't conflict. Setup nova-server: https://hcmlab.github.io/nova-server/docbuild/html/tutorials/introduction.html @@ -20,7 +21,7 @@ https://hcmlab.github.io/nova-server/docbuild/html/tutorials/introduction.html """ """ -send_request_to_nova_server(request_form) +send_request_to_nova_server(request_form, address) Function to send a request_form to the server, containing all the information we parsed from the Nostr event and added in the module that is calling the server @@ -36,7 +37,7 @@ def send_request_to_nova_server(request_form, address): """ -check_nova_server_status(request_form) +check_nova_server_status(request_form, address) Function that requests the status of the current process with the jobID (we use the Nostr event as jobID). When the Job is successfully finished we grab the result and depending on the type return the output We throw an exception on error diff --git a/main.py b/main.py index 864a803..9be20b2 100644 --- a/main.py +++ b/main.py @@ -30,10 +30,10 @@ def run_nostr_dvm_with_local_config(): pdfextactor = TextExtractionPDF("PDF Extractor", dvm_config) d_tag = os.getenv(env.TASK_TEXTEXTRACTION_NIP89_DTAG) - content = "{\"name\":\"" + pdfextactor.NAME + ("\",\"image\":\"https://image.nostr.build" - "/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669" - ".jpg\",\"about\":\"I extract Text from pdf documents\"," - "\"nip90Params\":{}}") + content = ("{\"name\":\"" + pdfextactor.NAME + "\"," + "\"image\":\"https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg\"," + "\"about\":\"I extract Text from pdf documents\"," + "\"nip90Params\":{}}") dvm_config.NIP89 = pdfextactor.NIP89_announcement(d_tag, content) # Spawn DVM2 Kind 5002 Text Translation @@ -44,11 +44,10 @@ def run_nostr_dvm_with_local_config(): translator = Translation("Translator", dvm_config) d_tag = os.getenv(env.TASK_TRANSLATION_NIP89_DTAG) - content = "{\"name\":\"" + translator.NAME + ("\",\"image\":\"https://image.nostr.build" - "/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669" - ".jpg\",\"about\":\"I translate text from given text/event/job, " - "currently using Google Translation Services into language defined " - "in param. \",\"nip90Params\":{\"language\":{\"required\":true," + content = ("{\"name\":\"" + translator.NAME + "\"," + "\"image\":\"https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg\"," + "\"about\":\"I translate text from given text/event/job, ""currently using Google Translation Services into language defined in params.\"," + "\"nip90Params\":{\"language\":{\"required\":true," "\"values\":[\"af\",\"am\",\"ar\",\"az\",\"be\",\"bg\",\"bn\"," "\"bs\",\"ca\",\"ceb\",\"co\",\"cs\",\"cy\",\"da\",\"de\",\"el\"," "\"eo\",\"es\",\"et\",\"eu\",\"fa\",\"fi\",\"fr\",\"fy\",\"ga\"," @@ -72,9 +71,10 @@ def run_nostr_dvm_with_local_config(): dvm_config.LNBITS_URL = os.getenv(env.LNBITS_HOST) unstableartist = ImageGenerationSDXL("Unstable Diffusion", dvm_config, "unstable") d_tag = os.getenv(env.TASK_IMAGEGENERATION_NIP89_DTAG) - content = "{\"name\":\"" + unstableartist.NAME + ("\",\"image\":\"https://image.nostr.build" - "/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg" - "\",\"about\":\"I draw images based on a prompt with a Model called unstable diffusion.\",\"nip90Params\":{}}") + content = ("{\"name\":\"" + unstableartist.NAME + "\"," + "\"image\":\"https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg\"," + "\"about\":\"I draw images based on a prompt with a Model called unstable diffusion.\"," + "\"nip90Params\":{}}") dvm_config.NIP89 = unstableartist.NIP89_announcement(d_tag, content) @@ -86,9 +86,11 @@ def run_nostr_dvm_with_local_config(): #We add an optional AdminConfig for this one, and tell the dvm to rebroadcast its NIP89 sketcher = ImageGenerationSDXL("Sketcher", dvm_config, admin_config, default_model="mohawk", default_lora="timburton") d_tag = os.getenv(env.TASK_IMAGEGENERATION_NIP89_DTAG2) - content = "{\"name\":\"" + sketcher.NAME + ("\",\"image\":\"https://image.nostr.build" - "/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg" - "\",\"about\":\"I draw images based on a prompt in kind of Tim Burton style\",\"nip90Params\":{}}") + content = ("{\"name\":\"" + unstableartist.NAME + "\"," + "\"image\":\"https://image.nostr.build/229c14e440895da30de77b3ca145d66d4b04efb4027ba3c44ca147eecde891f1.jpg\"," + "\"about\":\"I draw images based on a prompt with a Model called unstable diffusion.\"," + "\"nip90Params\":{}}") + dvm_config.NIP89 = sketcher.NIP89_announcement(d_tag, content) From 7abea2dbcd6928f60797418f114144c0216f5387 Mon Sep 17 00:00:00 2001 From: Believethehype Date: Tue, 21 Nov 2023 14:22:34 +0100 Subject: [PATCH 08/18] updates --- dvm.py | 23 ++++++++++------------- tasks/imagegenerationsdxl.py | 12 +++++++++++- tasks/textextractionpdf.py | 2 +- tasks/translation.py | 2 +- test_client.py | 6 ++---- utils/backend_utils.py | 23 ++++++++++------------- utils/database_utils.py | 5 ----- utils/nostr_utils.py | 2 ++ 8 files changed, 37 insertions(+), 38 deletions(-) diff --git a/dvm.py b/dvm.py index 91071f9..f93f905 100644 --- a/dvm.py +++ b/dvm.py @@ -64,8 +64,8 @@ class DVM: keys = self.keys def handle(self, relay_url, nostr_event): - print(f"[Nostr] Received new NIP90 Job Request from {relay_url}: {nostr_event.as_json()}") if EventDefinitions.KIND_NIP90_EXTRACT_TEXT <= nostr_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC: + print("[" + self.dvm_config.NIP89.name + "] " + f"Received new NIP90 Job Request from {relay_url}: {nostr_event.as_json()}") handle_nip90_job_event(nostr_event) elif nostr_event.kind() == EventDefinitions.KIND_ZAP: handle_zap(nostr_event) @@ -74,9 +74,7 @@ class DVM: return def handle_nip90_job_event(nip90_event): - print(str(self.dvm_config.DB)) user = get_or_add_user(self.dvm_config.DB, nip90_event.pubkey().to_hex()) - print("got user") task_supported, task, duration = check_task_is_supported(nip90_event, client=self.client, get_duration=(not user.iswhitelisted), config=self.dvm_config) @@ -84,7 +82,7 @@ class DVM: if user.isblacklisted: send_job_status_reaction(nip90_event, "error", client=self.client, config=self.dvm_config) - print("[Nostr] Request by blacklisted user, skipped") + print("[" + self.dvm_config.NIP89.name + "] Request by blacklisted user, skipped") elif task_supported: print("Received new Task: " + task) @@ -98,7 +96,7 @@ class DVM: task_is_free = True if user.iswhitelisted or task_is_free: - print("[Nostr] Free or Whitelisted for task " + task + ". Starting processing..") + print("[" + self.dvm_config.NIP89.name + "] Free or Whitelisted for task " + task + ". Starting processing..") send_job_status_reaction(nip90_event, "processing", True, 0, client=self.client, config=self.dvm_config) do_work(nip90_event, is_from_bot=False) @@ -109,7 +107,7 @@ class DVM: if tag.as_vec()[0] == 'bid': bid = int(tag.as_vec()[1]) - print("[Nostr][Payment required] New Nostr " + task + " Job event: " + nip90_event.as_json()) + print("[" + self.dvm_config.NIP89.name + "] Payment required: New Nostr " + task + " Job event: " + nip90_event.as_json()) if bid > 0: bid_offer = int(bid / 1000) if bid_offer >= amount: @@ -118,7 +116,7 @@ class DVM: client=self.client, config=self.dvm_config) else: # If there is no bid, just request server rate from user - print("[Nostr] Requesting payment for Event: " + nip90_event.id().to_hex()) + print("[" + self.dvm_config.NIP89.name + "] Requesting payment for Event: " + nip90_event.id().to_hex()) send_job_status_reaction(nip90_event, "payment-required", False, amount, client=self.client, config=self.dvm_config) else: @@ -177,7 +175,7 @@ class DVM: config=self.dvm_config) if job_event is not None and task_supported: if amount <= invoice_amount: - print("[Nostr] Payment-request fulfilled...") + print("[" + self.dvm_config.NIP89.name + "] Payment-request fulfilled...") send_job_status_reaction(job_event, "processing", client=self.client, config=self.dvm_config) indices = [i for i, x in enumerate(self.job_list) if @@ -204,7 +202,7 @@ class DVM: send_job_status_reaction(job_event, "payment-rejected", False, invoice_amount, client=self.client, config=self.dvm_config) - print("[Nostr] Invoice was not paid sufficiently") + print("[" + self.dvm_config.NIP89.name + "] Invoice was not paid sufficiently") elif zapped_event.kind() in EventDefinitions.ANY_RESULT: print("Someone zapped the result of an exisiting Task. Nice") @@ -302,7 +300,7 @@ class DVM: response_kind = originalevent.kind() + 1000 event = EventBuilder(response_kind, str(content), replytags).to_event(key) send_event(event, key=key) - print("[Nostr] " + str(response_kind) + " Job Response event sent: " + event.as_json()) + print("[" + self.dvm_config.NIP89.name + "] " + str(response_kind) + " Job Response event sent: " + event.as_json()) return event.as_json() def respond_to_error(content, originaleventstr, is_from_bot=False, dvm_key=None): @@ -411,7 +409,7 @@ class DVM: status=status, result="", is_processed=False, bolt11=bolt11, payment_hash=payment_hash, expires=expires, from_bot=False)) - print(str(self.job_list)) + #print(str(self.job_list)) if status == "payment-required" or status == "payment-rejected" or ( status == "processing" and not is_paid) or ( status == "success" and not is_paid): @@ -428,8 +426,7 @@ class DVM: event = EventBuilder(EventDefinitions.KIND_FEEDBACK, reaction, tags).to_event(keys) send_event(event, key=keys) - print( - "[Nostr] Sent Kind " + str( + print("[" + self.dvm_config.NIP89.name + "]" + ": Sent Kind " + str( EventDefinitions.KIND_FEEDBACK) + " Reaction: " + status + " " + event.as_json()) return event.as_json() diff --git a/tasks/imagegenerationsdxl.py b/tasks/imagegenerationsdxl.py index 1e5199a..26feb27 100644 --- a/tasks/imagegenerationsdxl.py +++ b/tasks/imagegenerationsdxl.py @@ -18,7 +18,7 @@ Outputs: An url to an Image class ImageGenerationSDXL(DVMTaskInterface): - NAME: str + NAME: str = "" KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE TASK: str = "text-to-image" COST: int = 5 @@ -109,6 +109,16 @@ class ImageGenerationSDXL(DVMTaskInterface): prompt = prompt.replace(";", ",") request_form['data'] = '[{"id":"input_prompt","type":"input","src":"request:text","data":"' + prompt + '","active":"True"},{"id":"negative_prompt","type":"input","src":"request:text","data":"' + negative_prompt + '","active":"True"},{"id":"output_image","type":"output","src":"request:image","active":"True"}]' + request_form['options'] = ('[{"model":" + ' + model + + '","ratio":"' + str(ratio_width) + ':' + str(ratio_height) + + '","width":"' + str(width) + ':' + str(height) + + '","strength":"' + str(strength) + + '","guidance_scale":"' + str(guidance_scale) + + '","lora":"' + str(lora) + + '","lora_weight":"' + str(lora_weight) + + '"}]') + + request_form["optStr"] = ('model=' + model + ';ratio=' + str(ratio_width) + '-' + str(ratio_height) + ';size=' + str(width) + '-' + str(height) + ';strength=' + str(strength) + ';guidance_scale=' + str(guidance_scale) + ';lora=' + lora + ';lora_weight=' + lora_weight) diff --git a/tasks/textextractionpdf.py b/tasks/textextractionpdf.py index ba5bd94..c916590 100644 --- a/tasks/textextractionpdf.py +++ b/tasks/textextractionpdf.py @@ -18,7 +18,7 @@ Outputs: Text containing the extracted contents of the PDF file class TextExtractionPDF(DVMTaskInterface): - NAME: str + NAME: str = "" KIND: int = EventDefinitions.KIND_NIP90_EXTRACT_TEXT TASK: str = "pdf-to-text" COST: int = 20 diff --git a/tasks/translation.py b/tasks/translation.py index afe02d1..db48b77 100644 --- a/tasks/translation.py +++ b/tasks/translation.py @@ -16,7 +16,7 @@ Outputs: Text containing the Translation in the desired language. class Translation(DVMTaskInterface): - NAME: str + NAME: str = "" KIND: int = EventDefinitions.KIND_NIP90_TRANSLATE_TEXT TASK: str = "translation" COST: int = 0 diff --git a/test_client.py b/test_client.py index 29f3d97..d9903d8 100644 --- a/test_client.py +++ b/test_client.py @@ -83,10 +83,8 @@ def nostr_client(): client.subscribe([dm_zap_filter, dvm_filter]) # nostr_client_test_translation("This is the result of the DVM in spanish", "text", "es", 20, 20) - #nostr_client_test_translation("44a0a8b395ade39d46b9d20038b3f0c8a11168e67c442e3ece95e4a1703e2beb", "event", "es", 20, - # 20) - - # nostr_client_test_translation("9c5d6d054e1b7a34a6a4b26ac59469c96e77f7cba003a30456fa6a57974ea86d", "event", "zh", 20, 20) + #nostr_client_test_translation("note1p8cx2dz5ss5gnk7c59zjydcncx6a754c0hsyakjvnw8xwlm5hymsnc23rs", "event", "es", 20,20) + #nostr_client_test_translation("44a0a8b395ade39d46b9d20038b3f0c8a11168e67c442e3ece95e4a1703e2beb", "event", "zh", 20, 20) nostr_client_test_image("a beautiful purple ostrich watching the sunset") class NotificationHandler(HandleNotification): diff --git a/utils/backend_utils.py b/utils/backend_utils.py index b56fdde..08b19d3 100644 --- a/utils/backend_utils.py +++ b/utils/backend_utils.py @@ -55,9 +55,7 @@ def check_task_is_supported(event, client, get_duration=False, config=None): input_value = "" input_type = "" duration = 1 - - - + task = get_task(event, client=client, dvmconfig=dvm_config) for tag in event.tags(): if tag.as_vec()[0] == 'i': if len(tag.as_vec()) < 3: @@ -71,29 +69,28 @@ def check_task_is_supported(event, client, get_duration=False, config=None): if evt is None: print("Event not found") return False, "", 0 + elif input_type == 'url' and check_url_is_readable(input_value) is None: + print("Url not readable / supported") + return False, task, duration elif tag.as_vec()[0] == 'output': output = tag.as_vec()[1] - if not ( - output == "text/plain" or output == "text/json" or output == "json" or output == "image/png" or "image/jpg" or output == ""): + if not (output == "text/plain" + or output == "text/json" or output == "json" + or output == "image/png" or "image/jpg" + or output == "image/png;format=url" or output == "image/jpg;format=url" + or output == ""): print("Output format not supported, skipping..") return False, "", 0 - task = get_task(event, client=client, dvmconfig=dvm_config) for dvm in dvm_config.SUPPORTED_TASKS: if dvm.TASK == task: if not dvm.is_input_supported(input_type, event.content()): return False, task, duration - if input_type == 'url' and check_url_is_readable(input_value) is None: - print("url not readable") - return False, task, duration - if task not in (x.TASK for x in dvm_config.SUPPORTED_TASKS): return False, task, duration - - return True, task, duration @@ -121,7 +118,7 @@ def check_url_is_readable(url): def get_amount_per_task(task, dvm_config, duration=1): - for dvm in dvm_config.SUPPORTED_TASKS: + for dvm in dvm_config.SUPPORTED_TASKS: #this is currently just one if dvm.TASK == task: amount = dvm.COST * duration return amount diff --git a/utils/database_utils.py b/utils/database_utils.py index 2bab825..8562de0 100644 --- a/utils/database_utils.py +++ b/utils/database_utils.py @@ -58,9 +58,7 @@ def add_sql_table_column(db): def add_to_sql_table(db, npub, sats, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive): try: - print("ADD: " + str(db)) con = sqlite3.connect(db) - print("Connected") cur = con.cursor() data = (npub, sats, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive) cur.execute("INSERT or IGNORE INTO users VALUES(?, ?, ?, ?, ?, ?, ?, ?)", data) @@ -94,15 +92,12 @@ def update_sql_table(db, npub, sats, iswhitelisted, isblacklisted, nip05, lud16, def get_from_sql_table(db, npub): try: con = sqlite3.connect(db) - print("Connecting to DB") cur = con.cursor() cur.execute("SELECT * FROM users WHERE npub=?", (npub,)) row = cur.fetchone() con.close() - print(row) if row is None: user = None - print("returning None") return user else: user = User diff --git a/utils/nostr_utils.py b/utils/nostr_utils.py index 86e6a2a..df7da0f 100644 --- a/utils/nostr_utils.py +++ b/utils/nostr_utils.py @@ -19,6 +19,8 @@ def get_event_by_id(event_id, client=None, config=None): id_filter = Filter().author(split[1]).custom_tag(Alphabet.D, [split[2]]) events = client.get_events_of([id_filter], timedelta(seconds=config.RELAY_TIMEOUT)) else: + if str(event_id).startswith('note'): + event_id = EventId.from_bech32(event_id).to_hex() id_filter = Filter().id(event_id).limit(1) events = client.get_events_of([id_filter], timedelta(seconds=config.RELAY_TIMEOUT)) if is_new_client: From 1735a45513c3aed735f9f2c0ebe29a196c2105db Mon Sep 17 00:00:00 2001 From: Believethehype Date: Tue, 21 Nov 2023 14:29:16 +0100 Subject: [PATCH 09/18] Update dvm.py --- dvm.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dvm.py b/dvm.py index f93f905..70b8579 100644 --- a/dvm.py +++ b/dvm.py @@ -300,7 +300,7 @@ class DVM: response_kind = originalevent.kind() + 1000 event = EventBuilder(response_kind, str(content), replytags).to_event(key) send_event(event, key=key) - print("[" + self.dvm_config.NIP89.name + "] " + str(response_kind) + " Job Response event sent: " + event.as_json()) + print("[" + self.dvm_config.NIP89.name + "]" + str(response_kind) + " Job Response event sent: " + event.as_json()) return event.as_json() def respond_to_error(content, originaleventstr, is_from_bot=False, dvm_key=None): From 228670145375e2e6e77627be2fa3af7d3a529a2d Mon Sep 17 00:00:00 2001 From: Believethehype Date: Tue, 21 Nov 2023 23:51:48 +0100 Subject: [PATCH 10/18] some more cleanup, preparations to update nostr sdk --- backends/nova_server.py | 9 +- dvm.py | 192 ++++++++++++++--------------------- tasks/imagegenerationsdxl.py | 4 +- tasks/textextractionpdf.py | 2 +- tasks/translation.py | 13 ++- test_client.py | 7 +- utils/admin_utils.py | 2 +- utils/backend_utils.py | 4 +- utils/database_utils.py | 4 +- utils/definitions.py | 4 +- utils/dvmconfig.py | 1 + utils/nip89_utils.py | 17 ++-- utils/nostr_utils.py | 52 ++-------- utils/output_utils.py | 41 +++++++- utils/zap_utils.py | 29 +++--- 15 files changed, 173 insertions(+), 208 deletions(-) diff --git a/backends/nova_server.py b/backends/nova_server.py index 7df6d8c..ce34f4c 100644 --- a/backends/nova_server.py +++ b/backends/nova_server.py @@ -7,7 +7,7 @@ import pandas as pd import requests import PIL.Image as Image -from utils.output_utils import uploadMediaToHoster +from utils.output_utils import upload_media_to_hoster """ This file contains basic calling functions for ML tasks that are outsourced to nova-server @@ -78,7 +78,7 @@ def check_nova_server_status(jobID, address): if content_type == "image/jpeg": image = Image.open(io.BytesIO(response.content)) image.save("./outputs/image.jpg") - result = uploadMediaToHoster("./outputs/image.jpg") + result = upload_media_to_hoster("./outputs/image.jpg") os.remove("./outputs/image.jpg") elif content_type == 'text/plain; charset=utf-8': result = response.content.decode('utf-8') @@ -95,8 +95,9 @@ def check_nova_server_status(jobID, address): print(result) with open("response.zip", "wb") as f: f.write(response.content) - except: - zf.extractall() + except Exception as e: + #zf.extractall() + print(e) return result except Exception as e: diff --git a/dvm.py b/dvm.py index 70b8579..b9ab1d1 100644 --- a/dvm.py +++ b/dvm.py @@ -1,7 +1,7 @@ from nostr_sdk import PublicKey, Keys, Client, Tag, Event, EventBuilder, Filter, HandleNotification, Timestamp, \ init_logger, LogLevel + import time -import emoji from utils.definitions import EventDefinitions, RequiredJobToWatch, JobToWatch from utils.dvmconfig import DVMConfig @@ -10,8 +10,8 @@ from utils.backend_utils import get_amount_per_task, check_task_is_supported, ge from utils.database_utils import update_sql_table, get_from_sql_table, \ create_sql_table, get_or_add_user, update_user_balance from utils.nostr_utils import get_event_by_id, get_referenced_event_by_id, send_event -from utils.output_utils import post_process_result -from utils.zap_utils import check_bolt11_ln_bits_is_paid, parse_bolt11_invoice, \ +from utils.output_utils import post_process_result, build_status_reaction +from utils.zap_utils import check_bolt11_ln_bits_is_paid, parse_amount_from_bolt11_invoice, \ check_for_zapplepay, decrypt_private_zap_message, create_bolt11_ln_bits use_logger = False @@ -81,7 +81,7 @@ class DVM: print(task) if user.isblacklisted: - send_job_status_reaction(nip90_event, "error", client=self.client, config=self.dvm_config) + send_job_status_reaction(nip90_event, "error", client=self.client, dvm_config=self.dvm_config) print("[" + self.dvm_config.NIP89.name + "] Request by blacklisted user, skipped") elif task_supported: @@ -98,7 +98,7 @@ class DVM: if user.iswhitelisted or task_is_free: print("[" + self.dvm_config.NIP89.name + "] Free or Whitelisted for task " + task + ". Starting processing..") send_job_status_reaction(nip90_event, "processing", True, 0, client=self.client, - config=self.dvm_config) + dvm_config=self.dvm_config) do_work(nip90_event, is_from_bot=False) # otherwise send payment request else: @@ -113,28 +113,28 @@ class DVM: if bid_offer >= amount: send_job_status_reaction(nip90_event, "payment-required", False, amount, # bid_offer - client=self.client, config=self.dvm_config) + client=self.client, dvm_config=self.dvm_config) else: # If there is no bid, just request server rate from user print("[" + self.dvm_config.NIP89.name + "] Requesting payment for Event: " + nip90_event.id().to_hex()) send_job_status_reaction(nip90_event, "payment-required", - False, amount, client=self.client, config=self.dvm_config) + False, amount, client=self.client, dvm_config=self.dvm_config) else: print("Task not supported on this DVM, skipping..") - def handle_zap(event): + def handle_zap(zap_event): zapped_event = None invoice_amount = 0 anon = False - sender = event.pubkey() + sender = zap_event.pubkey() print("Zap received") try: - for tag in event.tags(): + for tag in zap_event.tags(): if tag.as_vec()[0] == 'bolt11': - invoice_amount = parse_bolt11_invoice(tag.as_vec()[1]) + invoice_amount = parse_amount_from_bolt11_invoice(tag.as_vec()[1]) elif tag.as_vec()[0] == 'e': - zapped_event = get_event_by_id(tag.as_vec()[1], config=self.dvm_config) + zapped_event = get_event_by_id(tag.as_vec()[1], client=self.client, config=self.dvm_config) elif tag.as_vec()[0] == 'description': zap_request_event = Event.from_json(tag.as_vec()[1]) sender = check_for_zapplepay(zap_request_event.pubkey().to_hex(), @@ -168,7 +168,7 @@ class DVM: if tag.as_vec()[0] == 'amount': amount = int(float(tag.as_vec()[1]) / 1000) elif tag.as_vec()[0] == 'e': - job_event = get_event_by_id(tag.as_vec()[1], config=self.dvm_config) + job_event = get_event_by_id(tag.as_vec()[1], client=self.client, config=self.dvm_config) task_supported, task, duration = check_task_is_supported(job_event, client=self.client, get_duration=False, @@ -177,7 +177,7 @@ class DVM: if amount <= invoice_amount: print("[" + self.dvm_config.NIP89.name + "] Payment-request fulfilled...") send_job_status_reaction(job_event, "processing", client=self.client, - config=self.dvm_config) + dvm_config=self.dvm_config) indices = [i for i, x in enumerate(self.job_list) if x.event_id == job_event.id().to_hex()] index = -1 @@ -187,8 +187,7 @@ class DVM: if self.job_list[index].is_processed: # If payment-required appears a processing self.job_list[index].is_paid = True check_and_return_event(self.job_list[index].result, - str(job_event.as_json()), - dvm_key=self.dvm_config.PRIVATE_KEY) + str(job_event.as_json())) elif not (self.job_list[index]).is_processed: # If payment-required appears before processing self.job_list.pop(index) @@ -201,7 +200,7 @@ class DVM: else: send_job_status_reaction(job_event, "payment-rejected", False, invoice_amount, client=self.client, - config=self.dvm_config) + dvm_config=self.dvm_config) print("[" + self.dvm_config.NIP89.name + "] Invoice was not paid sufficiently") elif zapped_event.kind() in EventDefinitions.ANY_RESULT: @@ -209,13 +208,13 @@ class DVM: elif not anon: print("Note Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( user.name)) - update_user_balance(self.dvm_config.DB, sender, invoice_amount, config=self.dvm_config) + update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, config=self.dvm_config) # a regular note elif not anon: print("Profile Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( user.name)) - update_user_balance(self.dvm_config.DB, sender, invoice_amount, config=self.dvm_config) + update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, config=self.dvm_config) except Exception as e: print(f"Error during content decryption: {e}") @@ -234,22 +233,21 @@ class DVM: input = tag.as_vec()[1] input_type = tag.as_vec()[2] if input_type == "job": - evt = get_referenced_event_by_id(input, EventDefinitions.ANY_RESULT, client, - config=dvmconfig) + evt = get_referenced_event_by_id(event_id=input, client=client, kinds=EventDefinitions.ANY_RESULT, + dvm_config=dvmconfig) if evt is None: if append: job = RequiredJobToWatch(event=nevent, timestamp=Timestamp.now().as_secs()) self.jobs_on_hold_list.append(job) send_job_status_reaction(nevent, "chain-scheduled", True, 0, client=client, - config=dvmconfig) + dvm_config=dvmconfig) return False else: return True - def check_and_return_event(data, original_event_str: str, dvm_key=""): + def check_and_return_event(data, original_event_str: str): original_event = Event.from_json(original_event_str) - keys = Keys.from_sk_str(dvm_key) for x in self.job_list: if x.event_id == original_event.id().to_hex(): @@ -258,63 +256,58 @@ class DVM: x.result = data x.is_processed = True if self.dvm_config.SHOWRESULTBEFOREPAYMENT and not is_paid: - send_nostr_reply_event(data, original_event_str, key=keys) + send_nostr_reply_event(data, original_event_str,) send_job_status_reaction(original_event, "success", amount, - config=self.dvm_config) # or payment-required, or both? + dvm_config=self.dvm_config) # or payment-required, or both? elif not self.dvm_config.SHOWRESULTBEFOREPAYMENT and not is_paid: send_job_status_reaction(original_event, "success", amount, - config=self.dvm_config) # or payment-required, or both? + dvm_config=self.dvm_config) # or payment-required, or both? if self.dvm_config.SHOWRESULTBEFOREPAYMENT and is_paid: self.job_list.remove(x) elif not self.dvm_config.SHOWRESULTBEFOREPAYMENT and is_paid: self.job_list.remove(x) - send_nostr_reply_event(data, original_event_str, key=keys) + send_nostr_reply_event(data, original_event_str) break try: post_processed_content = post_process_result(data, original_event) - send_nostr_reply_event(post_processed_content, original_event_str, key=keys) + send_nostr_reply_event(post_processed_content, original_event_str) except Exception as e: - respond_to_error(e, original_event_str, False, self.dvm_config.PRIVATE_KEY) + respond_to_error(str(e), original_event_str, False) - def send_nostr_reply_event(content, original_event_as_str, key=None): - originalevent = Event.from_json(original_event_as_str) - requesttag = Tag.parse(["request", original_event_as_str.replace("\\", "")]) - etag = Tag.parse(["e", originalevent.id().to_hex()]) - ptag = Tag.parse(["p", originalevent.pubkey().to_hex()]) - alttag = Tag.parse(["alt", "This is the result of a NIP90 DVM AI task with kind " + str( - originalevent.kind()) + ". The task was: " + originalevent.content()]) - statustag = Tag.parse(["status", "success"]) - replytags = [requesttag, etag, ptag, alttag, statustag] - for tag in originalevent.tags(): + def send_nostr_reply_event(content, original_event_as_str): + original_event = Event.from_json(original_event_as_str) + request_tag = Tag.parse(["request", original_event_as_str.replace("\\", "")]) + e_tag = Tag.parse(["e", original_event.id().to_hex()]) + p_tag = Tag.parse(["p", original_event.pubkey().to_hex()]) + alt_tag = Tag.parse(["alt", "This is the result of a NIP90 DVM AI task with kind " + str( + original_event.kind()) + ". The task was: " + original_event.content()]) + status_tag = Tag.parse(["status", "success"]) + reply_tags = [request_tag, e_tag, p_tag, alt_tag, status_tag] + for tag in original_event.tags(): if tag.as_vec()[0] == "i": - icontent = tag.as_vec()[1] - ikind = tag.as_vec()[2] - itag = Tag.parse(["i", icontent, ikind]) - replytags.append(itag) + i_content = tag.as_vec()[1] + i_kind = tag.as_vec()[2] + i_tag = Tag.parse(["i", i_content, i_kind]) + reply_tags.append(i_tag) - if key is None: - key = Keys.from_sk_str(self.dvm_config.PRIVATE_KEY) + key = Keys.from_sk_str(self.dvm_config.PRIVATE_KEY) - response_kind = originalevent.kind() + 1000 - event = EventBuilder(response_kind, str(content), replytags).to_event(key) - send_event(event, key=key) - print("[" + self.dvm_config.NIP89.name + "]" + str(response_kind) + " Job Response event sent: " + event.as_json()) - return event.as_json() + response_kind = original_event.kind() + 1000 + reply_event = EventBuilder(response_kind, str(content), reply_tags).to_event(key) + send_event(reply_event, client=self.client, dvm_config=self.dvm_config) + print("[" + self.dvm_config.NIP89.name + "]" + str(response_kind) + " Job Response event sent: " + reply_event.as_json()) + return reply_event.as_json() - def respond_to_error(content, originaleventstr, is_from_bot=False, dvm_key=None): - print("ERROR") - if dvm_key is None: - keys = Keys.from_sk_str(self.dvm_config.PRIVATE_KEY) - else: - keys = Keys.from_sk_str(dvm_key) - - original_event = Event.from_json(originaleventstr) + def respond_to_error(content: str, original_event_as_str: str, is_from_bot=False): + print("ERROR: " + str(content)) + keys = Keys.from_sk_str(self.dvm_config.PRIVATE_KEY) + original_event = Event.from_json(original_event_as_str) sender = "" task = "" if not is_from_bot: - send_job_status_reaction(original_event, "error", content=str(content), key=dvm_key) + send_job_status_reaction(original_event, "error", content=content, dvm_config=self.dvm_config) # TODO Send Zap back else: for tag in original_event.tags(): @@ -336,46 +329,14 @@ class DVM: evt = EventBuilder.new_encrypted_direct_msg(keys, PublicKey.from_hex(sender), message, None).to_event(keys) - send_event(evt, key=keys) + send_event(evt, client=self.client, dvm_config=self.dvm_config) def send_job_status_reaction(original_event, status, is_paid=True, amount=0, client=None, content=None, - config=None, - key=None): - dvmconfig = config - alt_description = "This is a reaction to a NIP90 DVM AI task. " - task = get_task(original_event, client=client, dvmconfig=dvmconfig) - if status == "processing": - alt_description = "NIP90 DVM AI task " + task + " started processing. " - reaction = alt_description + emoji.emojize(":thumbs_up:") - elif status == "success": - alt_description = "NIP90 DVM AI task " + task + " finished successfully. " - reaction = alt_description + emoji.emojize(":call_me_hand:") - elif status == "chain-scheduled": - alt_description = "NIP90 DVM AI task " + task + " Chain Task scheduled" - reaction = alt_description + emoji.emojize(":thumbs_up:") - elif status == "error": - alt_description = "NIP90 DVM AI task " + task + " had an error. " - if content is None: - reaction = alt_description + emoji.emojize(":thumbs_down:") - else: - reaction = alt_description + emoji.emojize(":thumbs_down:") + content - - elif status == "payment-required": - - alt_description = "NIP90 DVM AI task " + task + " requires payment of min " + str( - amount) + " Sats. " - reaction = alt_description + emoji.emojize(":orange_heart:") - - elif status == "payment-rejected": - alt_description = "NIP90 DVM AI task " + task + " payment is below required amount of " + str( - amount) + " Sats. " - reaction = alt_description + emoji.emojize(":thumbs_down:") - elif status == "user-blocked-from-service": - alt_description = "NIP90 DVM AI task " + task + " can't be performed. User has been blocked from Service. " - reaction = alt_description + emoji.emojize(":thumbs_down:") - else: - reaction = emoji.emojize(":thumbs_down:") + dvm_config=None): + dvm_config = dvm_config + task = get_task(original_event, client=client, dvmconfig=dvm_config) + alt_description, reaction = build_status_reaction(status, task, amount, content) e_tag = Tag.parse(["e", original_event.id().to_hex()]) p_tag = Tag.parse(["p", original_event.pubkey().to_hex()]) @@ -394,9 +355,9 @@ class DVM: payment_hash = "" expires = original_event.created_at().as_secs() + (60 * 60 * 24) if status == "payment-required" or (status == "processing" and not is_paid): - if dvmconfig.LNBITS_INVOICE_KEY != "": + if dvm_config.LNBITS_INVOICE_KEY != "": try: - bolt11, payment_hash = create_bolt11_ln_bits(amount, dvmconfig) + bolt11, payment_hash = create_bolt11_ln_bits(amount, dvm_config) except Exception as e: print(e) @@ -410,22 +371,20 @@ class DVM: payment_hash=payment_hash, expires=expires, from_bot=False)) #print(str(self.job_list)) - if status == "payment-required" or status == "payment-rejected" or ( - status == "processing" and not is_paid) or ( - status == "success" and not is_paid): + if (status == "payment-required" or status == "payment-rejected" or ( + status == "processing" and not is_paid) + or (status == "success" and not is_paid)): - if dvmconfig.LNBITS_INVOICE_KEY != "": + if dvm_config.LNBITS_INVOICE_KEY != "": amount_tag = Tag.parse(["amount", str(amount * 1000), bolt11]) else: amount_tag = Tag.parse(["amount", str(amount * 1000)]) # to millisats tags.append(amount_tag) - if key is not None: - keys = Keys.from_sk_str(key) - else: - keys = Keys.from_sk_str(dvmconfig.PRIVATE_KEY) + + keys = Keys.from_sk_str(dvm_config.PRIVATE_KEY) event = EventBuilder(EventDefinitions.KIND_FEEDBACK, reaction, tags).to_event(keys) - send_event(event, key=keys) + send_event(event, client=self.client, dvm_config=self.dvm_config) print("[" + self.dvm_config.NIP89.name + "]" + ": Sent Kind " + str( EventDefinitions.KIND_FEEDBACK) + " Reaction: " + status + " " + event.as_json()) return event.as_json() @@ -443,12 +402,11 @@ class DVM: request_form = dvm.create_request_form_from_nostr_event(job_event, self.client, self.dvm_config) result = dvm.process(request_form) - check_and_return_event(result, str(job_event.as_json()), - dvm_key=self.dvm_config.PRIVATE_KEY) + check_and_return_event(result, str(job_event.as_json())) except Exception as e: print(e) - respond_to_error(e, job_event.as_json(), is_from_bot, self.dvm_config.PRIVATE_KEY) + respond_to_error(str(e), job_event.as_json(), is_from_bot) return self.client.handle_notifications(NotificationHandler()) @@ -457,11 +415,11 @@ class DVM: if job.bolt11 != "" and job.payment_hash != "" and not job.is_paid: if str(check_bolt11_ln_bits_is_paid(job.payment_hash, self.dvm_config)) == "True": job.is_paid = True - event = get_event_by_id(job.event_id, config=self.dvm_config) + event = get_event_by_id(job.event_id, client=self.client, config=self.dvm_config) if event is not None: send_job_status_reaction(event, "processing", True, 0, client=self.client, - config=self.dvm_config) + dvm_config=self.dvm_config) print("do work from joblist") do_work(event, is_from_bot=False) @@ -469,13 +427,13 @@ class DVM: try: self.job_list.remove(job) except: - continue + print("Error removing Job from List after payment") if Timestamp.now().as_secs() > job.expires: try: self.job_list.remove(job) except: - continue + print("Error removing Job from List after expiry") for job in self.jobs_on_hold_list: if check_event_has_not_unfinished_job_input(job.event, False, client=self.client, @@ -484,9 +442,9 @@ class DVM: try: self.jobs_on_hold_list.remove(job) except: - continue + print("Error removing Job on Hold from List after expiry") if Timestamp.now().as_secs() > job.timestamp + 60 * 20: # remove jobs to look for after 20 minutes.. self.jobs_on_hold_list.remove(job) - time.sleep(2.0) + time.sleep(1.0) diff --git a/tasks/imagegenerationsdxl.py b/tasks/imagegenerationsdxl.py index 26feb27..42bfee1 100644 --- a/tasks/imagegenerationsdxl.py +++ b/tasks/imagegenerationsdxl.py @@ -21,7 +21,7 @@ class ImageGenerationSDXL(DVMTaskInterface): NAME: str = "" KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE TASK: str = "text-to-image" - COST: int = 5 + COST: int = 50 PK: str def __init__(self, name, dvm_config: DVMConfig, admin_config: AdminConfig = None, default_model=None, default_lora=None): @@ -102,8 +102,6 @@ class ImageGenerationSDXL(DVMTaskInterface): if len(split) > 1: width = split[0] height = split[1] - print(width) - print(height) elif tag.as_vec()[1] == "model": model = tag.as_vec()[2] diff --git a/tasks/textextractionpdf.py b/tasks/textextractionpdf.py index c916590..95aad4f 100644 --- a/tasks/textextractionpdf.py +++ b/tasks/textextractionpdf.py @@ -56,7 +56,7 @@ class TextExtractionPDF(DVMTaskInterface): url = input_content # if event contains url to pdf, we checked for a pdf link before elif input_type == "event": - evt = get_event_by_id(input_content, config=dvm_config) + evt = get_event_by_id(input_content, client=client, config=dvm_config) url = re.search("(?Phttps?://[^\s]+)", evt.content()).group("url") request_form["optStr"] = 'url=' + url diff --git a/tasks/translation.py b/tasks/translation.py index db48b77..a13748d 100644 --- a/tasks/translation.py +++ b/tasks/translation.py @@ -58,7 +58,7 @@ class Translation(DVMTaskInterface): if input_type == "event": for tag in event.tags(): if tag.as_vec()[0] == 'i': - evt = get_event_by_id(tag.as_vec()[1], config=dvm_config) + evt = get_event_by_id(tag.as_vec()[1], client=client, config=dvm_config) text = evt.content() break @@ -71,12 +71,11 @@ class Translation(DVMTaskInterface): elif input_type == "job": for tag in event.tags(): if tag.as_vec()[0] == 'i': - evt = get_referenced_event_by_id(tag.as_vec()[1], - [EventDefinitions.KIND_NIP90_RESULT_EXTRACT_TEXT, - EventDefinitions.KIND_NIP90_RESULT_SUMMARIZE_TEXT, - EventDefinitions.KIND_NIP90_RESULT_TRANSLATE_TEXT], - client, - config=dvm_config) + evt = get_referenced_event_by_id(event_id=tag.as_vec()[1], client=client, + kinds=[EventDefinitions.KIND_NIP90_RESULT_EXTRACT_TEXT, + EventDefinitions.KIND_NIP90_RESULT_SUMMARIZE_TEXT, + EventDefinitions.KIND_NIP90_RESULT_TRANSLATE_TEXT], + dvm_config=dvm_config) text = evt.content() break diff --git a/test_client.py b/test_client.py index d9903d8..6b5230d 100644 --- a/test_client.py +++ b/test_client.py @@ -7,6 +7,7 @@ from threading import Thread import dotenv from nostr_sdk import Keys, Client, Tag, EventBuilder, Filter, HandleNotification, Timestamp, nip04_decrypt +from utils.dvmconfig import DVMConfig from utils.nostr_utils import send_event from utils.definitions import EventDefinitions, RELAY_LIST @@ -36,7 +37,8 @@ def nostr_client_test_translation(input, kind, lang, sats, satsmax): for relay in relay_list: client.add_relay(relay) client.connect() - send_event(event, client, keys) + config = DVMConfig + send_event(event, client=client, dvm_config=config) return event.as_json() @@ -62,7 +64,8 @@ def nostr_client_test_image(prompt): for relay in relay_list: client.add_relay(relay) client.connect() - send_event(event, client, keys) + config = DVMConfig + send_event(event, client=client, dvm_config=config) return event.as_json() def nostr_client(): diff --git a/utils/admin_utils.py b/utils/admin_utils.py index a178daa..379315d 100644 --- a/utils/admin_utils.py +++ b/utils/admin_utils.py @@ -73,4 +73,4 @@ def admin_make_database_updates(adminconfig: AdminConfig = None, dvmconfig: DVMC list_db(db) if rebroadcast_nip89: - nip89_announce_tasks(dvmconfig) + nip89_announce_tasks(dvmconfig, client=client) diff --git a/utils/backend_utils.py b/utils/backend_utils.py index 08b19d3..bd56e96 100644 --- a/utils/backend_utils.py +++ b/utils/backend_utils.py @@ -28,7 +28,7 @@ def get_task(event, client, dvmconfig): else: return "unknown job" elif tag.as_vec()[2] == "event": - evt = get_event_by_id(tag.as_vec()[1], config=dvmconfig) + evt = get_event_by_id(tag.as_vec()[1], client=client, config=dvmconfig) if evt is not None: if evt.kind() == 1063: for tg in evt.tags(): @@ -65,7 +65,7 @@ def check_task_is_supported(event, client, get_duration=False, config=None): input_value = tag.as_vec()[1] input_type = tag.as_vec()[2] if input_type == "event": - evt = get_event_by_id(input_value, config=dvm_config) + evt = get_event_by_id(input_value, client=client, config=dvm_config) if evt is None: print("Event not found") return False, "", 0 diff --git a/utils/database_utils.py b/utils/database_utils.py index 8562de0..3b9b85e 100644 --- a/utils/database_utils.py +++ b/utils/database_utils.py @@ -155,7 +155,7 @@ def list_db(db): print(e) -def update_user_balance(db, sender, sats, config=None): +def update_user_balance(db, sender, sats, client, config): user = get_from_sql_table(db, sender) if user is None: add_to_sql_table(db, sender, (int(sats) + NEW_USER_BALANCE), False, False, @@ -186,7 +186,7 @@ def update_user_balance(db, sender, sats, config=None): evt = EventBuilder.new_encrypted_direct_msg(keys, PublicKey.from_hex(sender), message, None).to_event(keys) - send_event(evt, key=keys) + send_event(evt, client=client, dvm_config=config) def get_or_add_user(db, sender): diff --git a/utils/definitions.py b/utils/definitions.py index 2616015..666868e 100644 --- a/utils/definitions.py +++ b/utils/definitions.py @@ -7,9 +7,7 @@ from utils import env NEW_USER_BALANCE: int = 250 # Free credits for new users -RELAY_LIST = ["wss://relay.damus.io", "wss://nostr-pub.wellorder.net", "wss://nos.lol", "wss://nostr.wine", - "wss://relay.nostfiles.dev", "wss://nostr.mom", "wss://nostr.oxtr.dev", "wss://relay.nostr.bg", - "wss://relay.f7z.io"] + class EventDefinitions: diff --git a/utils/dvmconfig.py b/utils/dvmconfig.py index 3de7079..be93749 100644 --- a/utils/dvmconfig.py +++ b/utils/dvmconfig.py @@ -11,6 +11,7 @@ class DVMConfig: RELAY_LIST = ["wss://relay.damus.io", "wss://nostr-pub.wellorder.net", "wss://nos.lol", "wss://nostr.wine", "wss://relay.nostfiles.dev", "wss://nostr.mom", "wss://nostr.oxtr.dev", "wss://relay.nostr.bg", "wss://relay.f7z.io"] + RELAY_TIMEOUT = 5 LNBITS_INVOICE_KEY = '' LNBITS_URL = 'https://lnbits.com' diff --git a/utils/nip89_utils.py b/utils/nip89_utils.py index 65be301..ddcd11f 100644 --- a/utils/nip89_utils.py +++ b/utils/nip89_utils.py @@ -1,6 +1,8 @@ from nostr_sdk import Tag, Keys, EventBuilder + from utils.nostr_utils import send_event + class NIP89Announcement: name: str kind: int @@ -8,11 +10,12 @@ class NIP89Announcement: pk: str content: str -def nip89_announce_tasks(dvmconfig): - k_tag = Tag.parse(["k", str(dvmconfig.NIP89.kind)]) - d_tag = Tag.parse(["d", dvmconfig.NIP89.dtag]) - keys = Keys.from_sk_str(dvmconfig.NIP89.pk) - content = dvmconfig.NIP89.content + +def nip89_announce_tasks(dvm_config, client): + k_tag = Tag.parse(["k", str(dvm_config.NIP89.kind)]) + d_tag = Tag.parse(["d", dvm_config.NIP89.dtag]) + keys = Keys.from_sk_str(dvm_config.NIP89.pk) + content = dvm_config.NIP89.content event = EventBuilder(31990, content, [k_tag, d_tag]).to_event(keys) - send_event(event, key=keys) - print("Announced NIP 89 for " + dvmconfig.NIP89.name) \ No newline at end of file + send_event(event, client=client, dvm_config=dvm_config) + print("Announced NIP 89 for " + dvm_config.NIP89.name) diff --git a/utils/nostr_utils.py b/utils/nostr_utils.py index df7da0f..adf5e2a 100644 --- a/utils/nostr_utils.py +++ b/utils/nostr_utils.py @@ -1,19 +1,8 @@ from datetime import timedelta -from nostr_sdk import Keys, Filter, Client, Alphabet, EventId, Options - -from utils.definitions import RELAY_LIST +from nostr_sdk import Keys, Filter, Client, Alphabet, EventId, Options, Event -def get_event_by_id(event_id, client=None, config=None): - is_new_client = False - if client is None: - keys = Keys.from_sk_str(config.PRIVATE_KEY) - client = Client(keys) - for relay in config.RELAY_LIST: - client.add_relay(relay) - client.connect() - is_new_client = True - +def get_event_by_id(event_id: str, client: Client, config=None) -> Event | None: split = event_id.split(":") if len(split) == 3: id_filter = Filter().author(split[1]).custom_tag(Alphabet.D, [split[2]]) @@ -23,69 +12,44 @@ def get_event_by_id(event_id, client=None, config=None): event_id = EventId.from_bech32(event_id).to_hex() id_filter = Filter().id(event_id).limit(1) events = client.get_events_of([id_filter], timedelta(seconds=config.RELAY_TIMEOUT)) - if is_new_client: - client.disconnect() if len(events) > 0: return events[0] else: return None -def get_referenced_event_by_id(event_id, kinds=None, client=None, config=None): - if kinds is None: - kinds = [] - is_new_client = False - if client is None: - keys = Keys.from_sk_str(config.PRIVATE_KEY) - client = Client(keys) - for relay in config.RELAY_LIST: - client.add_relay(relay) - client.connect() - is_new_client = True +def get_referenced_event_by_id(event_id, client, dvm_config, kinds) -> Event | None: if kinds is None: kinds = [] + if len(kinds) > 0: job_id_filter = Filter().kinds(kinds).event(EventId.from_hex(event_id)).limit(1) else: job_id_filter = Filter().event(EventId.from_hex(event_id)).limit(1) - events = client.get_events_of([job_id_filter], timedelta(seconds=config.RELAY_TIMEOUT)) + events = client.get_events_of([job_id_filter], timedelta(seconds=dvm_config.RELAY_TIMEOUT)) - if is_new_client: - client.disconnect() if len(events) > 0: return events[0] else: return None -def send_event(event, client=None, key=None): +def send_event(event: Event, client: Client, dvm_config) -> EventId: relays = [] - is_new_client = False for tag in event.tags(): if tag.as_vec()[0] == 'relays': relays = tag.as_vec()[1].split(',') - if client is None: - opts = Options().wait_for_send(False).send_timeout(timedelta(seconds=5)).skip_disconnected_relays(True) - client = Client.with_opts(key, opts) - for relay in RELAY_LIST: - client.add_relay(relay) - client.connect() - is_new_client = True - for relay in relays: - if relay not in RELAY_LIST: + if relay not in dvm_config.RELAY_LIST: client.add_relay(relay) event_id = client.send_event(event) for relay in relays: - if relay not in RELAY_LIST: + if relay not in dvm_config.RELAY_LIST: client.remove_relay(relay) - if is_new_client: - client.disconnect() - return event_id diff --git a/utils/output_utils.py b/utils/output_utils.py index aea0e60..d505ee7 100644 --- a/utils/output_utils.py +++ b/utils/output_utils.py @@ -2,6 +2,8 @@ import json import datetime as datetime import os from types import NoneType + +import emoji import requests from pyupload.uploader import CatboxUploader @@ -109,7 +111,7 @@ Will probably need to switch to another system in the future. ''' -def uploadMediaToHoster(filepath): +def upload_media_to_hoster(filepath: str): print("Uploading image: " + filepath) try: files = {'file': open(filepath, 'rb')} @@ -144,3 +146,40 @@ def uploadMediaToHoster(filepath): return result except: return "Upload not possible, all hosters didn't work" + + +def build_status_reaction(status, task, amount, content): + alt_description = "This is a reaction to a NIP90 DVM AI task. " + + if status == "processing": + alt_description = "NIP90 DVM AI task " + task + " started processing. " + reaction = alt_description + emoji.emojize(":thumbs_up:") + elif status == "success": + alt_description = "NIP90 DVM AI task " + task + " finished successfully. " + reaction = alt_description + emoji.emojize(":call_me_hand:") + elif status == "chain-scheduled": + alt_description = "NIP90 DVM AI task " + task + " Chain Task scheduled" + reaction = alt_description + emoji.emojize(":thumbs_up:") + elif status == "error": + alt_description = "NIP90 DVM AI task " + task + " had an error. " + if content is None: + reaction = alt_description + emoji.emojize(":thumbs_down:") + else: + reaction = alt_description + emoji.emojize(":thumbs_down:") + content + + elif status == "payment-required": + alt_description = "NIP90 DVM AI task " + task + " requires payment of min " + str( + amount) + " Sats. " + reaction = alt_description + emoji.emojize(":orange_heart:") + + elif status == "payment-rejected": + alt_description = "NIP90 DVM AI task " + task + " payment is below required amount of " + str( + amount) + " Sats. " + reaction = alt_description + emoji.emojize(":thumbs_down:") + elif status == "user-blocked-from-service": + alt_description = "NIP90 DVM AI task " + task + " can't be performed. User has been blocked from Service. " + reaction = alt_description + emoji.emojize(":thumbs_down:") + else: + reaction = emoji.emojize(":thumbs_down:") + + return alt_description, reaction diff --git a/utils/zap_utils.py b/utils/zap_utils.py index 4eca69c..adc74d7 100644 --- a/utils/zap_utils.py +++ b/utils/zap_utils.py @@ -4,10 +4,11 @@ import json import requests from Crypto.Cipher import AES from bech32 import bech32_decode, convertbits -from nostr_sdk import PublicKey, nostr_sdk +from nostr_sdk import nostr_sdk, PublicKey, SecretKey +from utils.dvmconfig import DVMConfig -def parse_bolt11_invoice(invoice): +def parse_amount_from_bolt11_invoice(bolt11_invoice: str) -> int: def get_index_of_first_letter(ip): index = 0 for c in ip: @@ -17,7 +18,7 @@ def parse_bolt11_invoice(invoice): index = index + 1 return len(ip) - remaining_invoice = invoice[4:] + remaining_invoice = bolt11_invoice[4:] index = get_index_of_first_letter(remaining_invoice) identifier = remaining_invoice[index] number_string = remaining_invoice[:index] @@ -34,9 +35,9 @@ def parse_bolt11_invoice(invoice): return int(number) -def create_bolt11_ln_bits(sats, config): +def create_bolt11_ln_bits(sats: int, config: DVMConfig) -> (str, str): url = config.LNBITS_URL + "/api/v1/payments" - data = {'out': False, 'amount': sats, 'memo': "Nostr-DVM"} + data = {'out': False, 'amount': sats, 'memo': "Nostr-DVM " + config.NIP89.name} headers = {'X-API-Key': config.LNBITS_INVOICE_KEY, 'Content-Type': 'application/json', 'charset': 'UTF-8'} try: res = requests.post(url, json=data, headers=headers) @@ -44,35 +45,35 @@ def create_bolt11_ln_bits(sats, config): return obj["payment_request"], obj["payment_hash"] except Exception as e: print("LNBITS: " + str(e)) - return None + return None, None -def check_bolt11_ln_bits_is_paid(payment_hash, config): +def check_bolt11_ln_bits_is_paid(payment_hash: str, config: DVMConfig): url = config.LNBITS_URL + "/api/v1/payments/" + payment_hash headers = {'X-API-Key': config.LNBITS_INVOICE_KEY, 'Content-Type': 'application/json', 'charset': 'UTF-8'} try: res = requests.get(url, headers=headers) obj = json.loads(res.text) - return obj["paid"] + return obj["paid"] #TODO cast except Exception as e: return None # DECRYPT ZAPS -def check_for_zapplepay(sender, content): +def check_for_zapplepay(pubkey_hex: str, content: str): try: # Special case Zapplepay - if sender == PublicKey.from_bech32("npub1wxl6njlcgygduct7jkgzrvyvd9fylj4pqvll6p32h59wyetm5fxqjchcan").to_hex(): + if pubkey_hex == PublicKey.from_bech32("npub1wxl6njlcgygduct7jkgzrvyvd9fylj4pqvll6p32h59wyetm5fxqjchcan").to_hex(): real_sender_bech32 = content.replace("From: nostr:", "") - sender = PublicKey.from_bech32(real_sender_bech32).to_hex() - return sender + pubkey_hex = PublicKey.from_bech32(real_sender_bech32).to_hex() + return pubkey_hex except Exception as e: print(e) - return sender + return pubkey_hex -def decrypt_private_zap_message(msg, privkey, pubkey): +def decrypt_private_zap_message(msg: str, privkey: SecretKey, pubkey: PublicKey): shared_secret = nostr_sdk.generate_shared_key(privkey, pubkey) if len(shared_secret) != 16 and len(shared_secret) != 32: return "invalid shared secret size" From d7a101d20212adce708d68d836c1d558d0c8049f Mon Sep 17 00:00:00 2001 From: Believethehype Date: Wed, 22 Nov 2023 10:16:30 +0100 Subject: [PATCH 11/18] Fetch name/nip05/lud16 --- dvm.py | 7 +++---- utils/admin_utils.py | 2 +- utils/database_utils.py | 39 ++++++++++++++++++++++++--------------- 3 files changed, 28 insertions(+), 20 deletions(-) diff --git a/dvm.py b/dvm.py index b9ab1d1..2cf123a 100644 --- a/dvm.py +++ b/dvm.py @@ -74,18 +74,17 @@ class DVM: return def handle_nip90_job_event(nip90_event): - user = get_or_add_user(self.dvm_config.DB, nip90_event.pubkey().to_hex()) + user = get_or_add_user(self.dvm_config.DB, nip90_event.pubkey().to_hex(), client=self.client) task_supported, task, duration = check_task_is_supported(nip90_event, client=self.client, get_duration=(not user.iswhitelisted), config=self.dvm_config) - print(task) if user.isblacklisted: send_job_status_reaction(nip90_event, "error", client=self.client, dvm_config=self.dvm_config) print("[" + self.dvm_config.NIP89.name + "] Request by blacklisted user, skipped") elif task_supported: - print("Received new Task: " + task) + print("Received new Task: " + task + " from " + user.name) amount = get_amount_per_task(task, self.dvm_config, duration) if amount is None: return @@ -155,7 +154,7 @@ class DVM: else: anon = True print("Anonymous Zap received. Unlucky, I don't know from whom, and never will") - user = get_or_add_user(self.dvm_config.DB, sender) + user = get_or_add_user(self.dvm_config.DB, sender, client=self.client) print(str(user)) if zapped_event is not None: diff --git a/utils/admin_utils.py b/utils/admin_utils.py index 379315d..5063418 100644 --- a/utils/admin_utils.py +++ b/utils/admin_utils.py @@ -50,7 +50,7 @@ def admin_make_database_updates(adminconfig: AdminConfig = None, dvmconfig: DVMC if whitelistuser: - user = get_or_add_user(db, publickey) + user = get_or_add_user(db, publickey, client=client) update_sql_table(db, user.npub, user.balance, True, False, user.nip05, user.lud16, user.name, user.lastactive) user = get_from_sql_table(db, publickey) print(str(user.name) + " is whitelisted: " + str(user.iswhitelisted)) diff --git a/utils/database_utils.py b/utils/database_utils.py index 3b9b85e..735ad0e 100644 --- a/utils/database_utils.py +++ b/utils/database_utils.py @@ -189,20 +189,29 @@ def update_user_balance(db, sender, sats, client, config): send_event(evt, client=client, dvm_config=config) -def get_or_add_user(db, sender): - user = get_from_sql_table(db, sender) +def get_or_add_user(db, npub, client): + user = get_from_sql_table(db, npub) if user is None: - print("Adding User") - add_to_sql_table(db, sender, NEW_USER_BALANCE, False, False, None, - None, None, Timestamp.now().as_secs()) - user = get_from_sql_table(db, sender) + + name, nip05, lud16 = fetch_user_metadata(npub, client) + print("Adding User: " + name + " (" + npub +")") + add_to_sql_table(db, npub, NEW_USER_BALANCE, False, False, nip05, + lud16, name, Timestamp.now().as_secs()) + user = get_from_sql_table(db, npub) print(user) + else: + # update Name, Nip05 and lud16 lnaddress + user.name, user.nip05, user.lud16 = fetch_user_metadata(npub, client) + update_sql_table(db, user.npub, user.balance, user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16, + user.name, Timestamp.now().as_secs()) return user -def update_user_metadata(db, sender, client): - user = get_from_sql_table(db, sender) +def fetch_user_metadata(sender, client) -> (str, str, str): + name = "" + nip05 = "" + lud16 = "" try: profile_filter = Filter().kind(0).author(sender).limit(1) events = client.get_events_of([profile_filter], timedelta(seconds=3)) @@ -211,12 +220,12 @@ def update_user_metadata(db, sender, client): metadata = Metadata.from_json(ev.content()) name = metadata.get_display_name() if str(name) == "" or name is None: - user.name = metadata.get_name() - user.nip05 = metadata.get_nip05() - user.lud16 = metadata.get_lud16() + name = metadata.get_name() + nip05 = metadata.get_nip05() + lud16 = metadata.get_lud16() + except: print("Couldn't get meta information") - update_sql_table(db, user.npub, user.balance, user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16, - user.name, Timestamp.now().as_secs()) - user = get_from_sql_table(db, user.npub) - return user + + return name, nip05, lud16 + From dc727021f6267f2463a007a7c46535cd6b6808c9 Mon Sep 17 00:00:00 2001 From: Believethehype Date: Wed, 22 Nov 2023 19:20:34 +0100 Subject: [PATCH 12/18] updated option structure --- .gitignore | 1 - backends/nova_server.py | 10 +++--- dvm.py | 16 ++++----- interfaces/dvmtaskinterface.py | 14 ++++++-- main.py | 7 ++-- tasks/imagegenerationsdxl.py | 64 +++++++++++++++++++++++----------- tasks/textextractionpdf.py | 11 ++++-- tasks/translation.py | 17 +++++---- test_client.py | 7 ++-- utils/database_utils.py | 1 - utils/dvmconfig.py | 2 +- utils/output_utils.py | 3 +- 12 files changed, 98 insertions(+), 55 deletions(-) diff --git a/.gitignore b/.gitignore index 279dbf3..bd811a6 100644 --- a/.gitignore +++ b/.gitignore @@ -158,6 +158,5 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ -nostrzaps.db .DS_Store *.db diff --git a/backends/nova_server.py b/backends/nova_server.py index ce34f4c..9142193 100644 --- a/backends/nova_server.py +++ b/backends/nova_server.py @@ -33,7 +33,7 @@ def send_request_to_nova_server(request_form, address): url = ('http://' + address + '/process') headers = {'Content-type': 'application/x-www-form-urlencoded'} response = requests.post(url, headers=headers, data=request_form) - return response.content + return response.text """ @@ -58,9 +58,9 @@ def check_nova_server_status(jobID, address): response_status = requests.post(url_status, headers=headers, data=data) response_log = requests.post(url_log, headers=headers, data=data) status = int(json.loads(response_status.text)['status']) - - log = str(response_log.content)[length:] - length = len(str(response_log.content)) + log_content = str(json.loads(response_log.text)['message']).replace("ERROR", "").replace("INFO", "") + log = log_content[length:] + length = len(log_content) if log != "": print(log + " Status: " + str(status)) # WAITING = 0, RUNNING = 1, FINISHED = 2, ERROR = 3 @@ -74,7 +74,7 @@ def check_nova_server_status(jobID, address): data = {"jobID": jobID} response = requests.post(url_fetch, headers=headers, data=data) content_type = response.headers['content-type'] - print(content_type) + print("Content-type: " + str(content_type)) if content_type == "image/jpeg": image = Image.open(io.BytesIO(response.content)) image.save("./outputs/image.jpg") diff --git a/dvm.py b/dvm.py index 2cf123a..2fb5a64 100644 --- a/dvm.py +++ b/dvm.py @@ -84,7 +84,7 @@ class DVM: print("[" + self.dvm_config.NIP89.name + "] Request by blacklisted user, skipped") elif task_supported: - print("Received new Task: " + task + " from " + user.name) + print("[" + self.dvm_config.NIP89.name + "] Received new Task: " + task + " from " + user.name) amount = get_amount_per_task(task, self.dvm_config, duration) if amount is None: return @@ -95,11 +95,11 @@ class DVM: task_is_free = True if user.iswhitelisted or task_is_free: - print("[" + self.dvm_config.NIP89.name + "] Free or Whitelisted for task " + task + ". Starting processing..") + print("[" + self.dvm_config.NIP89.name + "] Free task or Whitelisted for task " + task + ". Starting processing..") send_job_status_reaction(nip90_event, "processing", True, 0, client=self.client, dvm_config=self.dvm_config) do_work(nip90_event, is_from_bot=False) - # otherwise send payment request + else: bid = 0 for tag in nip90_event.tags(): @@ -254,17 +254,17 @@ class DVM: amount = x.amount x.result = data x.is_processed = True - if self.dvm_config.SHOWRESULTBEFOREPAYMENT and not is_paid: + if self.dvm_config.SHOW_RESULT_BEFORE_PAYMENT and not is_paid: send_nostr_reply_event(data, original_event_str,) send_job_status_reaction(original_event, "success", amount, dvm_config=self.dvm_config) # or payment-required, or both? - elif not self.dvm_config.SHOWRESULTBEFOREPAYMENT and not is_paid: + elif not self.dvm_config.SHOW_RESULT_BEFORE_PAYMENT and not is_paid: send_job_status_reaction(original_event, "success", amount, dvm_config=self.dvm_config) # or payment-required, or both? - if self.dvm_config.SHOWRESULTBEFOREPAYMENT and is_paid: + if self.dvm_config.SHOW_RESULT_BEFORE_PAYMENT and is_paid: self.job_list.remove(x) - elif not self.dvm_config.SHOWRESULTBEFOREPAYMENT and is_paid: + elif not self.dvm_config.SHOW_RESULT_BEFORE_PAYMENT and is_paid: self.job_list.remove(x) send_nostr_reply_event(data, original_event_str) break @@ -296,7 +296,7 @@ class DVM: response_kind = original_event.kind() + 1000 reply_event = EventBuilder(response_kind, str(content), reply_tags).to_event(key) send_event(reply_event, client=self.client, dvm_config=self.dvm_config) - print("[" + self.dvm_config.NIP89.name + "]" + str(response_kind) + " Job Response event sent: " + reply_event.as_json()) + print("[" + self.dvm_config.NIP89.name + "] " + str(response_kind) + " Job Response event sent: " + reply_event.as_json()) return reply_event.as_json() def respond_to_error(content: str, original_event_as_str: str, is_from_bot=False): diff --git a/interfaces/dvmtaskinterface.py b/interfaces/dvmtaskinterface.py index e5c02db..426fbcd 100644 --- a/interfaces/dvmtaskinterface.py +++ b/interfaces/dvmtaskinterface.py @@ -1,3 +1,5 @@ +import json + from utils.nip89_utils import NIP89Announcement @@ -30,13 +32,19 @@ class DVMTaskInterface: pass @staticmethod - def setOptions(request_form): + def set_options(request_form): print("Setting options...") opts = [] - if request_form.get("optStr"): + if request_form.get("options"): + opts = json.loads(request_form["options"]) + print(opts) + + # old format, deprecated, will remove + elif request_form.get("optStr"): + opts = [] for k, v in [option.split("=") for option in request_form["optStr"].split(";")]: t = (k, v) opts.append(t) - print(k + "=" + v) print("...done.") + return dict(opts) diff --git a/main.py b/main.py index 9be20b2..dd10f61 100644 --- a/main.py +++ b/main.py @@ -1,5 +1,6 @@ import os from pathlib import Path +from threading import Thread import dotenv import utils.env as env @@ -14,7 +15,7 @@ def run_nostr_dvm_with_local_config(): #Generate a optional Admin Config, in this case, whenever we give our DVMS this config, they will (re)broadcast # their NIP89 announcement admin_config = AdminConfig() - admin_config.REBROADCASTNIP89 = True + admin_config.REBROADCASTNIP89 = False # Spawn the DVMs # Add NIP89 events for each DVM @@ -86,7 +87,7 @@ def run_nostr_dvm_with_local_config(): #We add an optional AdminConfig for this one, and tell the dvm to rebroadcast its NIP89 sketcher = ImageGenerationSDXL("Sketcher", dvm_config, admin_config, default_model="mohawk", default_lora="timburton") d_tag = os.getenv(env.TASK_IMAGEGENERATION_NIP89_DTAG2) - content = ("{\"name\":\"" + unstableartist.NAME + "\"," + content = ("{\"name\":\"" + sketcher.NAME + "\"," "\"image\":\"https://image.nostr.build/229c14e440895da30de77b3ca145d66d4b04efb4027ba3c44ca147eecde891f1.jpg\"," "\"about\":\"I draw images based on a prompt with a Model called unstable diffusion.\"," "\"nip90Params\":{}}") @@ -94,6 +95,8 @@ def run_nostr_dvm_with_local_config(): dvm_config.NIP89 = sketcher.NIP89_announcement(d_tag, content) + + if __name__ == '__main__': env_path = Path('.env') diff --git a/tasks/imagegenerationsdxl.py b/tasks/imagegenerationsdxl.py index 42bfee1..524c437 100644 --- a/tasks/imagegenerationsdxl.py +++ b/tasks/imagegenerationsdxl.py @@ -1,3 +1,4 @@ +import json import os from multiprocessing.pool import ThreadPool from threading import Thread @@ -14,6 +15,8 @@ This File contains a Module to transform Text input on NOVA-Server and receive r Accepted Inputs: Prompt (text) Outputs: An url to an Image +Params: -model # models: juggernaut, dynavision, colossusProject, newreality, unstable + -lora # loras (weights on top of models) voxel, """ @@ -24,7 +27,8 @@ class ImageGenerationSDXL(DVMTaskInterface): COST: int = 50 PK: str - def __init__(self, name, dvm_config: DVMConfig, admin_config: AdminConfig = None, default_model=None, default_lora=None): + def __init__(self, name, dvm_config: DVMConfig, admin_config: AdminConfig = None, default_model=None, + default_lora=None): self.NAME = name dvm_config.SUPPORTED_TASKS = [self] dvm_config.DB = "db/" + self.NAME + ".db" @@ -36,14 +40,13 @@ class ImageGenerationSDXL(DVMTaskInterface): nostr_dvm_thread = Thread(target=dvm, args=[dvm_config, admin_config]) nostr_dvm_thread.start() - def is_input_supported(self, input_type, input_content): if input_type != "text": return False return True def create_request_form_from_nostr_event(self, event, client=None, dvm_config=None): - request_form = {"jobID": event.id().to_hex() + "_"+ self.NAME.replace(" ", "")} + request_form = {"jobID": event.id().to_hex() + "_" + self.NAME.replace(" ", "")} request_form["trainerFilePath"] = 'modules\\stablediffusionxl\\stablediffusionxl.trainer' prompt = "" @@ -53,7 +56,6 @@ class ImageGenerationSDXL(DVMTaskInterface): else: model = self.default_model - # models: juggernautXL, dynavisionXL, colossusProjectXL, newrealityXL, unstable ratio_width = "1" ratio_height = "1" width = "" @@ -72,7 +74,7 @@ class ImageGenerationSDXL(DVMTaskInterface): prompt = tag.as_vec()[1] elif tag.as_vec()[0] == 'param': - print(tag.as_vec()[2]) + print("Param: " + tag.as_vec()[1] + ": " + tag.as_vec()[2]) if tag.as_vec()[1] == "negative_prompt": negative_prompt = tag.as_vec()[2] elif tag.as_vec()[1] == "lora": @@ -91,12 +93,12 @@ class ImageGenerationSDXL(DVMTaskInterface): split = tag.as_vec()[2].split(":") ratio_width = split[0] ratio_height = split[1] - #if size is set it will overwrite ratio. + # if size is set it will overwrite ratio. elif tag.as_vec()[1] == "size": if len(tag.as_vec()) > 3: - width = (tag.as_vec()[2]) - height = (tag.as_vec()[3]) + width = (tag.as_vec()[2]) + height = (tag.as_vec()[3]) elif len(tag.as_vec()) == 3: split = tag.as_vec()[2].split("x") if len(split) > 1: @@ -105,18 +107,39 @@ class ImageGenerationSDXL(DVMTaskInterface): elif tag.as_vec()[1] == "model": model = tag.as_vec()[2] - prompt = prompt.replace(";", ",") - request_form['data'] = '[{"id":"input_prompt","type":"input","src":"request:text","data":"' + prompt + '","active":"True"},{"id":"negative_prompt","type":"input","src":"request:text","data":"' + negative_prompt + '","active":"True"},{"id":"output_image","type":"output","src":"request:image","active":"True"}]' - request_form['options'] = ('[{"model":" + ' + model - + '","ratio":"' + str(ratio_width) + ':' + str(ratio_height) - + '","width":"' + str(width) + ':' + str(height) - + '","strength":"' + str(strength) - + '","guidance_scale":"' + str(guidance_scale) - + '","lora":"' + str(lora) - + '","lora_weight":"' + str(lora_weight) - + '"}]') + io_input = { + "id": "input_prompt", + "type": "input", + "src": "request:text", + "data": prompt + } + io_negative = { + "id": "negative_prompt", + "type": "input", + "src": "request:text", + "data": negative_prompt + } + io_output = { + "id": "output_image", + "type": "output", + "src": "request:image" + } + request_form['data'] = json.dumps([io_input, io_negative, io_output]) + options = { + "model": model, + "ratio": ratio_width + '-' + ratio_height, + "width": width, + "height": height, + "strength": strength, + "guidance_scale": guidance_scale, + "lora": lora, + "lora_weight": lora_weight + } + request_form['options'] = json.dumps(options) + + # old format, deprecated, will remove request_form["optStr"] = ('model=' + model + ';ratio=' + str(ratio_width) + '-' + str(ratio_height) + ';size=' + str(width) + '-' + str(height) + ';strength=' + str(strength) + ';guidance_scale=' + str(guidance_scale) + ';lora=' + lora + ';lora_weight=' + lora_weight) @@ -126,8 +149,9 @@ class ImageGenerationSDXL(DVMTaskInterface): def process(self, request_form): try: # Call the process route of NOVA-Server with our request form. - success = send_request_to_nova_server(request_form, os.environ["NOVA_SERVER"]) - print(success) + response = send_request_to_nova_server(request_form, os.environ["NOVA_SERVER"]) + if bool(json.loads(response)['success']): + print("Job " + request_form['jobID'] + " sent to nova-server") pool = ThreadPool(processes=1) thread = pool.apply_async(check_nova_server_status, (request_form['jobID'], os.environ["NOVA_SERVER"])) diff --git a/tasks/textextractionpdf.py b/tasks/textextractionpdf.py index 95aad4f..c5ce214 100644 --- a/tasks/textextractionpdf.py +++ b/tasks/textextractionpdf.py @@ -1,3 +1,4 @@ +import json import os import re from threading import Thread @@ -14,6 +15,7 @@ This File contains a Module to extract Text from a PDF file locally on the DVM M Accepted Inputs: Url to pdf file, Event containing an URL to a PDF file Outputs: Text containing the extracted contents of the PDF file +Params: None """ @@ -21,7 +23,7 @@ class TextExtractionPDF(DVMTaskInterface): NAME: str = "" KIND: int = EventDefinitions.KIND_NIP90_EXTRACT_TEXT TASK: str = "pdf-to-text" - COST: int = 20 + COST: int = 0 PK: str def __init__(self, name, dvm_config: DVMConfig, admin_config: AdminConfig = None): @@ -59,7 +61,10 @@ class TextExtractionPDF(DVMTaskInterface): evt = get_event_by_id(input_content, client=client, config=dvm_config) url = re.search("(?Phttps?://[^\s]+)", evt.content()).group("url") - request_form["optStr"] = 'url=' + url + options = { + "url": url, + } + request_form['options'] = json.dumps(options) return request_form def process(self, request_form): @@ -67,7 +72,7 @@ class TextExtractionPDF(DVMTaskInterface): from pathlib import Path import requests - options = DVMTaskInterface.setOptions(request_form) + options = DVMTaskInterface.set_options(request_form) try: file_path = Path('temp.pdf') diff --git a/tasks/translation.py b/tasks/translation.py index a13748d..1511fbe 100644 --- a/tasks/translation.py +++ b/tasks/translation.py @@ -1,3 +1,4 @@ +import json from threading import Thread from dvm import DVM @@ -12,6 +13,7 @@ This File contains a Module to call Google Translate Services locally on the DVM Accepted Inputs: Text, Events, Jobs (Text Extraction, Summary, Translation) Outputs: Text containing the Translation in the desired language. +Params: -language The target language """ @@ -79,15 +81,18 @@ class Translation(DVMTaskInterface): text = evt.content() break - request_form["optStr"] = ('translation_lang=' + translation_lang + ';text=' + - text.replace('\U0001f919', "").replace("=", "equals"). - replace(";", ",")) + options = { + "text": text, + "language": translation_lang + } + request_form['options'] = json.dumps(options) + return request_form def process(self, request_form): from translatepy.translators.google import GoogleTranslate - options = DVMTaskInterface.setOptions(request_form) + options = DVMTaskInterface.set_options(request_form) gtranslate = GoogleTranslate() length = len(options["text"]) @@ -98,7 +103,7 @@ class Translation(DVMTaskInterface): text_part = options["text"][step:step + 5000] step = step + 5000 try: - translated_text_part = str(gtranslate.translate(text_part, options["translation_lang"])) + translated_text_part = str(gtranslate.translate(text_part, options["language"])) print("Translated Text part:\n\n " + translated_text_part) except Exception as e: raise Exception(e) @@ -108,7 +113,7 @@ class Translation(DVMTaskInterface): if step < length: text_part = options["text"][step:length] try: - translated_text_part = str(gtranslate.translate(text_part, options["translation_lang"])) + translated_text_part = str(gtranslate.translate(text_part, options["language"])) print("Translated Text part:\n " + translated_text_part) except Exception as e: raise Exception(e) diff --git a/test_client.py b/test_client.py index 6b5230d..3083c96 100644 --- a/test_client.py +++ b/test_client.py @@ -9,7 +9,7 @@ from nostr_sdk import Keys, Client, Tag, EventBuilder, Filter, HandleNotificatio from utils.dvmconfig import DVMConfig from utils.nostr_utils import send_event -from utils.definitions import EventDefinitions, RELAY_LIST +from utils.definitions import EventDefinitions import utils.env as env @@ -74,7 +74,8 @@ def nostr_client(): pk = keys.public_key() print(f"Nostr Client public key: {pk.to_bech32()}, Hex: {pk.to_hex()} ") client = Client(keys) - for relay in RELAY_LIST: + dvmconfig = DVMConfig() + for relay in dvmconfig.RELAY_LIST: client.add_relay(relay) client.connect() @@ -85,7 +86,7 @@ def nostr_client(): EventDefinitions.KIND_FEEDBACK]).since(Timestamp.now())) # public events client.subscribe([dm_zap_filter, dvm_filter]) - # nostr_client_test_translation("This is the result of the DVM in spanish", "text", "es", 20, 20) + #nostr_client_test_translation("This is the result of the DVM in spanish", "text", "es", 20, 20) #nostr_client_test_translation("note1p8cx2dz5ss5gnk7c59zjydcncx6a754c0hsyakjvnw8xwlm5hymsnc23rs", "event", "es", 20,20) #nostr_client_test_translation("44a0a8b395ade39d46b9d20038b3f0c8a11168e67c442e3ece95e4a1703e2beb", "event", "zh", 20, 20) diff --git a/utils/database_utils.py b/utils/database_utils.py index 735ad0e..47ba275 100644 --- a/utils/database_utils.py +++ b/utils/database_utils.py @@ -198,7 +198,6 @@ def get_or_add_user(db, npub, client): add_to_sql_table(db, npub, NEW_USER_BALANCE, False, False, nip05, lud16, name, Timestamp.now().as_secs()) user = get_from_sql_table(db, npub) - print(user) else: # update Name, Nip05 and lud16 lnaddress user.name, user.nip05, user.lud16 = fetch_user_metadata(npub, client) diff --git a/utils/dvmconfig.py b/utils/dvmconfig.py index be93749..74fd5a2 100644 --- a/utils/dvmconfig.py +++ b/utils/dvmconfig.py @@ -19,7 +19,7 @@ class DVMConfig: NIP89: NIP89Announcement REQUIRES_NIP05: bool = False - SHOWRESULTBEFOREPAYMENT: bool = True # if this is true show results even when not paid right after autoprocess + SHOW_RESULT_BEFORE_PAYMENT: bool = True # if this is true show results even when not paid right after autoprocess diff --git a/utils/output_utils.py b/utils/output_utils.py index d505ee7..6d1b0cd 100644 --- a/utils/output_utils.py +++ b/utils/output_utils.py @@ -15,12 +15,11 @@ Post process results to either given output format or a Nostr readable plain tex def post_process_result(anno, original_event): - print("post-processing...") + print("Post-processing...") if isinstance(anno, pandas.DataFrame): # if input is an anno we parse it to required output format for tag in original_event.tags(): print(tag.as_vec()[0]) if tag.as_vec()[0] == "output": - print("HAS OUTPUT TAG") output_format = tag.as_vec()[1] print("requested output is " + str(tag.as_vec()[1]) + "...") try: From e610825de8b354376a8747063b4c9ca1b64a5038 Mon Sep 17 00:00:00 2001 From: believethehype Date: Thu, 23 Nov 2023 06:18:29 +0100 Subject: [PATCH 13/18] Update database_utils.py --- utils/database_utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/utils/database_utils.py b/utils/database_utils.py index 47ba275..01f7c68 100644 --- a/utils/database_utils.py +++ b/utils/database_utils.py @@ -220,8 +220,8 @@ def fetch_user_metadata(sender, client) -> (str, str, str): name = metadata.get_display_name() if str(name) == "" or name is None: name = metadata.get_name() - nip05 = metadata.get_nip05() - lud16 = metadata.get_lud16() + nip05 = metadata.get_nip05() + lud16 = metadata.get_lud16() except: print("Couldn't get meta information") From 8a7e3168f5fa30917f87bc270c61967b9ae93077 Mon Sep 17 00:00:00 2001 From: Believethehype Date: Thu, 23 Nov 2023 11:53:57 +0100 Subject: [PATCH 14/18] simplifiying code, adding a basic bot functionality --- .env_example | 21 ++-- backends/nova_server.py | 2 +- bot.py | 160 +++++++++++++++++++++++++++ dvm.py | 57 ++++++++-- interfaces/dvmtaskinterface.py | 11 ++ main.py | 109 +++++------------- playground.py | 129 +++++++++++++++++++++ tasks/imagegenerationsdxl.py | 33 +++--- tasks/textextractionpdf.py | 14 ++- tasks/translation.py | 14 ++- test_client.py => test_dvm_client.py | 8 +- utils/admin_utils.py | 4 +- utils/database_utils.py | 4 +- utils/definitions.py | 6 - utils/dvmconfig.py | 4 +- utils/env.py | 14 --- 16 files changed, 434 insertions(+), 156 deletions(-) create mode 100644 bot.py create mode 100644 playground.py rename test_client.py => test_dvm_client.py (95%) delete mode 100644 utils/env.py diff --git a/.env_example b/.env_example index 31ca32a..a2cbb86 100644 --- a/.env_example +++ b/.env_example @@ -1,15 +1,22 @@ -NOSTR_PRIVATE_KEY = "nostrSecretkeyinhex" -NOSTR_TEST_CLIENT_PRIVATE_KEY = "nostrSecretkeyinhex_forthetestclient" -USER_DB_PATH = "nostrzaps.db" +NOSTR_PRIVATE_KEY = "a secret hexkey for some demo dvms" +NOSTR_PRIVATE_KEY2 = "another secret hexkey for demo dvm with another key" +BOT_PRIVATE_KEY = "The private key for a test bot that communicates with dvms" +NOSTR_TEST_CLIENT_PRIVATE_KEY = "a secret hex key for the test dvm client" -# Optional LNBITS options to create invoices (if empty, it will use the lud16 from profile) + +# Optional LNBITS options to create invoices (if empty, it will use the lud16 from profile, make sure to set one) LNBITS_INVOICE_KEY = "" LNBITS_HOST = "https://lnbits.com" -TASK_TEXTEXTRACTION_NIP89_DTAG = "asdd" + +# Some d tags we use in the testfile to announce or dvms. Create one at vendata.io) +TASK_TEXT_EXTRACTION_NIP89_DTAG = "asdd" TASK_TRANSLATION_NIP89_DTAG = "abcded" -TASK_IMAGEGENERATION_NIP89_DTAG = "fgdfgdf" -TASK_IMAGEGENERATION_NIP89_DTAG2 = "fgdfgdf" +TASK_IMAGE_GENERATION_NIP89_DTAG = "fgdfgdf" +TASK_IMAGE_GENERATION_NIP89_DTAG2 = "fdgdfg" #Backend Specific Options for tasks that require them + +#nova-server is a local backend supporting some AI modules and needs to be installed separately, +#if dvms supporting it should be used NOVA_SERVER = "127.0.0.1:37318" \ No newline at end of file diff --git a/backends/nova_server.py b/backends/nova_server.py index 9142193..885e352 100644 --- a/backends/nova_server.py +++ b/backends/nova_server.py @@ -62,7 +62,7 @@ def check_nova_server_status(jobID, address): log = log_content[length:] length = len(log_content) if log != "": - print(log + " Status: " + str(status)) + print(log) # WAITING = 0, RUNNING = 1, FINISHED = 2, ERROR = 3 time.sleep(1.0) diff --git a/bot.py b/bot.py new file mode 100644 index 0000000..d23ead4 --- /dev/null +++ b/bot.py @@ -0,0 +1,160 @@ +import json +import time +from threading import Thread + +from nostr_sdk import Keys, Client, Timestamp, Filter, nip04_decrypt, HandleNotification, EventBuilder, PublicKey, Event + +from utils.admin_utils import admin_make_database_updates +from utils.database_utils import get_or_add_user, update_user_balance +from utils.definitions import EventDefinitions +from utils.nostr_utils import send_event, get_event_by_id +from utils.zap_utils import parse_amount_from_bolt11_invoice, check_for_zapplepay, decrypt_private_zap_message + + +class Bot: + job_list: list + + def __init__(self, dvm_config, admin_config=None): + self.dvm_config = dvm_config + self.admin_config = admin_config + self.keys = Keys.from_sk_str(dvm_config.PRIVATE_KEY) + self.client = Client(self.keys) + self.job_list = [] + + pk = self.keys.public_key() + self.dvm_config.DB = "db/bot.db" + + print("Nostr BOT public key: " + str(pk.to_bech32()) + " Hex: " + str(pk.to_hex()) + " Supported DVM tasks: " + + ', '.join(p.NAME + ":" + p.TASK for p in self.dvm_config.SUPPORTED_TASKS) + "\n") + + for relay in self.dvm_config.RELAY_LIST: + self.client.add_relay(relay) + self.client.connect() + + dm_zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM, EventDefinitions.KIND_ZAP]).since( + Timestamp.now()) + self.client.subscribe([dm_zap_filter]) + + admin_make_database_updates(adminconfig=self.admin_config, dvmconfig=self.dvm_config, client=self.client) + + class NotificationHandler(HandleNotification): + client = self.client + dvm_config = self.dvm_config + keys = self.keys + + def handle(self, relay_url, nostr_event): + if EventDefinitions.KIND_DM: + print( + "[Bot] " + f"Received new DM from {relay_url}: {nostr_event.as_json()}") + handle_dm(nostr_event) + elif nostr_event.kind() == EventDefinitions.KIND_ZAP: + print("yay zap") + handle_zap(nostr_event) + + def handle_msg(self, relay_url, msg): + return + + def handle_dm(nostr_event): + sender = nostr_event.pubkey().to_hex() + try: + decrypted_text = nip04_decrypt(self.keys.secret_key(), nostr_event.pubkey(), nostr_event.content()) + # TODO more advanced logic, more parsing, just very basic test functions for now + if decrypted_text[0].isdigit(): + index = int(decrypted_text.split(' ')[0]) - 1 + i_tag = decrypted_text.replace(decrypted_text.split(' ')[0] + " ", "") + + keys = Keys.from_sk_str(self.dvm_config.SUPPORTED_TASKS[index].PK) + params = { + "sender": nostr_event.pubkey().to_hex(), + "input": i_tag, + "task": self.dvm_config.SUPPORTED_TASKS[index].TASK + } + message = json.dumps(params) + evt = EventBuilder.new_encrypted_direct_msg(self.keys, keys.public_key(), + message, None).to_event(self.keys) + send_event(evt, client=self.client, dvm_config=dvm_config) + + elif decrypted_text.startswith('{"result":'): + + dvm_result = json.loads(decrypted_text) + + job_event = EventBuilder.new_encrypted_direct_msg(self.keys, + PublicKey.from_hex(dvm_result["sender"]), + dvm_result["result"], + None).to_event(self.keys) + send_event(job_event, client=self.client, dvm_config=dvm_config) + user = get_or_add_user(db=self.dvm_config.DB, npub=dvm_result["sender"], client=self.client) + print("BOT received and forwarded to " + user.name + ": " + str(decrypted_text)) + + + else: + message = "DVMs that I support:\n\n" + index = 1 + for p in self.dvm_config.SUPPORTED_TASKS: + message += str(index) + " " + p.NAME + " " + p.TASK + "\n" + index += 1 + + evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(), + message + "\nSelect an Index and provide an input (" + "e.g. 1 A purple ostrich)", + nostr_event.id()).to_event(self.keys) + send_event(evt, client=self.client, dvm_config=dvm_config) + except Exception as e: + print(e) + + def handle_zap(zap_event): + zapped_event = None + invoice_amount = 0 + anon = False + sender = zap_event.pubkey() + print("Zap received") + + try: + for tag in zap_event.tags(): + if tag.as_vec()[0] == 'bolt11': + invoice_amount = parse_amount_from_bolt11_invoice(tag.as_vec()[1]) + elif tag.as_vec()[0] == 'e': + zapped_event = get_event_by_id(tag.as_vec()[1], client=self.client, config=self.dvm_config) + elif tag.as_vec()[0] == 'description': + zap_request_event = Event.from_json(tag.as_vec()[1]) + sender = check_for_zapplepay(zap_request_event.pubkey().to_hex(), + zap_request_event.content()) + for ztag in zap_request_event.tags(): + if ztag.as_vec()[0] == 'anon': + if len(ztag.as_vec()) > 1: + print("Private Zap received.") + decrypted_content = decrypt_private_zap_message(ztag.as_vec()[1], + self.keys.secret_key(), + zap_request_event.pubkey()) + decrypted_private_event = Event.from_json(decrypted_content) + if decrypted_private_event.kind() == 9733: + sender = decrypted_private_event.pubkey().to_hex() + message = decrypted_private_event.content() + if message != "": + print("Zap Message: " + message) + else: + anon = True + print("Anonymous Zap received. Unlucky, I don't know from whom, and never will") + user = get_or_add_user(self.dvm_config.DB, sender, client=self.client) + print(str(user.name)) + + if zapped_event is not None: + if not anon: + print("Note Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( + user.name)) + update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, + config=self.dvm_config) + + # a regular note + elif not anon: + print("Profile Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( + user.name)) + update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, + config=self.dvm_config) + + except Exception as e: + print(f"Error during content decryption: {e}") + + self.client.handle_notifications(NotificationHandler()) + while True: + time.sleep(1.0) diff --git a/dvm.py b/dvm.py index 2fb5a64..6201646 100644 --- a/dvm.py +++ b/dvm.py @@ -1,5 +1,7 @@ +import json + from nostr_sdk import PublicKey, Keys, Client, Tag, Event, EventBuilder, Filter, HandleNotification, Timestamp, \ - init_logger, LogLevel + init_logger, LogLevel, nip04_decrypt import time @@ -39,21 +41,23 @@ class DVM: pk = self.keys.public_key() - print("Nostr DVM public key: " + str(pk.to_bech32()) + "Hex: " + str(pk.to_hex()) + " Supported DVM tasks: " + + print("Nostr DVM public key: " + str(pk.to_bech32()) + " Hex: " + str(pk.to_hex()) + " Supported DVM tasks: " + ', '.join(p.NAME + ":" + p.TASK for p in self.dvm_config.SUPPORTED_TASKS) + "\n") for relay in self.dvm_config.RELAY_LIST: self.client.add_relay(relay) self.client.connect() - dm_zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_ZAP]).since(Timestamp.now()) + zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_ZAP]).since(Timestamp.now()) + bot_dm_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM]).authors(self.dvm_config.DM_ALLOWED).since(Timestamp.now()) + #TODO only from allowed account kinds = [EventDefinitions.KIND_NIP90_GENERIC] for dvm in self.dvm_config.SUPPORTED_TASKS: if dvm.KIND not in kinds: kinds.append(dvm.KIND) dvm_filter = (Filter().kinds(kinds).since(Timestamp.now())) - self.client.subscribe([dm_zap_filter, dvm_filter]) + self.client.subscribe([dvm_filter, zap_filter, bot_dm_filter]) create_sql_table(self.dvm_config.DB) admin_make_database_updates(adminconfig=self.admin_config, dvmconfig=self.dvm_config, client=self.client) @@ -69,6 +73,8 @@ class DVM: handle_nip90_job_event(nostr_event) elif nostr_event.kind() == EventDefinitions.KIND_ZAP: handle_zap(nostr_event) + elif nostr_event.kind() == EventDefinitions.KIND_DM: + handle_dm(nostr_event) def handle_msg(self, relay_url, msg): return @@ -84,7 +90,7 @@ class DVM: print("[" + self.dvm_config.NIP89.name + "] Request by blacklisted user, skipped") elif task_supported: - print("[" + self.dvm_config.NIP89.name + "] Received new Task: " + task + " from " + user.name) + print("[" + self.dvm_config.NIP89.name + "] Received new Request: " + task + " from " + user.name) amount = get_amount_per_task(task, self.dvm_config, duration) if amount is None: return @@ -218,6 +224,23 @@ class DVM: except Exception as e: print(f"Error during content decryption: {e}") + def handle_dm(dm_event): + decrypted_text = nip04_decrypt(self.keys.secret_key(), dm_event.pubkey(), dm_event.content()) + ob = json.loads(decrypted_text) + + #TODO SOME PARSING, OPTIONS, ZAP HANDLING + + # One key might host multiple dvms, so we check current task + if ob['task'] == self.dvm_config.SUPPORTED_TASKS[0].TASK: + j_tag = Tag.parse(["j", self.dvm_config.SUPPORTED_TASKS[0].TASK]) + i_tag = Tag.parse(["i", ob['input'], "text"]) + tags = [j_tag, i_tag] + tags.append(Tag.parse(["y", dm_event.pubkey().to_hex()])) + tags.append(Tag.parse(["z", ob['sender']])) + job_event = EventBuilder(EventDefinitions.KIND_DM, "", tags).to_event(self.keys) + + do_work(job_event, is_from_bot=True) + def check_event_has_not_unfinished_job_input(nevent, append, client, dvmconfig): task_supported, task, duration = check_task_is_supported(nevent, client, False, config=dvmconfig) if not task_supported: @@ -245,7 +268,7 @@ class DVM: else: return True - def check_and_return_event(data, original_event_str: str): + def check_and_return_event(data, original_event_str: str, is_from_bot: bool): original_event = Event.from_json(original_event_str) for x in self.job_list: @@ -271,7 +294,22 @@ class DVM: try: post_processed_content = post_process_result(data, original_event) - send_nostr_reply_event(post_processed_content, original_event_str) + if is_from_bot: + for tag in original_event.tags(): + if tag.as_vec()[0] == "y": # TODO we temporally use internal tags to move information + receiver_key = PublicKey.from_hex(tag.as_vec()[1]) + elif tag.as_vec()[0] == "z": + original_sender = tag.as_vec()[1] + + params = { + "result": post_processed_content, + "sender": original_sender + } + message = json.dumps(params) + response_event = EventBuilder.new_encrypted_direct_msg(self.keys, receiver_key, message, None).to_event(self.keys) + send_event(response_event, client=self.client, dvm_config=self.dvm_config) + else: + send_nostr_reply_event(post_processed_content, original_event_str) except Exception as e: respond_to_error(str(e), original_event_str, False) @@ -401,11 +439,11 @@ class DVM: request_form = dvm.create_request_form_from_nostr_event(job_event, self.client, self.dvm_config) result = dvm.process(request_form) - check_and_return_event(result, str(job_event.as_json())) + check_and_return_event(result, str(job_event.as_json()), is_from_bot=is_from_bot) except Exception as e: print(e) - respond_to_error(str(e), job_event.as_json(), is_from_bot) + respond_to_error(str(e), job_event.as_json(), is_from_bot=is_from_bot) return self.client.handle_notifications(NotificationHandler()) @@ -447,3 +485,4 @@ class DVM: self.jobs_on_hold_list.remove(job) time.sleep(1.0) + diff --git a/interfaces/dvmtaskinterface.py b/interfaces/dvmtaskinterface.py index 426fbcd..32efa2c 100644 --- a/interfaces/dvmtaskinterface.py +++ b/interfaces/dvmtaskinterface.py @@ -1,6 +1,10 @@ import json +from threading import Thread +from utils.admin_utils import AdminConfig +from utils.dvmconfig import DVMConfig from utils.nip89_utils import NIP89Announcement +from dvm import DVM class DVMTaskInterface: @@ -9,6 +13,9 @@ class DVMTaskInterface: TASK: str COST: int PK: str + DVM = DVM + dvm_config: DVMConfig + admin_config: AdminConfig def NIP89_announcement(self, d_tag, content): nip89 = NIP89Announcement() @@ -19,6 +26,10 @@ class DVMTaskInterface: nip89.content = content return nip89 + def run(self): + nostr_dvm_thread = Thread(target=self.DVM, args=[self.dvm_config, self.admin_config]) + nostr_dvm_thread.start() + def is_input_supported(self, input_type, input_content) -> bool: """Check if input is supported for current Task.""" pass diff --git a/main.py b/main.py index dd10f61..d01efd1 100644 --- a/main.py +++ b/main.py @@ -3,98 +3,49 @@ from pathlib import Path from threading import Thread import dotenv -import utils.env as env -from tasks.imagegenerationsdxl import ImageGenerationSDXL -from tasks.textextractionpdf import TextExtractionPDF -from tasks.translation import Translation -from utils.admin_utils import AdminConfig +from nostr_sdk import Keys + +from bot import Bot +from playground import build_pdf_extractor, build_translator, build_unstable_diffusion, build_sketcher from utils.dvmconfig import DVMConfig def run_nostr_dvm_with_local_config(): - #Generate a optional Admin Config, in this case, whenever we give our DVMS this config, they will (re)broadcast - # their NIP89 announcement - admin_config = AdminConfig() - admin_config.REBROADCASTNIP89 = False + # We extract the Publickey from our bot, so the DVMs know who they should listen and react to. + bot_publickey = Keys.from_sk_str(os.getenv("BOT_PRIVATE_KEY")).public_key().to_hex() - # Spawn the DVMs - # Add NIP89 events for each DVM - # Add the dtag here or in your .env file, so you can update your dvm later and change the content as needed. - # Get a dtag and the content at vendata.io + # Spawn some DVMs in the playground and run them + # You can add arbitrary DVMs there and instantiate them here # Spawn DVM1 Kind 5000 Text Extractor from PDFs - - dvm_config = DVMConfig() - dvm_config.PRIVATE_KEY = os.getenv(env.NOSTR_PRIVATE_KEY) - dvm_config.LNBITS_INVOICE_KEY = os.getenv(env.LNBITS_INVOICE_KEY) - dvm_config.LNBITS_URL = os.getenv(env.LNBITS_HOST) - - pdfextactor = TextExtractionPDF("PDF Extractor", dvm_config) - d_tag = os.getenv(env.TASK_TEXTEXTRACTION_NIP89_DTAG) - content = ("{\"name\":\"" + pdfextactor.NAME + "\"," - "\"image\":\"https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg\"," - "\"about\":\"I extract Text from pdf documents\"," - "\"nip90Params\":{}}") - dvm_config.NIP89 = pdfextactor.NIP89_announcement(d_tag, content) + pdfextractor = build_pdf_extractor("PDF Extractor", [bot_publickey]) + pdfextractor.run() # Spawn DVM2 Kind 5002 Text Translation - dvm_config = DVMConfig() - dvm_config.PRIVATE_KEY = os.getenv(env.NOSTR_PRIVATE_KEY) - dvm_config.LNBITS_INVOICE_KEY = os.getenv(env.LNBITS_INVOICE_KEY) - dvm_config.LNBITS_URL = os.getenv(env.LNBITS_HOST) + translator = build_translator("Translator", [bot_publickey]) + translator.run() - translator = Translation("Translator", dvm_config) - d_tag = os.getenv(env.TASK_TRANSLATION_NIP89_DTAG) - content = ("{\"name\":\"" + translator.NAME + "\"," - "\"image\":\"https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg\"," - "\"about\":\"I translate text from given text/event/job, ""currently using Google Translation Services into language defined in params.\"," - "\"nip90Params\":{\"language\":{\"required\":true," - "\"values\":[\"af\",\"am\",\"ar\",\"az\",\"be\",\"bg\",\"bn\"," - "\"bs\",\"ca\",\"ceb\",\"co\",\"cs\",\"cy\",\"da\",\"de\",\"el\"," - "\"eo\",\"es\",\"et\",\"eu\",\"fa\",\"fi\",\"fr\",\"fy\",\"ga\"," - "\"gd\",\"gl\",\"gu\",\"ha\",\"haw\",\"hi\",\"hmn\",\"hr\",\"ht\"," - "\"hu\",\"hy\",\"id\",\"ig\",\"is\",\"it\",\"he\",\"ja\",\"jv\"," - "\"ka\",\"kk\",\"km\",\"kn\",\"ko\",\"ku\",\"ky\",\"la\",\"lb\"," - "\"lo\",\"lt\",\"lv\",\"mg\",\"mi\",\"mk\",\"ml\",\"mn\",\"mr\"," - "\"ms\",\"mt\",\"my\",\"ne\",\"nl\",\"no\",\"ny\",\"or\",\"pa\"," - "\"pl\",\"ps\",\"pt\",\"ro\",\"ru\",\"sd\",\"si\",\"sk\",\"sl\"," - "\"sm\",\"sn\",\"so\",\"sq\",\"sr\",\"st\",\"su\",\"sv\",\"sw\"," - "\"ta\",\"te\",\"tg\",\"th\",\"tl\",\"tr\",\"ug\",\"uk\",\"ur\"," - "\"uz\",\"vi\",\"xh\",\"yi\",\"yo\",\"zh\",\"zu\"]}}}") + # Spawn DVM3 Kind 5100 Image Generation This one uses a specific backend called nova-server. + # If you want to use it, see the instructions in backends/nova_server + unstable_artist = build_unstable_diffusion("Unstable Diffusion", [bot_publickey]) + unstable_artist.run() - dvm_config.NIP89 = translator.NIP89_announcement(d_tag, content) - - # Spawn DVM3 Kind 5100 Image Generation This one uses a specific backend called nova-server. If you want to use - # it see the instructions in backends/nova_server - dvm_config = DVMConfig() - dvm_config.PRIVATE_KEY = os.getenv(env.NOSTR_PRIVATE_KEY) - dvm_config.LNBITS_INVOICE_KEY = os.getenv(env.LNBITS_INVOICE_KEY) - dvm_config.LNBITS_URL = os.getenv(env.LNBITS_HOST) - unstableartist = ImageGenerationSDXL("Unstable Diffusion", dvm_config, "unstable") - d_tag = os.getenv(env.TASK_IMAGEGENERATION_NIP89_DTAG) - content = ("{\"name\":\"" + unstableartist.NAME + "\"," - "\"image\":\"https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg\"," - "\"about\":\"I draw images based on a prompt with a Model called unstable diffusion.\"," - "\"nip90Params\":{}}") - dvm_config.NIP89 = unstableartist.NIP89_announcement(d_tag, content) - - - # Spawn another Instance of text-to-image but use a different privatekey, model and lora this time. - dvm_config = DVMConfig() - dvm_config.PRIVATE_KEY = "73b262d31edc6ea1316dffcc7daa772651d661e6475761b7b78291482c1bf5cb" - dvm_config.LNBITS_INVOICE_KEY = os.getenv(env.LNBITS_INVOICE_KEY) - dvm_config.LNBITS_URL = os.getenv(env.LNBITS_HOST) - #We add an optional AdminConfig for this one, and tell the dvm to rebroadcast its NIP89 - sketcher = ImageGenerationSDXL("Sketcher", dvm_config, admin_config, default_model="mohawk", default_lora="timburton") - d_tag = os.getenv(env.TASK_IMAGEGENERATION_NIP89_DTAG2) - content = ("{\"name\":\"" + sketcher.NAME + "\"," - "\"image\":\"https://image.nostr.build/229c14e440895da30de77b3ca145d66d4b04efb4027ba3c44ca147eecde891f1.jpg\"," - "\"about\":\"I draw images based on a prompt with a Model called unstable diffusion.\"," - "\"nip90Params\":{}}") - - dvm_config.NIP89 = sketcher.NIP89_announcement(d_tag, content) + # Spawn DVM4, another Instance of text-to-image, as before but use a different privatekey, model and lora this time. + sketcher = build_sketcher("Sketcher", [bot_publickey]) + sketcher.run() + # We will run an optional bot that can communicate with the DVMs + # Note this is very basic for now and still under development + bot_config = DVMConfig() + bot_config.PRIVATE_KEY = os.getenv("BOT_PRIVATE_KEY") + bot_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + bot_config.LNBITS_URL = os.getenv("LNBITS_HOST") + # Finally we add some of the DVMs we created before to the Bot and start it. + bot_config.SUPPORTED_TASKS = [sketcher, unstable_artist, translator] + bot = Bot + nostr_dvm_thread = Thread(target=bot, args=[bot_config]) + nostr_dvm_thread.start() if __name__ == '__main__': diff --git a/playground.py b/playground.py new file mode 100644 index 0000000..6dfab48 --- /dev/null +++ b/playground.py @@ -0,0 +1,129 @@ +import json +import os + +from tasks.imagegenerationsdxl import ImageGenerationSDXL +from tasks.textextractionpdf import TextExtractionPDF +from tasks.translation import Translation +from utils.admin_utils import AdminConfig +from utils.dvmconfig import DVMConfig + +# Generate an optional Admin Config, in this case, whenever we give our DVMs this config, they will (re)broadcast +# their NIP89 announcement +admin_config = AdminConfig() +admin_config.REBROADCAST_NIP89 = False + + +def build_pdf_extractor(name, dm_allowed_keys): + dvm_config = DVMConfig() + dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY") + dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") + dvm_config.DM_ALLOWED = dm_allowed_keys + # Add NIP89 + d_tag = os.getenv("TASK_TEXT_EXTRACTION_NIP89_DTAG") + nip90params = {} + nip89info = { + "name": name, + "image": "https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg", + "about": "I extract text from pdf documents", + "nip90Params": nip90params + } + return TextExtractionPDF(name=name, dvm_config=dvm_config, nip89d_tag=d_tag, nip89info=json.dumps(nip89info), + admin_config=admin_config) + + +def build_translator(name, dm_allowed_keys): + dvm_config = DVMConfig() + dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY") + dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") + dvm_config.DM_ALLOWED = dm_allowed_keys + + d_tag = os.getenv("TASK_TRANSLATION_NIP89_DTAG") + nip90params = { + "language": { + "required": False, + "values": ["en", "az", "be", "bg", "bn", "bs", "ca", "ceb", "co", "cs", "cy", "da", "de", "el", "eo", "es", + "et", "eu", "fa", "fi", "fr", "fy", "ga", "gd", "gl","gu", "ha", "haw", "hi", "hmn", "hr", "ht", + "hu", "hy", "id", "ig", "is", "it", "he", "ja", "jv", "ka", "kk","km", "kn", "ko", "ku", "ky", + "la", "lb", "lo", "lt", "lv", "mg", "mi", "mk", "ml", "mn", "mr", "ms", "mt", "my", "ne", "nl", + "no", "ny", "or", "pa", "pl", "ps", "pt", "ro", "ru", "sd", "si", "sk", "sl", "sm", "sn", "so", + "sq", "sr", "st", "su", "sv", "sw", "ta", "te","tg", "th", "tl", "tr", "ug", "uk", "ur", "uz", + "vi", "xh", "yi", "yo", "zh", "zu"] + } + } + nip89info = { + "name": name, + "image": "https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg", + "about": "I translate text from given text/event/job. Currently using Google Translation Services to translate " + "input into the language defined in params.", + "nip90Params": nip90params + } + return Translation(name=name, dvm_config=dvm_config, nip89d_tag=d_tag, nip89info=json.dumps(nip89info), + admin_config=admin_config) + + +def build_unstable_diffusion(name, dm_allowed_keys): + dvm_config = DVMConfig() + dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY") + dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") + dvm_config.DM_ALLOWED = dm_allowed_keys + + # A module might have options it can be initialized with, here we set a default model, and the nova-server + # address it should use. These parameters can be freely defined in the task component + options = {'default_model': "unstable", 'nova_server': os.getenv("NOVA_SERVER")} + + d_tag = os.getenv("TASK_IMAGE_GENERATION_NIP89_DTAG") + nip90params = { + "negative_prompt": { + "required": False, + "values": [] + }, + "ratio": { + "required": False, + "values": ["1:1", "4:3", "16:9", "3:4", "9:16", "10:16"] + } + } + nip89info = { + "name": name, + "image": "https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg", + "about": "I draw images based on a prompt with a Model called unstable diffusion", + "nip90Params": nip90params + } + return ImageGenerationSDXL(name=name, dvm_config=dvm_config, nip89d_tag=d_tag, nip89info=json.dumps(nip89info), + admin_config=admin_config, options=options) + + +def build_sketcher(name, dm_allowed_keys): + dvm_config = DVMConfig() + dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY2") + dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") + dvm_config.DM_ALLOWED = dm_allowed_keys + + d_tag = os.getenv("TASK_IMAGE_GENERATION_NIP89_DTAG2") + nip90params = { + "negative_prompt": { + "required": False, + "values": [] + }, + "ratio": { + "required": False, + "values": ["1:1", "4:3", "16:9", "3:4", "9:16", "10:16"] + } + } + nip89info = { + "name": name, + "image": "https://image.nostr.build/229c14e440895da30de77b3ca145d66d4b04efb4027ba3c44ca147eecde891f1.jpg", + "about": "I draw images based on a prompt in the style of paper sketches", + "nip90Params": nip90params + } + + # A module might have options it can be initialized with, here we set a default model, lora and the nova-server + # address it should use. These parameters can be freely defined in the task component + options = {'default_model': "mohawk", 'default_lora': "timburton", 'nova_server': os.getenv("NOVA_SERVER")} + + # We add an optional AdminConfig for this one, and tell the dvm to rebroadcast its NIP89 + return ImageGenerationSDXL(name=name, dvm_config=dvm_config, nip89d_tag=d_tag, nip89info=json.dumps(nip89info), + admin_config=admin_config, options=options) \ No newline at end of file diff --git a/tasks/imagegenerationsdxl.py b/tasks/imagegenerationsdxl.py index 524c437..0af0be0 100644 --- a/tasks/imagegenerationsdxl.py +++ b/tasks/imagegenerationsdxl.py @@ -26,19 +26,18 @@ class ImageGenerationSDXL(DVMTaskInterface): TASK: str = "text-to-image" COST: int = 50 PK: str + DVM = DVM - def __init__(self, name, dvm_config: DVMConfig, admin_config: AdminConfig = None, default_model=None, - default_lora=None): + def __init__(self, name, dvm_config: DVMConfig, nip89d_tag: str, nip89info: str, admin_config: AdminConfig = None, options=None): self.NAME = name + self.PK = dvm_config.PRIVATE_KEY + dvm_config.SUPPORTED_TASKS = [self] dvm_config.DB = "db/" + self.NAME + ".db" - self.PK = dvm_config.PRIVATE_KEY - self.default_model = default_model - self.default_lora = default_lora - - dvm = DVM - nostr_dvm_thread = Thread(target=dvm, args=[dvm_config, admin_config]) - nostr_dvm_thread.start() + dvm_config.NIP89 = self.NIP89_announcement(nip89d_tag, nip89info) + self.dvm_config = dvm_config + self.admin_config = admin_config + self.options = options def is_input_supported(self, input_type, input_content): if input_type != "text": @@ -51,19 +50,19 @@ class ImageGenerationSDXL(DVMTaskInterface): prompt = "" negative_prompt = "" - if self.default_model is None: - model = "stabilityai/stable-diffusion-xl-base-1.0" + if self.options.get("default_model"): + model = self.options['default_model'] else: - model = self.default_model + model = "stabilityai/stable-diffusion-xl-base-1.0" ratio_width = "1" ratio_height = "1" width = "" height = "" - if self.default_lora == None: - lora = "" + if self.options.get("default_lora"): + lora = self.options['default_lora'] else: - lora = self.default_lora + lora = "" lora_weight = "" strength = "" guidance_scale = "" @@ -149,12 +148,12 @@ class ImageGenerationSDXL(DVMTaskInterface): def process(self, request_form): try: # Call the process route of NOVA-Server with our request form. - response = send_request_to_nova_server(request_form, os.environ["NOVA_SERVER"]) + response = send_request_to_nova_server(request_form, self.options['nova_server']) if bool(json.loads(response)['success']): print("Job " + request_form['jobID'] + " sent to nova-server") pool = ThreadPool(processes=1) - thread = pool.apply_async(check_nova_server_status, (request_form['jobID'], os.environ["NOVA_SERVER"])) + thread = pool.apply_async(check_nova_server_status, (request_form['jobID'], self.options['nova_server'])) print("Wait for results of NOVA-Server...") result = thread.get() return str(result) diff --git a/tasks/textextractionpdf.py b/tasks/textextractionpdf.py index c5ce214..80121d0 100644 --- a/tasks/textextractionpdf.py +++ b/tasks/textextractionpdf.py @@ -25,16 +25,18 @@ class TextExtractionPDF(DVMTaskInterface): TASK: str = "pdf-to-text" COST: int = 0 PK: str + DVM = DVM - def __init__(self, name, dvm_config: DVMConfig, admin_config: AdminConfig = None): + def __init__(self, name, dvm_config: DVMConfig, nip89d_tag: str, nip89info: str, admin_config: AdminConfig = None): self.NAME = name - dvm_config.SUPPORTED_TASKS = [self] - dvm_config.DB = "db/" + self.NAME + ".db" self.PK = dvm_config.PRIVATE_KEY - dvm = DVM - nostr_dvm_thread = Thread(target=dvm, args=[dvm_config, admin_config]) - nostr_dvm_thread.start() + dvm_config.SUPPORTED_TASKS = [self] + dvm_config.DB = "db/" + self.NAME + ".db" + dvm_config.NIP89 = self.NIP89_announcement(nip89d_tag, nip89info) + self.dvm_config = dvm_config + self.admin_config = admin_config + def is_input_supported(self, input_type, input_content): if input_type != "url" and input_type != "event": diff --git a/tasks/translation.py b/tasks/translation.py index 1511fbe..9596927 100644 --- a/tasks/translation.py +++ b/tasks/translation.py @@ -23,16 +23,18 @@ class Translation(DVMTaskInterface): TASK: str = "translation" COST: int = 0 PK: str + DVM = DVM - def __init__(self, name, dvm_config: DVMConfig, admin_config: AdminConfig = None): + def __init__(self, name, dvm_config: DVMConfig, nip89d_tag: str, nip89info: str, admin_config: AdminConfig = None): self.NAME = name - dvm_config.SUPPORTED_TASKS = [self] - dvm_config.DB = "db/" + self.NAME + ".db" self.PK = dvm_config.PRIVATE_KEY - dvm = DVM - nostr_dvm_thread = Thread(target=dvm, args=[dvm_config, admin_config]) - nostr_dvm_thread.start() + dvm_config.SUPPORTED_TASKS = [self] + dvm_config.DB = "db/" + self.NAME + ".db" + dvm_config.NIP89 = self.NIP89_announcement(nip89d_tag, nip89info) + self.dvm_config = dvm_config + self.admin_config = admin_config + def is_input_supported(self, input_type, input_content): if input_type != "event" and input_type != "job" and input_type != "text": diff --git a/test_client.py b/test_dvm_client.py similarity index 95% rename from test_client.py rename to test_dvm_client.py index 3083c96..8455a28 100644 --- a/test_client.py +++ b/test_dvm_client.py @@ -11,12 +11,10 @@ from utils.dvmconfig import DVMConfig from utils.nostr_utils import send_event from utils.definitions import EventDefinitions -import utils.env as env - # TODO HINT: Best use this path with a previously whitelisted privkey, as zapping events is not implemented in the lib/code def nostr_client_test_translation(input, kind, lang, sats, satsmax): - keys = Keys.from_sk_str(os.getenv(env.NOSTR_TEST_CLIENT_PRIVATE_KEY)) + keys = Keys.from_sk_str(os.getenv("NOSTR_TEST_CLIENT_PRIVATE_KEY")) if kind == "text": iTag = Tag.parse(["i", input, "text"]) elif kind == "event": @@ -43,7 +41,7 @@ def nostr_client_test_translation(input, kind, lang, sats, satsmax): def nostr_client_test_image(prompt): - keys = Keys.from_sk_str(os.getenv(env.NOSTR_TEST_CLIENT_PRIVATE_KEY)) + keys = Keys.from_sk_str(os.getenv("NOSTR_TEST_CLIENT_PRIVATE_KEY")) iTag = Tag.parse(["i", prompt, "text"]) outTag = Tag.parse(["output", "image/png;format=url"]) @@ -69,7 +67,7 @@ def nostr_client_test_image(prompt): return event.as_json() def nostr_client(): - keys = Keys.from_sk_str(os.getenv(env.NOSTR_TEST_CLIENT_PRIVATE_KEY)) + keys = Keys.from_sk_str(os.getenv("NOSTR_TEST_CLIENT_PRIVATE_KEY")) sk = keys.secret_key() pk = keys.public_key() print(f"Nostr Client public key: {pk.to_bech32()}, Hex: {pk.to_hex()} ") diff --git a/utils/admin_utils.py b/utils/admin_utils.py index 5063418..bde1110 100644 --- a/utils/admin_utils.py +++ b/utils/admin_utils.py @@ -10,7 +10,7 @@ from utils.nip89_utils import nip89_announce_tasks from utils.nostr_utils import send_event class AdminConfig: - REBROADCASTNIP89: bool = False + REBROADCAST_NIP89: bool = False WHITELISTUSER: bool = False UNWHITELISTUSER: bool = False BLACKLISTUSER: bool = False @@ -34,7 +34,7 @@ def admin_make_database_updates(adminconfig: AdminConfig = None, dvmconfig: DVMC db = dvmconfig.DB - rebroadcast_nip89 = adminconfig.REBROADCASTNIP89 + rebroadcast_nip89 = adminconfig.REBROADCAST_NIP89 cleandb = adminconfig.ClEANDB listdatabase = adminconfig.LISTDATABASE deleteuser = adminconfig.DELETEUSER diff --git a/utils/database_utils.py b/utils/database_utils.py index 47ba275..01f7c68 100644 --- a/utils/database_utils.py +++ b/utils/database_utils.py @@ -220,8 +220,8 @@ def fetch_user_metadata(sender, client) -> (str, str, str): name = metadata.get_display_name() if str(name) == "" or name is None: name = metadata.get_name() - nip05 = metadata.get_nip05() - lud16 = metadata.get_lud16() + nip05 = metadata.get_nip05() + lud16 = metadata.get_lud16() except: print("Couldn't get meta information") diff --git a/utils/definitions.py b/utils/definitions.py index 666868e..ceed65e 100644 --- a/utils/definitions.py +++ b/utils/definitions.py @@ -3,13 +3,9 @@ from dataclasses import dataclass from nostr_sdk import Event -from utils import env - NEW_USER_BALANCE: int = 250 # Free credits for new users - - class EventDefinitions: KIND_DM: int = 4 KIND_ZAP: int = 9735 @@ -41,8 +37,6 @@ class EventDefinitions: KIND_NIP90_RESULT_GENERIC] - - @dataclass class JobToWatch: event_id: str diff --git a/utils/dvmconfig.py b/utils/dvmconfig.py index 74fd5a2..5e16096 100644 --- a/utils/dvmconfig.py +++ b/utils/dvmconfig.py @@ -1,12 +1,11 @@ import os -from utils import env from utils.nip89_utils import NIP89Announcement class DVMConfig: SUPPORTED_TASKS = [] - PRIVATE_KEY: str = os.getenv(env.NOSTR_PRIVATE_KEY) + PRIVATE_KEY: str = os.getenv("NOSTR_PRIVATE_KEY") RELAY_LIST = ["wss://relay.damus.io", "wss://nostr-pub.wellorder.net", "wss://nos.lol", "wss://nostr.wine", "wss://relay.nostfiles.dev", "wss://nostr.mom", "wss://nostr.oxtr.dev", "wss://relay.nostr.bg", @@ -17,6 +16,7 @@ class DVMConfig: LNBITS_URL = 'https://lnbits.com' DB: str NIP89: NIP89Announcement + DM_ALLOWED = [] REQUIRES_NIP05: bool = False SHOW_RESULT_BEFORE_PAYMENT: bool = True # if this is true show results even when not paid right after autoprocess diff --git a/utils/env.py b/utils/env.py deleted file mode 100644 index 6945dfd..0000000 --- a/utils/env.py +++ /dev/null @@ -1,14 +0,0 @@ -NOSTR_PRIVATE_KEY = "NOSTR_PRIVATE_KEY" -NOSTR_TEST_CLIENT_PRIVATE_KEY = "NOSTR_TEST_CLIENT_PRIVATE_KEY" - -USER_DB_PATH = "USER_DB_PATH" - -LNBITS_INVOICE_KEY = "LNBITS_INVOICE_KEY" -LNBITS_HOST = "LNBITS_HOST" - -TASK_TRANSLATION_NIP89_DTAG = "TASK_TRANSLATION_NIP89_DTAG" -TASK_TEXTEXTRACTION_NIP89_DTAG = "TASK_TEXTEXTRACTION_NIP89_DTAG" -TASK_IMAGEGENERATION_NIP89_DTAG = "TASK_IMAGEGENERATION_NIP89_DTAG" -TASK_IMAGEGENERATION_NIP89_DTAG2 = "TASK_IMAGEGENERATION_NIP89_DTAG2" - - From f5c98d2c12399829fdebd35232208c5c75220572 Mon Sep 17 00:00:00 2001 From: Believethehype Date: Thu, 23 Nov 2023 12:32:44 +0100 Subject: [PATCH 15/18] adjustments for updating nostr sdk to 0.0.5 (but some delays appear) --- main.py | 2 +- requirements.txt | 2 +- utils/database_utils.py | 3 ++- utils/nostr_utils.py | 10 +++++++--- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/main.py b/main.py index d01efd1..dc999a3 100644 --- a/main.py +++ b/main.py @@ -12,7 +12,7 @@ from utils.dvmconfig import DVMConfig def run_nostr_dvm_with_local_config(): # We extract the Publickey from our bot, so the DVMs know who they should listen and react to. - bot_publickey = Keys.from_sk_str(os.getenv("BOT_PRIVATE_KEY")).public_key().to_hex() + bot_publickey = Keys.from_sk_str(os.getenv("BOT_PRIVATE_KEY")).public_key() # Spawn some DVMs in the playground and run them # You can add arbitrary DVMs there and instantiate them here diff --git a/requirements.txt b/requirements.txt index a1df8ea..82a7a88 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,7 +9,7 @@ ffmpegio-core==0.8.5 idna==3.4 inquirer==3.1.3 install==1.3.5 -nostr-sdk==0.0.4 +nostr-sdk==0.0.5 numpy==1.26.2 packaging==23.2 pandas==2.1.3 diff --git a/utils/database_utils.py b/utils/database_utils.py index 01f7c68..ebbc58d 100644 --- a/utils/database_utils.py +++ b/utils/database_utils.py @@ -212,7 +212,8 @@ def fetch_user_metadata(sender, client) -> (str, str, str): nip05 = "" lud16 = "" try: - profile_filter = Filter().kind(0).author(sender).limit(1) + pk = PublicKey.from_hex(sender) + profile_filter = Filter().kind(0).author(pk).limit(1) events = client.get_events_of([profile_filter], timedelta(seconds=3)) if len(events) > 0: ev = events[0] diff --git a/utils/nostr_utils.py b/utils/nostr_utils.py index adf5e2a..b30ee6e 100644 --- a/utils/nostr_utils.py +++ b/utils/nostr_utils.py @@ -1,15 +1,19 @@ from datetime import timedelta -from nostr_sdk import Keys, Filter, Client, Alphabet, EventId, Options, Event +from nostr_sdk import Keys, Filter, Client, Alphabet, EventId, Options, Event, PublicKey def get_event_by_id(event_id: str, client: Client, config=None) -> Event | None: split = event_id.split(":") if len(split) == 3: - id_filter = Filter().author(split[1]).custom_tag(Alphabet.D, [split[2]]) + pk = PublicKey.from_hex(split[1]) + id_filter = Filter().author(pk).custom_tag(Alphabet.D, [split[2]]) events = client.get_events_of([id_filter], timedelta(seconds=config.RELAY_TIMEOUT)) else: if str(event_id).startswith('note'): - event_id = EventId.from_bech32(event_id).to_hex() + event_id = EventId.from_bech32(event_id) + else: + event_id = EventId.from_hex(event_id) + id_filter = Filter().id(event_id).limit(1) events = client.get_events_of([id_filter], timedelta(seconds=config.RELAY_TIMEOUT)) if len(events) > 0: From 5ccdfd2671667a80c86da0c923690a6f2b5d28e7 Mon Sep 17 00:00:00 2001 From: Believethehype Date: Thu, 23 Nov 2023 13:30:09 +0100 Subject: [PATCH 16/18] renaming, fixes, client with opts --- .env_example | 7 +++++- bot.py | 38 +++++++++++++++++------------ dvm.py | 47 ++++++++++++++++++++++++------------ main.py | 2 +- tasks/imagegenerationsdxl.py | 2 +- tasks/textextractionpdf.py | 2 +- tasks/translation.py | 3 +-- utils/backend_utils.py | 6 ++--- utils/dvmconfig.py | 4 +-- 9 files changed, 68 insertions(+), 43 deletions(-) diff --git a/.env_example b/.env_example index a2cbb86..b88e846 100644 --- a/.env_example +++ b/.env_example @@ -1,3 +1,9 @@ +#This is needed for the test_client +NOSTR_TEST_CLIENT_PRIVATE_KEY = "a secret hex key for the test dvm client" +#This is needed for the (optional) bot +BOT_PRIVATE_KEY = "The private key for a test bot that communicates with dvms" + +#These are all for the playground and can be replaced and adjusted however needed NOSTR_PRIVATE_KEY = "a secret hexkey for some demo dvms" NOSTR_PRIVATE_KEY2 = "another secret hexkey for demo dvm with another key" BOT_PRIVATE_KEY = "The private key for a test bot that communicates with dvms" @@ -16,7 +22,6 @@ TASK_IMAGE_GENERATION_NIP89_DTAG = "fgdfgdf" TASK_IMAGE_GENERATION_NIP89_DTAG2 = "fdgdfg" #Backend Specific Options for tasks that require them - #nova-server is a local backend supporting some AI modules and needs to be installed separately, #if dvms supporting it should be used NOVA_SERVER = "127.0.0.1:37318" \ No newline at end of file diff --git a/bot.py b/bot.py index d23ead4..60d49b9 100644 --- a/bot.py +++ b/bot.py @@ -1,11 +1,13 @@ import json import time +from datetime import timedelta from threading import Thread -from nostr_sdk import Keys, Client, Timestamp, Filter, nip04_decrypt, HandleNotification, EventBuilder, PublicKey, Event +from nostr_sdk import Keys, Client, Timestamp, Filter, nip04_decrypt, HandleNotification, EventBuilder, PublicKey, \ + Event, Options from utils.admin_utils import admin_make_database_updates -from utils.database_utils import get_or_add_user, update_user_balance +from utils.database_utils import get_or_add_user, update_user_balance, create_sql_table from utils.definitions import EventDefinitions from utils.nostr_utils import send_event, get_event_by_id from utils.zap_utils import parse_amount_from_bolt11_invoice, check_for_zapplepay, decrypt_private_zap_message @@ -18,14 +20,19 @@ class Bot: self.dvm_config = dvm_config self.admin_config = admin_config self.keys = Keys.from_sk_str(dvm_config.PRIVATE_KEY) - self.client = Client(self.keys) + wait_for_send = True + skip_disconnected_relays = True + opts = (Options().wait_for_send(wait_for_send).send_timeout(timedelta(seconds=self.dvm_config.RELAY_TIMEOUT)) + .skip_disconnected_relays(skip_disconnected_relays)) + + self.client = Client.with_opts(self.keys, opts) self.job_list = [] pk = self.keys.public_key() self.dvm_config.DB = "db/bot.db" print("Nostr BOT public key: " + str(pk.to_bech32()) + " Hex: " + str(pk.to_hex()) + " Supported DVM tasks: " + - ', '.join(p.NAME + ":" + p.TASK for p in self.dvm_config.SUPPORTED_TASKS) + "\n") + ', '.join(p.NAME + ":" + p.TASK for p in self.dvm_config.SUPPORTED_DVMS) + "\n") for relay in self.dvm_config.RELAY_LIST: self.client.add_relay(relay) @@ -35,6 +42,7 @@ class Bot: Timestamp.now()) self.client.subscribe([dm_zap_filter]) + create_sql_table(self.dvm_config.DB) admin_make_database_updates(adminconfig=self.admin_config, dvmconfig=self.dvm_config, client=self.client) class NotificationHandler(HandleNotification): @@ -55,7 +63,6 @@ class Bot: return def handle_dm(nostr_event): - sender = nostr_event.pubkey().to_hex() try: decrypted_text = nip04_decrypt(self.keys.secret_key(), nostr_event.pubkey(), nostr_event.content()) # TODO more advanced logic, more parsing, just very basic test functions for now @@ -63,11 +70,11 @@ class Bot: index = int(decrypted_text.split(' ')[0]) - 1 i_tag = decrypted_text.replace(decrypted_text.split(' ')[0] + " ", "") - keys = Keys.from_sk_str(self.dvm_config.SUPPORTED_TASKS[index].PK) + keys = Keys.from_sk_str(self.dvm_config.SUPPORTED_DVMS[index].PK) params = { "sender": nostr_event.pubkey().to_hex(), "input": i_tag, - "task": self.dvm_config.SUPPORTED_TASKS[index].TASK + "task": self.dvm_config.SUPPORTED_DVMS[index].TASK } message = json.dumps(params) evt = EventBuilder.new_encrypted_direct_msg(self.keys, keys.public_key(), @@ -77,20 +84,20 @@ class Bot: elif decrypted_text.startswith('{"result":'): dvm_result = json.loads(decrypted_text) + # user = get_or_add_user(db=self.dvm_config.DB, npub=dvm_result["sender"], client=self.client) + # print("BOT received and forwarded to " + user.name + ": " + str(decrypted_text)) + reply_event = EventBuilder.new_encrypted_direct_msg(self.keys, + PublicKey.from_hex(dvm_result["sender"]), + dvm_result["result"], + None).to_event(self.keys) + send_event(reply_event, client=self.client, dvm_config=dvm_config) - job_event = EventBuilder.new_encrypted_direct_msg(self.keys, - PublicKey.from_hex(dvm_result["sender"]), - dvm_result["result"], - None).to_event(self.keys) - send_event(job_event, client=self.client, dvm_config=dvm_config) - user = get_or_add_user(db=self.dvm_config.DB, npub=dvm_result["sender"], client=self.client) - print("BOT received and forwarded to " + user.name + ": " + str(decrypted_text)) else: message = "DVMs that I support:\n\n" index = 1 - for p in self.dvm_config.SUPPORTED_TASKS: + for p in self.dvm_config.SUPPORTED_DVMS: message += str(index) + " " + p.NAME + " " + p.TASK + "\n" index += 1 @@ -136,7 +143,6 @@ class Bot: anon = True print("Anonymous Zap received. Unlucky, I don't know from whom, and never will") user = get_or_add_user(self.dvm_config.DB, sender, client=self.client) - print(str(user.name)) if zapped_event is not None: if not anon: diff --git a/dvm.py b/dvm.py index 6201646..b392181 100644 --- a/dvm.py +++ b/dvm.py @@ -1,7 +1,8 @@ import json +from datetime import timedelta from nostr_sdk import PublicKey, Keys, Client, Tag, Event, EventBuilder, Filter, HandleNotification, Timestamp, \ - init_logger, LogLevel, nip04_decrypt + init_logger, LogLevel, nip04_decrypt, EventId, Options import time @@ -35,14 +36,20 @@ class DVM: self.dvm_config = dvmconfig self.admin_config = adminconfig self.keys = Keys.from_sk_str(dvmconfig.PRIVATE_KEY) - self.client = Client(self.keys) + wait_for_send = True + skip_disconnected_relays = True + opts = (Options().wait_for_send(wait_for_send).send_timeout(timedelta(seconds=self.dvm_config.RELAY_TIMEOUT)) + .skip_disconnected_relays(skip_disconnected_relays)) + + self.client = Client.with_opts(self.keys, opts) + self.job_list = [] self.jobs_on_hold_list = [] pk = self.keys.public_key() print("Nostr DVM public key: " + str(pk.to_bech32()) + " Hex: " + str(pk.to_hex()) + " Supported DVM tasks: " + - ', '.join(p.NAME + ":" + p.TASK for p in self.dvm_config.SUPPORTED_TASKS) + "\n") + ', '.join(p.NAME + ":" + p.TASK for p in self.dvm_config.SUPPORTED_DVMS) + "\n") for relay in self.dvm_config.RELAY_LIST: self.client.add_relay(relay) @@ -50,10 +57,9 @@ class DVM: zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_ZAP]).since(Timestamp.now()) bot_dm_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM]).authors(self.dvm_config.DM_ALLOWED).since(Timestamp.now()) - #TODO only from allowed account kinds = [EventDefinitions.KIND_NIP90_GENERIC] - for dvm in self.dvm_config.SUPPORTED_TASKS: + for dvm in self.dvm_config.SUPPORTED_DVMS: if dvm.KIND not in kinds: kinds.append(dvm.KIND) dvm_filter = (Filter().kinds(kinds).since(Timestamp.now())) @@ -96,7 +102,7 @@ class DVM: return task_is_free = False - for dvm in self.dvm_config.SUPPORTED_TASKS: + for dvm in self.dvm_config.SUPPORTED_DVMS: if dvm.TASK == task and dvm.COST == 0: task_is_free = True @@ -161,7 +167,7 @@ class DVM: anon = True print("Anonymous Zap received. Unlucky, I don't know from whom, and never will") user = get_or_add_user(self.dvm_config.DB, sender, client=self.client) - print(str(user)) + if zapped_event is not None: if zapped_event.kind() == EventDefinitions.KIND_FEEDBACK: # if a reaction by us got zapped @@ -228,12 +234,19 @@ class DVM: decrypted_text = nip04_decrypt(self.keys.secret_key(), dm_event.pubkey(), dm_event.content()) ob = json.loads(decrypted_text) - #TODO SOME PARSING, OPTIONS, ZAP HANDLING + # One key might host multiple DVMs, so we check current task + if ob['task'] == self.dvm_config.SUPPORTED_DVMS[0].TASK: + input_type = "text" + print(decrypted_text) + if str(ob['input']).startswith("http"): + input_type = "url" + #elif str(ob['input']).startswith("nostr:nevent"): + # ob['input'] = str(ob['input']).replace("nostr:", "") + # ob['input'] = EventId.from_bech32(ob['input']).to_hex() + # input_type = "event" - # One key might host multiple dvms, so we check current task - if ob['task'] == self.dvm_config.SUPPORTED_TASKS[0].TASK: - j_tag = Tag.parse(["j", self.dvm_config.SUPPORTED_TASKS[0].TASK]) - i_tag = Tag.parse(["i", ob['input'], "text"]) + j_tag = Tag.parse(["j", self.dvm_config.SUPPORTED_DVMS[0].TASK]) + i_tag = Tag.parse(["i", ob['input'], input_type]) tags = [j_tag, i_tag] tags.append(Tag.parse(["y", dm_event.pubkey().to_hex()])) tags.append(Tag.parse(["z", ob['sender']])) @@ -294,7 +307,9 @@ class DVM: try: post_processed_content = post_process_result(data, original_event) + if is_from_bot: + # Reply to Bot for tag in original_event.tags(): if tag.as_vec()[0] == "y": # TODO we temporally use internal tags to move information receiver_key = PublicKey.from_hex(tag.as_vec()[1]) @@ -306,9 +321,11 @@ class DVM: "sender": original_sender } message = json.dumps(params) + print(message) response_event = EventBuilder.new_encrypted_direct_msg(self.keys, receiver_key, message, None).to_event(self.keys) send_event(response_event, client=self.client, dvm_config=self.dvm_config) else: + #Regular DVM reply send_nostr_reply_event(post_processed_content, original_event_str) except Exception as e: respond_to_error(str(e), original_event_str, False) @@ -427,13 +444,11 @@ class DVM: return event.as_json() def do_work(job_event, is_from_bot=False): - if (( - EventDefinitions.KIND_NIP90_EXTRACT_TEXT <= job_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC) + if ((EventDefinitions.KIND_NIP90_EXTRACT_TEXT <= job_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC) or job_event.kind() == EventDefinitions.KIND_DM): task = get_task(job_event, client=self.client, dvmconfig=self.dvm_config) - result = "" - for dvm in self.dvm_config.SUPPORTED_TASKS: + for dvm in self.dvm_config.SUPPORTED_DVMS: try: if task == dvm.TASK: request_form = dvm.create_request_form_from_nostr_event(job_event, self.client, diff --git a/main.py b/main.py index d01efd1..8057378 100644 --- a/main.py +++ b/main.py @@ -41,7 +41,7 @@ def run_nostr_dvm_with_local_config(): bot_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") bot_config.LNBITS_URL = os.getenv("LNBITS_HOST") # Finally we add some of the DVMs we created before to the Bot and start it. - bot_config.SUPPORTED_TASKS = [sketcher, unstable_artist, translator] + bot_config.SUPPORTED_DVMS = [sketcher, unstable_artist, translator] bot = Bot nostr_dvm_thread = Thread(target=bot, args=[bot_config]) diff --git a/tasks/imagegenerationsdxl.py b/tasks/imagegenerationsdxl.py index 0af0be0..24e6027 100644 --- a/tasks/imagegenerationsdxl.py +++ b/tasks/imagegenerationsdxl.py @@ -32,7 +32,7 @@ class ImageGenerationSDXL(DVMTaskInterface): self.NAME = name self.PK = dvm_config.PRIVATE_KEY - dvm_config.SUPPORTED_TASKS = [self] + dvm_config.SUPPORTED_DVMS = [self] dvm_config.DB = "db/" + self.NAME + ".db" dvm_config.NIP89 = self.NIP89_announcement(nip89d_tag, nip89info) self.dvm_config = dvm_config diff --git a/tasks/textextractionpdf.py b/tasks/textextractionpdf.py index 80121d0..3e4453a 100644 --- a/tasks/textextractionpdf.py +++ b/tasks/textextractionpdf.py @@ -31,7 +31,7 @@ class TextExtractionPDF(DVMTaskInterface): self.NAME = name self.PK = dvm_config.PRIVATE_KEY - dvm_config.SUPPORTED_TASKS = [self] + dvm_config.SUPPORTED_DVMS = [self] dvm_config.DB = "db/" + self.NAME + ".db" dvm_config.NIP89 = self.NIP89_announcement(nip89d_tag, nip89info) self.dvm_config = dvm_config diff --git a/tasks/translation.py b/tasks/translation.py index 9596927..331d984 100644 --- a/tasks/translation.py +++ b/tasks/translation.py @@ -29,13 +29,12 @@ class Translation(DVMTaskInterface): self.NAME = name self.PK = dvm_config.PRIVATE_KEY - dvm_config.SUPPORTED_TASKS = [self] + dvm_config.SUPPORTED_DVMS = [self] dvm_config.DB = "db/" + self.NAME + ".db" dvm_config.NIP89 = self.NIP89_announcement(nip89d_tag, nip89info) self.dvm_config = dvm_config self.admin_config = admin_config - def is_input_supported(self, input_type, input_content): if input_type != "event" and input_type != "job" and input_type != "text": return False diff --git a/utils/backend_utils.py b/utils/backend_utils.py index bd56e96..22617d0 100644 --- a/utils/backend_utils.py +++ b/utils/backend_utils.py @@ -83,12 +83,12 @@ def check_task_is_supported(event, client, get_duration=False, config=None): print("Output format not supported, skipping..") return False, "", 0 - for dvm in dvm_config.SUPPORTED_TASKS: + for dvm in dvm_config.SUPPORTED_DVMS: if dvm.TASK == task: if not dvm.is_input_supported(input_type, event.content()): return False, task, duration - if task not in (x.TASK for x in dvm_config.SUPPORTED_TASKS): + if task not in (x.TASK for x in dvm_config.SUPPORTED_DVMS): return False, task, duration return True, task, duration @@ -118,7 +118,7 @@ def check_url_is_readable(url): def get_amount_per_task(task, dvm_config, duration=1): - for dvm in dvm_config.SUPPORTED_TASKS: #this is currently just one + for dvm in dvm_config.SUPPORTED_DVMS: #this is currently just one if dvm.TASK == task: amount = dvm.COST * duration return amount diff --git a/utils/dvmconfig.py b/utils/dvmconfig.py index 5e16096..679597e 100644 --- a/utils/dvmconfig.py +++ b/utils/dvmconfig.py @@ -4,14 +4,14 @@ from utils.nip89_utils import NIP89Announcement class DVMConfig: - SUPPORTED_TASKS = [] + SUPPORTED_DVMS= [] PRIVATE_KEY: str = os.getenv("NOSTR_PRIVATE_KEY") RELAY_LIST = ["wss://relay.damus.io", "wss://nostr-pub.wellorder.net", "wss://nos.lol", "wss://nostr.wine", "wss://relay.nostfiles.dev", "wss://nostr.mom", "wss://nostr.oxtr.dev", "wss://relay.nostr.bg", "wss://relay.f7z.io"] - RELAY_TIMEOUT = 5 + RELAY_TIMEOUT = 3 LNBITS_INVOICE_KEY = '' LNBITS_URL = 'https://lnbits.com' DB: str From 215916c1ef05939b8b72e2a0bbe0ba4202472941 Mon Sep 17 00:00:00 2001 From: Believethehype Date: Fri, 24 Nov 2023 17:20:29 +0100 Subject: [PATCH 17/18] added dall-e, reworked bot, added nip89config --- .env_example | 3 + .idea/dataSources.xml | 36 +++++ backends/nova_server.py | 2 +- bot.py | 139 +++++++++++++----- dvm.py | 11 +- interfaces/dvmtaskinterface.py | 17 +-- main.py | 20 ++- playground.py | 80 ++++++++-- tasks/imagegeneration_openai_dalle.py | 112 ++++++++++++++ ...erationsdxl.py => imagegeneration_sdxl.py} | 10 +- tasks/textextractionpdf.py | 5 +- tasks/translation.py | 5 +- test_dvm_client.py | 4 +- utils/backend_utils.py | 2 +- utils/database_utils.py | 58 ++++++-- utils/nip89_utils.py | 5 + 16 files changed, 410 insertions(+), 99 deletions(-) create mode 100644 tasks/imagegeneration_openai_dalle.py rename tasks/{imagegenerationsdxl.py => imagegeneration_sdxl.py} (95%) diff --git a/.env_example b/.env_example index b88e846..6468622 100644 --- a/.env_example +++ b/.env_example @@ -20,8 +20,11 @@ TASK_TEXT_EXTRACTION_NIP89_DTAG = "asdd" TASK_TRANSLATION_NIP89_DTAG = "abcded" TASK_IMAGE_GENERATION_NIP89_DTAG = "fgdfgdf" TASK_IMAGE_GENERATION_NIP89_DTAG2 = "fdgdfg" +TASK_IMAGE_GENERATION_NIP89_DTAG3 = "asdasd" + #Backend Specific Options for tasks that require them #nova-server is a local backend supporting some AI modules and needs to be installed separately, #if dvms supporting it should be used +OPENAI_API_KEY = "your-openai-api-key" NOVA_SERVER = "127.0.0.1:37318" \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index 211f211..a9eee01 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -8,5 +8,41 @@ jdbc:sqlite:$PROJECT_DIR$/nostrzaps.db $ProjectFileDir$ + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:$PROJECT_DIR$/db/bot.db + $ProjectFileDir$ + + + file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.43.0/org/xerial/sqlite-jdbc/3.43.0.0/sqlite-jdbc-3.43.0.0.jar + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:$PROJECT_DIR$/db/Dall-E 3.db + $ProjectFileDir$ + + + file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.43.0/org/xerial/sqlite-jdbc/3.43.0.0/sqlite-jdbc-3.43.0.0.jar + + + + + sqlite.xerial + true + org.sqlite.JDBC + jdbc:sqlite:$PROJECT_DIR$/db/Translator.db + $ProjectFileDir$ + + + file://$APPLICATION_CONFIG_DIR$/jdbc-drivers/Xerial SQLiteJDBC/3.43.0/org/xerial/sqlite-jdbc/3.43.0.0/sqlite-jdbc-3.43.0.0.jar + + + \ No newline at end of file diff --git a/backends/nova_server.py b/backends/nova_server.py index 885e352..a961082 100644 --- a/backends/nova_server.py +++ b/backends/nova_server.py @@ -71,7 +71,7 @@ def check_nova_server_status(jobID, address): result = "" url_fetch = 'http://' + address + '/fetch_result' print("Fetching Results from NOVA-Server...") - data = {"jobID": jobID} + data = {"jobID": jobID, "delete_after_download": True} response = requests.post(url_fetch, headers=headers, data=data) content_type = response.headers['content-type'] print("Content-type: " + str(content_type)) diff --git a/bot.py b/bot.py index 60d49b9..493415f 100644 --- a/bot.py +++ b/bot.py @@ -1,22 +1,22 @@ import json import time from datetime import timedelta -from threading import Thread from nostr_sdk import Keys, Client, Timestamp, Filter, nip04_decrypt, HandleNotification, EventBuilder, PublicKey, \ Event, Options from utils.admin_utils import admin_make_database_updates -from utils.database_utils import get_or_add_user, update_user_balance, create_sql_table +from utils.backend_utils import get_amount_per_task +from utils.database_utils import get_or_add_user, update_user_balance, create_sql_table, update_sql_table, User from utils.definitions import EventDefinitions from utils.nostr_utils import send_event, get_event_by_id from utils.zap_utils import parse_amount_from_bolt11_invoice, check_for_zapplepay, decrypt_private_zap_message class Bot: - job_list: list - def __init__(self, dvm_config, admin_config=None): + self.NAME = "Bot" + dvm_config.DB = "db/" + self.NAME + ".db" self.dvm_config = dvm_config self.admin_config = admin_config self.keys = Keys.from_sk_str(dvm_config.PRIVATE_KEY) @@ -24,23 +24,22 @@ class Bot: skip_disconnected_relays = True opts = (Options().wait_for_send(wait_for_send).send_timeout(timedelta(seconds=self.dvm_config.RELAY_TIMEOUT)) .skip_disconnected_relays(skip_disconnected_relays)) - self.client = Client.with_opts(self.keys, opts) - self.job_list = [] pk = self.keys.public_key() - self.dvm_config.DB = "db/bot.db" - print("Nostr BOT public key: " + str(pk.to_bech32()) + " Hex: " + str(pk.to_hex()) + " Supported DVM tasks: " + + print("Nostr BOT public key: " + str(pk.to_bech32()) + " Hex: " + str(pk.to_hex()) + " Name: " + self.NAME + " Supported DVM tasks: " + ', '.join(p.NAME + ":" + p.TASK for p in self.dvm_config.SUPPORTED_DVMS) + "\n") for relay in self.dvm_config.RELAY_LIST: self.client.add_relay(relay) self.client.connect() - dm_zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM, EventDefinitions.KIND_ZAP]).since( + zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_ZAP]).since(Timestamp.now()) + dm_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM]).since( Timestamp.now()) - self.client.subscribe([dm_zap_filter]) + + self.client.subscribe([zap_filter, dm_filter]) create_sql_table(self.dvm_config.DB) admin_make_database_updates(adminconfig=self.admin_config, dvmconfig=self.dvm_config, client=self.client) @@ -52,59 +51,127 @@ class Bot: def handle(self, relay_url, nostr_event): if EventDefinitions.KIND_DM: - print( - "[Bot] " + f"Received new DM from {relay_url}: {nostr_event.as_json()}") handle_dm(nostr_event) elif nostr_event.kind() == EventDefinitions.KIND_ZAP: - print("yay zap") handle_zap(nostr_event) def handle_msg(self, relay_url, msg): return def handle_dm(nostr_event): + sender = nostr_event.pubkey().to_hex() + try: decrypted_text = nip04_decrypt(self.keys.secret_key(), nostr_event.pubkey(), nostr_event.content()) - # TODO more advanced logic, more parsing, just very basic test functions for now + user = get_or_add_user(db=self.dvm_config.DB, npub=sender, client=self.client) + + # user = User + # user.npub = sender + # user.balance = 250 + # user.iswhitelisted = False + # user.isblacklisted = False + # user.name = "Test" + # user.nip05 = "Test@test" + # user.lud16 = "Test@test" + + # We do a selection of tasks now, maybe change this later, Idk. if decrypted_text[0].isdigit(): index = int(decrypted_text.split(' ')[0]) - 1 - i_tag = decrypted_text.replace(decrypted_text.split(' ')[0] + " ", "") + task = self.dvm_config.SUPPORTED_DVMS[index].TASK + print("["+ self.NAME + "] Request from " + str(user.name) + " (" + str(user.nip05) + ", Balance: "+ str(user.balance)+ " Sats) Task: " + str(task)) - keys = Keys.from_sk_str(self.dvm_config.SUPPORTED_DVMS[index].PK) - params = { - "sender": nostr_event.pubkey().to_hex(), - "input": i_tag, - "task": self.dvm_config.SUPPORTED_DVMS[index].TASK - } - message = json.dumps(params) - evt = EventBuilder.new_encrypted_direct_msg(self.keys, keys.public_key(), - message, None).to_event(self.keys) - send_event(evt, client=self.client, dvm_config=dvm_config) + required_amount = self.dvm_config.SUPPORTED_DVMS[index].COST + if user.isblacklisted: + # For some reason an admin might blacklist npubs, e.g. for abusing the service + evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(), + "Your are currently blocked from all services.", + None).to_event(self.keys) + send_event(evt, client=self.client, dvm_config=dvm_config) + + elif user.iswhitelisted or user.balance >= required_amount or required_amount == 0: + if not user.iswhitelisted: + + balance = max(user.balance - required_amount, 0) + update_sql_table(db=self.dvm_config.DB, npub=user.npub, balance=balance, + iswhitelisted=user.iswhitelisted, isblacklisted=user.isblacklisted, + nip05=user.nip05, lud16=user.lud16, name=user.name, + lastactive=Timestamp.now().as_secs()) + + evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(), + "Your Job is now scheduled. New balance is " + + str(balance) + + " Sats.\nI will DM you once I'm done " + "processing.", + nostr_event.id()).to_event(self.keys) + else: + evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(), + "Your Job is now scheduled. As you are " + "whitelisted, your balance remains at" + + str(user.balance) + " Sats.\n" + "I will DM you once I'm " + "done processing.", + nostr_event.id()).to_event(self.keys) + + print("[" + self.NAME + "] Replying " + user.name + " with \"scheduled\" confirmation") + time.sleep(2.0) + send_event(evt, client=self.client, dvm_config=dvm_config) + + i_tag = decrypted_text.replace(decrypted_text.split(' ')[0] + " ", "") + # TODO more advanced logic, more parsing, params etc, just very basic test functions for now + dvm_keys = Keys.from_sk_str(self.dvm_config.SUPPORTED_DVMS[index].PK) + params = { + "sender": nostr_event.pubkey().to_hex(), + "input": i_tag, + "task": self.dvm_config.SUPPORTED_DVMS[index].TASK + } + message = json.dumps(params) + evt = EventBuilder.new_encrypted_direct_msg(self.keys, dvm_keys.public_key(), + message, None).to_event(self.keys) + print("[" + self.NAME + "] Forwarding task " + self.dvm_config.SUPPORTED_DVMS[index].TASK + + " for user " + user.name + " to " + self.dvm_config.SUPPORTED_DVMS[index].NAME) + send_event(evt, client=self.client, dvm_config=dvm_config) + else: + print("payment-required") + time.sleep(2.0) + evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(), + "Balance required, please zap me with at least " + str( + int(required_amount - user.balance)) + + " Sats, then try again.", + nostr_event.id()).to_event(self.keys) + time.sleep(2.0) + send_event(evt, client=self.client, dvm_config=dvm_config) + + + # TODO if we receive the result from one of the dvms, need some better management, maybe check for keys elif decrypted_text.startswith('{"result":'): + dvm_result = json.loads(decrypted_text) - # user = get_or_add_user(db=self.dvm_config.DB, npub=dvm_result["sender"], client=self.client) - # print("BOT received and forwarded to " + user.name + ": " + str(decrypted_text)) + user_npub_hex = dvm_result["sender"] + user = get_or_add_user(db=self.dvm_config.DB, npub=user_npub_hex, client=self.client) + print("[" + self.NAME + "] Received results, message to orignal sender " + user.name) reply_event = EventBuilder.new_encrypted_direct_msg(self.keys, - PublicKey.from_hex(dvm_result["sender"]), + PublicKey.from_hex(user.npub), dvm_result["result"], None).to_event(self.keys) + time.sleep(2.0) send_event(reply_event, client=self.client, dvm_config=dvm_config) - - else: + print("Message from " + user.name + ": " + decrypted_text) message = "DVMs that I support:\n\n" index = 1 for p in self.dvm_config.SUPPORTED_DVMS: - message += str(index) + " " + p.NAME + " " + p.TASK + "\n" + message += str(index) + " " + p.NAME + " " + p.TASK + " " + str(p.COST) + " Sats" + "\n" index += 1 evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(), message + "\nSelect an Index and provide an input (" "e.g. 1 A purple ostrich)", - nostr_event.id()).to_event(self.keys) + None).to_event(self.keys) + #nostr_event.id()).to_event(self.keys) + time.sleep(3) send_event(evt, client=self.client, dvm_config=dvm_config) except Exception as e: print(e) @@ -126,11 +193,11 @@ class Bot: zap_request_event = Event.from_json(tag.as_vec()[1]) sender = check_for_zapplepay(zap_request_event.pubkey().to_hex(), zap_request_event.content()) - for ztag in zap_request_event.tags(): - if ztag.as_vec()[0] == 'anon': - if len(ztag.as_vec()) > 1: + for z_tag in zap_request_event.tags(): + if z_tag.as_vec()[0] == 'anon': + if len(z_tag.as_vec()) > 1: print("Private Zap received.") - decrypted_content = decrypt_private_zap_message(ztag.as_vec()[1], + decrypted_content = decrypt_private_zap_message(z_tag.as_vec()[1], self.keys.secret_key(), zap_request_event.pubkey()) decrypted_private_event = Event.from_json(decrypted_content) diff --git a/dvm.py b/dvm.py index b392181..9c0db4b 100644 --- a/dvm.py +++ b/dvm.py @@ -240,10 +240,6 @@ class DVM: print(decrypted_text) if str(ob['input']).startswith("http"): input_type = "url" - #elif str(ob['input']).startswith("nostr:nevent"): - # ob['input'] = str(ob['input']).replace("nostr:", "") - # ob['input'] = EventId.from_bech32(ob['input']).to_hex() - # input_type = "event" j_tag = Tag.parse(["j", self.dvm_config.SUPPORTED_DVMS[0].TASK]) i_tag = Tag.parse(["i", ob['input'], input_type]) @@ -370,12 +366,11 @@ class DVM: elif tag.as_vec()[0] == "i": task = tag.as_vec()[1] - user = get_from_sql_table(self.dvm_config.DB, sender) + user = get_or_add_user(self.dvm_config.DB, sender, self.client) if not user.iswhitelisted: amount = int(user.balance) + get_amount_per_task(task, self.dvm_config) - update_sql_table(self.dvm_config.DB, sender, amount, user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16, - user.name, - Timestamp.now().as_secs()) + update_sql_table(self.dvm_config.DB, sender, amount, user.iswhitelisted, user.isblacklisted, + user.nip05, user.lud16, user.name, Timestamp.now().as_secs()) message = "There was the following error : " + content + ". Credits have been reimbursed" else: # User didn't pay, so no reimbursement diff --git a/interfaces/dvmtaskinterface.py b/interfaces/dvmtaskinterface.py index 32efa2c..0c4f0a3 100644 --- a/interfaces/dvmtaskinterface.py +++ b/interfaces/dvmtaskinterface.py @@ -3,7 +3,7 @@ from threading import Thread from utils.admin_utils import AdminConfig from utils.dvmconfig import DVMConfig -from utils.nip89_utils import NIP89Announcement +from utils.nip89_utils import NIP89Announcement, NIP89Config from dvm import DVM @@ -17,19 +17,20 @@ class DVMTaskInterface: dvm_config: DVMConfig admin_config: AdminConfig - def NIP89_announcement(self, d_tag, content): + def NIP89_announcement(self, nip89config: NIP89Config): nip89 = NIP89Announcement() nip89.name = self.NAME nip89.kind = self.KIND nip89.pk = self.PK - nip89.dtag = d_tag - nip89.content = content + nip89.dtag = nip89config.DTAG + nip89.content = nip89config.CONTENT return nip89 def run(self): nostr_dvm_thread = Thread(target=self.DVM, args=[self.dvm_config, self.admin_config]) nostr_dvm_thread.start() + def is_input_supported(self, input_type, input_content) -> bool: """Check if input is supported for current Task.""" pass @@ -50,12 +51,4 @@ class DVMTaskInterface: opts = json.loads(request_form["options"]) print(opts) - # old format, deprecated, will remove - elif request_form.get("optStr"): - opts = [] - for k, v in [option.split("=") for option in request_form["optStr"].split(";")]: - t = (k, v) - opts.append(t) - print("...done.") - return dict(opts) diff --git a/main.py b/main.py index d968c40..718b06d 100644 --- a/main.py +++ b/main.py @@ -1,4 +1,7 @@ import os +import signal +import sys +import time from pathlib import Path from threading import Thread @@ -6,7 +9,7 @@ import dotenv from nostr_sdk import Keys from bot import Bot -from playground import build_pdf_extractor, build_translator, build_unstable_diffusion, build_sketcher +from playground import build_pdf_extractor, build_translator, build_unstable_diffusion, build_sketcher, build_dalle from utils.dvmconfig import DVMConfig @@ -34,6 +37,9 @@ def run_nostr_dvm_with_local_config(): sketcher = build_sketcher("Sketcher", [bot_publickey]) sketcher.run() + dalle = build_dalle("Dall-E 3", [bot_publickey]) + dalle.run() + # We will run an optional bot that can communicate with the DVMs # Note this is very basic for now and still under development bot_config = DVMConfig() @@ -41,20 +47,26 @@ def run_nostr_dvm_with_local_config(): bot_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") bot_config.LNBITS_URL = os.getenv("LNBITS_HOST") # Finally we add some of the DVMs we created before to the Bot and start it. - bot_config.SUPPORTED_DVMS = [sketcher, unstable_artist, translator] + bot_config.SUPPORTED_DVMS = [sketcher, unstable_artist, dalle, translator] bot = Bot nostr_dvm_thread = Thread(target=bot, args=[bot_config]) nostr_dvm_thread.start() + # Keep the main function alive for libraries like openai + try: + while True: + time.sleep(10) + except KeyboardInterrupt: + print('Stay weird!') + os.kill(os.getpid(), signal.SIGKILL) + 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} ') - run_nostr_dvm_with_local_config() diff --git a/playground.py b/playground.py index 6dfab48..d3978c1 100644 --- a/playground.py +++ b/playground.py @@ -1,11 +1,32 @@ import json import os -from tasks.imagegenerationsdxl import ImageGenerationSDXL +from tasks.imagegeneration_openai_dalle import ImageGenerationDALLE +from tasks.imagegeneration_sdxl import ImageGenerationSDXL from tasks.textextractionpdf import TextExtractionPDF from tasks.translation import Translation from utils.admin_utils import AdminConfig from utils.dvmconfig import DVMConfig +from utils.nip89_utils import NIP89Config +""" +This File is a playground to create DVMs. It shows some examples of DVMs that make use of the modules in the tasks folder +These DVMs should be considered examples and will be extended in the future. env variables are used to not commit keys, +but if used privatley, these can also be directly filled in this file. The main.py function calls some of the functions +defined here and starts the DVMs. + +Note that the admin_config is optional, and if given commands as defined in admin_utils will be called at start of the +DVM. For example the NIP89 event can be rebroadcasted (store the d_tag somewhere). + +DM_ALLOWED is used to tell the DVM to which npubs it should listen to. We use this here to listen to our bot, +as defined in main.py to perform jobs on it's behalf and reply. + +if LNBITS_INVOICE_KEY is not set (=""), the DVM is still zappable but a lud16 address in required in the profile. + +additional options can be set, for example to preinitalize vaiables or give parameters that are required to perform a +task, for example an address or an API key. + + +""" # Generate an optional Admin Config, in this case, whenever we give our DVMs this config, they will (re)broadcast # their NIP89 announcement @@ -20,7 +41,6 @@ def build_pdf_extractor(name, dm_allowed_keys): dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") dvm_config.DM_ALLOWED = dm_allowed_keys # Add NIP89 - d_tag = os.getenv("TASK_TEXT_EXTRACTION_NIP89_DTAG") nip90params = {} nip89info = { "name": name, @@ -28,7 +48,11 @@ def build_pdf_extractor(name, dm_allowed_keys): "about": "I extract text from pdf documents", "nip90Params": nip90params } - return TextExtractionPDF(name=name, dvm_config=dvm_config, nip89d_tag=d_tag, nip89info=json.dumps(nip89info), + + nip89config = NIP89Config() + nip89config.DTAG = os.getenv("TASK_TEXT_EXTRACTION_NIP89_DTAG") + nip89config.CONTENT = json.dumps(nip89info) + return TextExtractionPDF(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config) @@ -39,7 +63,6 @@ def build_translator(name, dm_allowed_keys): dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") dvm_config.DM_ALLOWED = dm_allowed_keys - d_tag = os.getenv("TASK_TRANSLATION_NIP89_DTAG") nip90params = { "language": { "required": False, @@ -59,7 +82,10 @@ def build_translator(name, dm_allowed_keys): "input into the language defined in params.", "nip90Params": nip90params } - return Translation(name=name, dvm_config=dvm_config, nip89d_tag=d_tag, nip89info=json.dumps(nip89info), + nip89config = NIP89Config() + nip89config.DTAG = os.getenv("TASK_TRANSLATION_NIP89_DTAG") + nip89config.CONTENT = json.dumps(nip89info) + return Translation(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config) @@ -74,7 +100,6 @@ def build_unstable_diffusion(name, dm_allowed_keys): # address it should use. These parameters can be freely defined in the task component options = {'default_model': "unstable", 'nova_server': os.getenv("NOVA_SERVER")} - d_tag = os.getenv("TASK_IMAGE_GENERATION_NIP89_DTAG") nip90params = { "negative_prompt": { "required": False, @@ -91,7 +116,10 @@ def build_unstable_diffusion(name, dm_allowed_keys): "about": "I draw images based on a prompt with a Model called unstable diffusion", "nip90Params": nip90params } - return ImageGenerationSDXL(name=name, dvm_config=dvm_config, nip89d_tag=d_tag, nip89info=json.dumps(nip89info), + nip89config = NIP89Config() + nip89config.DTAG = os.getenv("TASK_IMAGE_GENERATION_NIP89_DTAG") + nip89config.CONTENT = json.dumps(nip89info) + return ImageGenerationSDXL(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config, options=options) @@ -102,7 +130,6 @@ def build_sketcher(name, dm_allowed_keys): dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") dvm_config.DM_ALLOWED = dm_allowed_keys - d_tag = os.getenv("TASK_IMAGE_GENERATION_NIP89_DTAG2") nip90params = { "negative_prompt": { "required": False, @@ -124,6 +151,39 @@ def build_sketcher(name, dm_allowed_keys): # address it should use. These parameters can be freely defined in the task component options = {'default_model': "mohawk", 'default_lora': "timburton", 'nova_server': os.getenv("NOVA_SERVER")} + nip89config = NIP89Config() + nip89config.DTAG = os.getenv("TASK_IMAGE_GENERATION_NIP89_DTAG2") + nip89config.CONTENT = json.dumps(nip89info) # We add an optional AdminConfig for this one, and tell the dvm to rebroadcast its NIP89 - return ImageGenerationSDXL(name=name, dvm_config=dvm_config, nip89d_tag=d_tag, nip89info=json.dumps(nip89info), - admin_config=admin_config, options=options) \ No newline at end of file + return ImageGenerationSDXL(name=name, dvm_config=dvm_config, nip89config=nip89config, + admin_config=admin_config, options=options) + +def build_dalle(name, dm_allowed_keys): + dvm_config = DVMConfig() + dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY3") + dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") + dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") + dvm_config.DM_ALLOWED = dm_allowed_keys + + nip90params = { + "size": { + "required": False, + "values": ["1024:1024", "1024x1792", "1792x1024"] + } + } + nip89info = { + "name": name, + "image": "https://image.nostr.build/c33ca6fc4cc038ca4adb46fdfdfda34951656f87ee364ef59095bae1495ce669.jpg", + "about": "I use OpenAI's DALL·E 3", + "nip90Params": nip90params + } + + # A module might have options it can be initialized with, here we set a default model, lora and the nova-server + # address it should use. These parameters can be freely defined in the task component + + nip89config = NIP89Config() + nip89config.DTAG = os.getenv("TASK_IMAGE_GENERATION_NIP89_DTAG3") + nip89config.CONTENT = json.dumps(nip89info) + # We add an optional AdminConfig for this one, and tell the dvm to rebroadcast its NIP89 + return ImageGenerationDALLE(name=name, dvm_config=dvm_config, nip89config=nip89config, + admin_config=admin_config) \ No newline at end of file diff --git a/tasks/imagegeneration_openai_dalle.py b/tasks/imagegeneration_openai_dalle.py new file mode 100644 index 0000000..8d81aee --- /dev/null +++ b/tasks/imagegeneration_openai_dalle.py @@ -0,0 +1,112 @@ +import json +import os +import time +from multiprocessing.pool import ThreadPool +from threading import Thread + +from backends.nova_server import check_nova_server_status, send_request_to_nova_server +from dvm import DVM +from interfaces.dvmtaskinterface import DVMTaskInterface +from utils.admin_utils import AdminConfig +from utils.definitions import EventDefinitions +from utils.dvmconfig import DVMConfig +from utils.nip89_utils import NIP89Config + +""" +This File contains a Module to transform Text input on NOVA-Server and receive results back. + +Accepted Inputs: Prompt (text) +Outputs: An url to an Image +Params: -model # models: juggernaut, dynavision, colossusProject, newreality, unstable + -lora # loras (weights on top of models) voxel, +""" + + +class ImageGenerationDALLE(DVMTaskInterface): + NAME: str = "" + KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE + TASK: str = "text-to-image" + COST: int = 120 + PK: str + DVM = DVM + + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None, + options=None): + self.NAME = name + self.PK = dvm_config.PRIVATE_KEY + + dvm_config.SUPPORTED_DVMS = [self] + dvm_config.DB = "db/" + self.NAME + ".db" + dvm_config.NIP89 = self.NIP89_announcement(nip89config) + self.dvm_config = dvm_config + self.admin_config = admin_config + self.options = options + + def is_input_supported(self, input_type, input_content): + if input_type != "text": + return False + return True + + def create_request_form_from_nostr_event(self, event, client=None, dvm_config=None): + request_form = {"jobID": event.id().to_hex() + "_" + self.NAME.replace(" ", "")} + prompt = "" + width = "1024" + height = "1024" + model = "dall-e-3" + quality = "standard" + + for tag in event.tags(): + if tag.as_vec()[0] == 'i': + input_type = tag.as_vec()[2] + if input_type == "text": + prompt = tag.as_vec()[1] + + elif tag.as_vec()[0] == 'param': + print("Param: " + tag.as_vec()[1] + ": " + tag.as_vec()[2]) + if tag.as_vec()[1] == "size": + if len(tag.as_vec()) > 3: + width = (tag.as_vec()[2]) + height = (tag.as_vec()[3]) + elif len(tag.as_vec()) == 3: + split = tag.as_vec()[2].split("x") + if len(split) > 1: + width = split[0] + height = split[1] + elif tag.as_vec()[1] == "model": + model = tag.as_vec()[2] + elif tag.as_vec()[1] == "quality": + quality = tag.as_vec()[2] + + options = { + "prompt": prompt, + "size": width + "x" + height, + "model": model, + "quality": quality, + "number": 1 + } + request_form['options'] = json.dumps(options) + + return request_form + + def process(self, request_form): + try: + options = DVMTaskInterface.set_options(request_form) + + from openai import OpenAI + client = OpenAI() + print("Job " + request_form['jobID'] + " sent to OpenAI API..") + + response = client.images.generate( + model=options['model'], + prompt=options['prompt'], + size=options['size'], + quality=options['quality'], + n=int(options['number']), + ) + + image_url = response.data[0].url + return image_url + + except Exception as e: + print("Error in Module") + raise Exception(e) diff --git a/tasks/imagegenerationsdxl.py b/tasks/imagegeneration_sdxl.py similarity index 95% rename from tasks/imagegenerationsdxl.py rename to tasks/imagegeneration_sdxl.py index 24e6027..3ad712e 100644 --- a/tasks/imagegenerationsdxl.py +++ b/tasks/imagegeneration_sdxl.py @@ -1,7 +1,5 @@ import json -import os from multiprocessing.pool import ThreadPool -from threading import Thread from backends.nova_server import check_nova_server_status, send_request_to_nova_server from dvm import DVM @@ -9,6 +7,7 @@ from interfaces.dvmtaskinterface import DVMTaskInterface from utils.admin_utils import AdminConfig from utils.definitions import EventDefinitions from utils.dvmconfig import DVMConfig +from utils.nip89_utils import NIP89Config """ This File contains a Module to transform Text input on NOVA-Server and receive results back. @@ -28,13 +27,13 @@ class ImageGenerationSDXL(DVMTaskInterface): PK: str DVM = DVM - def __init__(self, name, dvm_config: DVMConfig, nip89d_tag: str, nip89info: str, admin_config: AdminConfig = None, options=None): + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None, options=None): self.NAME = name self.PK = dvm_config.PRIVATE_KEY dvm_config.SUPPORTED_DVMS = [self] dvm_config.DB = "db/" + self.NAME + ".db" - dvm_config.NIP89 = self.NIP89_announcement(nip89d_tag, nip89info) + dvm_config.NIP89 = self.NIP89_announcement(nip89config) self.dvm_config = dvm_config self.admin_config = admin_config self.options = options @@ -94,7 +93,6 @@ class ImageGenerationSDXL(DVMTaskInterface): ratio_height = split[1] # if size is set it will overwrite ratio. elif tag.as_vec()[1] == "size": - if len(tag.as_vec()) > 3: width = (tag.as_vec()[2]) height = (tag.as_vec()[3]) @@ -150,7 +148,7 @@ class ImageGenerationSDXL(DVMTaskInterface): # Call the process route of NOVA-Server with our request form. response = send_request_to_nova_server(request_form, self.options['nova_server']) if bool(json.loads(response)['success']): - print("Job " + request_form['jobID'] + " sent to nova-server") + print("Job " + request_form['jobID'] + " sent to NOVA-server") pool = ThreadPool(processes=1) thread = pool.apply_async(check_nova_server_status, (request_form['jobID'], self.options['nova_server'])) diff --git a/tasks/textextractionpdf.py b/tasks/textextractionpdf.py index 3e4453a..59b6c52 100644 --- a/tasks/textextractionpdf.py +++ b/tasks/textextractionpdf.py @@ -8,6 +8,7 @@ from interfaces.dvmtaskinterface import DVMTaskInterface from utils.admin_utils import AdminConfig from utils.definitions import EventDefinitions from utils.dvmconfig import DVMConfig +from utils.nip89_utils import NIP89Config from utils.nostr_utils import get_event_by_id """ @@ -27,13 +28,13 @@ class TextExtractionPDF(DVMTaskInterface): PK: str DVM = DVM - def __init__(self, name, dvm_config: DVMConfig, nip89d_tag: str, nip89info: str, admin_config: AdminConfig = None): + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None): self.NAME = name self.PK = dvm_config.PRIVATE_KEY dvm_config.SUPPORTED_DVMS = [self] dvm_config.DB = "db/" + self.NAME + ".db" - dvm_config.NIP89 = self.NIP89_announcement(nip89d_tag, nip89info) + dvm_config.NIP89 = self.NIP89_announcement(nip89config) self.dvm_config = dvm_config self.admin_config = admin_config diff --git a/tasks/translation.py b/tasks/translation.py index 331d984..08d5cc1 100644 --- a/tasks/translation.py +++ b/tasks/translation.py @@ -6,6 +6,7 @@ from interfaces.dvmtaskinterface import DVMTaskInterface from utils.admin_utils import AdminConfig from utils.definitions import EventDefinitions from utils.dvmconfig import DVMConfig +from utils.nip89_utils import NIP89Config from utils.nostr_utils import get_referenced_event_by_id, get_event_by_id """ @@ -25,13 +26,13 @@ class Translation(DVMTaskInterface): PK: str DVM = DVM - def __init__(self, name, dvm_config: DVMConfig, nip89d_tag: str, nip89info: str, admin_config: AdminConfig = None): + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None): self.NAME = name self.PK = dvm_config.PRIVATE_KEY dvm_config.SUPPORTED_DVMS = [self] dvm_config.DB = "db/" + self.NAME + ".db" - dvm_config.NIP89 = self.NIP89_announcement(nip89d_tag, nip89info) + dvm_config.NIP89 = self.NIP89_announcement(nip89config) self.dvm_config = dvm_config self.admin_config = admin_config diff --git a/test_dvm_client.py b/test_dvm_client.py index 8455a28..55d15a7 100644 --- a/test_dvm_client.py +++ b/test_dvm_client.py @@ -84,11 +84,11 @@ def nostr_client(): EventDefinitions.KIND_FEEDBACK]).since(Timestamp.now())) # public events client.subscribe([dm_zap_filter, dvm_filter]) - #nostr_client_test_translation("This is the result of the DVM in spanish", "text", "es", 20, 20) + nostr_client_test_translation("This is the result of the DVM in spanish", "text", "es", 20, 20) #nostr_client_test_translation("note1p8cx2dz5ss5gnk7c59zjydcncx6a754c0hsyakjvnw8xwlm5hymsnc23rs", "event", "es", 20,20) #nostr_client_test_translation("44a0a8b395ade39d46b9d20038b3f0c8a11168e67c442e3ece95e4a1703e2beb", "event", "zh", 20, 20) - nostr_client_test_image("a beautiful purple ostrich watching the sunset") + #nostr_client_test_image("a beautiful purple ostrich watching the sunset") class NotificationHandler(HandleNotification): def handle(self, relay_url, event): print(f"Received new event from {relay_url}: {event.as_json()}") diff --git a/utils/backend_utils.py b/utils/backend_utils.py index 22617d0..b9b1281 100644 --- a/utils/backend_utils.py +++ b/utils/backend_utils.py @@ -123,5 +123,5 @@ def get_amount_per_task(task, dvm_config, duration=1): amount = dvm.COST * duration return amount else: - print("[Nostr] Task " + task + " is currently not supported by this instance, skipping") + print("["+dvm_config.SUPPORTED_DVMS[0].NAME +"] Task " + task + " is currently not supported by this instance, skipping") return None diff --git a/utils/database_utils.py b/utils/database_utils.py index ebbc58d..4c13f04 100644 --- a/utils/database_utils.py +++ b/utils/database_utils.py @@ -1,4 +1,5 @@ # DATABASE LOGIC +import json import sqlite3 import time @@ -7,7 +8,7 @@ from dataclasses import dataclass from datetime import timedelta from logging import Filter -from nostr_sdk import Timestamp, Keys, PublicKey, EventBuilder, Metadata, Filter +from nostr_sdk import Timestamp, Keys, PublicKey, EventBuilder, Metadata, Filter, Options, Client from utils.definitions import NEW_USER_BALANCE from utils.nostr_utils import send_event @@ -68,11 +69,11 @@ def add_to_sql_table(db, npub, sats, iswhitelisted, isblacklisted, nip05, lud16, print(e) -def update_sql_table(db, npub, sats, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive): +def update_sql_table(db, npub, balance, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive): try: con = sqlite3.connect(db) cur = con.cursor() - data = (sats, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive, npub) + data = (balance, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive, npub) cur.execute(""" UPDATE users SET sats = ? , @@ -97,8 +98,7 @@ def get_from_sql_table(db, npub): row = cur.fetchone() con.close() if row is None: - user = None - return user + return None else: user = User user.npub = row[0] @@ -192,29 +192,55 @@ def update_user_balance(db, sender, sats, client, config): def get_or_add_user(db, npub, client): user = get_from_sql_table(db, npub) if user is None: - name, nip05, lud16 = fetch_user_metadata(npub, client) - print("Adding User: " + name + " (" + npub +")") + print("Adding User: " + npub + " (" + npub + ")") add_to_sql_table(db, npub, NEW_USER_BALANCE, False, False, nip05, lud16, name, Timestamp.now().as_secs()) user = get_from_sql_table(db, npub) - else: - # update Name, Nip05 and lud16 lnaddress - user.name, user.nip05, user.lud16 = fetch_user_metadata(npub, client) - update_sql_table(db, user.npub, user.balance, user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16, - user.name, Timestamp.now().as_secs()) + return user return user -def fetch_user_metadata(sender, client) -> (str, str, str): +def fetch_user_metadata(npub, client): + name = "" + nip05 = "" + lud16 = "" + + # Get metadata + pk = PublicKey.from_hex(npub) + print(f"\nGetting profile metadata for {pk.to_bech32()}...") + filter = Filter().kind(0).author(pk).limit(1) + events = client.get_events_of([filter], timedelta(seconds=3)) + if len(events) > 0: + latest_entry = events[0] + newest = 0 + for entry in events: + if entry.created_at().as_secs() > newest: + newest = entry.created_at().as_secs() + latest_entry = entry + + print(latest_entry.content()) + profile = json.loads(latest_entry.content()) + if profile.get("name"): + name = profile['name'] + if profile.get("nip05"): + nip05 = profile['nip05'] + if profile.get("lud16"): + lud16 = profile['lud16'] + + return name, nip05, lud16 + + +def fetch_user_metadata2(sender, client) -> (str, str, str): name = "" nip05 = "" lud16 = "" try: pk = PublicKey.from_hex(sender) + print(f"\nGetting profile metadata for {pk.to_bech32()}...") profile_filter = Filter().kind(0).author(pk).limit(1) - events = client.get_events_of([profile_filter], timedelta(seconds=3)) + events = client.get_events_of([profile_filter], timedelta(seconds=1)) if len(events) > 0: ev = events[0] metadata = Metadata.from_json(ev.content()) @@ -223,9 +249,11 @@ def fetch_user_metadata(sender, client) -> (str, str, str): name = metadata.get_name() nip05 = metadata.get_nip05() lud16 = metadata.get_lud16() + else: + print("Profile not found") + return name, nip05, lud16 except: print("Couldn't get meta information") return name, nip05, lud16 - diff --git a/utils/nip89_utils.py b/utils/nip89_utils.py index ddcd11f..be918dd 100644 --- a/utils/nip89_utils.py +++ b/utils/nip89_utils.py @@ -11,6 +11,11 @@ class NIP89Announcement: content: str +class NIP89Config: + DTAG: str + CONTENT: str + + def nip89_announce_tasks(dvm_config, client): k_tag = Tag.parse(["k", str(dvm_config.NIP89.kind)]) d_tag = Tag.parse(["d", dvm_config.NIP89.dtag]) From 6be3372d76a1a1ea7dbad5d7c310a3d2d4b5e872 Mon Sep 17 00:00:00 2001 From: Believethehype Date: Fri, 24 Nov 2023 21:29:24 +0100 Subject: [PATCH 18/18] basic bot is working, spawn dvms dependent on config --- .env_example | 6 +- README.md | 33 +++++--- bot.py | 95 ++++++++++------------ dvm.py | 85 ++++++++------------ interfaces/dvmtaskinterface.py | 13 ++++ main.py | 64 ++++++++------- playground.py | 34 +++++++- tasks/imagegeneration_openai_dalle.py | 11 +-- tasks/imagegeneration_sdxl.py | 12 +-- tasks/textextractionpdf.py | 15 +--- tasks/translation.py | 16 +--- utils/admin_utils.py | 2 +- utils/database_utils.py | 108 +++++++++----------------- utils/definitions.py | 4 - utils/dvmconfig.py | 5 +- utils/zap_utils.py | 39 +++++++++- 16 files changed, 263 insertions(+), 279 deletions(-) diff --git a/.env_example b/.env_example index 6468622..2779cf8 100644 --- a/.env_example +++ b/.env_example @@ -24,7 +24,5 @@ TASK_IMAGE_GENERATION_NIP89_DTAG3 = "asdasd" #Backend Specific Options for tasks that require them -#nova-server is a local backend supporting some AI modules and needs to be installed separately, -#if dvms supporting it should be used -OPENAI_API_KEY = "your-openai-api-key" -NOVA_SERVER = "127.0.0.1:37318" \ No newline at end of file +OPENAI_API_KEY = "" # Enter your OpenAI API Key to use DVMs with OpenAI services +NOVA_SERVER = "" # Enter the address of a nova-server instance, locally or on a machine in your network host:port \ No newline at end of file diff --git a/README.md b/README.md index 9a6ba7e..3910177 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,28 @@ -# NostrAI Data Vending Machine +# NostrAI: Nostr NIP90 Data Vending Machine Framework -This example DVM implementation in Python currently supports simple translations using Google translate, as well as extraction of text from links with pdf files. +This framework provides a way to easily build and/or run `Nostr NIP90 DVMs in Python`. -At a later stage, additional example tasks will be added, as well as the integration into a larger Machine Learning backend +This project is currently under development and additional tasks and features are added along the way. +This means the project is in alpha status, interfaces might still change/break. -Place .env file (based on .env_example) in main folder, install requirements.txt (python 3.10) run main.py. Optionally supports LNbits to create invoices instead of lnaddresses. +## To get started: +(Tested on Python 3.10) -Use vendata.io to create a nip89 announcement of your dvm and save the dtag in your .env config. +Create a new venv by running `"python -m venv venv"` + - Place .env file (based on .env_example) in main folder. + - Set your own private hex keys, create NIP89 dtags on vendata.io, + - Install requirements.txt + - Run python main.py. -A tutorial on how to add additional tasks, as well as the larger server backend will be added soon. +In `playground.py` some DVMs are already prepared. Feel free to play along with the existing ones. +You can also add new tasks by using the interface, just like the existing tasks in the `tasks` folder. -Known Issues: -- After refactoring DVMs work independent from each other for the most part. - - Some functions might work easier than they did before (need some refactoring) -- Bot currently not implemented -- Some basic functionality is still missing, e.g. handling various mediasources -- Interface might still change a lot and brick things. +A `bot` is running by default that lists and communicates with the `DVMs` added to it, +so your DVMs can be controled via any regular client as well. + +The Framework optionally supports `LNbits` to create invoices instead of using a `lightning address`. If LNBits is not used, +make sure your nostr accounts have a valid lightning address. + + +A tutorial on how to add additional tasks, as well as the larger server backend will be added at a later stage. diff --git a/bot.py b/bot.py index 493415f..55c5b7e 100644 --- a/bot.py +++ b/bot.py @@ -1,6 +1,7 @@ import json import time from datetime import timedelta +from threading import Thread from nostr_sdk import Keys, Client, Timestamp, Filter, nip04_decrypt, HandleNotification, EventBuilder, PublicKey, \ Event, Options @@ -10,7 +11,8 @@ from utils.backend_utils import get_amount_per_task from utils.database_utils import get_or_add_user, update_user_balance, create_sql_table, update_sql_table, User from utils.definitions import EventDefinitions from utils.nostr_utils import send_event, get_event_by_id -from utils.zap_utils import parse_amount_from_bolt11_invoice, check_for_zapplepay, decrypt_private_zap_message +from utils.zap_utils import parse_amount_from_bolt11_invoice, check_for_zapplepay, decrypt_private_zap_message, \ + parse_zap_event_tags class Bot: @@ -28,7 +30,8 @@ class Bot: pk = self.keys.public_key() - print("Nostr BOT public key: " + str(pk.to_bech32()) + " Hex: " + str(pk.to_hex()) + " Name: " + self.NAME + " Supported DVM tasks: " + + print("Nostr BOT public key: " + str(pk.to_bech32()) + " Hex: " + str(pk.to_hex()) + " Name: " + self.NAME + + " Supported DVM tasks: " + ', '.join(p.NAME + ":" + p.TASK for p in self.dvm_config.SUPPORTED_DVMS) + "\n") for relay in self.dvm_config.RELAY_LIST: @@ -36,8 +39,7 @@ class Bot: self.client.connect() zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_ZAP]).since(Timestamp.now()) - dm_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM]).since( - Timestamp.now()) + dm_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM]).since(Timestamp.now()) self.client.subscribe([zap_filter, dm_filter]) @@ -50,7 +52,7 @@ class Bot: keys = self.keys def handle(self, relay_url, nostr_event): - if EventDefinitions.KIND_DM: + if nostr_event.kind() == EventDefinitions.KIND_DM: handle_dm(nostr_event) elif nostr_event.kind() == EventDefinitions.KIND_ZAP: handle_zap(nostr_event) @@ -63,24 +65,20 @@ class Bot: try: decrypted_text = nip04_decrypt(self.keys.secret_key(), nostr_event.pubkey(), nostr_event.content()) - user = get_or_add_user(db=self.dvm_config.DB, npub=sender, client=self.client) + user = get_or_add_user(db=self.dvm_config.DB, npub=sender, client=self.client, config=self.dvm_config) - # user = User - # user.npub = sender - # user.balance = 250 - # user.iswhitelisted = False - # user.isblacklisted = False - # user.name = "Test" - # user.nip05 = "Test@test" - # user.lud16 = "Test@test" # We do a selection of tasks now, maybe change this later, Idk. if decrypted_text[0].isdigit(): index = int(decrypted_text.split(' ')[0]) - 1 task = self.dvm_config.SUPPORTED_DVMS[index].TASK - print("["+ self.NAME + "] Request from " + str(user.name) + " (" + str(user.nip05) + ", Balance: "+ str(user.balance)+ " Sats) Task: " + str(task)) + print("[" + self.NAME + "] Request from " + str(user.name) + " (" + str(user.nip05) + ", Balance: " + + str(user.balance)+ " Sats) Task: " + str(task)) + + duration = 1 + required_amount = get_amount_per_task(self.dvm_config.SUPPORTED_DVMS[index].TASK, + self.dvm_config, duration) - required_amount = self.dvm_config.SUPPORTED_DVMS[index].COST if user.isblacklisted: # For some reason an admin might blacklist npubs, e.g. for abusing the service @@ -135,8 +133,8 @@ class Bot: print("payment-required") time.sleep(2.0) evt = EventBuilder.new_encrypted_direct_msg(self.keys, nostr_event.pubkey(), - "Balance required, please zap me with at least " + str( - int(required_amount - user.balance)) + "Balance required, please zap me with at least " + + str(int(required_amount - user.balance)) + " Sats, then try again.", nostr_event.id()).to_event(self.keys) time.sleep(2.0) @@ -149,7 +147,8 @@ class Bot: dvm_result = json.loads(decrypted_text) user_npub_hex = dvm_result["sender"] - user = get_or_add_user(db=self.dvm_config.DB, npub=user_npub_hex, client=self.client) + user = get_or_add_user(db=self.dvm_config.DB, npub=user_npub_hex, + client=self.client, config=self.dvm_config) print("[" + self.NAME + "] Received results, message to orignal sender " + user.name) reply_event = EventBuilder.new_encrypted_direct_msg(self.keys, PublicKey.from_hex(user.npub), @@ -159,7 +158,7 @@ class Bot: send_event(reply_event, client=self.client, dvm_config=dvm_config) else: - print("Message from " + user.name + ": " + decrypted_text) + print("[" + self.NAME + "] Message from " + user.name + ": " + decrypted_text) message = "DVMs that I support:\n\n" index = 1 for p in self.dvm_config.SUPPORTED_DVMS: @@ -173,61 +172,45 @@ class Bot: #nostr_event.id()).to_event(self.keys) time.sleep(3) send_event(evt, client=self.client, dvm_config=dvm_config) - except Exception as e: - print(e) + except Exception as e: + pass + # TODO we still receive (broken content) events after fetching the metadata, but we don't listen to them. + # probably in client.get_events_of in fetch_user_metadata + print("Error in bot " + str(e)) def handle_zap(zap_event): - zapped_event = None - invoice_amount = 0 - anon = False - sender = zap_event.pubkey() - print("Zap received") + print("[" + self.NAME + "] Zap received") try: - for tag in zap_event.tags(): - if tag.as_vec()[0] == 'bolt11': - invoice_amount = parse_amount_from_bolt11_invoice(tag.as_vec()[1]) - elif tag.as_vec()[0] == 'e': - zapped_event = get_event_by_id(tag.as_vec()[1], client=self.client, config=self.dvm_config) - elif tag.as_vec()[0] == 'description': - zap_request_event = Event.from_json(tag.as_vec()[1]) - sender = check_for_zapplepay(zap_request_event.pubkey().to_hex(), - zap_request_event.content()) - for z_tag in zap_request_event.tags(): - if z_tag.as_vec()[0] == 'anon': - if len(z_tag.as_vec()) > 1: - print("Private Zap received.") - decrypted_content = decrypt_private_zap_message(z_tag.as_vec()[1], - self.keys.secret_key(), - zap_request_event.pubkey()) - decrypted_private_event = Event.from_json(decrypted_content) - if decrypted_private_event.kind() == 9733: - sender = decrypted_private_event.pubkey().to_hex() - message = decrypted_private_event.content() - if message != "": - print("Zap Message: " + message) - else: - anon = True - print("Anonymous Zap received. Unlucky, I don't know from whom, and never will") - user = get_or_add_user(self.dvm_config.DB, sender, client=self.client) + invoice_amount, zapped_event, sender, anon = parse_zap_event_tags(zap_event, + self.keys, self.NAME, + self.client, self.dvm_config) + + user = get_or_add_user(self.dvm_config.DB, sender, client=self.client, config=self.dvm_config) if zapped_event is not None: if not anon: - print("Note Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( + print("[" + self.NAME + "] Note Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( user.name)) update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, config=self.dvm_config) # a regular note elif not anon: - print("Profile Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( + print("[" + self.NAME + "] Profile Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( user.name)) update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, config=self.dvm_config) except Exception as e: - print(f"Error during content decryption: {e}") + print("[" + self.NAME + "] Error during content decryption:" + str(e)) self.client.handle_notifications(NotificationHandler()) while True: time.sleep(1.0) + + def run(self): + bot = Bot + nostr_dvm_thread = Thread(target=bot, args=[self.dvm_config]) + nostr_dvm_thread.start() + diff --git a/dvm.py b/dvm.py index 9c0db4b..d352346 100644 --- a/dvm.py +++ b/dvm.py @@ -15,15 +15,13 @@ from utils.database_utils import update_sql_table, get_from_sql_table, \ from utils.nostr_utils import get_event_by_id, get_referenced_event_by_id, send_event from utils.output_utils import post_process_result, build_status_reaction from utils.zap_utils import check_bolt11_ln_bits_is_paid, parse_amount_from_bolt11_invoice, \ - check_for_zapplepay, decrypt_private_zap_message, create_bolt11_ln_bits + check_for_zapplepay, decrypt_private_zap_message, create_bolt11_ln_bits, parse_zap_event_tags use_logger = False if use_logger: init_logger(LogLevel.DEBUG) - - class DVM: dvm_config: DVMConfig admin_config: AdminConfig @@ -32,7 +30,7 @@ class DVM: job_list: list jobs_on_hold_list: list - def __init__(self, dvmconfig, adminconfig = None): + def __init__(self, dvmconfig, adminconfig=None): self.dvm_config = dvmconfig self.admin_config = adminconfig self.keys = Keys.from_sk_str(dvmconfig.PRIVATE_KEY) @@ -56,7 +54,8 @@ class DVM: self.client.connect() zap_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_ZAP]).since(Timestamp.now()) - bot_dm_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM]).authors(self.dvm_config.DM_ALLOWED).since(Timestamp.now()) + bot_dm_filter = Filter().pubkey(pk).kinds([EventDefinitions.KIND_DM]).authors(self.dvm_config.DM_ALLOWED).since( + Timestamp.now()) kinds = [EventDefinitions.KIND_NIP90_GENERIC] for dvm in self.dvm_config.SUPPORTED_DVMS: @@ -75,7 +74,8 @@ class DVM: def handle(self, relay_url, nostr_event): if EventDefinitions.KIND_NIP90_EXTRACT_TEXT <= nostr_event.kind() <= EventDefinitions.KIND_NIP90_GENERIC: - print("[" + self.dvm_config.NIP89.name + "] " + f"Received new NIP90 Job Request from {relay_url}: {nostr_event.as_json()}") + print( + "[" + self.dvm_config.NIP89.name + "] " + f"Received new NIP90 Job Request from {relay_url}: {nostr_event.as_json()}") handle_nip90_job_event(nostr_event) elif nostr_event.kind() == EventDefinitions.KIND_ZAP: handle_zap(nostr_event) @@ -86,7 +86,8 @@ class DVM: return def handle_nip90_job_event(nip90_event): - user = get_or_add_user(self.dvm_config.DB, nip90_event.pubkey().to_hex(), client=self.client) + user = get_or_add_user(self.dvm_config.DB, nip90_event.pubkey().to_hex(), client=self.client, + config=self.dvm_config) task_supported, task, duration = check_task_is_supported(nip90_event, client=self.client, get_duration=(not user.iswhitelisted), config=self.dvm_config) @@ -107,7 +108,8 @@ class DVM: task_is_free = True if user.iswhitelisted or task_is_free: - print("[" + self.dvm_config.NIP89.name + "] Free task or Whitelisted for task " + task + ". Starting processing..") + print( + "[" + self.dvm_config.NIP89.name + "] Free task or Whitelisted for task " + task + ". Starting processing..") send_job_status_reaction(nip90_event, "processing", True, 0, client=self.client, dvm_config=self.dvm_config) do_work(nip90_event, is_from_bot=False) @@ -118,7 +120,8 @@ class DVM: if tag.as_vec()[0] == 'bid': bid = int(tag.as_vec()[1]) - print("[" + self.dvm_config.NIP89.name + "] Payment required: New Nostr " + task + " Job event: " + nip90_event.as_json()) + print( + "[" + self.dvm_config.NIP89.name + "] Payment required: New Nostr " + task + " Job event: " + nip90_event.as_json()) if bid > 0: bid_offer = int(bid / 1000) if bid_offer >= amount: @@ -127,47 +130,21 @@ class DVM: client=self.client, dvm_config=self.dvm_config) else: # If there is no bid, just request server rate from user - print("[" + self.dvm_config.NIP89.name + "] Requesting payment for Event: " + nip90_event.id().to_hex()) + print( + "[" + self.dvm_config.NIP89.name + "] Requesting payment for Event: " + nip90_event.id().to_hex()) send_job_status_reaction(nip90_event, "payment-required", False, amount, client=self.client, dvm_config=self.dvm_config) else: print("Task not supported on this DVM, skipping..") def handle_zap(zap_event): - zapped_event = None - invoice_amount = 0 - anon = False - sender = zap_event.pubkey() print("Zap received") try: - for tag in zap_event.tags(): - if tag.as_vec()[0] == 'bolt11': - invoice_amount = parse_amount_from_bolt11_invoice(tag.as_vec()[1]) - elif tag.as_vec()[0] == 'e': - zapped_event = get_event_by_id(tag.as_vec()[1], client=self.client, config=self.dvm_config) - elif tag.as_vec()[0] == 'description': - zap_request_event = Event.from_json(tag.as_vec()[1]) - sender = check_for_zapplepay(zap_request_event.pubkey().to_hex(), - zap_request_event.content()) - for ztag in zap_request_event.tags(): - if ztag.as_vec()[0] == 'anon': - if len(ztag.as_vec()) > 1: - print("Private Zap received.") - decrypted_content = decrypt_private_zap_message(ztag.as_vec()[1], - self.keys.secret_key(), - zap_request_event.pubkey()) - decrypted_private_event = Event.from_json(decrypted_content) - if decrypted_private_event.kind() == 9733: - sender = decrypted_private_event.pubkey().to_hex() - message = decrypted_private_event.content() - if message != "": - print("Zap Message: " + message) - else: - anon = True - print("Anonymous Zap received. Unlucky, I don't know from whom, and never will") - user = get_or_add_user(self.dvm_config.DB, sender, client=self.client) - + invoice_amount, zapped_event, sender, anon = parse_zap_event_tags(zap_event, + self.keys, self.dvm_config.NIP89.name, + self.client, self.dvm_config) + user = get_or_add_user(db=self.dvm_config.DB, npub=sender, client=self.client, config=self.dvm_config) if zapped_event is not None: if zapped_event.kind() == EventDefinitions.KIND_FEEDBACK: # if a reaction by us got zapped @@ -219,13 +196,15 @@ class DVM: elif not anon: print("Note Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( user.name)) - update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, config=self.dvm_config) + update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, + config=self.dvm_config) # a regular note elif not anon: print("Profile Zap received for Bot balance: " + str(invoice_amount) + " Sats from " + str( user.name)) - update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, config=self.dvm_config) + update_user_balance(self.dvm_config.DB, sender, invoice_amount, client=self.client, + config=self.dvm_config) except Exception as e: print(f"Error during content decryption: {e}") @@ -264,7 +243,8 @@ class DVM: input = tag.as_vec()[1] input_type = tag.as_vec()[2] if input_type == "job": - evt = get_referenced_event_by_id(event_id=input, client=client, kinds=EventDefinitions.ANY_RESULT, + evt = get_referenced_event_by_id(event_id=input, client=client, + kinds=EventDefinitions.ANY_RESULT, dvm_config=dvmconfig) if evt is None: if append: @@ -287,7 +267,7 @@ class DVM: x.result = data x.is_processed = True if self.dvm_config.SHOW_RESULT_BEFORE_PAYMENT and not is_paid: - send_nostr_reply_event(data, original_event_str,) + send_nostr_reply_event(data, original_event_str, ) send_job_status_reaction(original_event, "success", amount, dvm_config=self.dvm_config) # or payment-required, or both? elif not self.dvm_config.SHOW_RESULT_BEFORE_PAYMENT and not is_paid: @@ -318,10 +298,11 @@ class DVM: } message = json.dumps(params) print(message) - response_event = EventBuilder.new_encrypted_direct_msg(self.keys, receiver_key, message, None).to_event(self.keys) + response_event = EventBuilder.new_encrypted_direct_msg(self.keys, receiver_key, message, + None).to_event(self.keys) send_event(response_event, client=self.client, dvm_config=self.dvm_config) else: - #Regular DVM reply + # Regular DVM reply send_nostr_reply_event(post_processed_content, original_event_str) except Exception as e: respond_to_error(str(e), original_event_str, False) @@ -347,7 +328,8 @@ class DVM: response_kind = original_event.kind() + 1000 reply_event = EventBuilder(response_kind, str(content), reply_tags).to_event(key) send_event(reply_event, client=self.client, dvm_config=self.dvm_config) - print("[" + self.dvm_config.NIP89.name + "] " + str(response_kind) + " Job Response event sent: " + reply_event.as_json()) + print("[" + self.dvm_config.NIP89.name + "] " + str( + response_kind) + " Job Response event sent: " + reply_event.as_json()) return reply_event.as_json() def respond_to_error(content: str, original_event_as_str: str, is_from_bot=False): @@ -366,7 +348,7 @@ class DVM: elif tag.as_vec()[0] == "i": task = tag.as_vec()[1] - user = get_or_add_user(self.dvm_config.DB, sender, self.client) + user = get_or_add_user(db=self.dvm_config.DB, npub=sender, client=self.client, config=self.dvm_config) if not user.iswhitelisted: amount = int(user.balance) + get_amount_per_task(task, self.dvm_config) update_sql_table(self.dvm_config.DB, sender, amount, user.iswhitelisted, user.isblacklisted, @@ -419,7 +401,7 @@ class DVM: status=status, result="", is_processed=False, bolt11=bolt11, payment_hash=payment_hash, expires=expires, from_bot=False)) - #print(str(self.job_list)) + # print(str(self.job_list)) if (status == "payment-required" or status == "payment-rejected" or ( status == "processing" and not is_paid) or (status == "success" and not is_paid)): @@ -435,7 +417,7 @@ class DVM: send_event(event, client=self.client, dvm_config=self.dvm_config) print("[" + self.dvm_config.NIP89.name + "]" + ": Sent Kind " + str( - EventDefinitions.KIND_FEEDBACK) + " Reaction: " + status + " " + event.as_json()) + EventDefinitions.KIND_FEEDBACK) + " Reaction: " + status + " " + event.as_json()) return event.as_json() def do_work(job_event, is_from_bot=False): @@ -495,4 +477,3 @@ class DVM: self.jobs_on_hold_list.remove(job) time.sleep(1.0) - diff --git a/interfaces/dvmtaskinterface.py b/interfaces/dvmtaskinterface.py index 0c4f0a3..10fa5a4 100644 --- a/interfaces/dvmtaskinterface.py +++ b/interfaces/dvmtaskinterface.py @@ -26,6 +26,19 @@ class DVMTaskInterface: nip89.content = nip89config.CONTENT return nip89 + def init(self, name, dvm_config, admin_config, nip89config): + self.NAME = name + self.PK = dvm_config.PRIVATE_KEY + if dvm_config.COST is not None: + self.COST = dvm_config.COST + + dvm_config.SUPPORTED_DVMS = [self] + dvm_config.DB = "db/" + self.NAME + ".db" + dvm_config.NIP89 = self.NIP89_announcement(nip89config) + self.dvm_config = dvm_config + self.admin_config = admin_config + + def run(self): nostr_dvm_thread = Thread(target=self.DVM, args=[self.dvm_config, self.admin_config]) nostr_dvm_thread.start() diff --git a/main.py b/main.py index 718b06d..dfc08ec 100644 --- a/main.py +++ b/main.py @@ -17,41 +17,49 @@ def run_nostr_dvm_with_local_config(): # We extract the Publickey from our bot, so the DVMs know who they should listen and react to. bot_publickey = Keys.from_sk_str(os.getenv("BOT_PRIVATE_KEY")).public_key() - # Spawn some DVMs in the playground and run them - # You can add arbitrary DVMs there and instantiate them here - - # Spawn DVM1 Kind 5000 Text Extractor from PDFs - pdfextractor = build_pdf_extractor("PDF Extractor", [bot_publickey]) - pdfextractor.run() - - # Spawn DVM2 Kind 5002 Text Translation - translator = build_translator("Translator", [bot_publickey]) - translator.run() - - # Spawn DVM3 Kind 5100 Image Generation This one uses a specific backend called nova-server. - # If you want to use it, see the instructions in backends/nova_server - unstable_artist = build_unstable_diffusion("Unstable Diffusion", [bot_publickey]) - unstable_artist.run() - - # Spawn DVM4, another Instance of text-to-image, as before but use a different privatekey, model and lora this time. - sketcher = build_sketcher("Sketcher", [bot_publickey]) - sketcher.run() - - dalle = build_dalle("Dall-E 3", [bot_publickey]) - dalle.run() - # We will run an optional bot that can communicate with the DVMs # Note this is very basic for now and still under development bot_config = DVMConfig() bot_config.PRIVATE_KEY = os.getenv("BOT_PRIVATE_KEY") bot_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") bot_config.LNBITS_URL = os.getenv("LNBITS_HOST") - # Finally we add some of the DVMs we created before to the Bot and start it. - bot_config.SUPPORTED_DVMS = [sketcher, unstable_artist, dalle, translator] - bot = Bot - nostr_dvm_thread = Thread(target=bot, args=[bot_config]) - nostr_dvm_thread.start() + # Spawn some DVMs in the playground and run them + # You can add arbitrary DVMs there and instantiate them here + + # Spawn DVM1 Kind 5000: A local Text Extractor from PDFs + pdfextractor = build_pdf_extractor("PDF Extractor", [bot_publickey]) + # If we don't add it to the bot, the bot will not provide access to the DVM + pdfextractor.run() + + # Spawn DVM2 Kind 5002 Local Text Translation, calling the free Google API. + translator = build_translator("Translator", [bot_publickey]) + bot_config.SUPPORTED_DVMS.append(translator) # We add translator to the bot + translator.run() + + # Spawn DVM3 Kind 5100 Image Generation This one uses a specific backend called nova-server. + # If you want to use it, see the instructions in backends/nova_server + if os.getenv("NOVA_SERVER") is not None and os.getenv("NOVA_SERVER") != "": + unstable_artist = build_unstable_diffusion("Unstable Diffusion", [bot_publickey]) + bot_config.SUPPORTED_DVMS.append(unstable_artist) # We add unstable Diffusion to the bot + unstable_artist.run() + + # Spawn DVM4, another Instance of text-to-image, as before but use a different privatekey, model and lora this time. + if os.getenv("NOVA_SERVER") is not None and os.getenv("NOVA_SERVER") != "": + sketcher = build_sketcher("Sketcher", [bot_publickey]) + bot_config.SUPPORTED_DVMS.append(sketcher) # We also add Sketcher to the bot + sketcher.run() + + # Spawn DVM5, this one requires an OPENAI API Key and balance with OpenAI, you will move the task to them and pay + # per call. Make sure you have enough balance and the DVM's cost is set higher than what you pay yourself, except, you know, + # you're being generous. + if os.getenv("OPENAI_API_KEY") is not None and os.getenv("OPENAI_API_KEY") != "": + dalle = build_dalle("Dall-E 3", [bot_publickey]) + bot_config.SUPPORTED_DVMS.append(dalle) + dalle.run() + + bot = Bot(bot_config) + bot.run() # Keep the main function alive for libraries like openai try: diff --git a/playground.py b/playground.py index d3978c1..9666a9f 100644 --- a/playground.py +++ b/playground.py @@ -8,6 +8,7 @@ from tasks.translation import Translation from utils.admin_utils import AdminConfig from utils.dvmconfig import DVMConfig from utils.nip89_utils import NIP89Config + """ This File is a playground to create DVMs. It shows some examples of DVMs that make use of the modules in the tasks folder These DVMs should be considered examples and will be extended in the future. env variables are used to not commit keys, @@ -32,6 +33,8 @@ task, for example an address or an API key. # their NIP89 announcement admin_config = AdminConfig() admin_config.REBROADCAST_NIP89 = False +# Set rebroadcast to true once you have set your NIP89 descriptions and d tags. You only need to rebroadcast once you +# want to update your NIP89 descriptions def build_pdf_extractor(name, dm_allowed_keys): @@ -67,11 +70,11 @@ def build_translator(name, dm_allowed_keys): "language": { "required": False, "values": ["en", "az", "be", "bg", "bn", "bs", "ca", "ceb", "co", "cs", "cy", "da", "de", "el", "eo", "es", - "et", "eu", "fa", "fi", "fr", "fy", "ga", "gd", "gl","gu", "ha", "haw", "hi", "hmn", "hr", "ht", - "hu", "hy", "id", "ig", "is", "it", "he", "ja", "jv", "ka", "kk","km", "kn", "ko", "ku", "ky", + "et", "eu", "fa", "fi", "fr", "fy", "ga", "gd", "gl", "gu", "ha", "haw", "hi", "hmn", "hr", "ht", + "hu", "hy", "id", "ig", "is", "it", "he", "ja", "jv", "ka", "kk", "km", "kn", "ko", "ku", "ky", "la", "lb", "lo", "lt", "lv", "mg", "mi", "mk", "ml", "mn", "mr", "ms", "mt", "my", "ne", "nl", "no", "ny", "or", "pa", "pl", "ps", "pt", "ro", "ru", "sd", "si", "sk", "sl", "sm", "sn", "so", - "sq", "sr", "st", "su", "sv", "sw", "ta", "te","tg", "th", "tl", "tr", "ug", "uk", "ur", "uz", + "sq", "sr", "st", "su", "sv", "sw", "ta", "te", "tg", "th", "tl", "tr", "ug", "uk", "ur", "uz", "vi", "xh", "yi", "yo", "zh", "zu"] } } @@ -158,12 +161,15 @@ def build_sketcher(name, dm_allowed_keys): return ImageGenerationSDXL(name=name, dvm_config=dvm_config, nip89config=nip89config, admin_config=admin_config, options=options) + def build_dalle(name, dm_allowed_keys): dvm_config = DVMConfig() dvm_config.PRIVATE_KEY = os.getenv("NOSTR_PRIVATE_KEY3") dvm_config.LNBITS_INVOICE_KEY = os.getenv("LNBITS_INVOICE_KEY") dvm_config.LNBITS_URL = os.getenv("LNBITS_HOST") dvm_config.DM_ALLOWED = dm_allowed_keys + profit_in_sats = 10 + dvm_config.COST = int(((4.0 / (get_price_per_sat("USD") * 100)) + profit_in_sats)) nip90params = { "size": { @@ -186,4 +192,24 @@ def build_dalle(name, dm_allowed_keys): nip89config.CONTENT = json.dumps(nip89info) # We add an optional AdminConfig for this one, and tell the dvm to rebroadcast its NIP89 return ImageGenerationDALLE(name=name, dvm_config=dvm_config, nip89config=nip89config, - admin_config=admin_config) \ No newline at end of file + admin_config=admin_config) + + +# Little Gimmick: +# For Dalle where we have to pay 4cent per image, we fetch current sat price in fiat +# and update cost at each start +def get_price_per_sat(currency): + import requests + + url = "https://api.coinstats.app/public/v1/coins" + params = {"skip": 0, "limit": 1, "currency": currency} + try: + response = requests.get(url, params=params) + response_json = response.json() + + bitcoin_price = response_json["coins"][0]["price"] + price_currency_per_sat = bitcoin_price / 100000000.0 + except: + price_currency_per_sat = 0.0004 + + return price_currency_per_sat diff --git a/tasks/imagegeneration_openai_dalle.py b/tasks/imagegeneration_openai_dalle.py index 8d81aee..1b1784a 100644 --- a/tasks/imagegeneration_openai_dalle.py +++ b/tasks/imagegeneration_openai_dalle.py @@ -23,23 +23,14 @@ Params: -model # models: juggernaut, dynavision, colossusProject, newrea class ImageGenerationDALLE(DVMTaskInterface): - NAME: str = "" KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE TASK: str = "text-to-image" COST: int = 120 - PK: str - DVM = DVM def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None, options=None): - self.NAME = name - self.PK = dvm_config.PRIVATE_KEY - dvm_config.SUPPORTED_DVMS = [self] - dvm_config.DB = "db/" + self.NAME + ".db" - dvm_config.NIP89 = self.NIP89_announcement(nip89config) - self.dvm_config = dvm_config - self.admin_config = admin_config + self.init(name, dvm_config, admin_config, nip89config) self.options = options def is_input_supported(self, input_type, input_content): diff --git a/tasks/imagegeneration_sdxl.py b/tasks/imagegeneration_sdxl.py index 3ad712e..df07576 100644 --- a/tasks/imagegeneration_sdxl.py +++ b/tasks/imagegeneration_sdxl.py @@ -20,22 +20,12 @@ Params: -model # models: juggernaut, dynavision, colossusProject, newrea class ImageGenerationSDXL(DVMTaskInterface): - NAME: str = "" KIND: int = EventDefinitions.KIND_NIP90_GENERATE_IMAGE TASK: str = "text-to-image" COST: int = 50 - PK: str - DVM = DVM def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None, options=None): - self.NAME = name - self.PK = dvm_config.PRIVATE_KEY - - dvm_config.SUPPORTED_DVMS = [self] - dvm_config.DB = "db/" + self.NAME + ".db" - dvm_config.NIP89 = self.NIP89_announcement(nip89config) - self.dvm_config = dvm_config - self.admin_config = admin_config + self.init(name, dvm_config, admin_config, nip89config) self.options = options def is_input_supported(self, input_type, input_content): diff --git a/tasks/textextractionpdf.py b/tasks/textextractionpdf.py index 59b6c52..ba85899 100644 --- a/tasks/textextractionpdf.py +++ b/tasks/textextractionpdf.py @@ -21,22 +21,13 @@ Params: None class TextExtractionPDF(DVMTaskInterface): - NAME: str = "" KIND: int = EventDefinitions.KIND_NIP90_EXTRACT_TEXT TASK: str = "pdf-to-text" COST: int = 0 - PK: str - DVM = DVM - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None): - self.NAME = name - self.PK = dvm_config.PRIVATE_KEY - - dvm_config.SUPPORTED_DVMS = [self] - dvm_config.DB = "db/" + self.NAME + ".db" - dvm_config.NIP89 = self.NIP89_announcement(nip89config) - self.dvm_config = dvm_config - self.admin_config = admin_config + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None, options=None): + self.init(name, dvm_config, admin_config, nip89config) + self.options = options def is_input_supported(self, input_type, input_content): diff --git a/tasks/translation.py b/tasks/translation.py index 08d5cc1..d852265 100644 --- a/tasks/translation.py +++ b/tasks/translation.py @@ -19,22 +19,14 @@ Params: -language The target language class Translation(DVMTaskInterface): - NAME: str = "" KIND: int = EventDefinitions.KIND_NIP90_TRANSLATE_TEXT TASK: str = "translation" COST: int = 0 - PK: str - DVM = DVM - def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None): - self.NAME = name - self.PK = dvm_config.PRIVATE_KEY - - dvm_config.SUPPORTED_DVMS = [self] - dvm_config.DB = "db/" + self.NAME + ".db" - dvm_config.NIP89 = self.NIP89_announcement(nip89config) - self.dvm_config = dvm_config - self.admin_config = admin_config + def __init__(self, name, dvm_config: DVMConfig, nip89config: NIP89Config, admin_config: AdminConfig = None, + options=None): + self.init(name, dvm_config, admin_config, nip89config) + self.options = options def is_input_supported(self, input_type, input_content): if input_type != "event" and input_type != "job" and input_type != "text": diff --git a/utils/admin_utils.py b/utils/admin_utils.py index bde1110..96a1f54 100644 --- a/utils/admin_utils.py +++ b/utils/admin_utils.py @@ -50,7 +50,7 @@ def admin_make_database_updates(adminconfig: AdminConfig = None, dvmconfig: DVMC if whitelistuser: - user = get_or_add_user(db, publickey, client=client) + user = get_or_add_user(db, publickey, client=client, config=dvmconfig) update_sql_table(db, user.npub, user.balance, True, False, user.nip05, user.lud16, user.name, user.lastactive) user = get_from_sql_table(db, publickey) print(str(user.name) + " is whitelisted: " + str(user.iswhitelisted)) diff --git a/utils/database_utils.py b/utils/database_utils.py index 4c13f04..a022645 100644 --- a/utils/database_utils.py +++ b/utils/database_utils.py @@ -8,9 +8,7 @@ from dataclasses import dataclass from datetime import timedelta from logging import Filter -from nostr_sdk import Timestamp, Keys, PublicKey, EventBuilder, Metadata, Filter, Options, Client - -from utils.definitions import NEW_USER_BALANCE +from nostr_sdk import Timestamp, Keys, PublicKey, EventBuilder, Filter, Client, Options from utils.nostr_utils import send_event @@ -66,7 +64,7 @@ def add_to_sql_table(db, npub, sats, iswhitelisted, isblacklisted, nip05, lud16, con.commit() con.close() except Error as e: - print(e) + print("Error when Adding to DB: " + str(e)) def update_sql_table(db, npub, balance, iswhitelisted, isblacklisted, nip05, lud16, name, lastactive): @@ -87,7 +85,7 @@ def update_sql_table(db, npub, balance, iswhitelisted, isblacklisted, nip05, lud con.commit() con.close() except Error as e: - print(e) + print("Error Updating DB: " + str(e)) def get_from_sql_table(db, npub): @@ -113,7 +111,7 @@ def get_from_sql_table(db, npub): return user except Error as e: - print(e) + print("Error Getting from DB: " + str(e)) def delete_from_sql_table(db, npub): @@ -155,72 +153,70 @@ def list_db(db): print(e) -def update_user_balance(db, sender, sats, client, config): - user = get_from_sql_table(db, sender) +def update_user_balance(db, npub, additional_sats, client, config): + user = get_from_sql_table(db, npub) if user is None: - add_to_sql_table(db, sender, (int(sats) + NEW_USER_BALANCE), False, False, - "", "", "", Timestamp.now().as_secs()) - print("NEW USER: " + sender + " Zap amount: " + str(sats) + " Sats.") + name, nip05, lud16 = fetch_user_metadata(npub, client) + add_to_sql_table(db, npub, (int(additional_sats) + config.NEW_USER_BALANCE), False, False, + nip05, lud16, name, Timestamp.now().as_secs()) + print("Adding User: " + npub + " (" + npub + ")") else: - user = get_from_sql_table(db, sender) - print(str(sats)) - - if user.nip05 is None: - user.nip05 = "" - if user.lud16 is None: - user.lud16 = "" - if user.name is None: - user.name = "" - - new_balance = int(user.balance) + int(sats) - update_sql_table(db, sender, new_balance, user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16, + user = get_from_sql_table(db, npub) + new_balance = int(user.balance) + int(additional_sats) + update_sql_table(db, npub, new_balance, user.iswhitelisted, user.isblacklisted, user.nip05, user.lud16, user.name, Timestamp.now().as_secs()) - print("UPDATE USER BALANCE: " + str(user.name) + " Zap amount: " + str(sats) + " Sats.") + print("Updated user balance for: " + str(user.name) + + " Zap amount: " + str(additional_sats) + " Sats. New balance: " + str(new_balance) +" Sats") if config is not None: keys = Keys.from_sk_str(config.PRIVATE_KEY) time.sleep(1.0) - message = ("Added " + str(sats) + " Sats to balance. New balance is " + str(new_balance) + " Sats. ") + message = ("Added " + str(additional_sats) + " Sats to balance. New balance is " + str(new_balance) + " Sats.") - evt = EventBuilder.new_encrypted_direct_msg(keys, PublicKey.from_hex(sender), message, + evt = EventBuilder.new_encrypted_direct_msg(keys, PublicKey.from_hex(npub), message, None).to_event(keys) send_event(evt, client=client, dvm_config=config) -def get_or_add_user(db, npub, client): +def get_or_add_user(db, npub, client, config): user = get_from_sql_table(db, npub) if user is None: - name, nip05, lud16 = fetch_user_metadata(npub, client) - print("Adding User: " + npub + " (" + npub + ")") - add_to_sql_table(db, npub, NEW_USER_BALANCE, False, False, nip05, - lud16, name, Timestamp.now().as_secs()) - user = get_from_sql_table(db, npub) - return user + try: + name, nip05, lud16 = fetch_user_metadata(npub, client) + print("Adding User: " + npub + " (" + npub + ")") + add_to_sql_table(db, npub, config.NEW_USER_BALANCE, False, False, nip05, + lud16, name, Timestamp.now().as_secs()) + user = get_from_sql_table(db, npub) + return user + except Exception as e: + print("Error Adding User to DB: " + str(e)) return user +class DvmConfig: + pass + + def fetch_user_metadata(npub, client): name = "" nip05 = "" lud16 = "" - - # Get metadata pk = PublicKey.from_hex(npub) print(f"\nGetting profile metadata for {pk.to_bech32()}...") - filter = Filter().kind(0).author(pk).limit(1) - events = client.get_events_of([filter], timedelta(seconds=3)) + profile_filter = Filter().kind(0).author(pk).limit(1) + events = client.get_events_of([profile_filter], timedelta(seconds=5)) + #TODO, it seems our client is still subscribed after that + if len(events) > 0: latest_entry = events[0] - newest = 0 + latest_time = 0 for entry in events: - if entry.created_at().as_secs() > newest: - newest = entry.created_at().as_secs() + if entry.created_at().as_secs() > latest_time: + latest_time = entry.created_at().as_secs() latest_entry = entry - - print(latest_entry.content()) profile = json.loads(latest_entry.content()) if profile.get("name"): name = profile['name'] @@ -228,32 +224,4 @@ def fetch_user_metadata(npub, client): nip05 = profile['nip05'] if profile.get("lud16"): lud16 = profile['lud16'] - - return name, nip05, lud16 - - -def fetch_user_metadata2(sender, client) -> (str, str, str): - name = "" - nip05 = "" - lud16 = "" - try: - pk = PublicKey.from_hex(sender) - print(f"\nGetting profile metadata for {pk.to_bech32()}...") - profile_filter = Filter().kind(0).author(pk).limit(1) - events = client.get_events_of([profile_filter], timedelta(seconds=1)) - if len(events) > 0: - ev = events[0] - metadata = Metadata.from_json(ev.content()) - name = metadata.get_display_name() - if str(name) == "" or name is None: - name = metadata.get_name() - nip05 = metadata.get_nip05() - lud16 = metadata.get_lud16() - else: - print("Profile not found") - return name, nip05, lud16 - - except: - print("Couldn't get meta information") - return name, nip05, lud16 diff --git a/utils/definitions.py b/utils/definitions.py index ceed65e..28a173e 100644 --- a/utils/definitions.py +++ b/utils/definitions.py @@ -2,10 +2,6 @@ import os from dataclasses import dataclass from nostr_sdk import Event - -NEW_USER_BALANCE: int = 250 # Free credits for new users - - class EventDefinitions: KIND_DM: int = 4 KIND_ZAP: int = 9735 diff --git a/utils/dvmconfig.py b/utils/dvmconfig.py index 679597e..a42d479 100644 --- a/utils/dvmconfig.py +++ b/utils/dvmconfig.py @@ -6,6 +6,7 @@ from utils.nip89_utils import NIP89Announcement class DVMConfig: SUPPORTED_DVMS= [] PRIVATE_KEY: str = os.getenv("NOSTR_PRIVATE_KEY") + COST: int = None RELAY_LIST = ["wss://relay.damus.io", "wss://nostr-pub.wellorder.net", "wss://nos.lol", "wss://nostr.wine", "wss://relay.nostfiles.dev", "wss://nostr.mom", "wss://nostr.oxtr.dev", "wss://relay.nostr.bg", @@ -15,11 +16,11 @@ class DVMConfig: LNBITS_INVOICE_KEY = '' LNBITS_URL = 'https://lnbits.com' DB: str + NEW_USER_BALANCE: int = 250 # Free credits for new users NIP89: NIP89Announcement DM_ALLOWED = [] - REQUIRES_NIP05: bool = False - SHOW_RESULT_BEFORE_PAYMENT: bool = True # if this is true show results even when not paid right after autoprocess + SHOW_RESULT_BEFORE_PAYMENT: bool = False # if this is true show results even when not paid right after autoprocess diff --git a/utils/zap_utils.py b/utils/zap_utils.py index adc74d7..2e6c372 100644 --- a/utils/zap_utils.py +++ b/utils/zap_utils.py @@ -4,8 +4,9 @@ import json import requests from Crypto.Cipher import AES from bech32 import bech32_decode, convertbits -from nostr_sdk import nostr_sdk, PublicKey, SecretKey +from nostr_sdk import nostr_sdk, PublicKey, SecretKey, Event from utils.dvmconfig import DVMConfig +from utils.nostr_utils import get_event_by_id def parse_amount_from_bolt11_invoice(bolt11_invoice: str) -> int: @@ -35,6 +36,42 @@ def parse_amount_from_bolt11_invoice(bolt11_invoice: str) -> int: return int(number) +def parse_zap_event_tags(zap_event, keys, name, client, config): + zapped_event = None + invoice_amount = 0 + anon = False + sender = zap_event.pubkey() + + for tag in zap_event.tags(): + if tag.as_vec()[0] == 'bolt11': + invoice_amount = parse_amount_from_bolt11_invoice(tag.as_vec()[1]) + elif tag.as_vec()[0] == 'e': + zapped_event = get_event_by_id(tag.as_vec()[1], client=client, config=config) + elif tag.as_vec()[0] == 'description': + zap_request_event = Event.from_json(tag.as_vec()[1]) + sender = check_for_zapplepay(zap_request_event.pubkey().to_hex(), + zap_request_event.content()) + for z_tag in zap_request_event.tags(): + if z_tag.as_vec()[0] == 'anon': + if len(z_tag.as_vec()) > 1: + print("[" + name + "] Private Zap received.") + decrypted_content = decrypt_private_zap_message(z_tag.as_vec()[1], + keys.secret_key(), + zap_request_event.pubkey()) + decrypted_private_event = Event.from_json(decrypted_content) + if decrypted_private_event.kind() == 9733: + sender = decrypted_private_event.pubkey().to_hex() + message = decrypted_private_event.content() + if message != "": + print("Zap Message: " + message) + else: + anon = True + print( + "[" + name + "] Anonymous Zap received. Unlucky, I don't know from whom, and never will") + + return invoice_amount, zapped_event, sender, anon + + def create_bolt11_ln_bits(sats: int, config: DVMConfig) -> (str, str): url = config.LNBITS_URL + "/api/v1/payments" data = {'out': False, 'amount': sats, 'memo': "Nostr-DVM " + config.NIP89.name}