UNPKG

addsearch-js-client

Version:

AddSearch API JavaScript client

374 lines 12.7 kB
'use strict'; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; Object.defineProperty(exports, "__esModule", { value: true }); exports.putSentimentClick = exports.executeAiAnswersNonStreamingFetch = exports.executeAiAnswersStreamingFetch = void 0; require("es6-promise/auto"); const api_1 = require("./api"); /** * Execute AI Answers query with streaming * * @param apiHostname - API hostname * @param sitekey - Site key * @param settings - Query settings * @param cb - Callback function called progressively during streaming */ const executeAiAnswersStreamingFetch = (apiHostname, sitekey, settings, cb) => { executeStreamingAiAnswers(apiHostname, sitekey, settings, cb); }; exports.executeAiAnswersStreamingFetch = executeAiAnswersStreamingFetch; /** * Execute AI Answers query without streaming * * @param apiHostname - API hostname * @param sitekey - Site key * @param settings - Query settings * @param cb - Callback function called once when complete */ const executeAiAnswersNonStreamingFetch = (apiHostname, sitekey, settings, cb) => { executeNonStreamingAiAnswers(apiHostname, sitekey, settings, cb); }; exports.executeAiAnswersNonStreamingFetch = executeAiAnswersNonStreamingFetch; /** * Manages throttled callback execution for streaming responses */ class CallbackThrottler { constructor(cb) { this.cb = cb; this.lastCallbackTime = 0; this.throttleTimeout = null; this.pendingCallback = false; } /** * Call callback immediately, bypassing throttling * @param response - Response data to pass to callback */ callImmediate(response) { this.cleanup(); this.lastCallbackTime = Date.now(); this.cb(response); } /** * Call callback with throttling applied * @param response - Response data to pass to callback */ callThrottled(response) { this.throttle(response); } /** * Cleanup any pending throttled callbacks */ cleanup() { if (this.throttleTimeout) { clearTimeout(this.throttleTimeout); this.throttleTimeout = null; } this.pendingCallback = false; } throttle(response) { const now = Date.now(); const timeSinceLastCallback = now - this.lastCallbackTime; if (timeSinceLastCallback >= CallbackThrottler.THROTTLE_MS) { this.lastCallbackTime = now; this.pendingCallback = false; this.cb(response); } else { this.scheduleCallback(response, CallbackThrottler.THROTTLE_MS - timeSinceLastCallback); } } scheduleCallback(response, delay) { this.pendingCallback = true; if (this.throttleTimeout) { clearTimeout(this.throttleTimeout); } this.throttleTimeout = setTimeout(() => { if (this.pendingCallback) { this.lastCallbackTime = Date.now(); this.pendingCallback = false; this.cb(response); } }, delay); } } CallbackThrottler.THROTTLE_MS = 100; /** * Manages accumulated state during streaming */ class StreamState { constructor() { this.conversationId = ''; this.answer = ''; this.sources = []; this.completedNormally = false; } getCurrentResponse(isComplete) { return { conversation_id: this.conversationId, answer: this.answer, sources: this.sources, is_streaming_complete: isComplete }; } } /** * Parse SSE data line into event object */ const parseSSEEvent = (line) => { if (!line.startsWith('data: ')) { return null; } const dataStr = line.substring(6).trim(); try { return JSON.parse(dataStr); } catch (parseError) { console.error('AI Answers: Error parsing Streaming event:', parseError, 'Data:', dataStr); throw new Error('Streaming request failed: ' + parseError); } }; /** * Handle a single SSE event and update state * @returns true if stream should end */ const handleSSEEvent = (event, state, throttler) => { switch (event.type) { case 'metadata': state.conversationId = event.conversation_id || ''; throttler.callImmediate(state.getCurrentResponse(false)); return false; case 'token': state.answer += event.content || ''; throttler.callThrottled(state.getCurrentResponse(false)); return false; case 'sources': state.sources = event.sources || []; throttler.callImmediate(state.getCurrentResponse(false)); return false; case 'done': state.completedNormally = true; throttler.callImmediate(state.getCurrentResponse(true)); return true; default: return false; } }; /** * Process lines from stream buffer * @returns true if stream should end */ const processStreamLines = (lines, state, throttler) => { for (const line of lines) { const event = parseSSEEvent(line); if (event) { const shouldEnd = handleSSEEvent(event, state, throttler); if (shouldEnd) { return true; } } } return false; }; /** * Execute AI Answers with streaming (new endpoint) */ const executeStreamingAiAnswers = (apiHostname, sitekey, settings, cb) => { const streamingEndpoint = `https://${apiHostname}/v2/indices/${sitekey}/conversations`; const throttler = new CallbackThrottler(cb); const state = new StreamState(); const handleError = (error) => { console.error('AI Answers streaming error:', error); throttler.cleanup(); cb({ conversation_id: '', answer: '', sources: [], is_streaming_complete: true, error: { response: api_1.RESPONSE_SERVER_ERROR, message: 'Streaming request failed: ' + error.message } }); }; const handleUnexpectedDisconnection = () => { console.warn('AI Answers: Stream ended unexpectedly, returning partial data'); throttler.cleanup(); cb({ conversation_id: state.conversationId || '', answer: state.answer, sources: state.sources, is_streaming_complete: true, error: { response: api_1.RESPONSE_SERVER_ERROR, message: 'Connection closed unexpectedly. Partial response returned.' } }); }; fetch(streamingEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question: settings === null || settings === void 0 ? void 0 : settings.keyword, filter: settings === null || settings === void 0 ? void 0 : settings.aiAnswersFilterObject, streaming: true }) }) .then((response) => __awaiter(void 0, void 0, void 0, function* () { var _a; if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const reader = (_a = response.body) === null || _a === void 0 ? void 0 : _a.getReader(); if (!reader) { throw new Error('No response body reader available'); } yield readStream(reader, state, throttler); if (!state.completedNormally) { handleUnexpectedDisconnection(); } })) .catch(handleError); }; /** * Read and process SSE stream */ const readStream = (reader, state, throttler) => __awaiter(void 0, void 0, void 0, function* () { const decoder = new TextDecoder(); let buffer = ''; let done = false; while (!done) { const { value, done: readerDone } = yield reader.read(); done = readerDone; if (value) { const chunk = decoder.decode(value, { stream: true }); buffer += chunk; const lines = buffer.split('\n'); buffer = lines.pop() || ''; try { const shouldEnd = processStreamLines(lines, state, throttler); if (shouldEnd) { done = true; } } catch (error) { throttler.cleanup(); throw error; } } } }); /** * Execute AI Answers with non-streaming endpoint */ const executeNonStreamingAiAnswers = (apiHostname, sitekey, settings, cb) => { fetch(`https://${apiHostname}/v2/indices/${sitekey}/conversations`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ question: settings === null || settings === void 0 ? void 0 : settings.keyword, filter: settings === null || settings === void 0 ? void 0 : settings.aiAnswersFilterObject }) }) .then((response) => { if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } return response.json(); }) .then((data) => { if (data.response) { cb({ conversation_id: data.response.conversation_id, answer: data.response.answer, sources: data.response.sources }); } else { cb({ conversation_id: '', answer: '', sources: [], is_streaming_complete: true, error: { response: api_1.RESPONSE_SERVER_ERROR, message: 'Could not get ai-answers response in the expected data format' } }); } }) .catch((error) => { console.error(error); cb({ conversation_id: '', answer: '', sources: [], is_streaming_complete: true, error: { response: api_1.RESPONSE_SERVER_ERROR, message: 'invalid server response' } }); }); }; /** * Convert sentiment value to numeric rating * @param sentimentValue - Sentiment value ('positive', 'negative', or 'neutral') * @returns Numeric rating: 1 for positive, -1 for negative, 0 for neutral */ const sentimentToNumericRating = (sentimentValue) => { if (sentimentValue === 'positive') { return 1; } if (sentimentValue === 'negative') { return -1; } return 0; }; /** * Submit a sentiment rating for an AI Answers conversation * * @param apiHostname - API hostname * @param sitekey - Site key * @param conversationId - Conversation ID to rate * @param sentimentValue - Sentiment value ('positive', 'negative', or 'neutral') * @returns Promise that resolves to true on success */ const putSentimentClick = (apiHostname, sitekey, conversationId, sentimentValue) => { return new Promise((resolve, reject) => { api_1.aiAnswersInteractionsInstance .put(`https://${apiHostname}/v2/indices/${sitekey}/conversations/${conversationId}/rating`, { value: sentimentToNumericRating(sentimentValue) }) .then((response) => { if (response.status === 200) { resolve(true); } else { reject(new Error(JSON.stringify({ type: api_1.RESPONSE_SERVER_ERROR, message: 'Unable to put sentiment click value.' }))); } }) .catch((error) => { console.error(error); reject(new Error(JSON.stringify({ type: api_1.RESPONSE_SERVER_ERROR, message: 'Unable to put sentiment click value.' }))); }); }); }; exports.putSentimentClick = putSentimentClick; //# sourceMappingURL=ai-answers-api.js.map