UNPKG

agentlang

Version:

The easiest way to build the most reliable AI agents - enterprise-grade teams of AI agents that collaborate with each other and humans

1,701 lines (1,548 loc) 75.4 kB
import { ArrayLiteral, CrudMap, Delete, Expr, FnCall, ForEach, FullTextSearch, Handler, If, isBinExpr, isGroup, isLiteral, isNegExpr, isNotExpr, isReturn, JoinSpec, Literal, MapKey, MapLiteral, Pattern, Purge, RelationshipPattern, Return, RuntimeHint, SelectIntoEntry, SelectIntoSpec, SetAttribute, Statement, } from '../language/generated/ast.js'; import { maybeInstanceAsString, defineAgentEvent, getOneOfRef, getRelationship, getWorkflow, Instance, InstanceAttributes, isAgentEventInstance, isBetweenRelationship, isContainsRelationship, isEmptyWorkflow, isEntityInstance, isEventInstance, isInstanceOfType, isTimer, makeInstance, newInstanceAttributes, PlaceholderRecordEntry, Relationship, Workflow, maybeSetMetaAttributes, } from './module.js'; import { JoinInfo, Resolver } from './resolvers/interface.js'; import { ResolverAuthInfo } from './resolvers/authinfo.js'; import { SqlDbResolver } from './resolvers/sqldb/impl.js'; import { CrudType, DefaultModuleName, escapeFqName, escapeQueryName, fqNameFromPath, isFqName, isPath, isString, makeCoreModuleName, makeFqName, Path, QuerySuffix, restoreSpecialChars, nameToPath, splitRefs, isCoreModule, preprocessRawConfig, } from './util.js'; import { getResolver, getResolverNameForPath } from './resolvers/registry.js'; import { parseStatement, parseWorkflow } from '../language/parser.js'; import { ActiveSessionInfo, AdminSession, AdminUserId } from './auth/defs.js'; import { AgentInstance, AgentEntityName, AgentFqName, findAgentByName, trimGeneratedCode, } from './modules/ai.js'; import { logger } from './logger.js'; import { FlowSuspensionTag, ParentAttributeName, PathAttributeName, PathAttributeNameQuery, } from './defs.js'; import { addCreateAudit, addDeleteAudit, addUpdateAudit, createSuspension, flushMonitoringData, maybeCancelTimer, setTimerRunning, } from './modules/core.js'; import { invokeModuleFn } from './jsmodules.js'; import { invokeOpenApiEvent, isOpenApiEventInstance } from './openapi.js'; import { fetchDoc } from './docs.js'; import { FlowSpec, FlowStep, getAgentFlow } from './agents/flows.js'; import { isMonitoringEnabled } from './state.js'; import { Monitor, MonitorEntry } from './monitor.js'; export type Result = any; const EmptyResult: Result = null; export function isEmptyResult(r: Result): boolean { return r == EmptyResult; } type BetweenRelInfo = { relationship: Relationship; connectedInstance: Instance; }; function mkEnvName(name: string | undefined, parent: Environment | undefined): string { if (name) return name; else { if (parent) { return `${parent.name}+`; } else { return 'env'; } } } type CatchHandlers = Map<string, Statement>; export class Environment extends Instance { parent: Environment | undefined; private activeModule: string; private activeEventInstance: Instance | undefined; private activeUser: string = AdminUserId; private activeUserSet: boolean = false; private lastResult: Result; private trashedResult: Result = undefined; private returnFlag: boolean = false; private parentPath: string | undefined; private normalizedParentPath: string | undefined; private betweenRelInfo: BetweenRelInfo | undefined; private activeResolvers: Map<string, Resolver>; private activeTransactions: Map<string, string>; private inUpsertMode: boolean = false; private inDeleteMode: boolean = false; private inKernelMode: boolean = false; private suspensionId: string | undefined; private preGeneratedSuspensionId: string; private activeCatchHandlers: Array<CatchHandlers>; private eventExecutor: Function | undefined = undefined; private statementsExecutor: Function | undefined = undefined; private scratchPad: any = undefined; private agentMode: 'chat' | 'planner' | undefined = undefined; private agentChatId: string | undefined = undefined; private monitor: Monitor | undefined = undefined; private activeUserData: any = undefined; constructor(name?: string, parent?: Environment) { super( PlaceholderRecordEntry, DefaultModuleName, mkEnvName(name, parent), newInstanceAttributes() ); if (parent !== undefined) { this.parent = parent; this.activeModule = parent.activeModule; this.activeUser = parent.activeUser; this.activeUserSet = parent.activeUserSet; this.setActiveEvent(parent.getActiveEventInstance()); this.lastResult = parent.lastResult; this.activeTransactions = parent.activeTransactions; this.activeResolvers = parent.activeResolvers; this.inUpsertMode = parent.inUpsertMode; this.inKernelMode = parent.inKernelMode; this.activeCatchHandlers = parent.activeCatchHandlers; this.suspensionId = parent.suspensionId; this.eventExecutor = parent.eventExecutor; this.agentChatId = parent.agentChatId; this.monitor = parent.monitor; } else { this.activeModule = DefaultModuleName; this.activeResolvers = new Map<string, Resolver>(); this.activeTransactions = new Map<string, string>(); this.activeCatchHandlers = new Array<CatchHandlers>(); this.attributes.set('process', process); } this.preGeneratedSuspensionId = crypto.randomUUID(); } static from( parent: Environment, name?: string | undefined, isAsync: boolean = false ): Environment { const env = new Environment(name, parent); if (isAsync) { env.activeResolvers = new Map<string, Resolver>(); env.activeTransactions = new Map<string, string>(); env.activeCatchHandlers = new Array<CatchHandlers>(); env.preGeneratedSuspensionId = parent.preGeneratedSuspensionId; } return env; } static fromInstance(inst: Instance): Environment { const env = new Environment(); env.attributes = inst.attributes; return env; } override asSerializableObject(): object { const obj: any = super.asSerializableObject(); obj.activeModule = this.activeModule; if (this.activeEventInstance) { obj.activeEventInstance = this.activeEventInstance.asSerializableObject(); } obj.activeUser = this.activeUser; obj.activeUserSet = this.activeUserSet; obj.inUpsertMode = this.inUpsertMode; obj.inDeleteMode = this.inDeleteMode; obj.inKernelMode = this.inKernelMode; if (this.parent) { obj.parent = this.parent.asSerializableObject(); } return obj; } static override FromSerializableObject(obj: any): Environment { const inst = Instance.FromSerializableObject(obj, PlaceholderRecordEntry); const env = Environment.fromInstance(inst); env.activeModule = obj.activeModule; if (obj.activeEventInstance) { env.activeEventInstance = Instance.FromSerializableObject(obj.activeEventInstance); } env.activeUser = obj.activeUser; env.activeUserSet = obj.activeUserSet; env.inUpsertMode = obj.inUpsertMode; env.inDeleteMode = obj.inDeleteMode; env.inKernelMode = obj.inKernelMode; if (obj.parent) { env.parent = Environment.FromSerializableObject(obj.parent); } return env; } override lookup(k: string): Result { const v = this.attributes.get(k); if (v === undefined) { if (this.parent !== undefined) { return this.parent.lookup(k); } else if (this == GlobalEnvironment) { return EmptyResult; } else { return GlobalEnvironment.lookup(k); } } else return v; } bind(k: string, v: any): Environment { this.attributes.set(k, v); return this; } bindInstance(inst: Instance): Environment { const n: string = inst.name; this.attributes.set(n, inst); return this; } private static FlowContextTag = 'flow-context'; setFlowContext(s: string): Environment { this.attributes.set(Environment.FlowContextTag, s); return this; } resetFlowContext(): Environment { this.attributes.set(Environment.FlowContextTag, undefined); return this; } getFlowContext(): string | undefined { return this.attributes.get(Environment.FlowContextTag); } addToScratchPad(k: string, data: any): Environment { if (this.scratchPad === undefined) { this.scratchPad = {}; } if (isFqName(k)) { const parts = nameToPath(k); this.scratchPad[parts.getEntryName()] = data; } this.scratchPad[k] = data; return this.addAttributesToScratchPad(data); } private addAttributesToScratchPad(data: any): Environment { if (data instanceof Map) { data.forEach((v: any, k: any) => { this.addToScratchPad(k as string, v); }); } else if (data instanceof Array) { return this; } else if (data instanceof Object) { Object.keys(data).forEach((k: string) => { this.addToScratchPad(k, data[k]); }); } return this; } getScratchPad(): any { return this.scratchPad; } resetScratchPad(): Environment { this.scratchPad = undefined; return this; } setScratchPad(obj: any): Environment { this.scratchPad = obj; return this; } private templateMappings: Map<string, string> | undefined; setTemplateMapping(k: string, v: string): Environment { if (this.templateMappings === undefined) { this.templateMappings = new Map<string, string>(); } this.templateMappings.set(k, v); return this; } rewriteTemplateMappings(s: string): string { if (this.templateMappings !== undefined) { this.templateMappings.keys().forEach((k: string) => { const tk = `{{${k}}}`; const v = this.templateMappings?.get(k); if (v) { const tv = `{{${v}}}`; s = s.replaceAll(tk, tv); } }); } return s; } resetTemplateMappings(): Environment { this.templateMappings?.clear(); return this; } static SuspensionUserData = '^'; bindSuspensionUserData(userData: string): Environment { this.bind(Environment.SuspensionUserData, userData); return this; } lookupSuspensionUserData(): string | undefined { return this.lookup(Environment.SuspensionUserData); } maybeLookupAgentInstance(entryName: string): Instance | undefined { const v = this.lookup(entryName); if (v && isInstanceOfType(v, AgentFqName)) { return v as Instance; } else { return undefined; } } setActiveEvent(eventInst: Instance | undefined): Environment { if (eventInst) { if (!isEventInstance(eventInst)) throw new Error(`Not an event instance - ${eventInst.name}`); this.bindInstance(eventInst); this.activeModule = eventInst.moduleName; this.activeEventInstance = eventInst; if (!this.activeUserSet) { this.activeUser = eventInst.getAuthContextUserId(); this.activeUserSet = true; } } return this; } getActiveEventInstance(): Instance | undefined { return this.activeEventInstance; } isSuspended(): boolean { return this.suspensionId !== undefined; } suspend(): string { if (this.suspensionId === undefined) { const id = this.preGeneratedSuspensionId; this.propagateSuspension(id); return id; } else { return this.suspensionId; } } releaseSuspension(): Environment { this.suspensionId = undefined; this.preGeneratedSuspensionId = crypto.randomUUID(); return this; } fetchSuspensionId(): string { return this.preGeneratedSuspensionId; } markForReturn(): Environment { if (this.parent) { this.parent.markForReturn(); } this.returnFlag = true; return this; } isMarkedForReturn(): boolean { return this.returnFlag; } propagateLastResult(): Environment { if (this.parent) { this.parent.lastResult = this.lastResult; this.parent.propagateLastResult(); } return this; } resetReturnFlag(): Environment { if (this.returnFlag) { this.returnFlag = false; if (this.parent) { this.parent.resetReturnFlag(); } } return this; } protected propagateSuspension(suspId: string) { this.suspensionId = suspId; if (this.parent) { this.parent.propagateSuspension(suspId); } } getSuspensionId(): string { if (this.suspensionId) { return this.suspensionId; } else { throw new Error('SuspensionId is not set'); } } getActiveAuthContext(): ActiveSessionInfo | undefined { if (this.activeEventInstance) { return this.activeEventInstance.getAuthContext(); } return undefined; } getActiveToken(): string | undefined { if (this.activeEventInstance) { const sess = this.activeEventInstance.getAuthContext(); if (sess) { return sess.sessionId; } } return undefined; } setActiveUser(userId: string): Environment { this.activeUser = userId; this.activeUserSet = true; return this; } getActiveUser(): string { return this.activeUser; } setLastResult(result: Result): Environment { this.trashedResult = this.lastResult; this.lastResult = result; return this; } revokeLastResult(): Environment { if (this.trashedResult !== undefined) { this.lastResult = this.trashedResult; this.trashedResult = undefined; } return this; } getLastResult(): Result { return this.lastResult; } getActiveModuleName(): string { return this.activeModule; } switchActiveModuleName(newModuleName: string): string { const oldModuleName = this.activeModule; this.activeModule = newModuleName; return oldModuleName; } setParentPath(path: string): Environment { this.parentPath = path; return this; } getParentPath(): string | undefined { return this.parentPath; } setNormalizedParentPath(path: string): Environment { this.normalizedParentPath = path; return this; } getNormalizedParentPath(): string | undefined { return this.normalizedParentPath; } setBetweenRelInfo(info: BetweenRelInfo): Environment { this.betweenRelInfo = info; return this; } getBetweenRelInfo(): BetweenRelInfo | undefined { return this.betweenRelInfo; } setActiveResolvers(resolvers: Map<string, Resolver>): Environment { this.activeResolvers = resolvers; return this; } getActiveResolvers(): Map<string, Resolver> { return this.activeResolvers; } getResolver(resolverName: string): Resolver | undefined { const r: Resolver | undefined = this.getActiveResolvers().get(resolverName); if (r) { return r.setEnvironment(this); } return undefined; } async addResolver(resolver: Resolver): Promise<Environment> { this.getActiveResolvers().set(resolver.getName(), resolver); await this.ensureTransactionForResolver(resolver); resolver.setEnvironment(this); return this; } setActiveTransactions(txns: Map<string, string>): Environment { this.activeTransactions = txns; return this; } getActiveTransactions(): Map<string, string> { return this.activeTransactions; } async resetActiveTransactions(commit: boolean): Promise<Environment> { await this.endAllTransactions(commit); this.activeTransactions = new Map<string, string>(); return this; } async getTransactionForResolver(resolver: Resolver): Promise<string> { const n: string = resolver.getName(); let txnId: string | undefined = this.activeTransactions.get(n); if (txnId) { return txnId; } else { txnId = await resolver.startTransaction(); if (txnId) { this.activeTransactions.set(n, txnId); return txnId; } else { throw new Error(`Failed to start transaction for ${n}`); } } } async ensureTransactionForResolver(resolver: Resolver): Promise<Environment> { await this.getTransactionForResolver(resolver); return this; } private async endAllTransactions(commit: boolean): Promise<void> { const txns: Map<string, string> = this.activeTransactions; for (const n of txns.keys()) { const txnId: string | undefined = txns.get(n); if (txnId) { const res: Resolver | undefined = this.getResolver(n); if (res) { if (commit) await res.commitTransaction(txnId); else await res.rollbackTransaction(txnId); } } } } async callInTransaction(f: Function): Promise<any> { let result: any; let commit: boolean = true; await f() .then((r: any) => { result = r; }) .catch((r: any) => { commit = false; result = r; }); await this.endAllTransactions(commit); if (!commit) { throw result; } return result; } async commitAllTransactions(): Promise<void> { await this.endAllTransactions(true); } async rollbackAllTransactions(): Promise<void> { await this.endAllTransactions(false); } setInUpsertMode(flag: boolean): Environment { this.inUpsertMode = flag; return this; } isInUpsertMode(): boolean { return this.inUpsertMode; } setInDeleteMode(flag: boolean): Environment { this.inDeleteMode = flag; return this; } isInDeleteMode(): boolean { return this.inDeleteMode; } setInKernelMode(flag: boolean): Environment { this.inKernelMode = flag; return this; } isInKernelMode(): boolean { return this.inKernelMode; } pushHandlers(handlers: CatchHandlers): boolean { if (handlers.has('error')) { this.activeCatchHandlers.push(handlers); return true; } return false; } hasHandlers(): boolean { return this.activeCatchHandlers.length > 0; } popHandlers(): CatchHandlers { const r = this.activeCatchHandlers.pop(); if (r === undefined) { throw new Error(`No more handlers to pop`); } return r; } setEventExecutor(exec: Function): Environment { this.eventExecutor = exec; return this; } getEventExecutor(): Function | undefined { return this.eventExecutor; } unsetEventExecutor(): Environment { this.eventExecutor = undefined; return this; } setStatementsExecutor(f: Function): Environment { this.statementsExecutor = f; return this; } getStatementsExecutor(): Function | undefined { return this.statementsExecutor; } async callWithStatementsExecutor(exec: Function, f: Function): Promise<any> { const oldExec = this.statementsExecutor; this.statementsExecutor = exec; try { return await f(); } finally { this.statementsExecutor = oldExec; } } setActiveUserData(data: any): Environment { this.activeUserData = data; return this; } getActiveUserData(): any { return this.activeUserData; } inChatAgentMode(): Environment { this.agentMode = 'chat'; return this; } inPlannerAgentMode(): Environment { this.agentMode = 'planner'; return this; } resetAgentMode(): Environment { this.agentMode = undefined; return this; } isInAgentChatMode(): boolean { return this.agentMode === 'chat'; } isAgentModeSet(): boolean { return this.agentMode !== undefined; } setAgentChatId(chatId: string): Environment { this.agentChatId = chatId; return this; } getAgentChatId(): string | undefined { return this.agentChatId; } appendEntryToMonitor(stmt: string): Environment { if (this.monitor === undefined) { if (this.activeEventInstance && isCoreModule(this.activeEventInstance.moduleName)) { return this; } this.monitor = new Monitor(this.activeEventInstance, this.activeUser); } this.monitor.addEntry(new MonitorEntry(stmt)); return this; } setMonitorEntryError(reason: string): Environment { if (this.monitor !== undefined) { this.monitor.setEntryError(reason); } return this; } setMonitorEntryResult(result: any): Environment { if (this.monitor !== undefined) { this.monitor.setEntryResult(result); } return this; } flagMonitorEntryAsLlm(): Environment { if (this.monitor !== undefined) { this.monitor.flagEntryAsLlm(); } return this; } flagMonitorEntryAsPlanner(): Environment { if (this.monitor !== undefined) { this.monitor.flagEntryAsPlanner(); } return this; } flagMonitorEntryAsFlow(): Environment { if (this.monitor !== undefined) { this.monitor.flagEntryAsFlow(); } return this; } flagMonitorEntryAsFlowStep(): Environment { if (this.monitor !== undefined) { this.monitor.flagEntryAsFlowStep(); } return this; } flagMonitorEntryAsDecision(): Environment { if (this.monitor !== undefined) { this.monitor.flagEntryAsDecision(); } return this; } setMonitorEntryLlmPrompt(s: string): Environment { if (this.monitor !== undefined) { this.monitor.setEntryLlmPrompt(s); } return this; } setMonitorEntryLlmResponse(s: string): Environment { if (this.monitor !== undefined) { this.monitor.setEntryLlmResponse(s); } return this; } incrementMonitor(): Environment { if (this.monitor !== undefined) { this.monitor = this.monitor.increment(); } return this; } decrementMonitor(): Environment { if (this.monitor !== undefined) { this.monitor = this.monitor.decrement(); } return this; } setMonitorFlowResult(): Environment { if (this.monitor !== undefined) { this.monitor.setFlowResult(this.lastResult); } return this; } } export const GlobalEnvironment = new Environment(); export let evaluate = async function ( eventInstance: Instance, continuation?: Function, activeEnv?: Environment, kernelCall?: boolean ): Promise<Result> { let env: Environment | undefined; let txnRolledBack: boolean = false; try { if (isEventInstance(eventInstance)) { const wf: Workflow = getWorkflow(eventInstance); if (!isEmptyWorkflow(wf)) { env = new Environment(eventInstance.name + '.env', activeEnv); env.setActiveEvent(eventInstance); if (kernelCall) { env.setInKernelMode(true); } await evaluateStatements(wf.statements, env, continuation); return env.getLastResult(); } else if (isAgentEventInstance(eventInstance)) { env = new Environment(eventInstance.name + '.env', activeEnv); await handleAgentInvocation(eventInstance, env); if (continuation) continuation(env.getLastResult()); } else if (isOpenApiEventInstance(eventInstance)) { env = new Environment(eventInstance.name + '.env', activeEnv); await handleOpenApiEvent(eventInstance, env); const r = env.getLastResult(); if (continuation) continuation(r); return r; } else { if (continuation) continuation(null); return null; } } else { throw new Error('Not an event - ' + eventInstance.name); } } catch (err) { if (env && env.hasHandlers()) { throw err; } else { if (env !== undefined && activeEnv === undefined) { await env.rollbackAllTransactions().then(() => { txnRolledBack = true; }); } throw err; } } finally { if (!txnRolledBack && env !== undefined && activeEnv === undefined) { await env.commitAllTransactions(); } if (isMonitoringEnabled()) { await flushMonitoringData(eventInstance.getId()); } } }; export function setEvaluateFn(f: any): Function { const oldf = evaluate; evaluate = f; return oldf; } export async function evaluateAsEvent( moduleName: string, eventName: string, attrs: Array<any> | object, activeSession?: ActiveSessionInfo, env?: Environment, kernelCall?: boolean ): Promise<Result> { const finalAttrs: Map<string, any> = attrs instanceof Array ? new Map(attrs) : new Map(Object.entries(attrs)); const eventInst: Instance = makeInstance(moduleName, eventName, finalAttrs).setAuthContext( activeSession || AdminSession ); let result: any; await evaluate(eventInst, (r: any) => (result = r), env, kernelCall); return result; } export function makeEventEvaluator(moduleName: string): Function { return async ( eventName: string, attrs: Array<any> | object, env: Environment, session?: ActiveSessionInfo, kernelCall: boolean = true ): Promise<Result> => { if (!env) { env = new Environment(); } return await evaluateAsEvent(moduleName, eventName, attrs, session, env, kernelCall); }; } export async function evaluateStatements( stmts: Statement[], env: Environment, continuation?: Function ) { for (let i = 0; i < stmts.length; ++i) { const stmt = stmts[i]; await evaluateStatement(stmt, env); if (env.isMarkedForReturn()) { break; } } if (continuation !== undefined) { continuation(env.getLastResult()); } } async function evaluateAsyncPattern( pat: Pattern, thenStmts: Statement[], handlers: CatchHandlers | undefined, hints: RuntimeHint[], env: Environment ): Promise<void> { try { await evaluatePattern(pat, env); maybeBindStatementResultToAlias(hints, env); if (env.isSuspended()) { await createSuspension( env.fetchSuspensionId(), thenStmts.map((s: Statement) => { if (s.$cstNode) { return s.$cstNode.text; } else { throw new Error('failed to extract code for suspension statement'); } }), env ); } else { await evaluateStatements(thenStmts, env); } } catch (reason: any) { await env.rollbackAllTransactions(); await maybeHandleError(handlers, reason, env); } finally { await env.commitAllTransactions(); } } export async function evaluateStatement(stmt: Statement, env: Environment): Promise<void> { const hints = stmt.hints; const hasHints = hints && hints.length > 0; const thenStmts: Statement[] | undefined = hasHints ? maybeFindThenStatements(hints) : undefined; const handlers: CatchHandlers | undefined = hasHints ? maybeFindHandlers(hints) : undefined; if (thenStmts) { evaluateAsyncPattern( stmt.pattern, thenStmts, handlers, hints, Environment.from(env, env.name + 'async', true) ); env.setLastResult(env.fetchSuspensionId()); if (isReturn(stmt.pattern)) { env.markForReturn(); } if (hasHints) { maybeBindStatementResultToAlias(hints, env); } return; } let handlersPushed = false; try { if (handlers) { handlersPushed = env.pushHandlers(handlers); } await evaluatePattern(stmt.pattern, env); if (hasHints) { maybeBindStatementResultToAlias(hints, env); } await maybeHandleNotFound(handlers, env); } catch (reason: any) { await maybeHandleError(handlers, reason, env); } finally { if (handlersPushed && env.hasHandlers()) { env.popHandlers(); } } } async function maybeHandleNotFound(handlers: CatchHandlers | undefined, env: Environment) { const lastResult: Result = env.getLastResult(); if ( lastResult === null || lastResult === undefined || (lastResult instanceof Array && lastResult.length == 0) ) { const onNotFound = handlers ? handlers.get('not_found') : undefined; if (onNotFound) { const newEnv = new Environment('not-found-env', env).unsetEventExecutor(); await evaluateStatement(onNotFound, newEnv); env.setLastResult(newEnv.getLastResult()); } } } async function maybeHandleError( handlers: CatchHandlers | undefined, reason: any, env: Environment ) { const handler = handlers ? handlers.get('error') : undefined; if (handler) { const newEnv = new Environment('handler-env', env).unsetEventExecutor(); await evaluateStatement(handler, newEnv); env.setLastResult(newEnv.getLastResult()); } else { throw reason; } } export function maybeBindStatementResultToAlias(hints: RuntimeHint[], env: Environment) { for (let i = 0; i < hints.length; ++i) { const rh = hints[i]; if (rh.aliasSpec) { if (rh.aliasSpec.alias !== undefined || rh.aliasSpec.aliases.length > 0) { const result: Result = env.getLastResult(); const alias: string | undefined = rh.aliasSpec.alias; if (alias !== undefined) { env.bind(alias, result); } else { const aliases: string[] = rh.aliasSpec.aliases; if (result instanceof Array) { const resArr: Array<any> = result as Array<any>; for (let i = 0; i < aliases.length; ++i) { const k: string = aliases[i]; if (k == '__') { env.bind(aliases[i + 1], resArr.splice(i)); break; } else if (k != '_') { env.bind(aliases[i], resArr[i]); } } } else { env.bind(aliases[0], result); } } } break; } } } function maybeFindHandlers(hints: RuntimeHint[]): Map<string, Statement> | undefined { for (let i = 0; i < hints.length; ++i) { const rh = hints[i]; if (rh.catchSpec) { const result = new Map<string, Statement>(); rh.catchSpec.handlers.forEach((h: Handler) => { result.set(h.except, h.stmt); }); return result; } } return undefined; } function maybeFindThenStatements(hints: RuntimeHint[]): Statement[] | undefined { for (let i = 0; i < hints.length; ++i) { const rh = hints[i]; if (rh.thenSpec) { return rh.thenSpec.statements; } } return undefined; } export let parseAndEvaluateStatement = async function ( stmtString: string, activeUserId?: string, actievEnv?: Environment ): Promise<Result> { const env = actievEnv ? actievEnv : new Environment(); if (activeUserId) { env.setActiveUser(activeUserId); } let commit: boolean = true; try { const stmt: Statement = await parseStatement(stmtString); if (stmt) { await evaluateStatement(stmt, env); return env.getLastResult(); } else { commit = false; } } catch (err) { commit = false; throw err; } finally { if (!actievEnv) { if (commit) { await env.commitAllTransactions(); } else { await env.rollbackAllTransactions(); } } } }; export function setParseAndEvaluateStatementFn(f: any): Function { const oldf = parseAndEvaluateStatement; parseAndEvaluateStatement = f; return oldf; } export async function lookupAllInstances(entityFqName: string): Promise<Instance[]> { return await parseAndEvaluateStatement(`{${entityFqName}? {}}`); } export class PatternHandler { async handleExpression(expr: Expr, env: Environment) { await evaluateExpression(expr, env); } async handleCrudMap(crudMap: CrudMap, env: Environment) { await evaluateCrudMap(crudMap, env); } async handleForEach(forEach: ForEach, env: Environment) { await evaluateForEach(forEach, env); } async handleIf(_if: If, env: Environment) { await evaluateIf(_if, env); } async handleDelete(del: Delete, env: Environment) { await evaluateDelete(del, env); } async handlePurge(purge: Purge, env: Environment) { await evaluatePurge(purge, env); } async handleFullTextSearch(fullTextSearch: FullTextSearch, env: Environment) { await evaluateFullTextSearch(fullTextSearch, env); } async handleReturn(ret: Return, env: Environment) { await evaluatePattern(ret.pattern, env); } } const DefaultPatternHandler = new PatternHandler(); export async function evaluatePattern( pat: Pattern, env: Environment, handler: PatternHandler = DefaultPatternHandler ): Promise<void> { if (pat.expr) { await handler.handleExpression(pat.expr, env); } else if (pat.crudMap) { await handler.handleCrudMap(pat.crudMap, env); } else if (pat.forEach) { await handler.handleForEach(pat.forEach, env); } else if (pat.if) { await handler.handleIf(pat.if, env); } else if (pat.delete) { await handler.handleDelete(pat.delete, env); } else if (pat.purge) { await handler.handlePurge(pat.purge, env); } else if (pat.fullTextSearch) { await handler.handleFullTextSearch(pat.fullTextSearch, env); } else if (pat.return) { await handler.handleReturn(pat.return, env); env.markForReturn(); } } async function evaluateFullTextSearch(fts: FullTextSearch, env: Environment): Promise<void> { let n = escapeQueryName(fts.name); if (!isFqName(n)) { const inst: Instance | undefined = env.getActiveEventInstance(); if (inst) { n = makeFqName(inst.moduleName, n); } else { throw new Error(`Fully qualified name required for full-text-search in ${n}`); } } const path = nameToPath(n); const entryName = path.getEntryName(); const moduleName = path.getModuleName(); const resolver = await getResolverForPath(entryName, moduleName, env); await evaluateLiteral(fts.query, env); const q = env.getLastResult(); if (!isString(q)) { throw new Error(`Full text search query must be a string - ${q}`); } let options: Map<string, any> | undefined; if (fts.options) { await realizeMap(fts.options, env); options = env.getLastResult(); } env.setLastResult(await resolver.fullTextSearch(entryName, moduleName, q, options)); } async function evaluateLiteral(lit: Literal, env: Environment): Promise<void> { if (lit.id !== undefined) env.setLastResult(env.lookup(lit.id)); else if (lit.ref !== undefined) env.setLastResult(await followReference(env, lit.ref)); else if (lit.fnCall !== undefined) await applyFn(lit.fnCall, env, false); else if (lit.asyncFnCall !== undefined) await applyFn(lit.asyncFnCall.fnCall, env, true); else if (lit.array !== undefined) await realizeArray(lit.array, env); else if (lit.map !== undefined) await realizeMap(lit.map, env); else if (lit.num !== undefined) env.setLastResult(lit.num); else if (lit.str !== undefined) env.setLastResult(restoreSpecialChars(lit.str)); else if (lit.bool !== undefined) env.setLastResult(lit.bool == 'true' ? true : false); } function getMapKey(k: MapKey): Result { if (k.str !== undefined) return k.str; else if (k.num !== undefined) return k.num; else if (k.bool !== undefined) return k.bool == 'true' ? true : false; } const DefaultResolverName: string = '-'; async function getResolverForPath( entryName: string, moduleName: string, env: Environment, isReadForUpdate: boolean = false, isReadForDelete: boolean = false ): Promise<Resolver> { const fqEntryName: string = isFqName(entryName) ? entryName : makeFqName(moduleName, entryName); const resN: string | undefined = getResolverNameForPath(fqEntryName); let res: Resolver | undefined; if (resN === undefined) { res = env.getResolver(DefaultResolverName); if (res === undefined) { res = new SqlDbResolver(DefaultResolverName); await env.addResolver(res); } } else { res = env.getResolver(resN); if (res === undefined) { res = getResolver(fqEntryName); await env.addResolver(res); } } const authInfo: ResolverAuthInfo = new ResolverAuthInfo( env.getActiveUser(), isReadForUpdate, isReadForDelete ); return res.setAuthInfo(authInfo); } async function lookupOneOfVals(fqName: string, env: Environment): Promise<Instance[] | null> { return await parseAndEvaluateStatement(`{${fqName}? {}}`, undefined, env); } async function patternToInstance( entryName: string, attributes: SetAttribute[] | undefined, env: Environment ): Promise<Instance> { const attrs: InstanceAttributes = newInstanceAttributes(); let qattrs: InstanceAttributes | undefined; let qattrVals: InstanceAttributes | undefined; const isQueryAll: boolean = entryName.endsWith(QuerySuffix); if (isQueryAll) { entryName = entryName.slice(0, entryName.length - 1); } if (attributes) { for (let i = 0; i < attributes.length; ++i) { const a: SetAttribute = attributes[i]; await evaluateExpression(a.value, env); const v: Result = env.getLastResult(); let aname: string = a.name; if (aname.endsWith(QuerySuffix)) { if (isQueryAll) { throw new Error(`Cannot specifiy query attribute ${aname} here`); } if (qattrs === undefined) qattrs = newInstanceAttributes(); if (qattrVals === undefined) qattrVals = newInstanceAttributes(); aname = aname.slice(0, aname.length - 1); qattrs.set(aname, a.op === undefined ? '=' : a.op); qattrVals.set(aname, v); } else { attrs.set(aname, v); } } } let moduleName = env.getActiveModuleName(); if (isFqName(entryName)) { const p: Path = nameToPath(entryName); if (p.hasModule()) moduleName = p.getModuleName(); if (p.hasEntry()) entryName = p.getEntryName(); } return makeInstance(moduleName, entryName, attrs, qattrs, qattrVals, isQueryAll); } async function instanceFromSource(crud: CrudMap, env: Environment): Promise<Instance> { if (crud.source) { await evaluateLiteral(crud.source, env); const attrsSrc = env.getLastResult(); if (attrsSrc && attrsSrc instanceof Object) { const attrs: InstanceAttributes = new Map(Object.entries(attrsSrc)); const nparts = nameToPath(crud.name); const n = nparts.getEntryName(); const m = nparts.hasModule() ? nparts.getModuleName() : env.getActiveModuleName(); return makeInstance(m, n, attrs); } else { throw new Error(`Failed to initialize instance of ${crud.name}, expected a map after @from.`); } } else { throw new Error( `Cannot create instance of ${crud.name}, CRUD pattern does not specify a source map.` ); } } async function maybeValidateOneOfRefs(inst: Instance, env: Environment) { const attrs = inst.record.oneOfRefAttributes; if (!attrs) return; for (let i = 0; i < attrs.length; ++i) { const n = attrs[i]; const v = inst.lookup(n); if (v === undefined) continue; const attrSpec = inst.record.schema.get(n); if (!attrSpec) continue; const r = getOneOfRef(attrSpec); if (!r) throw new Error(`Failed to fetch one-of-ref for ${n}`); if (r) { const parts = r.split('.'); const insts = await lookupOneOfVals(parts[0], env); if (!insts || insts.length == 0) { logger.warn(`No enum values set for ${n}`); continue; } if ( !insts.some((i: Instance) => { return i.lookup(parts[1]) == v; }) ) { throw new Error(`Invalid enum-value ${v} for ${n}`); } } } } async function evaluateCrudMap(crud: CrudMap, env: Environment): Promise<void> { if (!env.isInUpsertMode() && crud.upsert.length > 0) { return await evaluateUpsert(crud, env); } const inst: Instance = crud.source ? await instanceFromSource(crud, env) : await patternToInstance(crud.name, crud.body?.attributes, env); const entryName = inst.name; const moduleName = inst.moduleName; const attrs = inst.attributes; const qattrs = inst.queryAttributes; const isQueryAll = crud.name.endsWith(QuerySuffix); const distinct: boolean = crud.distinct.length > 0; if (attrs.size > 0) { await maybeValidateOneOfRefs(inst, env); } if (crud.into) { if (attrs.size > 0) { throw new Error( `Query pattern for ${entryName} with 'into' clause cannot be used to update attributes` ); } if (qattrs === undefined && !isQueryAll) { throw new Error(`Pattern for ${entryName} with 'into' clause must be a query`); } if (crud.join) { await evaluateJoinQuery(crud.join, crud.into, inst, distinct, env); } else { await evaluateJoinQueryWithRelationships(crud.into, inst, crud.relationships, distinct, env); } return; } if (isEntityInstance(inst) || isBetweenRelationship(inst.name, inst.moduleName)) { if (qattrs === undefined && !isQueryAll) { const parentPath: string | undefined = env.getParentPath(); if (parentPath) { inst.attributes.set(PathAttributeName, parentPath); inst.attributes.set(ParentAttributeName, env.getNormalizedParentPath() || ''); } const res: Resolver = await getResolverForPath(entryName, moduleName, env); let r: Instance | undefined; await computeExprAttributes(inst, undefined, undefined, env); maybeSetMetaAttributes(inst.attributes, env); if (env.isInUpsertMode()) { await runPreUpdateEvents(inst, env); r = await res.upsertInstance(inst); await runPostUpdateEvents(inst, undefined, env); } else { await runPreCreateEvents(inst, env); if (isTimer(inst)) triggerTimer(inst); r = await res.createInstance(inst); await runPostCreateEvents(inst, env); } if (r && entryName == AgentEntityName && inst.moduleName == CoreAIModuleName) { defineAgentEvent(env.getActiveModuleName(), r.lookup('name'), r.lookup('instruction')); } env.setLastResult(r); const betRelInfo: BetweenRelInfo | undefined = env.getBetweenRelInfo(); if (betRelInfo) { await res.connectInstances( betRelInfo.connectedInstance, env.getLastResult(), betRelInfo.relationship, env.isInUpsertMode() ); } if (crud.relationships !== undefined) { for (let i = 0; i < crud.relationships.length; ++i) { const rel: RelationshipPattern = crud.relationships[i]; const relEntry: Relationship = getRelationship(rel.name, moduleName); const newEnv: Environment = Environment.from(env); if (isContainsRelationship(rel.name, moduleName)) { const ppath = inst.attributes.get(PathAttributeName); newEnv.setParentPath(`${ppath}/${escapeFqName(relEntry.getFqName())}`); newEnv.setNormalizedParentPath(ppath); await evaluatePattern(rel.pattern, newEnv); const lastInst: Instance = env.getLastResult(); lastInst.attachRelatedInstances(rel.name, newEnv.getLastResult()); } else if (isBetweenRelationship(rel.name, moduleName)) { const lastInst: Instance = env.getLastResult() as Instance; await evaluatePattern(rel.pattern, newEnv); const relResult: any = newEnv.getLastResult(); const res: Resolver = await getResolverForPath(rel.name, moduleName, env); await res.connectInstances(lastInst, relResult, relEntry, env.isInUpsertMode()); lastInst.attachRelatedInstances(rel.name, newEnv.getLastResult()); } } } } else { const parentPath: string | undefined = env.getParentPath(); const betRelInfo: BetweenRelInfo | undefined = env.getBetweenRelInfo(); const isReadForUpdate = attrs.size > 0; let res: Resolver = Resolver.Default; if (parentPath !== undefined) { res = await getResolverForPath(inst.name, inst.moduleName, env); const insts: Instance[] = await res.queryChildInstances(parentPath, inst); env.setLastResult(insts); } else if (betRelInfo !== undefined) { res = await getResolverForPath( betRelInfo.relationship.name, betRelInfo.relationship.moduleName, env ); const insts: Instance[] = await res.queryConnectedInstances( betRelInfo.relationship, betRelInfo.connectedInstance, inst ); env.setLastResult(insts); } else { res = await getResolverForPath( inst.name, inst.moduleName, env, isReadForUpdate, env.isInDeleteMode() ); const insts: Instance[] = await res.queryInstances(inst, isQueryAll, distinct); env.setLastResult(insts); } if (crud.relationships !== undefined) { const lastRes: Instance[] = env.getLastResult(); for (let i = 0; i < crud.relationships.length; ++i) { const rel: RelationshipPattern = crud.relationships[i]; const relEntry: Relationship = getRelationship(rel.name, moduleName); for (let j = 0; j < lastRes.length; ++j) { const newEnv: Environment = Environment.from(env); if (isContainsRelationship(rel.name, moduleName)) { const currInst: Instance = lastRes[j]; let ppath = ''; if (relEntry.isParent(currInst)) { ppath = currInst.lookup(PathAttributeName); newEnv.setParentPath(ppath + '/' + escapeFqName(relEntry.getFqName())); } else { ppath = currInst.lookup(ParentAttributeName); newEnv.setParentPath(ppath); } newEnv.setNormalizedParentPath(ppath); await evaluatePattern(rel.pattern, newEnv); lastRes[j].attachRelatedInstances(rel.name, newEnv.getLastResult()); } else if (isBetweenRelationship(rel.name, moduleName)) { newEnv.setBetweenRelInfo({ relationship: relEntry, connectedInstance: lastRes[j] }); await evaluatePattern(rel.pattern, newEnv); lastRes[j].attachRelatedInstances(rel.name, newEnv.getLastResult()); } } } } if (isReadForUpdate) { const lastRes: Instance[] | Instance = env.getLastResult(); if (lastRes instanceof Array) { if (lastRes.length > 0) { const resolver: Resolver = await getResolverForPath( lastRes[0].name, lastRes[0].moduleName, env ); const res: Array<Instance> = new Array<Instance>(); for (let i = 0; i < lastRes.length; ++i) { await computeExprAttributes(lastRes[i], crud.body?.attributes, attrs, env); env.attributes.set('__patch', attrs); await runPreUpdateEvents(lastRes[i], env); maybeSetMetaAttributes(attrs, env, true); const finalInst: Instance = await resolver.updateInstance(lastRes[i], attrs); await runPostUpdateEvents(finalInst, lastRes[i], env); res.push(finalInst); } env.setLastResult(res); } else { env.setLastResult(lastRes); } } else { const res: Resolver = await getResolverForPath(lastRes.name, lastRes.moduleName, env); await computeExprAttributes(lastRes, crud.body?.attributes, attrs, env); await runPreUpdateEvents(lastRes, env); const finalInst: Instance = await res.updateInstance(lastRes, attrs); await runPostUpdateEvents(finalInst, lastRes, env); env.setLastResult(finalInst); } } } } else if (isEventInstance(inst)) { if (isAgentEventInstance(inst)) await handleAgentInvocation(inst, env); else if (isOpenApiEventInstance(inst)) await handleOpenApiEvent(inst, env); else if (isDocEventInstance(inst)) await handleDocEvent(inst, env); else { const eventExec = env.getEventExecutor(); const newEnv = new Environment(`${inst.name}.env`, env); if (eventExec) { await eventExec(inst, newEnv); env.setLastResult(newEnv.getLastResult()); } else { await evaluate(inst, (result: Result) => env.setLastResult(result), newEnv); } env.resetReturnFlag(); } } else { env.setLastResult(inst); } } const CoreAIModuleName = makeCoreModuleName('ai'); export const DocEventName = `${CoreAIModuleName}/doc`; function isDocEventInstance(inst: Instance): boolean { return isInstanceOfType(inst, DocEventName); } async function handleDocEvent(inst: Instance, env: Environment): Promise<void> { const s = await fetchDoc(inst.lookup('url')); if (s) { const title = inst.lookup('title'); await parseAndEvaluateStatement( `{${CoreAIModuleName}/Document {title "${title}", content "${s}"}}`, undefined, env ); } } function triggerTimer(timerInst: Instance): Instance { const dur = timerInst.lookup('duration'); const unit = timerInst.lookup('unit'); let millisecs = 0; switch (unit) { case 'millisecond': { millisecs = dur; break; } case 'second': { millisecs = dur * 1000; break; } case 'minute': { millisecs = dur * 60 * 1000; break; } case 'hour': { millisecs = dur * 60 * 60 * 1000; break; } } const eventName = nameToPath(timerInst.lookup('trigger')); const m = eventName.hasModule() ? eventName.getModuleName() : timerInst.moduleName; const n = eventName.getEntryName(); const inst = makeInstance(m, n, newInstanceAttributes()); const name = timerInst.lookup('name'); const timer = setInterval(async () => { const env = new Environment(); try { await evaluate( inst, (result: Result) => logger.debug(`Timer ${name} ran with result ${result}`), env ); await env.commitAllTransactions(); await maybeCancelTimer(name, timer, env); } catch (reason: any) { logger.error(`Timer ${name} raised error: ${reason}`); } }, millisecs); setTimerRunning(timerInst); return timerInst; } async function computeExprAttributes( inst: Instance, origAttrs: SetAttribute[] | undefined, updatedAttrs: InstanceAttributes | undefined, env: Environment ) { const exprAttrs = inst.getExprAttributes(); if (exprAttrs || origAttrs) { const newEnv = new Environment('expr-env', env); inst.attributes.forEach((v: any, k: string) => { if (v !== undefined) newEnv.bind(k, v); }); updatedAttrs?.forEach((v: any, k: string) => { if (v !== undefined) newEnv.bind(k, v); });