@convo-lang/convo-lang
Version:
The language of AI
824 lines • 31.2 kB
JavaScript
import { NotFoundError, aryRandomize, defineStringParam, deleteUndefined, getErrorMessage, getObjKeyCount, getSortedObjectHash, httpClient, joinPaths, objectToQueryParams } from "@iyio/common";
import { getSerializableFlatConvoConversation, passthroughConvoInputType, passthroughConvoOutputType } from "./convo-lib.js";
import { convoRagService } from "./convo-rag-lib.js";
import { convoCompletionService, convoTranscriptionService, convoTtsService } from "./convo.deps.js";
import { normalizeConvoNodePath } from "./db/convo-db-lib.js";
import { ConvoDbAuthManager } from "./db/ConvoDbAuthManager.js";
export const defaultConvoHttpEndpointPrefix = '/convo-lang';
export const defaultConvoHttpApiEndpointPrefix = '/api' + defaultConvoHttpEndpointPrefix;
export const httpConvoCompletionEndpointParam = defineStringParam('httpConvoCompletionEndpoint', defaultConvoHttpApiEndpointPrefix);
const endpointCache = {};
export const getHttpConvoCompletionServiceForEndpoint = (endpoint) => {
return endpointCache[endpoint] ?? (endpointCache[endpoint] = new HttpConvoCompletionService({ endpoint }));
};
export const convoHttpRelayModule = (scope) => {
scope.implementService(convoCompletionService, scope => HttpConvoCompletionService.fromScope(scope));
scope.implementService(convoRagService, scope => HttpConvoCompletionService.fromScope(scope));
scope.implementService(convoTranscriptionService, scope => HttpConvoCompletionService.fromScope(scope));
scope.implementService(convoTtsService, scope => HttpConvoCompletionService.fromScope(scope));
};
/**
* Forwards messages to an convo-lang api endpoint or pool of convo-lang api endpoints.
*
* ## Endpoint structure
*
* ### POST /completion (flat:FlatConvoConversationBase) => ConvoCompletionMessage[]
* Completes a posted flat conversation and returns the completed messages.
*
* ### POST /convo (convo:string) => string
* Completes a convo conversation as a string and returns the completed messages as a string
*
* ### GET /models ()=>ConvoModelInfo[]
* Returns all models known to the server
*/
export class HttpConvoCompletionService {
static fromScope(scope, endpoint, getRequestOptions) {
if (!endpoint) {
const ep = httpConvoCompletionEndpointParam().split(',').map(e => e.trim()).filter(e => e);
endpoint = ep.length === 1 ? ep[0] : ep;
}
if (!endpoint) {
throw new Error('Empty HttpConvoCompletionService provided');
}
return new HttpConvoCompletionService({
endpoint,
getRequestOptions
});
}
constructor({ endpoint, getRequestOptions, dbName = 'default', }) {
this.serviceId = 'http';
this.inputType = passthroughConvoInputType;
this.outputType = passthroughConvoOutputType;
this._isDisposed = false;
this.endpointIndex = 0;
this.supportMap = {};
this.transcribeSupportCache = {};
this.embeddingsSupportCache = {};
this._driver = {
selectEdgesByPathsAsync: (...args) => this.proxyDriverCallAsync('selectEdgesByPathsAsync', args),
selectNodesByPathsAsync: (...args) => this.proxyDriverCallAsync('selectNodesByPathsAsync', args),
selectNodePathsForPathAsync: (...args) => this.proxyDriverCallAsync('selectNodePathsForPathAsync', args),
selectNodePathsForConditionAsync: (...args) => this.proxyDriverCallAsync('selectNodePathsForConditionAsync', args),
selectNodePathsForPermissionAsync: (...args) => this.proxyDriverCallAsync('selectNodePathsForPermissionAsync', args),
selectNodePathsForEmbeddingAsync: (...args) => this.proxyDriverCallAsync('selectNodePathsForEmbeddingAsync', args),
selectEdgeNodePathsForConditionAsync: (...args) => this.proxyDriverCallAsync('selectEdgeNodePathsForConditionAsync', args),
insertNodeAsync: (...args) => this.proxyDriverCallAsync('insertNodeAsync', args),
updateNodeAsync: (...args) => this.proxyDriverCallAsync('updateNodeAsync', args),
deleteNodeAsync: (...args) => this.proxyDriverCallAsync('deleteNodeAsync', args),
insertEdgeAsync: (...args) => this.proxyDriverCallAsync('insertEdgeAsync', args),
updateEdgeAsync: (...args) => this.proxyDriverCallAsync('updateEdgeAsync', args),
deleteEdgeAsync: (...args) => this.proxyDriverCallAsync('deleteEdgeAsync', args),
insertEmbeddingAsync: (...args) => this.proxyDriverCallAsync('insertEmbeddingAsync', args),
deleteEmbeddingAsync: (...args) => this.proxyDriverCallAsync('deleteEmbeddingAsync', args),
updateEmbeddingAsync: (...args) => this.proxyDriverCallAsync('updateEmbeddingAsync', args),
queryEdgesAsync: (...args) => this.proxyDriverCallAsync('queryEdgesAsync', args),
queryEmbeddingsAsync: (...args) => this.proxyDriverCallAsync('queryEmbeddingsAsync', args),
hasBlobAsync: (...args) => this.proxyDriverCallAsync('hasBlobAsync', args),
openBlobAsync: (path) => {
return this._openBlobAsync(path);
},
writeBlobAsync: (path, blob) => {
return this._writeBlobAsync(path, blob);
}
};
if (Array.isArray(endpoint)) {
endpoint = [...endpoint];
aryRandomize(endpoint);
}
this.endpoint = endpoint;
this.dbName = dbName;
this.getRequestOptions = async () => {
let options = {};
if (!getRequestOptions && !this.auth.jwt) {
return options;
}
if (this.auth.jwt) {
options.headers = {
Authorization: `Bearer ${this.auth.jwt.jwt}`,
};
}
if (getRequestOptions) {
const o = await getRequestOptions();
options = { ...options, ...o, headers: { ...options.headers, ...o.headers } };
}
return options;
};
this.auth = new ConvoDbAuthManager(this);
}
initAsync() {
return Promise.resolve({
success: true,
});
}
get isDisposed() { return this._isDisposed; }
dispose() {
if (this._isDisposed) {
return;
}
this._isDisposed = true;
}
canComplete(model, flat) {
return true;
}
getEndpoint() {
if (Array.isArray(this.endpoint)) {
const e = this.endpoint[this.endpointIndex];
this.endpointIndex++;
if (this.endpointIndex >= this.endpoint.length) {
this.endpointIndex = 0;
aryRandomize(this.endpoint);
}
return e ?? '';
}
else {
return this.endpoint;
}
}
async getNodeByPathAsync(path, permissionFrom) {
const r = await this.queryNodesAsync({ steps: [{ path }], limit: 1, permissionFrom });
if (!r.success) {
return r;
}
return {
success: true,
result: r.result.nodes[0],
};
}
async requireNodeByPathAsync(path, permissionFrom) {
const node = await this.getNodeByPathAsync(path, permissionFrom);
if (!node.success) {
return node;
}
if (!node.result) {
return {
success: false,
error: `Node not found by path: ${path}`,
statusCode: 404,
};
}
return node;
}
async completeConvoAsync(flat, _, ctx) {
let support;
if (flat.enableStreaming && flat.model) {
support = await this.getSupportAsync(flat.model.name);
}
const stream = (flat.enableStreaming && support?.streaming) ?? false;
const options = await this.getRequestOptions?.();
if (!stream) {
const r = await httpClient().postAsync(joinPaths(this.getEndpoint(), '/completion' + (stream ? '/stream' : '')), getSerializableFlatConvoConversation(flat), options);
if (!r) {
throw new Error('convo-lang API endpoint returned empty response');
}
if (!Array.isArray(r) && r.messages) {
return r.messages;
}
else {
return r;
}
}
else {
for await (const evt of httpClient().streamSseAsync({
url: joinPaths(this.getEndpoint(), '/completion' + (stream ? '/stream' : '')),
body: getSerializableFlatConvoConversation(flat),
...options,
})) {
if (!evt.data) {
continue;
}
const chunk = evt.data;
if (ctx.onChunk) {
await ctx.onChunk(this, chunk, flat);
}
if (chunk.completion) {
return chunk.completion;
}
}
throw new Error('No completion found in stream');
}
}
async getSupportAsync(modelName) {
return await (this.supportMap[modelName] ?? (this.supportMap[modelName] = this._getSupportAsync(modelName)));
}
async _getSupportAsync(modelName) {
const options = await this.getRequestOptions?.();
const response = await httpClient().getAsync(joinPaths(this.getEndpoint(), `/completion/support/${encodeURIComponent(modelName)}`), {
...options,
returnFetchResponse: true,
});
if (!response) {
throw new Error('convo-lang API endpoint returned empty response');
}
if (response.status === 404) {
return {};
}
return await response.json();
}
async getModelsAsync() {
return await httpClient().getAsync(joinPaths(this.getEndpoint(), '/models'), await this.getRequestOptions?.());
}
async relayConvertConvoToInputAsync(flat, inputType) {
const request = {
flat: getSerializableFlatConvoConversation(flat),
inputType
};
const r = await httpClient().postAsync(joinPaths(this.getEndpoint(), '/convert'), request, await this.getRequestOptions?.());
if (!r) {
throw new NotFoundError();
}
return r;
}
async searchAsync(search) {
const r = await httpClient().postAsync(joinPaths(this.getEndpoint(), '/rag/search'), search, await this.getRequestOptions?.());
if (!r) {
throw new Error('convo-lang API endpoint returned empty response');
}
return r;
}
async canTranscribe(request) {
const key = getSortedObjectHash(request);
return await (this.transcribeSupportCache[key] ?? (this.transcribeSupportCache[key] = this._canTranscribe(request)));
}
async _canTranscribe(request) {
const options = await this.getRequestOptions?.();
const response = await httpClient().postAsync(joinPaths(this.getEndpoint(), `/transcribe/support`), request, {
...options,
returnFetchResponse: true,
});
if (!response) {
throw new Error('convo-lang API endpoint returned empty response');
}
if (response.status === 404) {
return false;
}
return (await response.json()) ? true : false;
}
async transcribeAsync(request) {
try {
const r = await httpClient().postAsync(joinPaths(this.getEndpoint(), '/transcribe'), {
audio: request.audio,
request: { ...request, audio: undefined }
}, {
...await this.getRequestOptions?.(),
convertBodyToFormData: true,
});
if (!r) {
return {
success: false,
error: {
message: 'transcribe endpoint returned empty result',
error: null,
}
};
}
if (r.success) {
return {
...r,
file: request.audio,
};
}
else {
return r;
}
}
catch (ex) {
return {
success: false,
error: {
message: getErrorMessage(ex),
error: ex,
}
};
}
}
async canConvertToSpeech(request) {
const key = getSortedObjectHash(request);
return await (this.transcribeSupportCache[key] ?? (this.transcribeSupportCache[key] = this._canConvertToSpeech(request)));
}
async _canConvertToSpeech(request) {
const options = await this.getRequestOptions?.();
const response = await httpClient().postAsync(joinPaths(this.getEndpoint(), `/tts/support`), request, {
...options,
returnFetchResponse: true,
});
if (!response) {
throw new Error('convo-lang API endpoint returned empty response');
}
if (response.status === 404) {
return false;
}
return (await response.json()) ? true : false;
}
async convertToSpeechAsync(request) {
try {
const r = await httpClient().postAsync(joinPaths(this.getEndpoint(), '/tts'), request, {
...await this.getRequestOptions?.(),
returnFetchResponse: true,
});
if (!r) {
return {
success: false,
error: 'tts endpoint returned empty result',
};
}
if (r.ok) {
const blob = await r.blob();
return {
success: true,
tts: {
audio: blob,
}
};
}
else {
const text = await r.text();
return {
success: false,
error: `Error returned from API - ${text}`
};
}
}
catch (ex) {
return {
success: false,
error: getErrorMessage(ex),
};
}
}
canGenerateEmbeddings(request) {
const key = `${request.provider ?? '.'}::${request.model ?? '.'}::${request.format ?? '.'}::${request.dimensions ?? '.'}`;
return this.embeddingsSupportCache[key] ?? (this.embeddingsSupportCache[key] = this._canGenerateEmbeddings(request));
}
async _canGenerateEmbeddings(request) {
const options = await this.getRequestOptions?.();
const response = await httpClient().postAsync(joinPaths(this.getEndpoint(), `/embeddings/support`), request, {
...options,
returnFetchResponse: true,
});
if (!response) {
return {
success: false,
error: 'Request failed',
statusCode: 500,
};
}
if (!response?.ok) {
try {
const text = await response.text();
return {
success: false,
error: text,
statusCode: response.status,
};
}
catch (ex) {
return {
success: false,
error: 'Error',
statusCode: response.status,
};
}
}
try {
return {
success: true,
result: (await response.json()) ? true : false,
};
}
catch (ex) {
return {
success: false,
error: 'Failed to ready response from server',
statusCode: 500,
};
}
}
async generateEmbeddingsAsync(request) {
try {
const r = await httpClient().postAsync(joinPaths(this.getEndpoint(), '/embeddings'), request, {
...await this.getRequestOptions?.(),
returnFetchResponse: true,
});
if (!r) {
return {
success: false,
error: 'Empty response from server',
statusCode: 500,
};
}
if (!r.ok) {
try {
return {
success: false,
error: await r.text(),
statusCode: r.status,
};
}
catch {
return {
success: false,
error: r.statusText,
statusCode: r.status,
};
}
}
const result = await r.json();
return {
success: true,
result,
};
}
catch (ex) {
return {
success: false,
error: getErrorMessage(ex),
statusCode: 500
};
}
}
async requestAsync(method, path, body) {
try {
if (method === 'GET' && body) {
deleteUndefined(body);
if (getObjKeyCount(body)) {
path += '?' + objectToQueryParams(body);
}
body = undefined;
}
const r = await httpClient().requestAsync(method, joinPaths(this.getEndpoint(), path), body, {
...await this.getRequestOptions?.(),
returnFetchResponse: true,
});
if (!r) {
return {
success: false,
error: 'Empty response from server',
statusCode: 500,
};
}
if (!r.ok) {
try {
return {
success: false,
error: await r.text(),
statusCode: r.status,
};
}
catch {
return {
success: false,
error: r.statusText,
statusCode: r.status,
};
}
}
return {
success: true,
result: await r.json(),
};
}
catch (ex) {
return {
success: false,
error: getErrorMessage(ex),
statusCode: 500
};
}
}
async *streamNodesAsync(query, cancel) {
const options = await this.getRequestOptions?.();
for await (const evt of httpClient().streamSseAsync({
url: joinPaths(this.getEndpoint(), `/db/${this.dbName}/stream`),
body: query,
...options,
})) {
if (cancel?.isCanceled) {
break;
}
if (!evt.data) {
continue;
}
yield evt.data;
}
}
async executeCommandAsync(command) {
const r = await this.executeCommandsAsync([command]);
if (!r.success) {
return r;
}
const first = r.result[0];
if (!first) {
return {
success: false,
error: 'No command result returned',
statusCode: 500
};
}
return {
success: true,
result: first,
};
}
async executeCommandsAsync(commands) {
return await this.requestAsync('POST', `/db/${this.dbName}`, commands);
}
async queryNodesAsync(query) {
const r = await this.executeCommandAsync({ queryNodes: { query } });
return r.success ? { success: true, result: r.result.queryNodes } : r;
}
async proxyDriverCallAsync(fn, args) {
const r = await this.executeCommandAsync({ driverCmd: { fn, args: args } });
return r.success ? { success: true, result: r.result.driverCmd } : r;
}
async _openBlobAsync(path, permissionFrom) {
try {
const p = normalizeConvoNodePath(path, 'none');
if (!p) {
return {
success: false,
error: 'Invalid path',
statusCode: 400,
};
}
const r = await httpClient().requestAsync('GET', joinPaths(this.getEndpoint(), `/db/${this.dbName}/blob${p}${permissionFrom ? `?permissionFrom=${encodeURIComponent(permissionFrom)}` : ''}`), undefined, {
...await this.getRequestOptions?.(),
returnFetchResponse: true,
});
if (!r) {
return {
success: false,
error: 'Empty http response',
statusCode: 500
};
}
if (!r.ok) {
try {
return {
success: false,
error: await r.text(),
statusCode: r.status,
};
}
catch {
return {
success: false,
error: 'HTTP request failed',
statusCode: r.status,
};
}
}
if (r.body) {
return {
success: true,
result: r.body,
};
}
else {
return {
success: true,
result: (await r.blob()).stream(),
};
}
}
catch (ex) {
return {
success: false,
error: getErrorMessage(ex),
statusCode: 500,
};
}
}
async readBlobStringAsync(path, permissionFrom) {
const r = await this.openBlobAsync(path, permissionFrom);
if (!r.success) {
if (r.statusCode === 404) {
return {
success: true,
result: undefined,
};
}
else {
return r;
}
}
try {
const response = new Response(r.result);
return {
success: true,
result: await response.text(),
};
}
catch (ex) {
return {
success: false,
error: getErrorMessage(ex),
statusCode: 500,
};
}
}
async _writeBlobAsync(path, blob, permissionFrom) {
try {
const p = normalizeConvoNodePath(path, 'none');
if (!p) {
return {
success: false,
error: 'Invalid path',
statusCode: 400,
};
}
const stream = (blob instanceof Blob) ? blob.stream() : blob;
const options = await this.getRequestOptions?.();
const r = await httpClient().requestAsync('POST', joinPaths(this.getEndpoint(), `/db/${this.dbName}/blob${p}${permissionFrom ? `?permissionFrom=${encodeURIComponent(permissionFrom)}` : ''}`), stream, {
...options,
returnFetchResponse: true,
rawBody: true,
headers: {
...options?.headers,
"Content-Type": "application/octet-stream"
}
});
if (!r) {
return {
success: false,
error: 'Empty http response',
statusCode: 500
};
}
if (!r.ok) {
try {
return {
success: false,
error: await r.text(),
statusCode: r.status,
};
}
catch {
return {
success: false,
error: 'HTTP request failed',
statusCode: r.status,
};
}
}
return {
success: true,
};
}
catch (ex) {
return {
success: false,
error: getErrorMessage(ex),
statusCode: 500,
};
}
}
async openBlobAsync(path, permissionFrom) {
return await this._openBlobAsync(path, permissionFrom);
}
async writeBlobAsync(path, blob, permissionFrom) {
if (typeof blob === 'string') {
const r = await this.executeCommandAsync({ writeBlob: { path, blob, permissionFrom } });
return r.success ? { success: true } : r;
}
else {
return await this._writeBlobAsync(path, blob, permissionFrom);
}
}
async hasBlobAsync(path, permissionFrom) {
const r = await this.executeCommandAsync({ hasBlob: { path, permissionFrom } });
return r.success ? { success: true, result: r.result.hasBlob } : r;
}
async getNodesByPathAsync(path, permissionFrom) {
const r = await this.executeCommandAsync({ getNodesByPath: { path, permissionFrom } });
return r.success ? { success: true, result: r.result.getNodesByPath } : r;
}
async getNodePermissionAsync(fromPath, toPath) {
const r = await this.executeCommandAsync({ getNodePermission: { fromPath, toPath } });
return r.success ? { success: true, result: r.result.getNodePermission } : r;
}
async checkNodePermissionAsync(fromPath, toPath, type, matchAny) {
const r = await this.executeCommandAsync({ checkNodePermission: { fromPath, toPath, type, matchAny } });
if (r.success && r.result.checkNodePermission === false) {
return { success: false, error: 'permission denied', statusCode: 401 };
}
return r.success ? { success: true } : r;
}
async insertNodeAsync(node, options) {
const r = await this.executeCommandAsync({ insertNode: { node, options } });
return r.success ? { success: true, result: r.result.insertNode } : r;
}
async updateNodeAsync(node, options) {
const r = await this.executeCommandAsync({ updateNode: { node, options } });
return r.success ? { success: true } : r;
}
async deleteNodeAsync(path, options) {
const r = await this.executeCommandAsync({ deleteNode: { path, options } });
return r.success ? { success: true } : r;
}
async queryEdgesAsync(query) {
const r = await this.executeCommandAsync({ queryEdges: { query } });
return r.success ? { success: true, result: r.result.queryEdges } : r;
}
async getEdgeByIdAsync(id, permissionFrom) {
const r = await this.executeCommandAsync({ getEdgeById: { id, permissionFrom } });
return r.success ? { success: true, result: r.result.getEdgeById } : r;
}
async insertEdgeAsync(edge, options) {
const r = await this.executeCommandAsync({ insertEdge: { edge, options } });
return r.success ? { success: true, result: r.result.insertEdge } : r;
}
async updateEdgeAsync(update, options) {
const r = await this.executeCommandAsync({ updateEdge: { update, options } });
return r.success ? { success: true } : r;
}
async deleteEdgeAsync(id, options) {
const r = await this.executeCommandAsync({ deleteEdge: { id, options } });
return r.success ? { success: true } : r;
}
async queryEmbeddingsAsync(query) {
const r = await this.executeCommandAsync({ queryEmbeddings: { query } });
return r.success ? { success: true, result: r.result.queryEmbeddings } : r;
}
async getEmbeddingByIdAsync(id, permissionFrom) {
const r = await this.executeCommandAsync({ getEmbeddingById: { id, permissionFrom } });
return r.success ? { success: true, result: r.result.getEmbeddingById } : r;
}
async insertEmbeddingAsync(embedding, options) {
const r = await this.executeCommandAsync({ insertEmbedding: { embedding, options } });
return r.success ? { success: true, result: r.result.insertEmbedding } : r;
}
async updateEmbeddingAsync(update, options) {
const r = await this.executeCommandAsync({ updateEmbedding: { update, options } });
return r.success ? { success: true } : r;
}
async deleteEmbeddingAsync(id, options) {
const r = await this.executeCommandAsync({ deleteEmbedding: { id, options } });
return r.success ? { success: true } : r;
}
async callFunctionAsync(path, args, permissionFrom) {
const r = await this.callFunctionReturnNodeAsync(path, args, permissionFrom);
if (!r.success) {
return r;
}
return {
success: true,
result: r.result?.data,
};
}
async callFunctionWithSchemaAsync(inputSchema, outputSchema, path, args, permissionFrom) {
const inputParsed = inputSchema.safeParse(args);
if (inputParsed.error) {
return {
success: false,
error: inputParsed.error.message,
statusCode: 400
};
}
const r = await this.callFunctionReturnValueAsync(path, inputParsed.data);
if (!r.success) {
return r;
}
const outputParsed = outputSchema.safeParse(r.result);
if (outputParsed.error) {
return {
success: false,
error: outputParsed.error.message,
statusCode: 500,
};
}
return {
success: true,
result: outputParsed.data,
};
}
async callFunctionReturnValueAsync(path, args, permissionFrom) {
const r = await this.callFunctionReturnNodeAsync(path, args, permissionFrom);
if (!r.success) {
return r;
}
const data = r.result?.data;
if (!data) {
return {
success: false,
error: 'Called function did not return a value',
statusCode: 500,
};
}
return {
success: true,
result: data,
};
}
async callFunctionReturnNodeAsync(path, args, permissionFrom) {
const r = await this.queryNodesAsync({ steps: [{ path, call: { args } }], limit: 1, permissionFrom });
if (!r.success) {
return r;
}
const node = r.result.nodes[0];
return {
success: true,
result: node,
};
}
}
//# sourceMappingURL=HttpConvoCompletionService.js.map