UNPKG

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
/** * 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