UNPKG

@convo-lang/convo-lang

Version:
458 lines (454 loc) 14.7 kB
import { getErrorMessage, strHashBase64 } from "@iyio/common"; import { Conversation } from "../Conversation.js"; import { parseConvoCode } from "../convo-parser.js"; import { convoValueToZodType } from "../convo-zod.js"; import { ConvoExecutionContext } from "../ConvoExecutionContext.js"; import { convoDbRuntimeCacheKey } from "./convo-db-types.js"; /** * Create a ConvoDbFunctionExecutionContext that can be passed to `executeConvoDbFunction` * @param compileAndParse If true main will be compiled if needed and args type will be parsed if needed. * @param node * @param keys * @param call * @param db * @param query * @param step * @param paths * @returns */ export const createConvoDbFunctionExecutionContextAsync = async (compileAndParse, node, keys, call, db, query, step, paths, fnNextToken, cancel) => { const fn = node?.data?.['function']; const isExecutable = node?.data?.['isExecutable']; if (!fn || (typeof fn !== 'object')) { return { success: false, error: 'Node did not define a ConvoDbFunction at `data.function`', statusCode: 500, }; } if (isExecutable !== true) { return { success: false, error: 'Function note marked as executable `data.isExecutable` should be set to true', statusCode: 500, }; } let mainWasCompiled = false; let argsTypeWereParsed = false; let argsTypeParsed = fn.argsTypeParsed; let mainCompiled = fn.mainCompiled; if (compileAndParse) { if (!argsTypeParsed && fn.argsType) { try { argsTypeParsed = parseArgs(fn.argsType); argsTypeWereParsed = true; } catch (ex) { return { success: false, error: `Failed to parse args type struct - ${getErrorMessage(ex)}`, statusCode: 500, }; } } if (!mainCompiled) { try { mainCompiled = await compileMainAsync(fn); mainWasCompiled = true; } catch (ex) { return { success: false, error: `Failed to compile main - ${getErrorMessage(ex)}`, statusCode: 500, }; } } } if (!mainCompiled) { return { success: false, error: 'No mainCompiled and compiling not enabled', statusCode: 500, }; } return { success: true, result: { ctx: { node, keys, query: query, step, args: call.args ?? {}, db, function: fn, paths: paths, effects: fn.effects, argsTypeParsed, mainCompiled, nextToken: fnNextToken, cancel }, mainWasCompiled, argsTypeWereParsed, } }; }; const parseArgs = (argsType) => { const r = parseConvoCode(`> define\n${argsType}`); if (r.error) { throw new Error(r.error.message); } if (!r.result) { throw new Error('Parsed args result empty'); } const s = r.result[0]?.statement; if (!s) { throw new Error('Invalid parsing result'); } const exe = new ConvoExecutionContext(); const exR = exe.executeStatement(s, r.result[0]); if (!exR) { throw new Error('Args struct return empty value'); } return { sourceHash: strHashBase64(argsType), parsedArgs: exR, }; }; export const parseConvoDbFunctionArgsType = (argsType) => { try { return { success: true, result: parseArgs(argsType), }; } catch (ex) { return { success: false, error: `Failed to parse args type struct - ${getErrorMessage(ex)}`, statusCode: 500, }; } }; const compileMainAsync = async (fn) => { switch (fn.format) { case 'convo': { const r = parseConvoCode(fn.main); if (r.error) { throw new Error(r.error.message); } if (!r.result) { throw new Error('Empty parsing result'); } return { sourceHash: strHashBase64(fn.main), compiled: r.result, }; } default: return { sourceHash: strHashBase64(fn.main), compiled: null, }; } }; const getImplementationAsync = async (fn, compiled, getRefFunction, verifyHash = true) => { if (compiled && (!verifyHash || strHashBase64(fn.main) === compiled.sourceHash)) { const cached = compiled[convoDbRuntimeCacheKey]; if (cached) { return { success: true, result: cached, }; } } let imp; switch (fn.format) { case 'convo': { const r = createConvoFnImp(fn.main, compiled); if (!r.success) { return r; } imp = r.result; break; } case 'js': case 'javascript': { const r = createJsFnImp(fn.main); if (!r.success) { return r; } imp = r.result; break; } case 'uri': { const r = await getRefFunction?.(fn.main); if (!r) { return { success: false, error: `Unsupported reference function: ${fn.main}`, statusCode: 404, }; } if (!r.success) { return r; } imp = r.result; break; } default: return { success: false, error: `Unsupported ConvoDbFunction format: ${fn.format}`, statusCode: 500, }; } compiled[convoDbRuntimeCacheKey] = imp; return { success: true, result: imp, }; }; const createResultNode = (value) => { return { path: '/null', type: 'function-result', data: { value } }; }; const createConvoFnImp = (src, compiled) => { const messages = compiled.compiled; if (!Array.isArray(messages)) { return { success: false, error: 'Compiled messages not an array', statusCode: 500, }; } return { success: true, result: async (ctx) => { try { const conversation = new Conversation({ defaultVars: { args: ctx.args, ctx, } }); conversation.append(messages, { disableAutoFlatten: true }); const r = await conversation.completeAsync(); const returnValue = r.exe?.getVar('__return'); if (returnValue !== undefined) { return { success: true, result: { node: createResultNode(returnValue) } }; } if (r.message?.format === 'json') { return { success: true, result: { node: createResultNode(JSON.parse(r.message.content ?? 'null')) } }; } else { return { success: true, result: { node: createResultNode(r.message?.content ?? '') } }; } } catch (ex) { return { success: false, error: `Failed to complete convo function conversation: ${getErrorMessage(ex)}`, statusCode: 500 }; } } }; }; const createJsFnImp = (src) => { // todo - run in sandbox try { const evalName = 'eval'; const ev = globalThis[evalName]; const entryPoints = ev(`((args,ctx)=>{ ${src}; const getFromLocalScope=(getter)=>{ try{ return getter(); }catch(ex){ return undefined } } return { handler:getFromLocalScope(()=>handler), nodeHandler:getFromLocalScope(()=>nodeHandler), resultHandler:getFromLocalScope(()=>resultHandler), streamHandler:getFromLocalScope(()=>streamHandler), } })`); return { success: true, result: async (ctx) => { try { const { handler, nodeHandler, resultHandler, streamHandler, } = entryPoints(); if (!handler && !nodeHandler && !resultHandler && !streamHandler) { return { success: false, error: 'No js handlers defined. handler, nodeHandler, resultHandler or streamHandler should be defined', statusCode: 500, }; } const nodes = []; if (handler) { const value = await handler(ctx.args, ctx); if (value !== undefined) { nodes.push(createResultNode(value)); } } if (nodeHandler) { const r = await nodeHandler(ctx.args, ctx); if (r) { if (Array.isArray(r)) { for (const node of r) { if (node) { nodes.push(node); } } } else { nodes.push(r); } } } if (resultHandler) { const r = await resultHandler(ctx.args, ctx); if (!r.success) { return r; } if (r.result) { if (Array.isArray(r.result)) { for (const node of r.result) { if (node) { nodes.push(node); } } } else { nodes.push(r.result); } } } const result = {}; if (nodes.length === 1) { result.node = nodes[0]; } else if (nodes.length > 1) { result.nodes = nodes; } if (streamHandler) { result.stream = streamHandler(ctx.args, ctx); } return { success: true, result, }; } catch (ex) { return { success: false, error: getErrorMessage(ex), statusCode: 500, }; } } }; } catch (ex) { return { success: false, error: `Failed to parse js function - ${getErrorMessage(ex)}`, statusCode: 500 }; } }; export async function* executeConvoDbFunction(ctx, getRefFunction) { try { const { function: fn, mainCompiled, argsTypeParsed, } = ctx; let args = ctx.args; if (argsTypeParsed) { let zodType = argsTypeParsed[convoDbRuntimeCacheKey]; if (!zodType) { try { zodType = convoValueToZodType(argsTypeParsed.parsedArgs); argsTypeParsed[convoDbRuntimeCacheKey] = zodType; } catch (ex) { yield { type: 'error', error: `Failed to convert args type to ZodType - ${getErrorMessage(ex)}`, statusCode: 500 }; return; } } const r = zodType.safeParse(args); if (r.error) { yield { type: 'error', error: `Invalid Args: ${r.error.message}`, statusCode: 400 }; return; } args = r.data; } const cancel = ctx.cancel; const impR = await getImplementationAsync(fn, mainCompiled, getRefFunction); if (cancel.isCanceled) { return; } if (!impR.success) { yield { type: 'error', error: impR.error, statusCode: impR.statusCode }; return; } const impCallResult = await impR.result(ctx); if (cancel.isCanceled) { return; } if (!impCallResult.success) { yield { type: 'error', error: impCallResult.error, statusCode: impCallResult.statusCode }; return impCallResult; } const { node, nodes, stream, } = impCallResult.result; if (node) { yield { type: 'node', node }; if (cancel.isCanceled) { return; } } if (nodes) { for (const node of nodes) { yield { type: 'node', node }; if (cancel.isCanceled) { return; } } } if (stream) { for await (const node of stream) { yield { type: 'node', node }; if (cancel.isCanceled) { return; } } } } catch (ex) { yield { type: 'error', error: getErrorMessage(ex), statusCode: 500 }; } } //# sourceMappingURL=convo-db-function-executor.js.map