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
text/typescript
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);
});