UNPKG

@convo-lang/convo-lang

Version:
824 lines 31.2 kB
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