askexperts
Version:
AskExperts SDK: build and use AI experts - ask them questions and pay with bitcoin on an open protocol
912 lines • 42.9 kB
JavaScript
/**
* Expert implementation for NIP-174
* Server-side component that handles asks and prompts
*/
var __classPrivateFieldSet = (this && this.__classPrivateFieldSet) || function (receiver, state, value, kind, f) {
if (kind === "m") throw new TypeError("Private method is not writable");
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a setter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot write private member to an object whose class did not declare it");
return (kind === "a" ? f.call(receiver, value) : f ? f.value = value : state.set(receiver, value)), value;
};
var __classPrivateFieldGet = (this && this.__classPrivateFieldGet) || function (receiver, state, kind, f) {
if (kind === "a" && !f) throw new TypeError("Private accessor was defined without a getter");
if (typeof state === "function" ? receiver !== state || !f : !state.has(receiver)) throw new TypeError("Cannot read private member from an object whose class did not declare it");
return kind === "m" ? f : kind === "a" ? f.call(receiver) : f ? f.value : state.get(receiver);
};
var _AskExpertsServerBase_privkey, _AskExpertsServerBase_nickname, _AskExpertsServerBase_description, _AskExpertsServerBase_discoveryRelays, _AskExpertsServerBase_promptRelays, _AskExpertsServerBase_hashtags, _AskExpertsServerBase_formats, _AskExpertsServerBase_paymentMethods, _AskExpertsServerBase_streamFactory, _AskExpertsServerBase_onAsk, _AskExpertsServerBase_onPrompt, _AskExpertsServerBase_onProof;
import { getPublicKey } from "nostr-tools";
import { z } from "zod";
import { debugExpert, debugError } from "../common/debug.js";
import { EVENT_KIND_ASK, EVENT_KIND_BID, EVENT_KIND_BID_PAYLOAD, EVENT_KIND_EXPERT_PROFILE, EVENT_KIND_PROMPT, EVENT_KIND_QUOTE, EVENT_KIND_PROOF, EVENT_KIND_REPLY, METHOD_LIGHTNING, DEFAULT_DISCOVERY_RELAYS, FORMAT_TEXT, FORMAT_OPENAI, SEARCH_RELAYS, } from "../common/constants.js";
import { getStreamFactory, createStreamMetadataEvent, parseStreamMetadataEvent, } from "../stream/index.js";
import { COMPRESSION_GZIP } from "../stream/compression.js";
import { encrypt, decrypt, createEvent, generateRandomKeyPair, } from "../common/crypto.js";
import { publishToRelays, subscribeToRelays, waitForEvent, } from "../common/relay.js";
/**
* Default interval for profile republishing (in milliseconds)
*/
const PROFILE_REPUBLISH_INTERVAL = 12 * 60 * 60 * 1000; // 12 hours
/**
* Zod schema for prompt payload
*/
const promptPayloadSchema = z.object({
format: z.string(),
payload: z.any(),
});
/**
* Zod schema for proof payload
*/
const proofPayloadSchema = z.object({
method: z.string().optional(),
preimage: z.string().optional(),
error: z.string().optional(),
});
export class AskExpertsServerBase {
/**
* Schedules the profile for republishing
*/
schedulePublishProfile() {
this.scheduledPublishProfile = true;
setImmediate(() => this.maybeRepublishProfile());
}
/**
* Schedules the ask subscription for updating
*/
scheduleSubscribeToAsks() {
this.scheduledSubscribeToAsks = true;
setImmediate(() => this.maybeSubscribeToAsks());
}
/**
* Checks if ask subscription needs to be updated and does so if necessary
*/
maybeSubscribeToAsks() {
if (this.scheduledSubscribeToAsks) {
this.scheduledSubscribeToAsks = false;
this.subscribeToAsks();
}
}
/**
* Creates a new Expert instance
*
* @param options - Configuration options
* @param options.privkey - Expert's private key (required)
* @param options.discoveryRelays - Relays for discovery phase
* @param options.promptRelays - Relays for prompt phase
* @param options.hashtags - Hashtags the expert is interested in
* @param options.formats - Formats supported by the expert
* @param options.paymentMethods - Payment methods supported by the expert
* @param options.onAsk - Callback for handling asks
* @param options.onPrompt - Callback for handling prompts
* @param options.onProof - Callback for handling proofs and executing prompts
* @param options.pool - SimplePool instance for relay operations
*/
constructor(options) {
/**
* Expert's private key
*/
_AskExpertsServerBase_privkey.set(this, void 0);
/**
* Expert's nickname (optional)
*/
_AskExpertsServerBase_nickname.set(this, void 0);
/**
* Expert description
*/
_AskExpertsServerBase_description.set(this, "Expert profile for NIP-174");
/**
* Relays for discovery phase
*/
_AskExpertsServerBase_discoveryRelays.set(this, void 0);
/**
* Relays for prompt phase
*/
_AskExpertsServerBase_promptRelays.set(this, void 0);
/**
* Hashtags the expert is interested in
*/
_AskExpertsServerBase_hashtags.set(this, void 0);
/**
* Formats supported by the expert
*/
_AskExpertsServerBase_formats.set(this, void 0);
/**
* Payment methods supported by the expert
*/
_AskExpertsServerBase_paymentMethods.set(this, void 0);
/**
* StreamFactory instance for creating stream readers and writers
*/
_AskExpertsServerBase_streamFactory.set(this, void 0);
/**
* Callback for handling asks
*/
_AskExpertsServerBase_onAsk.set(this, void 0);
/**
* Callback for handling prompts
*/
_AskExpertsServerBase_onPrompt.set(this, void 0);
/**
* Callback for handling proofs and executing prompts
*/
_AskExpertsServerBase_onProof.set(this, void 0);
/**
* Timer for periodic profile republishing
*/
this.profileRepublishTimer = null;
/**
* Flag to indicate that profile needs to be republished
*/
this.scheduledPublishProfile = false;
/**
* Flag to indicate that ask subscription needs to be updated
*/
this.scheduledSubscribeToAsks = false;
/**
* Flag to signal that start was called
*/
this.started = false;
// Required parameters
__classPrivateFieldSet(this, _AskExpertsServerBase_privkey, options.privkey, "f");
this.pubkey = getPublicKey(options.privkey);
__classPrivateFieldSet(this, _AskExpertsServerBase_nickname, options.nickname, "f");
if (options.description) {
__classPrivateFieldSet(this, _AskExpertsServerBase_description, options.description, "f");
}
__classPrivateFieldSet(this, _AskExpertsServerBase_discoveryRelays, options.discoveryRelays || DEFAULT_DISCOVERY_RELAYS, "f");
__classPrivateFieldSet(this, _AskExpertsServerBase_promptRelays, options.promptRelays || DEFAULT_DISCOVERY_RELAYS, "f");
__classPrivateFieldSet(this, _AskExpertsServerBase_hashtags, options.hashtags || [], "f");
__classPrivateFieldSet(this, _AskExpertsServerBase_formats, options.formats || [FORMAT_TEXT], "f");
__classPrivateFieldSet(this, _AskExpertsServerBase_onAsk, options.onAsk, "f");
__classPrivateFieldSet(this, _AskExpertsServerBase_onPrompt, options.onPrompt, "f");
__classPrivateFieldSet(this, _AskExpertsServerBase_onProof, options.onProof, "f");
// Optional parameters with defaults
__classPrivateFieldSet(this, _AskExpertsServerBase_paymentMethods, options.paymentMethods || [METHOD_LIGHTNING], "f");
__classPrivateFieldSet(this, _AskExpertsServerBase_streamFactory, options.streamFactory || getStreamFactory(), "f");
// Set the required pool
this.pool = options.pool;
}
// Getters and setters for private members
get nickname() {
return __classPrivateFieldGet(this, _AskExpertsServerBase_nickname, "f") || "";
}
set nickname(value) {
__classPrivateFieldSet(this, _AskExpertsServerBase_nickname, value, "f");
this.schedulePublishProfile();
}
get description() {
return __classPrivateFieldGet(this, _AskExpertsServerBase_description, "f");
}
set description(value) {
__classPrivateFieldSet(this, _AskExpertsServerBase_description, value, "f");
this.schedulePublishProfile();
}
get discoveryRelays() {
return __classPrivateFieldGet(this, _AskExpertsServerBase_discoveryRelays, "f");
}
set discoveryRelays(value) {
if (!value.length)
throw new Error("Empty relay list");
__classPrivateFieldSet(this, _AskExpertsServerBase_discoveryRelays, value, "f");
}
get promptRelays() {
return __classPrivateFieldGet(this, _AskExpertsServerBase_promptRelays, "f");
}
set promptRelays(value) {
if (!value.length)
throw new Error("Empty relay list");
__classPrivateFieldSet(this, _AskExpertsServerBase_promptRelays, value, "f");
this.schedulePublishProfile();
}
get hashtags() {
return __classPrivateFieldGet(this, _AskExpertsServerBase_hashtags, "f") || [];
}
set hashtags(value) {
__classPrivateFieldSet(this, _AskExpertsServerBase_hashtags, value, "f");
this.schedulePublishProfile();
this.scheduleSubscribeToAsks();
}
get formats() {
return __classPrivateFieldGet(this, _AskExpertsServerBase_formats, "f");
}
set formats(value) {
if (!value.length)
throw new Error("No formats");
__classPrivateFieldSet(this, _AskExpertsServerBase_formats, value, "f");
this.schedulePublishProfile();
}
get paymentMethods() {
return __classPrivateFieldGet(this, _AskExpertsServerBase_paymentMethods, "f");
}
set paymentMethods(value) {
if (!value.length)
throw new Error("No payment methods");
__classPrivateFieldSet(this, _AskExpertsServerBase_paymentMethods, value, "f");
this.schedulePublishProfile();
}
get onAsk() {
return __classPrivateFieldGet(this, _AskExpertsServerBase_onAsk, "f");
}
set onAsk(value) {
__classPrivateFieldSet(this, _AskExpertsServerBase_onAsk, value, "f");
}
get onPrompt() {
return __classPrivateFieldGet(this, _AskExpertsServerBase_onPrompt, "f");
}
set onPrompt(value) {
__classPrivateFieldSet(this, _AskExpertsServerBase_onPrompt, value, "f");
}
get onProof() {
return __classPrivateFieldGet(this, _AskExpertsServerBase_onProof, "f");
}
set onProof(value) {
__classPrivateFieldSet(this, _AskExpertsServerBase_onProof, value, "f");
}
get streamFactory() {
return __classPrivateFieldGet(this, _AskExpertsServerBase_streamFactory, "f");
}
set streamFactory(value) {
__classPrivateFieldSet(this, _AskExpertsServerBase_streamFactory, value, "f");
}
/**
* Starts the expert by subscribing to asks and prompts
*/
async start() {
if (this.started)
throw new Error("Already started");
this.started = true;
// Publish expert profile
this.schedulePublishProfile();
// Set up periodic republishing of expert profile
this.setupProfileRepublishing();
// Subscribe to asks
this.subscribeToAsks();
// Subscribe to prompts
this.subscribeToPrompts();
}
/**
* Sets up periodic republishing of expert profile
*/
setupProfileRepublishing() {
// Clear any existing timer
if (this.profileRepublishTimer) {
clearInterval(this.profileRepublishTimer);
}
// Set up a new timer to republish the profile every 12 hours
this.profileRepublishTimer = setInterval(async () => {
try {
debugExpert("Republishing expert profile (12-hour interval)");
this.schedulePublishProfile();
}
catch (error) {
debugError("Error republishing expert profile:", error);
}
}, PROFILE_REPUBLISH_INTERVAL);
}
/**
* Publishes the expert profile to discovery relays
*/
async publishExpertProfile() {
// Create tags for the expert profile
const tags = [
...__classPrivateFieldGet(this, _AskExpertsServerBase_promptRelays, "f").map((relay) => ["relay", relay]),
...__classPrivateFieldGet(this, _AskExpertsServerBase_formats, "f").map((format) => ["f", format]),
// Add streaming support tag
["s", "true"],
...__classPrivateFieldGet(this, _AskExpertsServerBase_paymentMethods, "f").map((method) => ["m", method]),
...(__classPrivateFieldGet(this, _AskExpertsServerBase_hashtags, "f")?.map((tag) => ["t", tag]) || []),
];
// Add name tag if nickname is provided
if (__classPrivateFieldGet(this, _AskExpertsServerBase_nickname, "f")) {
tags.push(["name", __classPrivateFieldGet(this, _AskExpertsServerBase_nickname, "f")]);
}
// Create and sign the expert profile event
const expertProfileEvent = createEvent(EVENT_KIND_EXPERT_PROFILE, __classPrivateFieldGet(this, _AskExpertsServerBase_description, "f"), tags, __classPrivateFieldGet(this, _AskExpertsServerBase_privkey, "f"));
// Publish the expert profile to discovery relays
const publishedRelays = await publishToRelays(expertProfileEvent, [...__classPrivateFieldGet(this, _AskExpertsServerBase_discoveryRelays, "f"), ...SEARCH_RELAYS], this.pool);
debugExpert(`Published expert profile to ${publishedRelays.length} relays`);
}
/**
* Checks if profile needs to be republished and does so if necessary
*/
async maybeRepublishProfile() {
if (this.started && this.scheduledPublishProfile) {
this.scheduledPublishProfile = false;
await this.publishExpertProfile();
}
}
/**
* Subscribes to ask events on discovery relays
*/
subscribeToAsks() {
// Clear previous sub
this.askSub?.close();
this.askSub = undefined;
if (!__classPrivateFieldGet(this, _AskExpertsServerBase_hashtags, "f"))
return;
// Create a single filter for ask events with all matching criteria
// This ensures we match asks that satisfy ALL conditions (AND logic)
const filter = {
kinds: [EVENT_KIND_ASK],
since: Math.floor(Date.now() / 1000) - 60, // Get events from the last minute
};
// Add hashtags to filter if specified
if (__classPrivateFieldGet(this, _AskExpertsServerBase_hashtags, "f")?.length > 0) {
filter["#t"] = __classPrivateFieldGet(this, _AskExpertsServerBase_hashtags, "f");
}
// Add formats to filter if specified
if (__classPrivateFieldGet(this, _AskExpertsServerBase_formats, "f").length > 0) {
filter["#f"] = __classPrivateFieldGet(this, _AskExpertsServerBase_formats, "f");
}
// Add streaming support to filter
filter["#s"] = ["true"];
// Add payment methods to filter if specified
if (__classPrivateFieldGet(this, _AskExpertsServerBase_paymentMethods, "f").length > 0) {
filter["#m"] = __classPrivateFieldGet(this, _AskExpertsServerBase_paymentMethods, "f");
}
// Subscribe to ask events with the combined filter
const sub = subscribeToRelays([filter], __classPrivateFieldGet(this, _AskExpertsServerBase_discoveryRelays, "f"), this.pool, {
onevent: async (event) => {
try {
await this.handleAskEvent(event);
}
catch (error) {
debugError("Error handling ask event:", error);
}
},
});
// Store the sub
this.askSub = sub;
}
/**
* Subscribes to prompt events on prompt relays
*/
subscribeToPrompts() {
// Clear previous sub
this.promptSub?.close();
this.promptSub = undefined;
// Create a filter for prompt events that tag the expert
const filter = {
kinds: [EVENT_KIND_PROMPT],
"#p": [this.pubkey],
since: Math.floor(Date.now() / 1000) - 60, // Get events from the last minute
};
// Subscribe to prompt events
const sub = subscribeToRelays([filter], __classPrivateFieldGet(this, _AskExpertsServerBase_promptRelays, "f"), this.pool, {
onevent: async (event) => {
try {
await this.handlePromptEvent(event);
}
catch (error) {
debugError("Error handling prompt event:", error);
}
},
});
// Store the sub
this.promptSub = sub;
}
/**
* Handles an ask event
*
* @param askEvent - The ask event
*/
async handleAskEvent(askEvent) {
try {
debugExpert(`Received ask event: ${askEvent.id}`);
if (!__classPrivateFieldGet(this, _AskExpertsServerBase_onAsk, "f")) {
debugExpert(`No ask handler for: ${askEvent.id}`);
return;
}
// Extract hashtags from the tags
const askHashtags = askEvent.tags
.filter((tag) => tag.length > 1 && tag[0] === "t")
.map((tag) => tag[1]);
// Extract formats from the tags
const askFormats = askEvent.tags
.filter((tag) => tag.length > 1 && tag[0] === "f")
.map((tag) => tag[1]);
// Check if streaming is supported
const streamTag = askEvent.tags.find((tag) => tag.length > 1 && tag[0] === "s" && tag[1] === "true");
const askStreamSupported = !!streamTag;
// Extract payment methods from the tags
const askMethods = askEvent.tags
.filter((tag) => tag.length > 1 && tag[0] === "m")
.map((tag) => tag[1]);
// Create an Ask object
const ask = {
id: askEvent.id,
pubkey: askEvent.pubkey,
summary: askEvent.content,
hashtags: askHashtags,
formats: askFormats,
stream: askStreamSupported,
methods: askMethods,
event: askEvent,
};
// Call the onAsk callback
const bid = await __classPrivateFieldGet(this, _AskExpertsServerBase_onAsk, "f").call(this, ask);
// If the callback returns a bid, send it
if (bid) {
await this.sendBid(ask, bid);
}
}
catch (error) {
debugError("Error handling ask event:", error);
}
}
/**
* Sends a bid in response to an ask
*
* @param ask - The ask
* @param bid - The bid
*/
async sendBid(ask, expertBid) {
try {
// Generate a random key pair for the bid
const { privateKey: bidPrivkey } = generateRandomKeyPair();
// Use provided values or defaults for optional fields
const formats = expertBid.formats || __classPrivateFieldGet(this, _AskExpertsServerBase_formats, "f");
const streamSupported = expertBid.stream !== undefined ? expertBid.stream : true;
const methods = expertBid.methods || __classPrivateFieldGet(this, _AskExpertsServerBase_paymentMethods, "f");
// Validate that provided values are compatible with supported values
const validFormats = formats.filter((format) => __classPrivateFieldGet(this, _AskExpertsServerBase_formats, "f").includes(format));
const validMethods = methods.filter((method) => __classPrivateFieldGet(this, _AskExpertsServerBase_paymentMethods, "f").includes(method));
// Create tags for the bid payload
const tags = [
...__classPrivateFieldGet(this, _AskExpertsServerBase_promptRelays, "f").map((relay) => ["relay", relay]),
...validFormats.map((format) => ["f", format]),
...(streamSupported ? [["s", "true"]] : []),
...validMethods.map((method) => ["m", method]),
];
// Create and sign the bid payload event
const bidPayloadEvent = createEvent(EVENT_KIND_BID_PAYLOAD, expertBid.offer, tags, __classPrivateFieldGet(this, _AskExpertsServerBase_privkey, "f"));
// Convert the bid payload event to a string
const bidPayloadStr = JSON.stringify(bidPayloadEvent);
// Encrypt the bid payload for the ask pubkey
const encryptedContent = encrypt(bidPayloadStr, ask.pubkey, bidPrivkey);
// Create and sign the bid event
const bidEvent = createEvent(EVENT_KIND_BID, encryptedContent, [["e", ask.id]], bidPrivkey);
// Publish the bid event to discovery relays
const publishedRelays = await publishToRelays(bidEvent, __classPrivateFieldGet(this, _AskExpertsServerBase_discoveryRelays, "f"), this.pool);
debugExpert(`Published bid to ${publishedRelays.length} relays`);
}
catch (error) {
debugError("Error sending bid:", error);
}
}
/**
* Handles a prompt event
*
* @param promptEvent - The prompt event
*/
async handlePromptEvent(promptEvent) {
try {
debugExpert(`Received prompt event: ${promptEvent.id}`);
if (!__classPrivateFieldGet(this, _AskExpertsServerBase_onPrompt, "f")) {
debugExpert(`No prompt handler for event: ${promptEvent.id}`);
return;
}
// First, decrypt the prompt payload from the event content to get the format
// This is required even if we're using streaming
let promptPayload;
try {
// Decrypt the prompt payload
const decryptedPrompt = decrypt(promptEvent.content, promptEvent.pubkey, __classPrivateFieldGet(this, _AskExpertsServerBase_privkey, "f"));
const rawPayload = JSON.parse(decryptedPrompt);
promptPayload = promptPayloadSchema.parse(rawPayload);
}
catch (error) {
debugError("Error decrypting or parsing prompt payload:", error);
throw error;
}
// Check if this is a streamed prompt
const streamTag = promptEvent.tags.find((tag) => tag.length > 1 && tag[0] === "stream");
// Check if client supports streaming replies
const clientSupportsStreaming = !!promptEvent.tags.find((tag) => tag.length > 1 && tag[0] === "s" && tag[1] === "true");
// Create the Prompt object with format from promptPayload
// Content will be set later based on whether we have a stream or not
const prompt = {
id: promptEvent.id,
expertPubkey: this.pubkey,
format: promptPayload.format,
content: undefined, // Will be set below
stream: clientSupportsStreaming, // Set the stream flag based on the 's' tag
event: promptEvent,
context: undefined,
};
// If we have a stream tag, get content from the stream
if (streamTag) {
try {
// Decrypt the stream metadata
const decryptedStreamTag = decrypt(streamTag[1], promptEvent.pubkey, __classPrivateFieldGet(this, _AskExpertsServerBase_privkey, "f"));
// Parse the stream metadata event
const streamMetadataEvent = JSON.parse(decryptedStreamTag);
// Parse the stream metadata
const streamMetadata = parseStreamMetadataEvent(streamMetadataEvent);
streamMetadata.receiver_privkey = __classPrivateFieldGet(this, _AskExpertsServerBase_privkey, "f");
// Create stream reader
const streamReader = await __classPrivateFieldGet(this, _AskExpertsServerBase_streamFactory, "f").createReader(streamMetadata, this.pool);
// Read all chunks from the stream
// Don't convert bytes to string if binary
let content = "";
let chunks = [];
for await (const chunk of streamReader) {
chunks.push(chunk);
}
// Concatenate chunks based on their type
if (!streamMetadata.binary) {
// String chunks
content = chunks.join("");
}
else {
// Binary chunks - concatenate Uint8Arrays
const totalLength = chunks.reduce((acc, chunk) => acc + chunk.length, 0);
content = new Uint8Array(totalLength);
let offset = 0;
for (const chunk of chunks) {
const typedChunk = chunk;
content.set(typedChunk, offset);
offset += typedChunk.length;
}
}
if (prompt.format === FORMAT_OPENAI) {
// Sanity check
if (content instanceof Uint8Array)
throw new Error("Format openai expects string, not bytes");
prompt.content = JSON.parse(content);
}
else {
prompt.content = content;
}
}
catch (error) {
debugError("Error processing streamed prompt:", error);
throw error;
}
}
else if (promptPayload) {
// No stream, use content from promptPayload
prompt.content = promptPayload.payload;
}
else {
throw new Error("No prompt content available");
}
try {
try {
// Call the onPrompt callback
const expertQuote = await __classPrivateFieldGet(this, _AskExpertsServerBase_onPrompt, "f").call(this, prompt);
// Create a full Quote from the ExpertQuote
const quote = {
pubkey: this.pubkey,
promptId: prompt.id,
invoices: expertQuote.invoices,
event: prompt.event, // Temporary placeholder, will be set in sendQuote
};
await this.sendQuote(prompt, quote);
// Wait for proof event with a timeout
// Create a filter for proof events that tag the prompt
const filter = {
kinds: [EVENT_KIND_PROOF],
"#e": [prompt.id],
"#p": [this.pubkey],
since: Math.floor(Date.now() / 1000) - 60, // Get events from the last minute
};
// Wait for the proof event (60 second timeout)
const proofEvent = await waitForEvent(filter, __classPrivateFieldGet(this, _AskExpertsServerBase_promptRelays, "f"), this.pool, 60000 // 60 second timeout
);
// If we received a proof event, handle it
if (proofEvent) {
await this.handleProofEvent(proofEvent, prompt, expertQuote);
}
else {
debugExpert(`No proof received for prompt ${prompt.id} after timeout`);
}
}
catch (error) {
// If the callback throws an error, send a quote with an error field
debugError("Error in onPrompt callback:", error);
// Send an error quote
await this.sendErrorQuote(prompt, error instanceof Error
? error.message
: "Unknown error in prompt processing");
}
}
catch (error) {
debugError("Error processing prompt payload:", error);
}
}
catch (error) {
debugError("Error handling prompt event:", error);
}
}
/**
* Sends a quote in response to a prompt
*
* @param prompt - The prompt
* @param quote - The quote
*/
async sendQuote(prompt, quote) {
try {
// Create the quote payload
const quotePayload = {
invoices: quote.invoices,
};
// Convert to JSON string
const quotePayloadStr = JSON.stringify(quotePayload);
// Encrypt the quote payload
const encryptedContent = encrypt(quotePayloadStr, prompt.event.pubkey, __classPrivateFieldGet(this, _AskExpertsServerBase_privkey, "f"));
// Create and sign the quote event
const quoteEvent = createEvent(EVENT_KIND_QUOTE, encryptedContent, [
["p", prompt.event.pubkey],
["e", prompt.id],
], __classPrivateFieldGet(this, _AskExpertsServerBase_privkey, "f"));
// Publish the quote event to prompt relays
const publishedRelays = await publishToRelays(quoteEvent, __classPrivateFieldGet(this, _AskExpertsServerBase_promptRelays, "f"), this.pool);
debugExpert(`Published quote to ${publishedRelays.length} relays`);
}
catch (error) {
debugError("Error sending quote:", error);
}
}
/**
* Sends an error quote in response to a prompt
*
* @param prompt - The prompt
* @param errorMessage - The error message
*/
async sendErrorQuote(prompt, errorMessage) {
try {
// Create the error quote payload
const errorQuotePayload = {
error: errorMessage,
};
// Convert to JSON string
const errorQuoteStr = JSON.stringify(errorQuotePayload);
// Encrypt the error quote payload
const encryptedContent = encrypt(errorQuoteStr, prompt.event.pubkey, __classPrivateFieldGet(this, _AskExpertsServerBase_privkey, "f"));
// Create and sign the quote event
const errorQuoteEvent = createEvent(EVENT_KIND_QUOTE, encryptedContent, [
["p", prompt.event.pubkey],
["e", prompt.id],
], __classPrivateFieldGet(this, _AskExpertsServerBase_privkey, "f"));
// Publish the error quote event to prompt relays
const publishedRelays = await publishToRelays(errorQuoteEvent, __classPrivateFieldGet(this, _AskExpertsServerBase_promptRelays, "f"), this.pool);
debugExpert(`Published error quote to ${publishedRelays.length} relays`);
}
catch (error) {
debugError("Error sending error quote:", error);
}
}
/**
* Handles a proof event
*
* @param proofEvent - The proof event
* @param prompt - The prompt
*/
async handleProofEvent(proofEvent, prompt, expertQuote) {
try {
debugExpert(`Received proof event: ${proofEvent.id}`);
// Decrypt the proof payload
const decryptedProof = decrypt(proofEvent.content, proofEvent.pubkey, __classPrivateFieldGet(this, _AskExpertsServerBase_privkey, "f"));
try {
// Parse and validate the proof payload using Zod
const rawPayload = JSON.parse(decryptedProof);
const proofPayload = proofPayloadSchema.parse(rawPayload);
// Check if there's an error in the proof payload
if (proofPayload.error) {
debugError(`Proof error: ${proofPayload.error}`);
return;
}
// Create the Proof object
const proof = {
method: proofPayload.method,
preimage: proofPayload.preimage || "",
};
try {
// Return error if onProof not set
if (!__classPrivateFieldGet(this, _AskExpertsServerBase_onProof, "f"))
throw new Error("No proof handler");
// Call the onProof callback with prompt, expertQuote, and proof
const result = await __classPrivateFieldGet(this, _AskExpertsServerBase_onProof, "f").call(this, prompt, expertQuote, proof);
let useStreaming;
if (Symbol.asyncIterator in result) {
useStreaming = true;
}
else {
// Check if we need to use streaming based on content size
const SIZE_THRESHOLD = 48 * 1024; // 48KB
const contentSize = this.getContentSizeBytes(result.content);
useStreaming = contentSize > SIZE_THRESHOLD;
}
// Check if streaming is needed but client doesn't support it
if (useStreaming && !prompt.stream) {
throw new Error("Streaming is required for this response, but client doesn't support it");
}
// Always send 1 reply event
if (useStreaming) {
// It's ExpertReplies - use streaming
await this.streamExpertReplies(prompt, result);
}
else {
// Content is small, embed in reply event
await this.sendExpertReply(prompt, result.content);
}
}
catch (error) {
// If the callback throws an error, send a single error reply with done=true
debugError("Error in onProof callback:", error);
// Get error description
const errorString = error instanceof Error
? error.message
: "Unknown error in proof processing";
// Send the error reply
await this.sendExpertReply(prompt, undefined, errorString);
}
}
catch (error) {
debugError("Error processing proof payload:", error);
}
}
catch (error) {
debugError("Error handling proof event:", error);
}
}
/**
* Sends an expert reply to a prompt
*
* @param prompt - The prompt
* @param content - Content to send
* @param error - Error to send
*/
async sendExpertReply(prompt, content, error) {
try {
let encryptedContent = "";
if (content || error) {
// Create the reply payload
const replyPayload = {
payload: content,
error,
};
// Convert to JSON string
const replyPayloadStr = JSON.stringify(replyPayload);
// Use regular encryption for smaller content
encryptedContent = encrypt(replyPayloadStr, prompt.event.pubkey, __classPrivateFieldGet(this, _AskExpertsServerBase_privkey, "f"));
}
// Create and sign the reply event
const replyEvent = createEvent(EVENT_KIND_REPLY, encryptedContent, [
["p", prompt.event.pubkey],
["e", prompt.id],
], __classPrivateFieldGet(this, _AskExpertsServerBase_privkey, "f"));
// Publish the reply event to prompt relays
const publishedRelays = await publishToRelays(replyEvent, __classPrivateFieldGet(this, _AskExpertsServerBase_promptRelays, "f"), this.pool);
debugExpert(`Published reply to ${publishedRelays.length} relays for prompt ${prompt.id}`);
}
catch (error) {
debugError("Error sending expert reply:", error);
}
}
/**
* Sends expert replies to a prompt
*
* @param prompt - The prompt
* @param expertReplies - The expert replies
*/
/**
* Helper function to calculate the size of content in bytes
*
* @param content - The content to measure
* @returns Size in bytes
*/
getContentSizeBytes(content) {
if (content instanceof Uint8Array) {
return content.length;
}
else if (typeof content === "string") {
return new TextEncoder().encode(content).length;
}
else {
// For objects or other types, stringify then encode
return new TextEncoder().encode(JSON.stringify(content)).length;
}
}
/**
* Streams expert replies using a single reply event with stream metadata
*
* @param prompt - The prompt
* @param expertReplies - The expert replies
*/
async streamExpertReplies(prompt, expertReplies) {
try {
// Create stream metadata
const { privateKey: streamPrivkey, publicKey: streamPubkey } = generateRandomKeyPair();
// Generate a new key pair for encryption
const { privateKey: streamEncryptionPrivkey } = generateRandomKeyPair();
const binary = Symbol.asyncIterator in expertReplies
? expertReplies.binary
: expertReplies.content instanceof Uint8Array;
// Create stream metadata
const streamMetadata = {
streamId: streamPubkey,
relays: __classPrivateFieldGet(this, _AskExpertsServerBase_promptRelays, "f"),
encryption: "nip44",
compression: COMPRESSION_GZIP,
binary,
receiver_pubkey: prompt.event.pubkey,
version: "1",
};
// Create stream writer
const streamWriter = await __classPrivateFieldGet(this, _AskExpertsServerBase_streamFactory, "f").createWriter(streamMetadata, this.pool, streamPrivkey);
// Create stream metadata event
const streamMetadataEvent = createStreamMetadataEvent(streamMetadata, streamPrivkey);
// Encrypt the stream metadata event
const encryptedStreamMetadata = encrypt(JSON.stringify(streamMetadataEvent), prompt.event.pubkey, __classPrivateFieldGet(this, _AskExpertsServerBase_privkey, "f"));
// Create reply event with stream tag
const replyEvent = createEvent(EVENT_KIND_REPLY, "", // Empty content when using stream
[
["p", prompt.event.pubkey],
["e", prompt.id],
["stream", encryptedStreamMetadata],
], __classPrivateFieldGet(this, _AskExpertsServerBase_privkey, "f"));
// Publish the reply event
const publishedRelays = await publishToRelays(replyEvent, __classPrivateFieldGet(this, _AskExpertsServerBase_promptRelays, "f"), this.pool);
if (publishedRelays.length === 0) {
throw new Error("Failed to publish reply event to any relay");
}
// Stream each reply
try {
let stream = Symbol.asyncIterator in expertReplies
? expertReplies
: [expertReplies];
// Iterate through the expert replies
for await (const expertReply of stream) {
let content = expertReply.content;
if (content instanceof Uint8Array) {
if (!binary)
throw new Error("Non-bytes reply for binary stream");
}
else if (typeof content !== "string") {
// JSONL format
content = JSON.stringify(content) + "\n";
}
// Write content directly to stream without creating a payload structure
await streamWriter.write(content, false);
}
// Close the stream
await streamWriter.write(binary ? new Uint8Array() : "", true);
}
catch (error) {
debugError("Error streaming expert replies:", error);
// Try to write an error message and close the stream
try {
const errorMessage = error instanceof Error
? error.message
: "Unknown error streaming replies";
await streamWriter.error("INTERNAL", errorMessage);
}
catch (e) {
debugError("Error writing error to stream:", e);
}
}
debugExpert(`Streamed replies to ${publishedRelays.length} relays for prompt ${prompt.id}`);
}
catch (error) {
debugError("Error setting up stream for expert replies:", error);
}
}
/**
* Disposes of resources when the expert is no longer needed
*/
async [(_AskExpertsServerBase_privkey = new WeakMap(), _AskExpertsServerBase_nickname = new WeakMap(), _AskExpertsServerBase_description = new WeakMap(), _AskExpertsServerBase_discoveryRelays = new WeakMap(), _AskExpertsServerBase_promptRelays = new WeakMap(), _AskExpertsServerBase_hashtags = new WeakMap(), _AskExpertsServerBase_formats = new WeakMap(), _AskExpertsServerBase_paymentMethods = new WeakMap(), _AskExpertsServerBase_streamFactory = new WeakMap(), _AskExpertsServerBase_onAsk = new WeakMap(), _AskExpertsServerBase_onPrompt = new WeakMap(), _AskExpertsServerBase_onProof = new WeakMap(), Symbol.asyncDispose)]() {
debugExpert("Clearing AskExpertsServerBase");
// FIXME make sure existing queries are answered
// Close all subscriptions
this.askSub?.close();
this.promptSub?.close();
// Clear the profile republish timer
if (this.profileRepublishTimer) {
clearInterval(this.profileRepublishTimer);
this.profileRepublishTimer = null;
}
// The pool is managed externally, so we don't destroy it here
}
}
//# sourceMappingURL=AskExpertsServerBase.js.map