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,879 lines (1,664 loc) • 109 kB
text/typescript
import chalk from 'chalk';
import {
Statement,
KvPair,
Literal,
FnCall,
RelNodes,
isRelNodes,
AttributeDefinition,
PropertyDefinition,
NodeDefinition,
RecordSchemaDefinition,
MapEntry,
isLiteral,
MetaDefinition,
PrePostTriggerDefinition,
TriggerEntry,
Expr,
RbacSpecEntry,
RbacSpecEntries,
RbacOpr,
WorkflowHeader,
} from '../language/generated/ast.js';
import {
Path,
nameToPath,
isString,
isNumber,
isBoolean,
isFqName,
makeFqName,
DefaultModuleName,
DefaultModules,
joinStatements,
isMinusZero,
now,
findMetaSchema,
findAllPrePostTriggerSchema,
CrudType,
asCrudType,
isPath,
findUqCompositeAttributes,
escapeFqName,
encryptPassword,
splitFqName,
splitRefs,
forceAsFqName,
validateIdFormat,
nameContainsSepEscape,
registerInitFunction,
} from './util.js';
import { parseStatement } from '../language/parser.js';
import { ActiveSessionInfo, AdminSession } from './auth/defs.js';
import { FetchModuleFn, FkSpec, PathAttributeName } from './defs.js';
import { logger } from './logger.js';
import { CasePattern, FlowStepPattern } from '../language/syntax.js';
import {
AgentCondition,
AgentGlossaryEntry,
AgentScenario,
getAgentDirectives,
getAgentDirectivesJson,
getAgentGlossary,
getAgentGlossaryJson,
getAgentResponseSchema,
getAgentScenarios,
getAgentScenariosJson,
registerAgentDirectives,
registerAgentGlossary,
registerAgentResponseSchema,
registerAgentScenarios,
removeAgentDirectives,
removeAgentGlossary,
removeAgentResponseSchema,
removeAgentScenarios,
} from './agents/common.js';
import { Environment } from './interpreter.js';
import { isNode } from '../utils/fs-utils.js';
export class ModuleEntry {
name: string;
moduleName: string;
private taggedAsPublic: boolean = false;
constructor(name: string, moduleName: string) {
if (nameContainsSepEscape(name)) {
throw new Error(`Name cannot contain reserved escape characters - ${name}`);
}
this.name = name;
this.moduleName = moduleName;
}
getFqName(): string {
return makeFqName(this.moduleName, this.name);
}
setPublic(flag: boolean): ModuleEntry {
this.taggedAsPublic = flag;
return this;
}
isPublic(): boolean {
return this.taggedAsPublic;
}
}
export type AttributeSpec = {
type: string;
properties?: Map<string, any> | undefined;
};
function normalizePropertyNames(props: Map<string, any>) {
// Convert iterator to array for compatibility with different Node.js versions
const normKs = Array.from(props.keys()).filter((k: string) => {
return k.charAt(0) === '@';
});
normKs.forEach((k: string) => {
const v: any = props.get(k);
props.delete(k);
props.set(k.substring(1), v);
});
}
const SystemAttributeProperty: string = 'system-attribute';
const SystemDefinedEvent = 'system-event';
function asSystemAttribute(attrSpec: AttributeSpec): AttributeSpec {
const props: Map<string, any> = attrSpec.properties ? attrSpec.properties : new Map();
props.set(SystemAttributeProperty, true);
attrSpec.properties = props;
return attrSpec;
}
function isSystemAttribute(attrSpec: AttributeSpec): boolean {
if (attrSpec.properties) {
return attrSpec.properties.get(SystemAttributeProperty) == true;
}
return false;
}
export type RecordSchema = Map<string, AttributeSpec>;
function recordSchemaToString(scm: RecordSchema): string {
const ss: Array<string> = [];
scm.forEach((attrSpec: AttributeSpec, n: string) => {
if (!isSystemAttribute(attrSpec)) {
ss.push(` ${n} ${attributeSpecToString(attrSpec)}`);
}
});
return `\n${ss.join(',\n')}`;
}
function attributeSpecToString(attrSpec: AttributeSpec): string {
let s: string = getEnumValues(attrSpec) || getOneOfRef(attrSpec) ? '' : `${attrSpec.type}`;
if (isArrayAttribute(attrSpec)) {
s = `${s}[]`;
}
if (attrSpec.properties) {
const ps: Array<string> = [];
const hasEnum = attrSpec.properties.has('enum');
const hasOneOf = attrSpec.properties.has('oneof');
const needsReordering = hasEnum || hasOneOf;
if (needsReordering) {
const priorityKeys = ['enum', 'oneof'];
const processedKeys = new Set<string>();
priorityKeys.forEach(k => {
if (attrSpec.properties!.has(k)) {
const v = attrSpec.properties!.get(k);
if (v == true) ps.push(` @${k}`);
else ps.push(` @${k}(${attributePropertyValueToString(k, v, attrSpec.type)})`);
processedKeys.add(k);
}
});
attrSpec.properties.forEach((v: any, k: string) => {
if (k !== 'array' && !processedKeys.has(k)) {
if (v == true) ps.push(` @${k}`);
else ps.push(` @${k}(${attributePropertyValueToString(k, v, attrSpec.type)})`);
}
});
} else {
attrSpec.properties.forEach((v: any, k: string) => {
if (k != 'array') {
if (v == true) ps.push(` @${k}`);
else ps.push(` @${k}(${attributePropertyValueToString(k, v, attrSpec.type)})`);
}
});
}
s = s.concat(ps.join(' '));
}
return s;
}
function attributePropertyValueToString(
propName: string,
propValue: any,
attrType: string
): string {
if (propName == EnumPropertyName) {
const v = propValue as Set<string>;
const ss = new Array<string>();
v.forEach((s: string) => {
ss.push(`"${s}"`);
});
return ss.join(',');
} else if (propName == 'default') {
if (isTextualType(attrType) && propValue != 'now()' && propValue != 'uuid()') {
return `"${propValue}"`;
}
} else if (propName == 'comment') {
return `"${propValue}"`;
}
return `${propValue}`;
}
export function newRecordSchema(): RecordSchema {
return new Map<string, AttributeSpec>();
}
type Meta = Map<string, any>;
export function newMeta(): Meta {
return new Map<string, any>();
}
export enum RecordType {
RECORD,
ENTITY,
EVENT,
RELATIONSHIP,
AGENT,
}
function normalizeMetaValue(metaValue: any): any {
if (!isLiteral(metaValue)) {
throw new Error(`Invalid entry ${metaValue} in meta specification - expected a literal`);
}
const v: Literal = metaValue as Literal;
if (v.array) {
return v.array.vals.map((value: Statement) => {
return normalizeMetaValue(value.pattern.expr);
});
} else if (v.bool !== undefined) {
return v.bool == 'true' ? true : false;
} else if (v.id) {
return v.id;
} else if (v.map) {
const result = new Map<any, any>();
v.map.entries.forEach((value: MapEntry) => {
result.set(value.key, normalizeMetaValue(value.value));
});
return result;
} else if (v.ref) {
return v.ref;
} else if (v.num) {
return v.num;
} else if (v.str) {
return v.str;
} else {
throw new Error(`Invalid value ${metaValue} passed to meta specification`);
}
}
export type TriggerInfo = {
eventName: string;
async: boolean;
};
function asTriggerInfo(te: any): TriggerInfo {
return {
eventName: te.event,
async: te.async ? true : false,
};
}
const EnumPropertyName = 'enum';
const OneOfPropertyName = 'oneof';
export function enumAttributeSpec(values: Set<string>): AttributeSpec {
return {
type: 'String',
properties: new Map().set(EnumPropertyName, values),
};
}
export function oneOfAttributeSpec(ref: string): AttributeSpec {
return {
type: 'String',
properties: new Map().set(OneOfPropertyName, ref),
};
}
export class Record extends ModuleEntry {
schema: RecordSchema;
meta: Meta | undefined;
type: RecordType = RecordType.RECORD;
parentEntryName: string | undefined;
afterTriggers: Map<CrudType, TriggerInfo> | undefined;
beforeTriggers: Map<CrudType, TriggerInfo> | undefined;
compositeUqAttributes: Array<string> | undefined;
oneOfRefAttributes: Array<string> | undefined;
protected rbac: RbacSpecification[] | undefined;
constructor(
name: string,
moduleName: string,
scm?: RecordSchemaDefinition,
parentEntryName?: string
) {
super(name, moduleName);
this.parentEntryName = parentEntryName;
this.schema = parentEntryName
? cloneParentSchema(parentEntryName, moduleName)
: newRecordSchema();
const attributes: AttributeDefinition[] | undefined = scm ? scm.attributes : undefined;
if (attributes !== undefined) {
attributes.forEach((a: AttributeDefinition) => {
verifyAttribute(a);
let props: Map<string, any> | undefined = asPropertiesMap(a.properties);
const isArrayType: boolean = a.arrayType ? true : false;
let t: string | undefined = isArrayType ? a.arrayType : a.type;
if (a.refSpec) {
let fp = a.refSpec.ref;
const rp = nameToPath(fp);
if (!rp.hasModule()) {
rp.setModuleName(this.moduleName);
fp = rp.asFqName();
}
if (props === undefined) {
props = new Map();
}
props.set('ref', escapeFqName(fp));
t = a.refSpec.type === undefined ? 'Any' : a.refSpec.type;
}
const enumValues: string[] | undefined = a.enumSpec?.values;
const oneOfRef: string | undefined = a.oneOfSpec?.ref;
if (!t) {
if (enumValues || oneOfRef) {
t = 'String';
} else {
throw new Error(`Attribute ${a.name} requires a type`);
}
}
if (a.expr) {
if (props === undefined) {
props = new Map();
}
props.set('expr', a.expr).set('optional', true);
}
const isObjectType: boolean = t == 'Map' || !isBuiltInType(t);
if (isArrayType || isObjectType || enumValues || oneOfRef) {
if (props === undefined) {
props = new Map<string, any>();
}
if (isArrayType) props.set('array', true);
if (isObjectType) props.set('object', true);
if (enumValues) props.set(EnumPropertyName, new Set(enumValues));
if (oneOfRef) {
props.set(OneOfPropertyName, oneOfRef);
this.addOneOfRefAttribute(a.name);
}
}
this.schema.set(a.name, { type: t, properties: props });
});
}
const meta: MetaDefinition | undefined = findMetaSchema(scm);
if (meta) {
meta.spec.entries.forEach((entry: MapEntry) => {
if (entry.key.str) this.addMeta(entry.key.str, normalizeMetaValue(entry.value));
else
throw new Error(
`Key must be a string for meta-definition in ${this.moduleName}/${this.name}`
);
});
}
const prepostTrigs: PrePostTriggerDefinition[] | undefined = findAllPrePostTriggerSchema(scm);
if (prepostTrigs) {
prepostTrigs.forEach((ppt: PrePostTriggerDefinition) => {
if (ppt.after) {
if (this.afterTriggers === undefined) {
this.afterTriggers = new Map();
}
ppt.after.triggers.entries.forEach((te: TriggerEntry) => {
if (this.afterTriggers) this.afterTriggers.set(asCrudType(te.on), asTriggerInfo(te));
});
} else if (ppt.before) {
if (this.beforeTriggers === undefined) {
this.beforeTriggers = new Map();
}
ppt.before.triggers.entries.forEach((te: TriggerEntry) => {
if (this.beforeTriggers) this.beforeTriggers.set(asCrudType(te.on), asTriggerInfo(te));
});
}
});
}
this.compositeUqAttributes = findUqCompositeAttributes(scm);
}
protected addMetaAttributes(): Record {
this.schema
.set(SysAttr_Created, SysAttr_CreatedSpec)
.set(SysAttr_CreatedBy, SysAttr_CreatedBySpec)
.set(SysAttr_LastModified, SysAttr_LastModifiedSpec)
.set(SysAttr_LastModifiedBy, SysAttr_LastModifiedBySpec);
return this;
}
private addOneOfRefAttribute(s: string): Record {
if (this.oneOfRefAttributes === undefined) {
this.oneOfRefAttributes = [];
}
this.oneOfRefAttributes.push(s);
return this;
}
public addAfterTrigger(te: any): Record {
if (this.afterTriggers === undefined) {
this.afterTriggers = new Map();
}
this.afterTriggers?.set(asCrudType(te.on), asTriggerInfo(te));
return this;
}
public addBeforeTrigger(te: any): Record {
if (this.beforeTriggers === undefined) {
this.beforeTriggers = new Map();
}
this.beforeTriggers?.set(asCrudType(te.on), asTriggerInfo(te));
return this;
}
getCompositeUniqueAttributes(): Array<string> | undefined {
return this.compositeUqAttributes;
}
getPreTriggerInfo(crudType: CrudType): TriggerInfo | undefined {
if (this.beforeTriggers) {
return this.beforeTriggers.get(crudType);
}
return undefined;
}
getPostTriggerInfo(crudType: CrudType): TriggerInfo | undefined {
if (this.afterTriggers) {
return this.afterTriggers.get(crudType);
}
return undefined;
}
addMeta(k: string, v: any): void {
if (!this.meta) {
this.meta = newMeta();
}
this.meta.set(k, v);
}
getMeta(k: string): any {
if (this.meta) {
return this.meta.get(k);
} else {
return undefined;
}
}
getFullTextSearchAttributes(): string[] | undefined {
const fts: string[] | string | undefined = this.getMeta('fullTextSearch');
if (fts) {
if (fts instanceof Array) {
return fts as string[];
} else if (fts == '*') {
return [...this.schema.keys()];
} else {
return undefined;
}
} else {
return undefined;
}
}
addAttribute(n: string, attrSpec: AttributeSpec): Record {
if (this.schema.has(n)) {
throw new Error(`Attribute named ${n} already exists in ${this.moduleName}.${this.name}`);
}
if (attrSpec.properties !== undefined) {
normalizePropertyNames(attrSpec.properties);
}
this.schema.set(n, attrSpec);
return this;
}
removeAttribute(n: string): Record {
this.schema.delete(n);
return this;
}
reorderAttributes(desiredOrder: string[]) {
this.schema = new Map(
[...this.schema].sort((a, b) => {
return desiredOrder.indexOf(a[0]) - desiredOrder.indexOf(b[0]);
})
);
}
addSystemAttribute(n: string, attrSpec: AttributeSpec): Record {
asSystemAttribute(attrSpec);
this.addAttribute(n, attrSpec);
return this;
}
findAttribute(predic: Function): AttributeEntry | undefined {
for (const k of this.schema.keys()) {
const attrSpec: AttributeSpec | undefined = this.schema.get(k);
if (attrSpec !== undefined) {
if (predic(attrSpec))
return {
name: k,
spec: attrSpec,
};
}
}
return undefined;
}
hasRefTo(modName: string, entryName: string): boolean {
if (
this.findAttribute((attrSpec: AttributeSpec) => {
if (attrSpec.properties !== undefined) {
const ref: Path | undefined = attrSpec.properties.get('ref');
if (ref !== undefined) {
if (ref.getModuleName() == modName && ref.getEntryName() == entryName) {
return true;
}
}
}
return false;
})
)
return true;
else return false;
}
getRefOnAttribute(attrName: string): [string, string] | undefined {
const fkspecs = this.getFkAttributeSpecs();
const a = fkspecs.find((v: FkSpec) => {
return v.columnName === attrName;
});
if (a) {
return [makeFqName(a.targetModuleName, a.targetEntityName), a.targetColumnName];
}
return undefined;
}
getIdAttributeName(): string | undefined {
const e: AttributeEntry | undefined = this.findAttribute((attrSpec: AttributeSpec) => {
return isIdAttribute(attrSpec);
});
if (e !== undefined) {
return e.name;
}
return undefined;
}
getFkAttributeSpecs(): FkSpec[] {
const result = new Array<FkSpec>();
this.schema.forEach((attrSpec: AttributeSpec, columnName: string) => {
const refSpec = getRefSpec(attrSpec);
if (refSpec) {
const targetNames = forceAsFqName(refSpec, this.moduleName);
const parts = splitFqName(targetNames);
const targetModuleName = parts[0];
const refs = splitRefs(parts[1]);
const targetEntityName = refs[0];
let targetColumnName = '';
if (refs.length <= 1) {
targetColumnName = PathAttributeName;
} else {
targetColumnName = refs[1];
}
result.push({
moduleName: this.moduleName,
entityName: this.name,
columnName,
targetModuleName,
targetEntityName,
targetColumnName,
onDelete: 'SET NULL',
onUpdate: 'CASCADE',
});
}
});
return result;
}
override toString(): string {
return this.toString_();
}
toString_(internParentSchema: boolean = false): string {
if (this.type == RecordType.EVENT && this.meta && this.meta.get(SystemDefinedEvent)) {
return '';
}
let s: string = `${RecordType[this.type].toLowerCase()} ${this.name}`;
let scm: RecordSchema = this.schema;
if (this.parentEntryName && !internParentSchema) {
s = s.concat(` extends ${this.parentEntryName}`);
scm = newRecordSchema();
let modName = this.moduleName;
let pname = this.parentEntryName;
if (isFqName(pname)) {
const parts = splitFqName(pname);
modName = parts[0];
pname = parts[1];
}
const p = fetchModule(modName).getEntry(pname) as Record;
const pks = new Set(p.schema.keys());
this.schema.forEach((v: AttributeSpec, n: string) => {
if (!pks.has(n)) {
scm.set(n, v);
}
});
}
let scms = recordSchemaToString(scm);
if (this.rbac && this.rbac.length > 0) {
const rbs = this.rbac.map((rs: RbacSpecification) => {
return rs.toString();
});
scms = `${scms},\n @rbac [${rbs.join(',\n')}]`;
}
if (this.meta && this.meta.size > 0) {
const metaObj = Object.fromEntries(this.meta);
const ms = `@meta ${JSON.stringify(metaObj)}`;
scms = `${scms},\n ${ms}`;
}
if (this.isPublic()) {
s = `@public ${s}`;
}
return s.concat('\n{', scms, '\n}\n');
}
getUserAttributes(): RecordSchema {
const recSchema: RecordSchema = newRecordSchema();
this.schema.forEach((attrSpec: AttributeSpec, n: string) => {
if (!isSystemAttribute(attrSpec)) {
recSchema.set(n, attrSpec);
}
});
return recSchema;
}
getUserAttributeNames(): string[] {
return [...this.getUserAttributes().keys()];
}
}
type FetchModuleByEntryNameResult = {
module: Module;
entryName: string;
moduleName: string;
};
function fetchModuleByEntryName(
entryName: string,
suspectModuleName: string
): FetchModuleByEntryNameResult {
if (isFqName(entryName)) {
const path: Path = nameToPath(entryName);
entryName = path.getEntryName();
suspectModuleName = path.getModuleName();
}
return {
module: fetchModule(suspectModuleName),
entryName: entryName,
moduleName: suspectModuleName,
};
}
function cloneParentSchema(parentName: string, currentModuleName: string): RecordSchema {
const fr: FetchModuleByEntryNameResult = fetchModuleByEntryName(parentName, currentModuleName);
parentName = fr.entryName;
currentModuleName = fr.moduleName;
const mod: Module = fr.module;
const entry: Record = mod.getEntry(parentName) as Record;
const result: RecordSchema = newRecordSchema();
entry.schema.forEach((attrSpec: AttributeSpec, attrName: string) => {
result.set(attrName, attrSpec);
});
return result;
}
function asPropertiesMap(props: PropertyDefinition[]): Map<string, any> | undefined {
if (props !== undefined && props.length > 0) {
const result: Map<string, any> = new Map<string, any>();
props.forEach((p: PropertyDefinition) => {
const n: string = p.name.substring(1);
if (p.value !== undefined && p.value.pairs !== undefined && p.value.pairs.length > 0) {
if (p.value.pairs.length == 1) {
const kvp: KvPair = p.value.pairs[0];
if (kvp.key === undefined) {
result.set(n, normalizeKvPairValue(kvp));
} else {
const v: Map<string, any> = new Map<string, any>();
v.set(kvp.key, normalizeKvPairValue(kvp));
result.set(n, v);
}
} else {
const v: Map<string, any> = new Map<string, any>();
p.value.pairs.forEach((kvp: KvPair) => {
let k: string = 'null';
if (kvp.key !== undefined) k = kvp.key;
v.set(k, normalizeKvPairValue(kvp));
});
result.set(n, v);
}
} else {
result.set(n, true);
}
});
return maybeProcessRefProperty(result);
}
return undefined;
}
function maybeProcessRefProperty(props: Map<string, any>): Map<string, any> {
const v: string | undefined = props.get('ref');
if (v !== undefined) {
const parts: Path = nameToPath(v);
if (!parts.hasModule()) {
parts.setModuleName(activeModule);
}
props.set('ref', parts);
}
return props;
}
function normalizeKvPairValue(kvp: KvPair): any | null {
const v: Literal | undefined = kvp.value;
if (v === undefined) return true;
if (v.str !== undefined) {
return v.str;
} else if (v.num !== undefined) {
return v.num;
} else if (v.bool !== undefined) {
return v.bool === 'true' ? true : false;
} else if (v.id !== undefined) {
return v.id;
} else if (v.ref !== undefined) {
return v.ref;
} else if (v.fnCall !== undefined) {
const fncall: FnCall = v.fnCall;
if (fncall.args.length > 0) {
throw new Error('Cannot allow arguments in properties function-call');
}
return fncall.name + '()';
} else if (v.array !== undefined) {
return v.array;
}
return null;
}
export const PlaceholderRecordEntry = new Record('--', DefaultModuleName);
export enum RbacPermissionFlag {
CREATE,
READ,
UPDATE,
DELETE,
}
type RbacExpression = {
lhs: string;
rhs: string;
};
export class RbacSpecification {
private static EmptyRoles: Set<string> = new Set();
resource: string = '';
roles: Set<string> = RbacSpecification.EmptyRoles;
permissions: Set<RbacPermissionFlag>;
expression: RbacExpression | undefined;
constructor() {
this.permissions = new Set();
}
static from(def: RbacSpecEntries): RbacSpecification {
const result = new RbacSpecification();
def.entries.forEach((se: RbacSpecEntry) => {
if (se.role) {
result.setRoles(se.role.roles);
} else if (se.allow) {
result.setPermissions(
se.allow.oprs.map((opr: RbacOpr) => {
return opr.value;
})
);
} else if (se.expr) {
result.setExpression(se.expr.lhs, se.expr.rhs);
}
});
return result;
}
setResource(s: string): RbacSpecification {
this.resource = s;
return this;
}
hasResource(): boolean {
return this.resource.length > 0;
}
setPermissions(perms: Array<string>): RbacSpecification {
const ps = new Set<RbacPermissionFlag>();
perms.forEach((v: string) => {
const idx: any = v.toUpperCase();
const a: any = RbacPermissionFlag[idx];
if (a === undefined) {
throw new Error(`Not a valid RBAC permission - ${v}`);
}
ps.add(a);
});
this.permissions = ps;
return this;
}
hasPermissions(): boolean {
return this.permissions.size > 0;
}
hasCreatePermission(): boolean {
return this.permissions.has(RbacPermissionFlag.CREATE);
}
hasReadPermission(): boolean {
return this.permissions.has(RbacPermissionFlag.READ);
}
hasUpdatePermission(): boolean {
return this.permissions.has(RbacPermissionFlag.UPDATE);
}
hasDeletePermission(): boolean {
return this.permissions.has(RbacPermissionFlag.DELETE);
}
setRoles(roles: Array<string>): RbacSpecification {
if (this.expression) {
throw new Error('Cannot set roles while `where` expression is set');
}
this.roles = new Set();
roles.forEach((r: string) => {
this.roles.add(r);
});
return this;
}
removeRoles(): RbacSpecification {
this.roles = RbacSpecification.EmptyRoles;
return this;
}
setExpression(lhs: string, rhs: string): RbacSpecification {
if (this.roles != RbacSpecification.EmptyRoles) {
logger.warn('Cannot set `where` expression along with roles, removing roles');
this.removeRoles();
}
this.expression = {
lhs: lhs,
rhs: rhs,
};
return this;
}
removeExpression(): RbacSpecification {
this.expression = undefined;
return this;
}
toString(): string {
if (this.permissions.size <= 0) {
throw new Error(`Cannot emit RbacSpecification, no permissions are set`);
}
const rs = new Array<string>();
this.roles.forEach((r: string) => {
rs.push(r);
});
let cond = '';
if (this.expression) {
cond = `where: ${this.expression.lhs} = ${this.expression.rhs}`;
} else {
cond = `roles: [${rs.join(',')}]`;
}
const perms = new Array<string>();
this.permissions.forEach((p: RbacPermissionFlag) => {
perms.push(RbacPermissionFlag[p].toLowerCase());
});
return `(${cond}, allow: [${perms.join(',')}])`;
}
}
export class Agent extends Record {
override type: RecordType = RecordType.AGENT;
attributes: InstanceAttributes;
constructor(name: string, moduleName: string, attrs?: InstanceAttributes) {
super(Agent.EscapeName(name), moduleName);
this.attributes = attrs ? attrs : newInstanceAttributes();
}
setName(n: string): Agent {
this.name = Agent.EscapeName(n);
return this;
}
setLLM(llm: string): Agent {
this.attributes.set('llm', llm);
return this;
}
getLLM(): string {
return this.attributes.get('llm');
}
private removeAgentAttribute(n: string): Agent {
this.attributes.delete(n);
return this;
}
removeLLM(): Agent {
return this.removeAgentAttribute('llm');
}
setInstruction(s: string): Agent {
this.attributes.set('instruction', s);
return this;
}
getInstruction(): string {
return this.attributes.get('instruction');
}
removeInstruction(): Agent {
return this.removeAgentAttribute('instruction');
}
setType(type: 'chat' | 'planner'): Agent {
this.attributes.set('type', type);
return this;
}
getType(): string {
return this.attributes.get('type');
}
removeType(): Agent {
return this.removeAgentAttribute('type');
}
private setStrings(attrName: string, v: string[]): Agent {
this.attributes.set(attrName, v.join(','));
return this;
}
private getStrings(attrName: string): string[] | undefined {
const v = this.attributes.get(attrName);
if (v) {
return v.split(',');
} else {
return undefined;
}
}
setTools(tools: string[]): Agent {
return this.setStrings('tools', tools);
}
getTools(): string[] | undefined {
return this.getStrings('tools');
}
removeTools(): Agent {
return this.removeAgentAttribute('tools');
}
setDocuments(docs: string[]): Agent {
return this.setStrings('documents', docs);
}
getDocuments(): string[] | undefined {
return this.getStrings('documents');
}
removeDocuments(): Agent {
return this.removeAgentAttribute('documents');
}
setFlows(flows: string[]): Agent {
return this.setStrings('flows', flows);
}
getFlows(): string[] | undefined {
return this.getStrings('flows');
}
getAgentFqName(): string {
return makeFqName(this.moduleName, this.getName());
}
setDirectives(conds: AgentCondition[]): Agent {
registerAgentDirectives(this.getAgentFqName(), conds);
return this;
}
removeDirectives(): Agent {
removeAgentDirectives(this.getAgentFqName());
return this;
}
getDirectives(): AgentCondition[] | undefined {
return getAgentDirectives(this.getAgentFqName());
}
setScenarios(scenarios: AgentScenario[]): Agent {
registerAgentScenarios(this.getAgentFqName(), scenarios);
return this;
}
getScenarios(): AgentScenario[] | undefined {
return getAgentScenarios(this.getAgentFqName());
}
removeScenarios(): Agent {
removeAgentScenarios(this.getAgentFqName());
return this;
}
setGlossary(glossary: AgentGlossaryEntry[]): Agent {
registerAgentGlossary(this.getAgentFqName(), glossary);
return this;
}
getGlossary(): AgentGlossaryEntry[] | undefined {
return getAgentGlossary(this.getAgentFqName());
}
removeGlossary(): Agent {
removeAgentGlossary(this.getAgentFqName());
return this;
}
setResponseSchema(entryName: string): Agent {
registerAgentResponseSchema(this.getAgentFqName(), entryName);
return this;
}
getResponseSchema(): string | undefined {
return getAgentResponseSchema(this.getAgentFqName());
}
removeResponseSchema(): Agent {
removeAgentResponseSchema(this.getAgentFqName());
return this;
}
override toString(): string {
const attrs = new Array<string>();
this.attributes.forEach((value: any, key: string) => {
const skip = key == 'moduleName' || (key == 'type' && value == 'flow-exec');
if (!skip && value !== null && value !== undefined) {
let v = value;
const isf = key == 'flows';
if (isf || key == 'tools') {
if (isf || v.indexOf(',') > 0 || v.indexOf('/') > 0) v = `[${v}]`;
else v = `"${v}"`;
} else if (isString(v)) {
v = `"${v}"`;
}
attrs.push(` ${key} ${v}`);
}
});
const fqName = makeFqName(this.moduleName, this.getName());
const conds = getAgentDirectivesJson(fqName);
if (conds) {
attrs.push(` directives ${conds}`);
}
const scns = getAgentScenariosJson(fqName);
if (scns) {
attrs.push(` scenarios ${scns}`);
}
const gls = getAgentGlossaryJson(fqName);
if (gls) {
attrs.push(` glossary ${gls}`);
}
const rscm = getAgentResponseSchema(fqName);
if (rscm) {
attrs.push(` responseSchema ${rscm}`);
}
const s = `agent ${Agent.NormalizeName(this.name)}
{
${attrs.join(',\n')}
}`;
if (this.isPublic()) {
return `@public ${s}`;
} else {
return s;
}
}
static Suffix = '_agent';
static EscapeName(n: string): string {
if (n.endsWith(Agent.Suffix)) {
return n;
}
return `${n}${Agent.Suffix}`;
}
static NormalizeName(n: string): string {
if (n.endsWith(Agent.Suffix)) {
return n.substring(0, n.lastIndexOf(Agent.Suffix));
} else {
return n;
}
}
getName(): string {
return Agent.NormalizeName(this.name);
}
}
const SysAttr_Created = '__created';
const SysAttr_LastModified = '__last_modified';
const SysAttr_CreatedBy = '__created_by';
const SysAttr_LastModifiedBy = '__last_modified_by';
const SysAttr_CreatedSpec: AttributeSpec = asSystemAttribute({
type: 'DateTime',
properties: new Map<string, any>().set('default', 'now()'),
});
const SysAttr_LastModifiedSpec = SysAttr_CreatedSpec;
const SysAttr_CreatedBySpec: AttributeSpec = asSystemAttribute({
type: 'String',
properties: new Map<string, any>().set('optional', true),
});
const SysAttr_LastModifiedBySpec = SysAttr_CreatedBySpec;
export class Entity extends Record {
override type: RecordType = RecordType.ENTITY;
constructor(
name: string,
moduleName: string,
scm?: RecordSchemaDefinition,
parentEntryName?: string
) {
super(name, moduleName, scm, parentEntryName);
this.addMetaAttributes();
}
setRbacSpecifications(rbac: RbacSpecification[]): Entity {
this.rbac = rbac;
return this;
}
getRbacSpecifications(): RbacSpecification[] | undefined {
return this.rbac;
}
isConfigEntity(): boolean {
const v = this.getMeta('configEntity');
return v == true;
}
}
export class Event extends Record {
override type: RecordType = RecordType.EVENT;
isSystemDefined(): boolean {
return this.meta?.get(SystemDefinedEvent) === 'true';
}
}
enum RelType {
CONTAINS,
BETWEEN,
}
export type RelationshipNode = {
path: Path;
alias: string;
origName: string;
origAlias: string | undefined;
};
export function newRelNodeEntry(nodeFqName: string, alias?: string): RelationshipNode {
const p: Path = nameToPath(nodeFqName);
return {
path: p,
alias: alias ? alias : p.getEntryName(),
origName: nodeFqName,
origAlias: alias,
};
}
function relNodeEntryToString(node: RelationshipNode): string {
let n = `${node.origName}`;
if (node.origAlias) {
n = n.concat(` @as ${node.origAlias}`);
}
return n;
}
function asRelNodeEntry(n: NodeDefinition): RelationshipNode {
const path: Path = nameToPath(n.name);
let modName = activeModule;
const entryName = path.getEntryName();
if (path.hasModule()) {
modName = path.getModuleName();
}
let alias = entryName;
if (n.alias !== undefined) {
alias = n.alias;
}
return {
path: new Path(modName, entryName),
alias: alias,
origName: n.name,
origAlias: n.alias,
};
}
const OneToOne = 'one_one';
const OneToMany = 'one_many';
const ManyToMany = 'many_many';
export class Relationship extends Record {
override type: RecordType = RecordType.RELATIONSHIP;
relType: RelType = RelType.CONTAINS;
node1: RelationshipNode;
node2: RelationshipNode;
properties: Map<string, any> | undefined;
constructor(
name: string,
typ: string,
node1: RelationshipNode,
node2: RelationshipNode,
moduleName: string,
scm?: RecordSchemaDefinition,
props?: Map<string, any>
) {
super(name, moduleName, scm);
if (typ == 'between') {
this.relType = RelType.BETWEEN;
this.addMetaAttributes();
}
this.node1 = node1;
this.node2 = node2;
this.properties = props;
this.updateSchemaWithNodeAttributes();
}
private updateSchemaWithNodeAttributes() {
const attrSpec1: AttributeSpec = {
type: 'string',
};
this.addSystemAttribute(this.node1.alias, attrSpec1);
const attrSpec2: AttributeSpec = {
type: 'string',
};
this.addSystemAttribute(this.node2.alias, attrSpec2);
if (this.relType == RelType.BETWEEN && this.isOneToMany()) {
const attrSpec3: AttributeSpec = {
type: 'string',
properties: new Map().set('unique', true),
};
this.addSystemAttribute(this.joinNodesAttributeName(), attrSpec3);
}
}
joinNodesAttributeName(): string {
return this.node1.alias + '_' + this.node2.alias;
}
setBetweenRef(inst: Instance, refPath: string, isQuery: boolean = false) {
const refAttrName: string = `__${this.node1.alias.toLowerCase()}`;
if (isQuery) {
inst.addQuery(refAttrName, '=', refPath);
} else {
inst.attributes.set(refAttrName, refPath);
}
}
isContains(): boolean {
return this.relType == RelType.CONTAINS;
}
isBetween(): boolean {
return this.relType == RelType.BETWEEN;
}
parentNode(): RelationshipNode {
return this.node1;
}
childNode(): RelationshipNode {
return this.node2;
}
hasBooleanFlagSet(flag: string): boolean {
if (this.properties !== undefined) {
return this.properties.get(flag) == true;
}
return false;
}
private setProperty(p: string, v: any): Relationship {
if (this.properties === undefined) {
this.properties = new Map();
}
this.properties.set(p, v);
return this;
}
isOneToOne(): boolean {
return this.isBetween() && this.hasBooleanFlagSet(OneToOne);
}
isOneToMany(): boolean {
return this.isBetween() && this.hasBooleanFlagSet(OneToMany);
}
isManyToMany(): boolean {
if (this.isBetween()) {
return (
this.hasBooleanFlagSet(ManyToMany) ||
(!this.hasBooleanFlagSet(OneToOne) && !this.hasBooleanFlagSet(OneToMany))
);
} else {
return false;
}
}
setOneToOne(flag: boolean = true): Relationship {
if (flag) {
this.setOneToMany(false).setManyToMany(false);
}
return this.setProperty(OneToOne, flag);
}
setOneToMany(flag: boolean = true): Relationship {
if (flag) {
this.setOneToOne(false).setManyToMany(false);
}
return this.setProperty(OneToMany, flag);
}
setManyToMany(flag: boolean = true): Relationship {
this.setOneToOne(false).setOneToMany(false);
return this.setProperty(ManyToMany, flag);
}
isFirstNode(inst: Instance): boolean {
return this.isFirstNodeName(inst.getFqName());
}
getAliasFor(inst: Instance): string {
return this.getAliasForName(inst.getFqName());
}
getInverseAliasFor(inst: Instance): string {
return this.getInverseAliasForName(inst.getFqName());
}
isFirstNodeName(fqName: string): boolean {
return fqName == this.node1.path.asFqName();
}
getAliasForName(fqName: string): string {
if (this.isFirstNodeName(fqName)) {
return this.node1.alias;
} else {
return this.node2.alias;
}
}
getInverseAliasForName(fqName: string): string {
if (this.isFirstNodeName(fqName)) {
return this.node2.alias;
} else {
return this.node1.alias;
}
}
isParent(inst: Instance): boolean {
return inst.getFqName() == this.node1.path.asFqName();
}
getParentFqName(): string {
return this.node1.path.asFqName();
}
getChildFqName(): string {
return this.node2.path.asFqName();
}
override toString(): string {
const n1 = relNodeEntryToString(this.node1);
const n2 = relNodeEntryToString(this.node2);
let s = `relationship ${this.name} ${RelType[this.relType].toLowerCase()} (${n1}, ${n2})`;
if (this.isBetween()) {
if (this.isOneToOne()) {
s = `${s} @${OneToOne}`;
} else if (this.isOneToMany()) {
s = `${s} @${OneToMany}`;
}
}
if (this.getUserAttributes().size > 0) {
const attrs: Array<string> = [];
this.getUserAttributes().forEach((attrSpec: AttributeSpec, n: string) => {
attrs.push(`${n} ${attributeSpecToString(attrSpec)}`);
});
s = s.concat(`{\n ${attrs.join(',\n')} }`);
}
return s.concat('\n');
}
}
export class Workflow extends ModuleEntry {
statements: Statement[];
isPrePost: boolean;
constructor(name: string, patterns: Statement[], moduleName: string, isPrePost: boolean = false) {
super(name, moduleName);
this.statements = patterns;
this.isPrePost = isPrePost;
}
async addStatement(stmtCode: string): Promise<Workflow> {
const result: Statement = await parseStatement(stmtCode);
this.statements.push(result);
return this;
}
setStatementAtHelper(
statements: Statement[],
newStmt: Statement | undefined,
index: number[]
): Workflow {
let stmt = statements[index[0]];
const isFe = stmt.pattern.forEach;
const isIf = stmt.pattern.if;
if (isFe || isIf) {
for (let i = 1; i < index.length; ++i) {
const found = i == index.length - 1;
let idx = index[i];
if (stmt.pattern.forEach) {
if (found) {
if (!newStmt) {
stmt.pattern.forEach.statements.splice(idx, 1);
} else {
stmt.pattern.forEach.statements[idx] = newStmt;
}
} else stmt = stmt.pattern.forEach.statements[idx];
} else if (stmt.pattern.if) {
if (idx < 0 || isMinusZero(idx)) {
if (stmt.pattern.if.else) {
idx *= -1;
if (found) {
if (!newStmt) {
stmt.pattern.if.else.statements.splice(idx, 1);
} else {
stmt.pattern.if.else.statements[idx] = newStmt;
}
} else stmt = stmt.pattern.if.else.statements[idx];
} else {
throw new Error('No else part in if');
}
} else {
if (found) {
if (!newStmt) {
stmt.pattern.if.statements.splice(idx, 1);
} else {
stmt.pattern.if.statements[idx] = newStmt;
}
} else stmt = stmt.pattern.if.statements[idx];
}
} else {
throw new Error('Cannot dig further into statements');
}
}
}
return this;
}
async setStatementAt(stmtCode: string, index: number | number[]): Promise<Workflow> {
const result: Statement = await parseStatement(stmtCode);
if (index instanceof Array) {
if (index.length == 1) {
this.statements[index[0]] = result;
return this;
} else {
return this.setStatementAtHelper(this.statements, result, index);
}
} else {
this.statements[index] = result;
}
return this;
}
removeStatementAt(index: number | number[]): Workflow {
if (index instanceof Array) {
if (index.length == 1) {
this.statements.splice(index[0], 1);
return this;
} else {
return this.setStatementAtHelper(this.statements, undefined, index);
}
} else {
this.statements.splice(index, 1);
}
return this;
}
private statementsToStringsHelper(statements: Statement[]): string[] {
const ss: Array<string> = [];
statements.forEach((stmt: Statement) => {
if (stmt.pattern.forEach) {
ss.push(` for ${stmt.pattern.forEach.var} in ${stmt.pattern.forEach.src.$cstNode?.text} {
${joinStatements(this.statementsToStringsHelper(stmt.pattern.forEach.statements))}
}`);
} else if (stmt.pattern.if) {
let s = ` if (${stmt.pattern.if.cond.$cstNode?.text}) {
${joinStatements(this.statementsToStringsHelper(stmt.pattern.if.statements))}
}`;
if (stmt.pattern.if.else) {
s = s.concat(` else {
${joinStatements(this.statementsToStringsHelper(stmt.pattern.if.else.statements))}
}`);
}
ss.push(s);
} else if (stmt.$cstNode) {
ss.push(` ${stmt.$cstNode.text.trimStart()}`);
}
});
return ss;
}
statementsToStrings(): string[] {
return this.statementsToStringsHelper(this.statements);
}
override setPublic(flag: boolean): ModuleEntry {
super.setPublic(flag);
if (!this.isPrePost) {
const n = normalizeWorkflowName(this.name);
const event = getEvent(n, this.moduleName);
event.setPublic(flag);
}
return this;
}
override toString() {
const n = this.isPrePost ? untangleWorkflowName(this.name) : this.name;
const nn = normalizeWorkflowName(n);
let s: string = `workflow ${nn} {\n`;
const ss = this.statementsToStringsHelper(this.statements);
s = s.concat(joinStatements(ss));
if (!this.isPrePost) {
const event = getEvent(nn, this.moduleName);
if ((event.isPublic() && event.isSystemDefined()) || this.isPublic()) {
s = `@public ${s}`;
}
} else if (this.isPublic()) {
s = `@public ${s}`;
}
return s.concat('\n}');
}
}
export type FlowGraphNode = {
label: string;
type: 'action' | 'condition';
on?: string[] | undefined;
next: string[];
};
export class Flow extends ModuleEntry {
flowSteps: string[];
constructor(name: string, moduleName: string, flow?: string) {
super(name, moduleName);
this.flowSteps = new Array<string>();
flow?.split('\n').forEach((step: string) => {
const s = step.trim();
if (s.length > 0) {
this.flowSteps.push(s);
}
});
}
getFlow(): string {
return this.flowSteps.join('\n');
}
removeStep(index: number): Flow {
this.flowSteps.splice(index, 1);
return this;
}
insertStep(index: number, s: string): Flow {
this.flowSteps.splice(index, 0, s);
return this;
}
appendStep(s: string): Flow {
this.flowSteps.push(s);
return this;
}
stepsCount(): number {
return this.flowSteps.length;
}
toGraph(): FlowGraphNode[] {
const result = new Array<FlowGraphNode>();
this.flowSteps.forEach((s: string) => {
const fp = FlowStepPattern.Parse(s);
if (fp.condition) {
const orig = result.find((v: FlowGraphNode) => {
return v.label == fp.first;
});
if (orig) {
const nxs = orig.next;
nxs?.push(fp.next);
const conds = orig.on;
conds?.push(fp.condition);
} else {
result.push({
label: fp.first,
type: 'condition',
on: [fp.condition],
next: [fp.next],
});
}
} else {
result.push({
label: fp.first,
type: 'action',
next: [fp.next],
});
}
});
return result;
}
static asFlowName(n: string): string {
return `${n}.flow`;
}
static normaliseFlowName(n: string): string {
const i = n.lastIndexOf('.flow');
if (i > 0) {
return n.substring(0, i);
} else {
return n;
}
}
override toString(): string {
return `flow ${Flow.normaliseFlowName(this.name)} {
${this.getFlow()}
}`;
}
}
export class Scenario extends ModuleEntry {
def: AgentScenario;
constructor(name: string, moduleName: string, scn: AgentScenario) {
super(name, moduleName);
this.def = scn;
}
override toString(): string {
if (this.def.ifPattern) {
return `scenario ${this.name} {\n ${this.def.ifPattern.toString()}\n}\n`;
}
const s = `if ("${this.def.user}") {
${this.def.ai}
}`;
return `scenario ${this.name} {\n ${s}\n}\n`;
}
}
export class Directive extends ModuleEntry {
private def: AgentCondition;
constructor(name: string, moduleName: string, def: AgentCondition) {
super(name, moduleName);
this.def = def;
}
override toString(): string {
if (this.def.ifPattern) {
return `directive ${this.name} {
${this.def.if}
}`;
} else {
const obj: any = {
if: this.def.if,
then: this.def.then,
};
return `directive ${this.name} ${JSON.stringify(obj)}`;
}
}
}
export class GlossaryEntry extends ModuleEntry {
private def: AgentGlossaryEntry;
constructor(name: string, moduleName: string, def: AgentGlossaryEntry) {
super(name, moduleName);
this.def = def;
}
override toString(): string {
const ss = new Array<string>();
ss.push(` name "${this.def.name}"`);
ss.push(` meaning "${this.def.meaning}"`);
if (this.def.synonyms) {
ss.push(` synonyms "${this.def.synonyms}"`);
}
return `glossaryEntry ${this.name} \n{\n${ss.join(',\n')}\n}`;
}
}
export function flowGraphNext(
graph: FlowGraphNode[],
currentNode?: FlowGraphNode,
onCondition?: string
): FlowGraphNode | undefined {
if (!currentNode) {
return graph[0];
}
const node = graph.find((n: FlowGraphNode) => {
return n.label == currentNode.label;
});
if (node) {
if (onCondition) {
const c = node.on?.findIndex((v: string) => {
return v == onCondition;
});
if (c !== undefined) {
const next = node.next[c];
const r = graph.find((n: FlowGraphNode) => {
return n.label == next;
});
return r || { label: next, type: 'action', next: [] };
} else {
return undefined;
}
} else {
return graph.find((n: FlowGraphNode) => {
return n.label == node.next[0];
});
}
}
return undefined;
}
type BackoffStrategy = 'e' | 'l' | 'c'; // exponential, linear, constant
type BackoffMagnitude = 'ms' | 's' | 'm'; // milliseconds, seconds, minutes
export type RetryBackoff = {
strategy: BackoffStrategy | undefined;
delay: number | undefined;
magnitude: BackoffMagnitude | undefined;
factor: number | undefined;
};
export class Retry extends ModuleEntry {
attempts: number;
private backoff: RetryBackoff;
constructor(name: string, moduleName: string, attempts: number) {
super(name, moduleName);
this.attempts = attempts <= 0 ? 0 : attempts;
this.backoff = {
strategy: undefined,
delay: undefined,
magnitude: undefined,
factor: undefined,
};
}
setExponentialBackoff(): Retry {
this.backoff.strategy = 'e';
return this;
}
isExponentialBackoff(): boolean {
return this.backoff.strategy === 'e';
}
setLinearBackoff(): Retry {
this.backoff.strategy = 'l';
return this;
}
isLinearBackoff(): boolean {
return this.backoff.strategy === 'l';
}
setConstantBackoff(): Retry {
this.backoff.strategy = 'c';
return this;
}
isConstantBackoff(): boolean {
return this.backoff.strategy === undefined || this.backoff.strategy === 'c';
}
setBackoffDelay(n: number): Retry {
if (n > 0) {
this.backoff.delay = n;
}
return this;
}
setBackoffMagnitudeAsMilliseconds(): Retry {
this.backoff.magnitude = 'ms';
return this;
}
backoffMagnitudeIsMilliseconds(): boolean {
return this.backoff.magnitude === 'ms';
}
setBackoffMagnitudeAsSeconds(): Retry {
this.backoff.magnitude = 's';
return this;
}
backoffMagnitudeIsSeconds(): boolean {
return this.backoff.magnitude === 's';
}
setBackoffMagnitudeAsMinutes(): Retry {
this.backoff.magnitude = 'm';
return this;
}
backoffMagnitudeIsMinutes(): boolean {
return this.backoff.magnitude === 'm';
}
setBackoffFactor(n: number): Retry {
if (n !== 0) this.backoff.factor = n;
return this;
}
getNextDelayMs(attempt: number): number {
if (attempt >= this.attempts) {
return 0;
}
const delay = this.backoff.delay === undefined ? 2 : this.backoff.delay;
if (attempt <= 0 || this.isConstantBackoff()) {
return this.normalizeDelay(delay);
}
let d = delay;
let i = 0;
const factor = this.backoff.factor === undefi