askexperts
Version:
AskExperts SDK: build and use AI experts - ask them questions and pay with bitcoin on an open protocol
182 lines • 7.51 kB
JavaScript
/**
* AskExpertsChatClient implementation
* Handles chat interactions with experts
*/
import { AskExpertsPayingClient } from "./AskExpertsPayingClient.js";
import { LightningPaymentManager } from "../payments/LightningPaymentManager.js";
import { FORMAT_OPENAI, FORMAT_TEXT, METHOD_LIGHTNING } from "../common/constants.js";
import { debugError, debugClient } from "../common/debug.js";
import { parseBolt11 } from "../common/bolt11.js";
/**
* Client for chatting with experts
*/
export class AskExpertsChatClient {
/**
* Creates a new AskExpertsChatClient
*
* @param expertPubkey The pubkey of the expert to chat with
* @param options Options for the chat client
*/
constructor(expertPubkey, options) {
this.expertPubkey = expertPubkey;
this.messageHistory = [];
this.expert = null;
this.maxAmountSats = 100; // Default value
this.options = options;
// Initialize properties
this.initializeProperties(options);
// Create the payment manager with the provided NWC string
const paymentManager = new LightningPaymentManager(options.nwcString);
// Try to get discovery relays from options or environment variables
let discoveryRelays = this.options.relays;
if (!discoveryRelays && process.env.DISCOVERY_RELAYS) {
discoveryRelays = process.env.DISCOVERY_RELAYS.split(",").map((relay) => relay.trim());
}
// Initialize the paying client
this.client = new AskExpertsPayingClient(paymentManager, {
maxAmountSats: this.maxAmountSats,
discoveryRelays,
onPaid: async (prompt, quote, proof) => {
try {
// Find the lightning invoice
const lightningInvoice = quote.invoices.find((inv) => inv.method === METHOD_LIGHTNING);
if (lightningInvoice && lightningInvoice.invoice) {
// Parse the invoice to get the amount
const { amount_sats } = parseBolt11(lightningInvoice.invoice);
// Get expert name or use a default
const expertName = this.expert?.name || "Expert";
// Print payment information to console
console.log(`Paid ${amount_sats} sats to ${expertName}`);
}
}
catch (error) {
debugError("Error in onPaid callback:", error);
}
}
});
}
/**
* Initialize properties that don't depend on the wallet
* @param options Client options
*/
initializeProperties(options) {
// Parse max amount
this.maxAmountSats = options.maxAmount
? parseInt(options.maxAmount, 10)
: 100;
if (isNaN(this.maxAmountSats) || this.maxAmountSats <= 0) {
throw new Error("Maximum amount must be a positive number.");
}
}
/**
* Initialize the chat client by fetching the expert profile
*/
async initialize() {
debugClient(`Starting chat with expert ${this.expertPubkey}`);
debugClient(`Maximum payment per message: ${this.maxAmountSats} sats`);
// Fetch the expert's profile once at the beginning
debugClient(`Fetching expert profile for ${this.expertPubkey}...`);
const experts = await this.client.fetchExperts({
pubkeys: [this.expertPubkey],
});
if (experts.length === 0) {
throw new Error(`Expert ${this.expertPubkey} not found. Make sure they have published an expert profile.`);
}
this.expert = experts[0];
debugClient(`Found expert: ${this.expert?.description || "Unknown"}`);
// Verify that the expert supports FORMAT_OPENAI
if (!this.expert || !this.expert.formats.includes(FORMAT_OPENAI)) {
throw new Error(`Expert ${this.expertPubkey} doesn't support OpenAI format. Supported formats: ${this.expert?.formats.join(", ") || "none"}`);
}
return this.expert;
}
/**
* Process a message and get a response from the expert
*
* @param message The message to send to the expert
* @returns The expert's reply
*/
async processMessage(message) {
if (!message) {
return "";
}
if (!this.expert) {
throw new Error("Expert not initialized. Call initialize() first.");
}
const start = Date.now();
try {
debugClient(`Sending message to expert ${this.expertPubkey} of ${message.length} chars...`);
// Add user message to history
this.messageHistory.push({
role: "user",
content: message,
});
// Create OpenAI format request with message history
const openaiRequest = {
model: this.expertPubkey,
messages: this.messageHistory,
stream: !!this.options.stream,
};
debugClient(`Sending message with history (${this.messageHistory.length} messages) to expert ${this.expertPubkey}`);
// Data format to use
const format = this.expert.formats.includes(FORMAT_OPENAI)
? FORMAT_OPENAI
: FORMAT_TEXT;
// Ask the expert using OpenAI format
const replies = await this.client.askExpert({
expert: this.expert,
content: openaiRequest,
format,
});
// Process the replies
let expertReply = "";
// Iterate through the replies
for await (const reply of replies) {
if (reply.done) {
debugClient(`Received final reply from expert ${this.expertPubkey}`);
// OpenAI format response
let chunk = "";
if (reply.content) {
if (format === FORMAT_OPENAI) {
chunk =
reply.content.choices[0]?.[this.options.stream ? "delta" : "message"].content;
}
else {
chunk = reply.content;
}
expertReply += chunk;
}
}
else {
debugClient(`Received chunk from expert ${this.expertPubkey}`);
if (!reply.content)
continue;
const chunk = reply.content.choices[0]?.delta.content;
expertReply += chunk;
}
}
// Add the full expert's response to the message history
if (expertReply)
this.messageHistory.push({
role: "assistant",
content: expertReply,
});
return expertReply;
}
catch (error) {
debugError("Error in chat:", error instanceof Error ? error.message : String(error));
throw error;
}
finally {
debugClient(`Message processed in ${Date.now() - start} ms`);
}
}
/**
* Clean up resources
*/
[Symbol.dispose]() {
// Dispose of the paying client
this.client[Symbol.dispose]();
}
}
//# sourceMappingURL=AskExpertsChatClient.js.map