basic bot is working, spawn dvms dependent on config

This commit is contained in:
Believethehype 2023-11-24 21:29:24 +01:00
parent 215916c1ef
commit 6be3372d76
16 changed files with 263 additions and 279 deletions

View File

@ -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"
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

View File

@ -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.

95
bot.py
View File

@ -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()

85
dvm.py
View File

@ -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)

View File

@ -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()

64
main.py
View File

@ -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:

View File

@ -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)
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

View File

@ -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):

View File

@ -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):

View File

@ -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):

View File

@ -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":

View File

@ -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))

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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}