addsearch-js-client
Version:
AddSearch API JavaScript client
374 lines • 12.7 kB
JavaScript
;
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