// $ bun run core/exec ask "hey" import { libx } from "libx.js/build/bundles/node.essentials"; import { FeedoxAIModule, IConvMessage, IPromptConfig } from "./providers/FeedoxAI"; import { WorkerClient } from "./providers/WorkerClient"; import { AiLibxClient } from "./providers/AiLibxClient"; import { supportedModels } from 'ai.libx.js'; /** * Module for handling AI-powered question answering functionality. * This module provides an interface to interact with AI models to get answers to questions, * with optional context and model overrides. It also supports saving question-answer pairs * for future reference. */ export class AskModule { /** * Creates a new instance of the AskModule * @param options Optional configuration for the module */ constructor(private ai: FeedoxAIModule | WorkerClient | AiLibxClient, public options?: Partial) { this.options = { ...new AskModuleOptions(), ...options }; } /** * Factory method to create AskModule with Worker client * @param options Optional configuration * @returns AskModule configured to use the Worker API */ public static withWorker(options?: Partial): AskModule { const workerClient = new WorkerClient(); return new AskModule(workerClient, options); } /** * Factory method to create AskModule with FeedoxAI (legacy) * @param options Optional configuration * @returns AskModule configured to use FeedoxAI */ public static withFeedoxAI(options?: Partial): AskModule { const feedoxAI = new FeedoxAIModule(); return new AskModule(feedoxAI, options); } /** * Factory method to create AskModule with ai.libx.js * @param options Optional configuration * @returns AskModule configured to use ai.libx.js */ public static withAiLibx(options?: Partial): AskModule { const aiLibx = new AiLibxClient(); return new AskModule(aiLibx, options); } /** * Main method to ask a question and get an answer from the AI * Returns an async iterator for streaming, or can be collected for full response. * @param question The question to ask * @param context Optional context to provide to the AI * @param _options Optional model override in format "provider/model" * @param conversationHistory Optional conversation history as messages * @returns AsyncIterableIterator */ public async *ask(question: string, context?: string, _options?: IPromptConfig, conversationHistory?: IConvMessage[]): AsyncIterableIterator { const config: IPromptConfig = { ..._options }; if (_options) { // Handle object format if (_options.provider) config.provider = _options.provider; if (_options.model) { // Try to locate the first model key that contains the model string const modelKey = Object.keys(supportedModels).find(key => key.toLowerCase().includes(_options.model.toLowerCase())); if (modelKey) { const [provider, model] = modelKey.split(/\/(.+)/); // split only on first / config.provider = provider; config.model = model; } else { config.model = _options.model; } } } for await (const chunk of this.executePrompt(question, context, config, conversationHistory)) { yield chunk; } } /** * Internal method to execute the prompt with the AI module * Returns an async iterator for streaming, or a single chunk if not streaming. * @param input The question/input to process * @param context Optional context for the question * @param config Additional configuration for the AI request * @param conversationHistory Optional conversation history as messages * @returns AsyncIterableIterator */ private async *executePrompt(input: string, context?: string, config?: any, conversationHistory?: IConvMessage[]): AsyncIterableIterator { libx.log.d('Ask: execute prompt', { input, context, conversationHistoryLength: conversationHistory?.length || 0 }); const variables = { context, }; // Build messages array starting with conversation history const messages: IConvMessage[] = [ ...(conversationHistory || []), { role: "user", content: `${input}`, } ]; const _config = { maxTokens: this.options.maxTokens, ...config, }; if (_config?.stream) { const stream = await this.ai.streamPrompt(this.options.workspaceId, this.options.promptId, variables, messages, _config); const reader = stream.getReader(); const decoder = new TextDecoder(); let buffer = ''; let isSSE: boolean | null = null; // null = unknown, true = SSE format, false = raw text while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); // Detect format on first chunk with enough data if (isSSE === null && buffer.length > 10) { isSSE = buffer.includes('data: '); } // Wait until we know the format if (isSSE === null) { continue; // Keep buffering until we can determine format } if (isSSE) { // Parse SSE format const lines = buffer.split('\n'); buffer = lines.pop() || ''; // Keep incomplete line for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('data:')) { const data = trimmed.slice(5).trim(); if (data === '[DONE]') { return; } if (data) { try { const parsed = JSON.parse(data); if (parsed.delta) { yield parsed.delta; } else if (parsed.error) { throw new Error(parsed.error); } } catch (e) { // If not JSON, yield as-is if (!(e instanceof SyntaxError)) throw e; yield data; } } } } } else { // Raw text format (FeedoxAI compatibility) yield buffer; buffer = ''; } } // Process any remaining buffer if (buffer) { if (isSSE && buffer.includes('data:')) { const lines = buffer.split('\n'); for (const line of lines) { const trimmed = line.trim(); if (trimmed.startsWith('data:')) { const data = trimmed.slice(5).trim(); if (data !== '[DONE]' && data) { try { const parsed = JSON.parse(data); if (parsed.delta) { yield parsed.delta; } } catch (e) { // Ignore parse errors at end } } } } } else if (!isSSE) { yield buffer; } } } else { const res = await this.ai.runPrompt(this.options.workspaceId, this.options.promptId, variables, messages, _config); if (res) yield res; } } /** * Helper to collect all chunks from an async iterator into a single string */ public static async collectAllChunks(iter: AsyncIterable): Promise { let result = ''; for await (const chunk of iter) { result += chunk; } return result; } } /** * Configuration options for the AskModule */ export class AskModuleOptions { /** The workspace and prompt ID used for asking questions (FeedoxAI legacy) */ public workspaceId = '6ab03c56d720b34033c2d06ef9ba6cdf'; public promptId = 'c3c104ef57cdcdf0790b69d69bdc2011'; /** The maximum number of tokens to generate */ public maxTokens = 8192; /** Whether to use Worker API (default: true if WORKER_API_URL is set) */ public useWorker = !!process.env.WORKER_API_URL || process.env.USE_WORKER === 'true'; /** Whether to use ai.libx.js (default: false, set USE_AILBX=true to enable) */ public useAiLibx = process.env.USE_AILBX === 'true'; }