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