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
352 lines (304 loc) • 10.5 kB
text/typescript
import { JoinSpec } from '../../language/generated/ast.js';
import {
callPostEventOnSubscription,
Environment,
runPostCreateEvents,
runPostDeleteEvents,
runPostUpdateEvents,
} from '../interpreter.js';
import { logger } from '../logger.js';
import {
Instance,
InstanceAttributes,
makeInstance,
newInstanceAttributes,
Relationship,
} from '../module.js';
import { CrudType, nameToPath } from '../util.js';
import { DefaultAuthInfo, ResolverAuthInfo } from './authinfo.js';
export type JoinInfo = {
relationship: Relationship;
queryInstance: Instance;
subJoins: JoinInfo[] | undefined;
};
const subscriptionEvents: Map<string, string> = new Map<string, string>();
export function setSubscriptionEvent(fqEventName: string, resolverName: string) {
subscriptionEvents.set(resolverName, fqEventName);
}
export function getSubscriptionEvent(resolverName: string): string | undefined {
return subscriptionEvents.get(resolverName);
}
export class Resolver {
protected authInfo: ResolverAuthInfo = DefaultAuthInfo;
protected env: Environment | undefined;
protected name: string = 'default';
static Default = new Resolver();
constructor(name?: string) {
if (name) this.name = name;
}
public setAuthInfo(authInfo: ResolverAuthInfo): Resolver {
this.authInfo = authInfo;
return this;
}
public setEnvironment(env: Environment): Resolver {
this.env = env;
return this;
}
public getEnvironment(): Environment | undefined {
return this.env;
}
public suspend(): Resolver {
this.env?.suspend();
return this;
}
public getName(): string {
return this.name;
}
protected notImpl(method: string) {
logger.warn(`Method ${method} not implemented in resolver ${this.name}`);
}
public onSetPath(moduleName: string, entryName: string): any {
this.notImpl(`onSetPath(${moduleName}, ${entryName})`);
}
public async createInstance(inst: Instance): Promise<any> {
this.notImpl(`createInstance(${inst})`);
}
public async upsertInstance(inst: Instance): Promise<any> {
return this.notImpl(`upsertInstance(${inst})`);
}
/**
* @param {Instance} inst - an Instance with query and update attributes
* @param {InstanceAttributes} newAttrs - updated attributes to set in instance
*/
public async updateInstance(inst: Instance, newAttrs: InstanceAttributes): Promise<any> {
return this.notImpl(`updateInstance(${inst}, ${newAttrs})`);
}
/**
* @param {Instance} inst - an Instance with query attributes
* @param {boolean} queryAll - if this flag is set, fetch all instances
*/
public async queryInstances(
inst: Instance,
queryAll: boolean,
distinct: boolean = false
): Promise<any> {
return this.notImpl(`queryInstances(${inst}, ${queryAll}, ${distinct})`);
}
/**
* Return all instances under the given parent-path.
* @param {string} parentPath - path of the parent with the relevant relationship name as the last component
* @param {Instance} inst - child Instance with query attributes
*/
public async queryChildInstances(parentPath: string, inst: Instance): Promise<any> {
return this.notImpl(`queryChildInstances(${parentPath}, ${inst})`);
}
/**
* Return all instances connected to connectedInstance via the given between-relationship
* @param relationship Between relationship
* @param connectedInstance The instance to traveres the relationship from
* @param inst Target instance with query attributes
*/
public async queryConnectedInstances(
relationship: Relationship,
connectedInstance: Instance,
inst: Instance
): Promise<any> {
return this.notImpl(`queryConnectedInstances(${relationship}, ${connectedInstance}, ${inst})`);
}
public async queryByJoin(
inst: Instance,
joinInfo: JoinInfo[],
intoSpec: Map<string, string>,
distinct: boolean = false,
rawJoinSpec?: JoinSpec
): Promise<any> {
return this.notImpl(
`queryByJoin(${inst}, ${joinInfo}, ${intoSpec}, ${distinct} ${rawJoinSpec})`
);
}
/**
* @param {Instance} inst - an Instance with query attributes
*/
public async deleteInstance(inst: Instance | Instance[], purge: boolean): Promise<any> {
return this.notImpl(`deleteInstance(${inst}, ${purge})`);
}
/**
* Connect instances via a between relationship
* @param node1 The main node to connect
* @param otherNodeOrNodes Nodes to be connected to node1
* @param relEntry Details of the repationship
*/
public async connectInstances(
node1: Instance,
otherNodeOrNodes: Instance | Instance[],
relEntry: Relationship,
orUpdate: boolean
): Promise<any> {
return this.notImpl(
`connectInstances(${node1}, ${otherNodeOrNodes}, ${relEntry}, ${orUpdate})`
);
}
public async fullTextSearch(
entryName: string,
moduleName: string,
query: string,
options?: Map<string, any>
): Promise<any> {
return this.notImpl(`fullTextSearch(${entryName}, ${moduleName}, ${query}, ${options})`);
}
// Return a transactionId
public async startTransaction(): Promise<any> {
this.notImpl('startTransaction()');
return 1;
}
public async commitTransaction(txnId: string): Promise<any> {
return this.notImpl(`commitTransaction(${txnId})`);
}
public async rollbackTransaction(txnId: string): Promise<any> {
return this.notImpl(`rollbackTransaction(${txnId})`);
}
public async subscribe(): Promise<any> {
return undefined;
}
private async onOutOfBandCrud(
inst: Instance,
operation: CrudType,
env: Environment
): Promise<any> {
switch (operation) {
case CrudType.CREATE:
return await runPostCreateEvents(inst, env);
case CrudType.UPDATE:
return await runPostUpdateEvents(inst, undefined, env);
case CrudType.DELETE:
return await runPostDeleteEvents(inst, env);
default:
return inst;
}
}
public async onCreate(inst: Instance, env: Environment): Promise<any> {
return this.onOutOfBandCrud(inst, CrudType.CREATE, env);
}
public async onUpdate(inst: Instance, env: Environment): Promise<any> {
return this.onOutOfBandCrud(inst, CrudType.UPDATE, env);
}
public async onDelete(inst: Instance, env: Environment): Promise<any> {
return this.onOutOfBandCrud(inst, CrudType.DELETE, env);
}
public async onSubscription(result: any, callPostCrudEvent: boolean = false): Promise<any> {
if (result !== undefined) {
try {
if (callPostCrudEvent) {
const inst = result as Instance;
return await callPostEventOnSubscription(CrudType.CREATE, inst);
} else {
const eventName = getSubscriptionEvent(this.name);
if (eventName) {
const path = nameToPath(eventName);
const inst = makeInstance(
path.getModuleName(),
path.getEntryName(),
newInstanceAttributes().set('data', result)
);
const { evaluate } = await import('../interpreter.js');
return await evaluate(inst);
}
}
} catch (err: any) {
logger.error(`Resolver ${this.name} raised error in onSubscription handler: ${err}`);
return undefined;
}
}
}
}
type MaybeFunction = Function | undefined;
export type GenericResolverMethods = {
create: MaybeFunction;
upsert: MaybeFunction;
update: MaybeFunction;
query: MaybeFunction;
delete: MaybeFunction;
startTransaction: MaybeFunction;
commitTransaction: MaybeFunction;
rollbackTransaction: MaybeFunction;
};
export type GenericResolverSubscription = {
subscribe: MaybeFunction;
};
export class GenericResolver extends Resolver {
implementation: GenericResolverMethods | undefined;
subs: GenericResolverSubscription | undefined;
constructor(name: string, implementation?: GenericResolverMethods) {
super(name);
this.implementation = implementation;
}
public override async createInstance(inst: Instance): Promise<any> {
if (this.implementation?.create) {
return await this.implementation.create(this, inst);
} else {
return await super.createInstance(inst);
}
}
public override async upsertInstance(inst: Instance): Promise<any> {
if (this.implementation?.upsert) {
return await this.implementation.upsert(this, inst);
}
return await super.upsertInstance(inst);
}
public override async updateInstance(inst: Instance, newAttrs: InstanceAttributes): Promise<any> {
if (this.implementation?.update) {
return await this.implementation.update(this, inst, newAttrs);
}
return await super.updateInstance(inst, newAttrs);
}
public override async queryInstances(inst: Instance, queryAll: boolean): Promise<any> {
if (this.implementation?.query) {
return await this.implementation.query(this, inst, queryAll);
}
return await super.queryInstances(inst, queryAll);
}
public override async deleteInstance(inst: Instance | Instance[], purge: boolean): Promise<any> {
if (this.implementation?.delete) {
return await this.implementation.delete(this, inst, purge);
}
return await super.deleteInstance(inst, purge);
}
public override async startTransaction(): Promise<any> {
if (this.implementation?.startTransaction) {
return await this.implementation.startTransaction(this);
}
return await super.startTransaction();
}
public override async commitTransaction(txnId: string): Promise<any> {
if (this.implementation?.commitTransaction) {
return await this.implementation.commitTransaction(this, txnId);
}
return await super.commitTransaction(txnId);
}
public override async rollbackTransaction(txnId: string): Promise<any> {
if (this.implementation?.rollbackTransaction) {
return await this.implementation.rollbackTransaction(this, txnId);
}
return await super.rollbackTransaction(txnId);
}
override async subscribe() {
const MaxErrors = 3;
let errCount = 0;
while (true) {
try {
if (this.subs?.subscribe) {
await this.subs.subscribe(this);
}
await super.subscribe();
return;
} catch (reason: any) {
logger.warn(`subscribe error in resolver ${this.name}: ${reason}`);
if (errCount >= MaxErrors) {
logger.warn(`exiting resolver subscription after ${errCount} retries`);
break;
}
++errCount;
}
}
}
}