UNPKG

askexperts

Version:

AskExperts SDK: build and use AI experts - ask them questions and pay with bitcoin on an open protocol

316 lines (315 loc) 12.9 kB
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 _OpenaiProxyExpertBase_onGetContext, _OpenaiProxyExpertBase_onGetSystemPrompt, _OpenaiProxyExpertBase_model; import { FORMAT_OPENAI, FORMAT_TEXT } from "../common/constants.js"; import { debugExpert, debugError } from "../common/debug.js"; import { encode } from "gpt-tokenizer"; import { DefaultStreamFactory } from "../stream/DefaultStreamFactory.js"; /** * OpenAI Expert implementation for NIP-174 * Provides direct access to OpenAI models with pricing based on token usage */ export class OpenaiProxyExpertBase { /** * Creates a new OpenaiExpert instance * * @param options - Configuration options */ constructor(options) { /** * Optional callback to get context for prompts */ _OpenaiProxyExpertBase_onGetContext.set(this, void 0); /** * Optional callback to get system prompt for prompts */ _OpenaiProxyExpertBase_onGetSystemPrompt.set(this, void 0); /** * Model id to use */ _OpenaiProxyExpertBase_model.set(this, void 0); __classPrivateFieldSet(this, _OpenaiProxyExpertBase_model, options.model, "f"); __classPrivateFieldSet(this, _OpenaiProxyExpertBase_onGetContext, options.onGetContext, "f"); __classPrivateFieldSet(this, _OpenaiProxyExpertBase_onGetSystemPrompt, options.onGetSystemPrompt, "f"); // Use the provided OpenAI client this.openai = options.openai; // Use provided server this.server = options.server; // Custom stream factory to make real-time delta streaming // work as we need it to const streamFactory = new DefaultStreamFactory(); streamFactory.writerConfig = { minChunkInterval: 1000, // Send a delta every second minChunkSize: 1024, // Send if >1KB of deltas }; this.server.streamFactory = streamFactory; } /** * Starts the expert */ async start() { // Ensure our callbacks, unless overridden by the client if (!this.server.onPromptPrice) this.server.onPromptPrice = this.onPromptPrice.bind(this); if (!this.server.onPromptPaid) this.server.onPromptPaid = this.onPromptPaid.bind(this); if (!this.server.formats.includes(FORMAT_OPENAI)) this.server.formats.push(FORMAT_OPENAI); // Start the server await this.server.start(); } /** * Gets the model ID used by this expert * * @returns The model ID */ get model() { return __classPrivateFieldGet(this, _OpenaiProxyExpertBase_model, "f"); } get onGetContext() { return __classPrivateFieldGet(this, _OpenaiProxyExpertBase_onGetContext, "f"); } set onGetContext(value) { __classPrivateFieldSet(this, _OpenaiProxyExpertBase_onGetContext, value, "f"); } get onGetSystemPrompt() { return __classPrivateFieldGet(this, _OpenaiProxyExpertBase_onGetSystemPrompt, "f"); } set onGetSystemPrompt(value) { __classPrivateFieldSet(this, _OpenaiProxyExpertBase_onGetSystemPrompt, value, "f"); } /** * Handles prompt events * * @param prompt - The prompt event * @returns Promise resolving to a quote */ /** * Count tokens using gpt-tokenizer * * @param text - Text to count tokens for * @returns Token count */ countTokens(text) { return encode(text).length; } /** * Callback that fetches the system prompt and context for * this prompt and estimates it's price. Made public to be * reusable. * @param prompt - prompt * @returns - expert price */ /** * Creates ChatCompletionCreateParams from a prompt * * @param prompt - The prompt to create params for * @returns ChatCompletionCreateParams object */ async createChatCompletionCreateParams(prompt) { let content; let systemPrompt; let contextText; // Get system prompt if callback is provided if (this.onGetSystemPrompt) { systemPrompt = await this.onGetSystemPrompt(prompt); debugExpert(`Got system prompt of ${systemPrompt.length} chars`); } // Get context if callback is provided if (this.onGetContext) { contextText = await this.onGetContext(prompt); debugExpert(`Got prompt context of ${contextText.length} chars`); } // Process the prompt based on its format switch (prompt.format) { case FORMAT_OPENAI: { // For OpenAI format, we will pass the content directly to the OpenAI API content = prompt.content; // Ensure proper model content.model = this.model; break; } case FORMAT_TEXT: { // For text format, convert to a single user message content = { model: this.model, messages: [ { role: "user", content: prompt.content, }, ], }; break; } default: throw new Error(`Unsupported format: ${prompt.format}`); } // If system prompt is set, replace all system/developer roles with user // and prepend our system prompt if (systemPrompt) { const messages = content.messages.map((msg) => { if (msg.role === "system") { return { ...msg, role: "user" }; } return msg; }); // Prepend system prompt messages.unshift({ role: "system", content: systemPrompt, }); content.messages = messages; } // If context is provided, prepend it to the last message if (contextText && content.messages.length > 0) { const lastMessage = content.messages[content.messages.length - 1]; if (typeof lastMessage.content === "string") { lastMessage.content = ` ### Context ${contextText} ### User Message ${lastMessage.content} `; } } return content; } async onPromptPrice(prompt) { try { debugExpert(`Received prompt: ${prompt.id}`); const context = {}; prompt.context = context; // Create ChatCompletionCreateParams context.content = await this.createChatCompletionCreateParams(prompt); // Client doesn't support streaming but requests it att app layer if (!prompt.stream && context.content.stream) { throw new Error("Streaming requested without client side support"); } // Use the OpenAI interface to estimate the price const priceEstimate = await this.openai.getQuote(this.model, context.content); // Store quote id to use it in chat completions context.quoteId = priceEstimate.quoteId; debugExpert(`Estimated price: ${priceEstimate.amountSats} sats (quoteId: ${priceEstimate.quoteId || "none"})`); // Return the price information return { amountSats: priceEstimate.amountSats, description: `Payment for ${this.model} completion`, }; } catch (error) { debugError("Error handling prompt:", error); throw error; } } produceReplies(stream, format) { // Create an async generator function const generator = async function* () { for await (const chunk of stream) { switch (format) { case FORMAT_OPENAI: // Return the full API response yield { content: chunk, }; break; case FORMAT_TEXT: // Return the text output only yield { content: chunk, }; break; default: throw new Error("Unsupported format"); } } }.bind(this)(); return generator; } /** * Executes prompts after the quote was paid * * @param prompt - The prompt event * @param quote - The quote * @returns Promise resolving to the expert's reply */ async onPromptPaid(prompt, quote) { try { debugExpert(`Processing paid prompt: ${prompt.id}`); try { const context = prompt.context; // Use the content that was created in onPromptPrice if (!context?.content) { throw new Error("Content not found in prompt context"); } if (!context.quoteId) { throw new Error("quoteId not found in prompt context"); } const content = context.content; // Call the OpenAI API if (content.stream) { const streamResult = await this.openai.execute(context.quoteId); // Check if the result is an AsyncIterable if (!("choices" in streamResult)) { const stream = streamResult; const replies = this.produceReplies(stream, prompt.format); return replies; } else { throw new Error("Expected streaming response but got non-streaming response"); } } else { const completion = await this.openai.execute(context.quoteId); // Check if the result is a ChatCompletion if ("choices" in completion) { // Extract content in text format const output = completion.choices[0]?.message?.content || ""; switch (prompt.format) { case FORMAT_OPENAI: // Return the full API response return { content: completion, }; case FORMAT_TEXT: // Return the output only return { content: output, }; default: throw new Error("Unsupported format"); } } else { throw new Error("Expected non-streaming response but got streaming response"); } } } catch (error) { debugError("Error processing prompt:", error); throw error; } } catch (error) { debugError("Error handling paid prompt:", error); throw error; } } /** * Disposes of resources when the expert is no longer needed */ async [(_OpenaiProxyExpertBase_onGetContext = new WeakMap(), _OpenaiProxyExpertBase_onGetSystemPrompt = new WeakMap(), _OpenaiProxyExpertBase_model = new WeakMap(), Symbol.asyncDispose)]() { debugExpert("Clearing OpenaiProxyExpertBase"); // Nothing to dispose here really } } //# sourceMappingURL=OpenaiProxyExpertBase.js.map