UNPKG

agentlang

Version:

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

1,184 lines (1,110 loc) 38 kB
import chalk from 'chalk'; import { createAgentlangServices } from '../language/agentlang-module.js'; import { Import, RbacSpecEntries, ModuleDefinition, Definition, isEntityDefinition, isEventDefinition, isRecordDefinition, isRelationshipDefinition, isWorkflowDefinition, EntityDefinition, RelationshipDefinition, WorkflowDefinition, RbacSpecDefinition, Statement, isStandaloneStatement, SchemaDefinition, isAgentDefinition, AgentDefinition, isResolverDefinition, ResolverDefinition, ResolverMethodSpec, GenericPropertyDef, isLiteral, ArrayLiteral, MapEntry, Expr, FlowDefinition, isFlowDefinition, Literal, isDecisionDefinition, DecisionDefinition, CaseEntry, isScenarioDefinition, ScenarioDefinition, DirectiveDefinition, GlossaryEntryDefinition, isDirectiveDefinition, isGlossaryEntryDefinition, isPublicWorkflowDefinition, isPublicAgentDefinition, isPublicEventDefinition, AgentXtraAttribute, If, isRetryDefinition, RetryDefinition, SetAttribute, CrudMap, } from '../language/generated/ast.js'; import { addEntity, addEvent, addModule, addRecord, addRelationship, addWorkflow, Entity, RbacSpecification, Record, Relationship, Module, Workflow, isModule, getUserModuleNames, removeModule, newInstanceAttributes, addAgent, fetchModule, Retry, } from './module.js'; import { asStringLiteralsMap, escapeSpecialChars, findRbacSchema, isFqName, makeFqName, maybeExtends, preprocessRawConfig, registerInitFunction, rootRef, } from './util.js'; import { getFileSystem, toFsPath, readFile, readdir, exists } from '../utils/fs-utils.js'; import { URI } from 'vscode-uri'; import { AstNode, LangiumCoreServices, LangiumDocument } from 'langium'; import { isNodeEnv, path } from '../utils/runtime.js'; import { CoreModules, registerCoreModules } from './modules/core.js'; import { canParse, introspectIf, maybeGetValidationErrors, maybeRaiseParserErrors, parse, parseModule, parseStatement, parseWorkflow, } from '../language/parser.js'; import { logger } from './logger.js'; import { Environment, evaluateStatements, GlobalEnvironment } from './interpreter.js'; import { createPermission, createRole } from './modules/auth.js'; import { AgentEntityName, CoreAIModuleName, LlmEntityName } from './modules/ai.js'; import { getDefaultLLMService } from './agents/registry.js'; import { GenericResolver, GenericResolverMethods } from './resolvers/interface.js'; import { registerResolver, setResolver } from './resolvers/registry.js'; import { Config, ConfigSchema, setAppConfig } from './state.js'; import { getModuleFn, importModule } from './jsmodules.js'; import { SetSubscription } from './defs.js'; import { ExtendedFileSystem } from '../utils/fs/interfaces.js'; import z from 'zod'; import { registerAgentFlow, registerFlow } from './agents/flows.js'; import { addAgentDirective, addAgentGlossaryEntry, addAgentScenario, AgentCondition, AgentGlossaryEntry, AgentScenario, registerAgentDirectives, registerAgentGlossary, registerAgentResponseSchema, registerAgentScenarios, registerAgentScratchNames, } from './agents/common.js'; export async function extractDocument( fileName: string, services: LangiumCoreServices ): Promise<LangiumDocument> { const extensions = services.LanguageMetaData.fileExtensions; if (isNodeEnv && typeof fileName === 'string') { if (!extensions.includes(path.extname(fileName))) { console.error( chalk.yellow(`Please choose a file with one of these extensions: ${extensions}.`) ); process.exit(1); } const fullFilePath = path.resolve(fileName); const fileExists = await exists(fullFilePath); if (!fileExists) { const errorMsg = `File ${fileName} does not exist.`; throw new Error(errorMsg); } } else if (!isNodeEnv && typeof fileName === 'string') { const fullFilePath = path.resolve(fileName); const fileExists = await exists(fullFilePath); if (!fileExists) { throw new Error(`File ${fileName} does not exist.`); } } else { throw new Error('Invalid input: expected file path (Node.js) or File object/content (browser)'); } const document = await services.shared.workspace.LangiumDocuments.getOrCreateDocument( URI.file(path.resolve(fileName)) ); // Build document await services.shared.workspace.DocumentBuilder.build([document], { validation: true, }); // Handle validation errors const errs = maybeGetValidationErrors(document); if (errs) { const errorMsg = `${errs.join('\n')}`; throw new Error(errorMsg); } return document; } export async function extractAstNode<T extends AstNode>( fileName: string, services: LangiumCoreServices ): Promise<T> { return (await extractDocument(fileName, services)).parseResult?.value as T; } export type ApplicationSpec = any; export const DefaultAppSpec: ApplicationSpec = { name: 'agentlang-app', version: '0.0.1', }; let CurrentAppSpec: ApplicationSpec = DefaultAppSpec; export function getAppSpec(): ApplicationSpec { return CurrentAppSpec; } async function getAllModules( dir: string, fs: ExtendedFileSystem, drill: boolean = true ): Promise<string[]> { let alFiles = new Array<string>(); if (!(await fs.exists(dir))) { return alFiles; } const directoryContents = await fs.readdir(dir); for (let i = 0; i < directoryContents.length; ++i) { const file = directoryContents[i]; if (path.extname(file).toLowerCase() == '.al') { alFiles.push(dir + path.sep + file); } else if (drill) { const fullPath = dir + path.sep + file; const stat = await fs.stat(fullPath); if (stat.isDirectory()) { alFiles = alFiles.concat(await getAllModules(fullPath, fs)); } } } return alFiles; } let dependenciesCallback: Function | undefined = undefined; export function setDependenciesCallback(cb: Function) { dependenciesCallback = cb; } export type DependencyInfo = { appName: string; url: string; }; async function loadApp(appDir: string, fsOptions?: any, callback?: Function): Promise<string> { // Initialize filesystem if not already done const fs = await getFileSystem(fsOptions); const appJsonFile = `${appDir}${path.sep}package.json`; const s: string = await fs.readFile(appJsonFile); const appSpec: ApplicationSpec = JSON.parse(s); CurrentAppSpec = appSpec; if (dependenciesCallback !== undefined && appSpec.dependencies) { const aldeps = new Array<DependencyInfo>(); for (const [k, v] of Object.entries(appSpec.dependencies)) { if (typeof v === 'string' && v.startsWith('git+http')) { aldeps.push({ appName: k, url: v, }); } } if (aldeps.length > 0) { await dependenciesCallback(aldeps); } } let lastModuleLoaded: string = ''; async function cont2() { const fls01 = await getAllModules(appDir, fs, false); const fls02 = await getAllModules(appDir + path.sep + 'src', fs); const alFiles0 = fls01.concat(fls02); const configFile = `${appDir}${path.sep}config.al`; const alFiles = alFiles0.filter((s: string) => { return s != configFile; }); for (let i = 0; i < alFiles.length; ++i) { lastModuleLoaded = (await loadModule(alFiles[i], fsOptions)).name; } if (callback) await callback(appSpec); } if (appSpec.dependencies !== undefined) { for (const [depName, _] of Object.entries(appSpec.dependencies)) { try { // In browser (with virtual filesystem), use absolute path relative to appDir // In Node.js, use relative path from current working directory const isBrowser = fsOptions && fsOptions.name; const depDirName = isBrowser ? `${appDir}${path.sep}node_modules${path.sep}${depName}` : `./node_modules/${depName}`; const fls01 = await fs.readdir(depDirName); const srcDir = depDirName + path.sep + 'src'; const hasSrc = await fs.exists(srcDir); const files = hasSrc ? fls01.concat(await fs.readdir(srcDir)) : fls01; if ( files.find(file => { return path.extname(file).toLowerCase() == '.al'; }) ) { await loadApp(depDirName, fsOptions); } } catch (error) { logger.error(`Error loading dependency ${depName}: ${error}`); } } } await cont2(); return appSpec.name || lastModuleLoaded; } /** * Load a module from a file * @param fileName Path to the file containing the module * @param fsOptions Optional configuration for the filesystem * @param callback Function to be called after loading the module * @returns Promise that resolves when the module is loaded */ export async function load( fileName: string, fsOptions?: any, callback?: Function ): Promise<ApplicationSpec> { let result: string = ''; if (path.basename(fileName).endsWith('.al')) { result = (await loadModule(fileName, fsOptions, callback)).name; } else { result = await loadApp(fileName, fsOptions, callback); } return { name: result, version: '0.0.1' }; } export function flushAllModules() { getUserModuleNames().forEach((n: string) => { removeModule(n); }); } /** * Removes all existing user-modules and loads the specified module-file. * @param fileName Path to the file containing the module * @param fsOptions Optional configuration for the filesystem * @param callback Function to be called after loading the module * @returns Promise that resolves when the module is loaded */ export async function flushAllAndLoad( fileName: string, fsOptions?: any, callback?: Function ): Promise<ApplicationSpec> { flushAllModules(); return await load(fileName, fsOptions, callback); } export async function loadAppConfig(configDir: string): Promise<Config> { let cfgObj: any = undefined; const fs = await getFileSystem(); const alCfgFile = `${configDir}${path.sep}config.al`; if (await fs.exists(alCfgFile)) { const cfgPats = await fs.readFile(alCfgFile); if (canParse(cfgPats)) { const cfgWf = `workflow createConfig{\n${cfgPats}}`; const wf = await parseWorkflow(cfgWf); const env = new Environment('config.env'); const cfgStmts = new Array<Statement>(); const initInsts = new Array<Statement>(); for (let i = 0; i < wf.statements.length; ++i) { const stmt: Statement = wf.statements[i]; if (stmt.pattern.crudMap) { initInsts.push(await makeDeleteAllConfigStatement(stmt.pattern.crudMap)); initInsts.push(stmt); } else { cfgStmts.push(stmt); } } if (initInsts.length > 0) { registerInitFunction(async () => { const env = new Environment('config.insts.env'); try { await evaluateStatements(initInsts, env); await env.commitAllTransactions(); } catch (reason: any) { await env.rollbackAllTransactions(); console.error(`Failed to initialize config instances: ${reason}`); } }); } await evaluateStatements(cfgStmts, env); cfgObj = env.getLastResult(); } } try { const cfg = cfgObj ? configFromObject(cfgObj) : await loadRawConfig(`${configDir}${path.sep}app.config.json`); return setAppConfig(cfg); } catch (err: any) { if (err instanceof z.ZodError) { console.log(chalk.red('Config validation failed:')); err.errors.forEach((error: any, index: number) => { console.log(chalk.red(` ${index + 1}. ${error.path.join('.')}: ${error.message}`)); }); } else { console.log(`Config loading failed: ${err}`); } throw err; } } async function makeDeleteAllConfigStatement(crudMap: CrudMap): Promise<Statement> { const n = crudMap.name; const p = `purge {${n}? {}}`; return await parseStatement(p); } export async function loadCoreModules() { if (CoreModules.length == 0) { registerCoreModules(); } for (let i = 0; i < CoreModules.length; ++i) { await internModule(await parseModule(CoreModules[i])); } } async function loadModule(fileName: string, fsOptions?: any, callback?: Function): Promise<Module> { // Initialize filesystem if not already done console.log(`loading ${fileName}`); const fs = await getFileSystem(fsOptions); const fsAdapter = getFsAdapter(fs); // Create services with our custom filesystem adapter const services = createAgentlangServices({ fileSystemProvider: _services => fsAdapter, }).Agentlang; // Extract the AST node const module = await extractAstNode<ModuleDefinition>(fileName, services); const result: Module = await internModule(module, fileName); console.log(chalk.green(`Module ${chalk.bold(result.name)} loaded`)); logger.info(`Module ${result.name} loaded`); if (callback) { await callback(); } return result; } let cachedFsAdapter: any = null; function getFsAdapter(fs: any) { if (cachedFsAdapter === null) { // Create an adapter to make our filesystem compatible with Langium cachedFsAdapter = { // Read file contents as text readFile: async (uri: URI) => { return await readFile(uri); }, // List directory contents with proper metadata readDirectory: async (uri: URI) => { const result = await readdir(uri); const dirPath = toFsPath(uri); // Convert string[] to FileSystemNode[] as required by Langium return Promise.all( result.map(async name => { const filePath = dirPath.endsWith('/') ? `${dirPath}${name}` : `${dirPath}/${name}`; const stats = await fs .stat(filePath) .catch(() => ({ isFile: () => true, isDirectory: () => false })); return { uri: URI.file(filePath), isFile: stats.isFile?.() ?? true, isDirectory: stats.isDirectory?.() ?? false, }; }) ); }, }; } return cachedFsAdapter; } function setRbacForEntity(entity: Entity, rbacSpec: RbacSpecDefinition) { const rbac: RbacSpecification[] = rbacSpec.specEntries.map((rs: RbacSpecEntries) => { return RbacSpecification.from(rs).setResource(makeFqName(entity.moduleName, entity.name)); }); if (rbac.length > 0) { const f = async () => { for (let i = 0; i < rbac.length; ++i) { await createRolesAndPermissions(rbac[i]); } }; registerInitFunction(f); entity.setRbacSpecifications(rbac); } } async function createRolesAndPermissions(rbacSpec: RbacSpecification) { const roles: Array<string> = [...rbacSpec.roles]; const env: Environment = new Environment(); async function f() { for (let i = 0; i < roles.length; ++i) { const r = roles[i]; await createRole(r, env); if (rbacSpec.hasPermissions() && rbacSpec.hasResource()) { await createPermission( `${r}_permission_${rbacSpec.resource}`, r, rbacSpec.resource, rbacSpec.hasCreatePermission(), rbacSpec.hasReadPermission(), rbacSpec.hasUpdatePermission(), rbacSpec.hasDeletePermission(), env ); } } } await env.callInTransaction(f); } function addEntityFromDef(def: EntityDefinition, moduleName: string): Entity { const entity = addEntity(def.name, moduleName, def.schema, maybeExtends(def.extends)); const rbacSpec = findRbacSchema(def.schema); if (rbacSpec) { setRbacForEntity(entity, rbacSpec); } return entity; } function addSchemaFromDef( def: SchemaDefinition, moduleName: string, ispub: boolean = false ): Record { let result: Record | undefined; if (isEntityDefinition(def)) { result = addEntityFromDef(def, moduleName); } else if (isEventDefinition(def)) { result = addEvent(def.name, moduleName, def.schema, maybeExtends(def.extends)); } else if (isRecordDefinition(def)) { result = addRecord(def.name, moduleName, def.schema, maybeExtends(def.extends)); } else { throw new Error(`Cannot add schema definition in module ${moduleName} for ${def}`); } if (ispub) { result.setPublic(true); } return result; } export function addRelationshipFromDef( def: RelationshipDefinition, moduleName: string ): Relationship { return addRelationship(def.name, def.type, def.nodes, moduleName, def.schema, def.properties); } export function addWorkflowFromDef( def: WorkflowDefinition, moduleName: string, ispub: boolean = false ): Workflow { return addWorkflow(def.name || '', moduleName, def.statements, def.header, ispub); } const StandaloneStatements = new Map<string, Statement[]>(); function addStandaloneStatement(stmt: Statement, moduleName: string, userDefined = true) { let stmts: Array<Statement> | undefined = StandaloneStatements.get(moduleName); if (stmts === undefined) { stmts = new Array<Statement>(); } stmts.push(stmt); if (!StandaloneStatements.has(moduleName)) { StandaloneStatements.set(moduleName, stmts); } if (userDefined) { const m = fetchModule(moduleName); m.addStandaloneStatement(stmt); } } export async function runStandaloneStatements() { if (StandaloneStatements.size > 0) { await GlobalEnvironment.callInTransaction(async () => { const ks = [...StandaloneStatements.keys()]; for (let i = 0; i < ks.length; ++i) { const moduleName = ks[i]; const stmts: Statement[] | undefined = StandaloneStatements.get(moduleName); if (stmts) { const oldModule = GlobalEnvironment.switchActiveModuleName(moduleName); await evaluateStatements(stmts, GlobalEnvironment); GlobalEnvironment.switchActiveModuleName(oldModule); } } logger.debug(`Init eval result: ${GlobalEnvironment.getLastResult()}`); }); StandaloneStatements.clear(); } } function processAgentDirectives(agentName: string, value: Literal): AgentCondition[] | undefined { if (value.array) { const conds = new Array<AgentCondition>(); value.array.vals.forEach((stmt: Statement) => { const expr = stmt.pattern.expr; if (expr && isLiteral(expr) && expr.map) { let cond: string | undefined; let then: string | undefined; expr.map.entries.forEach((me: MapEntry) => { const v = isLiteral(me.value) ? me.value.str : undefined; if (v) { if (me.key.str == 'if') { cond = v; } else if (me.key.str == 'then') { then = v; } } }); if (cond && then) { conds?.push({ if: cond, then, internal: true, ifPattern: undefined }); } else { throw new Error(`Invalid condition spec in agent ${agentName}`); } } }); return conds; } return undefined; } function processAgentScenarios(agentName: string, value: Literal): AgentScenario[] | undefined { if (value.array) { const scenarios = new Array<AgentScenario>(); value.array.vals.forEach((stmt: Statement) => { const expr = stmt.pattern.expr; if (expr && isLiteral(expr) && expr.map) { let user: string | undefined; let ai: string | undefined; expr.map.entries.forEach((me: MapEntry) => { let v = isLiteral(me.value) ? me.value.str : undefined; if (v === undefined) { v = me.value.$cstNode?.text; } if (v) { if (me.key.str == 'user') { user = v; } else if (me.key.str == 'ai') { ai = v; } } }); if (user && ai) { const internal = true; scenarios.push({ user, ai, internal, ifPattern: undefined }); } else { throw new Error(`Invalid scenario spec in agent ${agentName}`); } } }); return scenarios; } return undefined; } function processAgentGlossary(agentName: string, value: Literal): AgentGlossaryEntry[] | undefined { if (value.array) { const gls = new Array<AgentGlossaryEntry>(); value.array.vals.forEach((stmt: Statement) => { const expr = stmt.pattern.expr; if (expr && isLiteral(expr) && expr.map) { let name: string | undefined; let meaning: string | undefined; let synonyms: string | undefined; expr.map.entries.forEach((me: MapEntry) => { const v = isLiteral(me.value) ? me.value.str : undefined; if (v) { if (me.key.str == 'name') { name = v; } else if (me.key.str == 'meaning') { meaning = v; } else if (me.key.str == 'synonyms') { synonyms = v; } } }); if (name && meaning) { const internal = true; gls.push({ name, meaning, synonyms, internal }); } else { throw new Error(`Invalid glossary spec in agent ${agentName}`); } } }); return gls; } return undefined; } function processAgentScratchNames(agentName: string, value: Literal): string[] | undefined { if (value.array) { const scratch = new Array<string>(); value.array.vals.forEach((stmt: Statement) => { const expr = stmt.pattern.expr; if (expr && isLiteral(expr) && (expr.id || expr.str)) { scratch.push(expr.id || expr.str || ''); } }); return scratch; } return undefined; } async function addAgentDefinition( def: AgentDefinition, moduleName: string, ispub: boolean = false ) { let llmName: string | undefined = undefined; const name = def.name; const attrsStrs = new Array<string>(); attrsStrs.push(`name "${name}"`); const attrs = newInstanceAttributes(); attrsStrs.push(`moduleName "${moduleName}"`); attrs.set('moduleName', moduleName); let conds: AgentCondition[] | undefined = undefined; let scenarios: AgentScenario[] | undefined = undefined; let glossary: AgentGlossaryEntry[] | undefined = undefined; let responseSchema: string | undefined = undefined; let scratchNames: string[] | undefined = undefined; def.body?.attributes.forEach((apdef: GenericPropertyDef) => { if (apdef.name == 'flows') { let fnames: string | undefined = undefined; if (apdef.value.array) { fnames = processAgentArray(apdef.value.array, name); } else { fnames = apdef.value.id || apdef.value.str; } if (fnames) { fnames.split(',').forEach((n: string) => { n = n.trim(); const fqn = isFqName(n) ? n : `${moduleName}/${n}`; registerAgentFlow(name, fqn); }); attrsStrs.push(`type "flow-exec"`); attrs.set('type', 'flow-exec'); attrsStrs.push(`flows "${fnames}"`); attrs.set('flows', fnames); } else { throw new Error(`Invalid flows list in agent ${name}`); } } else if (apdef.name == 'directives') { conds = processAgentDirectives(name, apdef.value); } else if (apdef.name == 'scenarios') { scenarios = processAgentScenarios(name, apdef.value); } else if (apdef.name == 'glossary') { glossary = processAgentGlossary(name, apdef.value); } else if (apdef.name == 'responseSchema') { const s = apdef.value.id || apdef.value.ref || apdef.value.str; if (s) { if (isFqName(s)) { responseSchema = s; } else { responseSchema = makeFqName(moduleName, s); } } else { throw new Error(`responseSchema must be a valid name in agent ${name}`); } } else if (apdef.name == 'scratch') { scratchNames = processAgentScratchNames(name, apdef.value); } else { let v: any = undefined; if (apdef.value.array) { v = processAgentArray(apdef.value.array, name); } else { v = apdef.value.str || apdef.value.id || apdef.value.ref || apdef.value.num; if (v === undefined) { v = apdef.value.bool; } } if (v === undefined) { throw new Error(`Cannot initialize agent ${name}, only literals can be set for attributes`); } if (apdef.name == 'llm') { llmName = v; } const ov = v; if (apdef.value.id || apdef.value.ref || apdef.value.array) { v = `"${v}"`; } else if (apdef.value.str) { v = `"${escapeSpecialChars(v)}"`; } attrsStrs.push(`${apdef.name} ${v}`); attrs.set(apdef.name, ov); } }); let createDefaultLLM = false; if (!attrs.has('llm')) { // Agent doesn't have an LLM specified, create a default one llmName = `${name}_llm`; createDefaultLLM = true; } // Create a copy of attrsStrs for the database operation const dbAttrsStrs = [...attrsStrs]; // Only add llm to database attributes if we have one if (llmName) { dbAttrsStrs.push(`llm "${llmName}"`); } const createAgent = `{${CoreAIModuleName}/${AgentEntityName} { ${dbAttrsStrs.join(',')} }, @upsert}`; let wf = createAgent; // Only create an LLM with default service if we're creating a default LLM // If the user specified an LLM name, don't create/upsert it (it should already exist) if (createDefaultLLM && llmName) { const service = getDefaultLLMService(); wf = `{${CoreAIModuleName}/${LlmEntityName} {name "${llmName}", service "${service}"}, @upsert}; ${wf}`; } (await parseWorkflow(`workflow A {${wf}}`)).statements.forEach((stmt: Statement) => { addStandaloneStatement(stmt, moduleName, false); }); const agentFqName = makeFqName(moduleName, name); if (conds) { registerAgentDirectives(agentFqName, conds); } if (scenarios) { registerAgentScenarios(agentFqName, scenarios); } if (glossary) { registerAgentGlossary(agentFqName, glossary); } if (responseSchema) { registerAgentResponseSchema(agentFqName, responseSchema); } if (scratchNames) { registerAgentScratchNames(agentFqName, scratchNames); } // Don't add llm to module attrs if it wasn't originally specified const agent = addAgent(def.name, attrs, moduleName); if (ispub) { agent.setPublic(true); } return agent; } function processAgentArray(array: ArrayLiteral, attrName: string): string { return array.vals .map((stmt: Statement) => { const expr = stmt.pattern.expr; return processAgentArrayValue(expr, attrName); }) .join(','); } function processAgentArrayValue(expr: Expr | undefined, attrName: string): string { if (expr && isLiteral(expr)) { const s = expr.str || expr.id || expr.ref || expr.bool; if (s !== undefined) { return s; } if (expr.array) { return processAgentArray(expr.array, attrName); } else if (expr.map) { const m = new Array<string>(); expr.map.entries.forEach((me: MapEntry) => { m.push( `${me.key.str || me.key.num || me.key.bool || ''}: ${processAgentArrayValue(me.value, attrName)}` ); }); return `{${m.join(',')}}`; } else { throw new Error(`Type not supported in agent-arrays - ${attrName}`); } } else { throw new Error(`Invalid value in array passed to agent ${attrName}`); } } function addFlowDefinition(def: FlowDefinition, moduleName: string) { const m = fetchModule(moduleName); const sdef = def.$cstNode?.text; let f = ''; if (sdef) { const idx = sdef.indexOf('{'); if (idx > 0) { f = sdef.substring(idx + 1, sdef.lastIndexOf('}')).trim(); } else { f = sdef; } } m.addFlow(def.name, f); registerFlow(`${moduleName}/${def.name}`, f); } function addDecisionDefinition(def: DecisionDefinition, moduleName: string) { const m = fetchModule(moduleName); const cases = def.body ? def.body.cases.map((ce: CaseEntry) => { return ce.$cstNode?.text; }) : new Array<string>(); m.addRawDecision(def.name, cases as string[]); } function agentXtraAttributesAsMap(xtras: AgentXtraAttribute[] | undefined): Map<string, string> { const result = new Map<string, string>(); xtras?.forEach((v: AgentXtraAttribute) => { result.set(v.name, v.value); }); return result; } function scenarioConditionAsMap(cond: If | undefined) { const result = new Map<string, any>(); if (cond) { if (isLiteral(cond.cond)) { const s = cond.cond.str; if (s === undefined) { throw new Error(`scenario condition must be a string - ${cond.cond.$cstNode?.text}`); } const stmt = cond.statements[0]; const v = stmt ? stmt.pattern.$cstNode?.text : ''; if (v === undefined) { throw new Error( `scenario consequent must be a string or name - ${cond.cond.$cstNode?.text}` ); } result.set('user', s).set('ai', v).set('if', introspectIf(cond)); } } return result; } function addScenarioDefintion(def: ScenarioDefinition, moduleName: string) { if (def.body || def.scn) { let n = rootRef(def.name); if (!isFqName(n)) { n = makeFqName(moduleName, n); } const m = def.body ? asStringLiteralsMap(def.body) : scenarioConditionAsMap(def.scn); const user = m.get('user'); const ai = m.get('ai'); const ifPattern = m.get('if'); if (user !== undefined && ai !== undefined) { const scn = { user: user, ai: ai, internal: false, ifPattern }; addAgentScenario(n, scn); fetchModule(moduleName).addScenario(def.name, scn); } else throw new Error(`scenario ${def.name} requires both user and ai entries`); } } function addDirectiveDefintion(def: DirectiveDefinition, moduleName: string) { if (def.body || def.dir) { let n = rootRef(def.name); if (!isFqName(n)) { n = makeFqName(moduleName, n); } if (def.body) { const m = asStringLiteralsMap(def.body); const cond = m.get('if'); const then = m.get('then'); if (cond && then) { const dir = { if: cond, then: then, internal: false, ifPattern: undefined }; addAgentDirective(n, dir); fetchModule(moduleName).addDirective(def.name, dir); } else throw new Error(`directive ${def.name} requires both if and then entries`); } else if (def.dir) { const cond = def.dir.$cstNode?.text; if (cond) { const ifPattern = introspectIf(def.dir); const dir = { if: cond, then: '', internal: false, ifPattern }; addAgentDirective(n, dir); fetchModule(moduleName).addDirective(def.name, dir); } else { throw new Error(`directive ${def.name} requires a valid if expression`); } } } } function addGlossaryEntryDefintion(def: GlossaryEntryDefinition, moduleName: string) { if (def.body || def.glos) { let n = rootRef(def.name); if (!isFqName(n)) { n = makeFqName(moduleName, n); } const m = def.body ? asStringLiteralsMap(def.body) : agentXtraAttributesAsMap(def.glos?.attributes); const name = m.get('name') || m.get('word'); const meaning = m.get('meaning'); const syn = m.get('synonyms'); if (name && meaning) { const ge = { name: name, meaning: meaning, synonyms: syn, internal: false, }; addAgentGlossaryEntry(n, ge); fetchModule(moduleName).addGlossaryEntry(def.name, ge); } else throw new Error(`glossaryEntry ${def.name} requires both name and meaning keys`); } } function addRetryDefinition(def: RetryDefinition, moduleName: string) { const retry = new Retry(def.name, moduleName, def.attempts !== undefined ? def.attempts : 0); if (def.backoff) { def.backoff.attributes.forEach((attr: SetAttribute) => { if (isLiteral(attr.value)) { switch (attr.name) { case 'strategy': switch (attr.value.id || attr.value.str) { case 'exponential': retry.setExponentialBackoff(); break; case 'linear': retry.setLinearBackoff(); break; case 'constant': retry.setConstantBackoff(); break; default: throw new Error(`Invalid backoff strategy ${attr.value} specified for ${def.name}`); } break; case 'delay': if (attr.value.num) { retry.setBackoffDelay(attr.value.num); } else { throw new Error(`Backoff delay must be a numeric value for ${def.name}`); } break; case 'magnitude': switch (attr.value.id || attr.value.str) { case 'milliseconds': retry.setBackoffMagnitudeAsMilliseconds(); break; case 'seconds': retry.setBackoffMagnitudeAsSeconds(); break; case 'minutes': retry.setBackoffMagnitudeAsMinutes(); break; default: throw new Error(`Invalid backoff magnitude ${attr.value} set for ${def.name}`); } break; case 'factor': if (attr.value.num) { retry.setBackoffFactor(attr.value.num); } else { throw new Error(`Backoff factor must be a number for ${def.name}`); } break; default: throw new Error(`Invalid backoff option ${attr.name} specified for ${def.name}`); } } else { throw new Error(`strategy must be a string in ${def.name}`); } }); } fetchModule(moduleName).addRetry(retry); } function addResolverDefinition(def: ResolverDefinition, moduleName: string) { const resolverName = `${moduleName}/${def.name}`; const paths = def.paths; if (paths.length == 0) { logger.warn(`Resolver has no associated paths - ${resolverName}`); return; } registerInitFunction(() => { const methods = new Map<string, Function>(); let subsFn: Function | undefined; let subsEvent: string | undefined; def.methods.forEach((spec: ResolverMethodSpec) => { const n = spec.key.name; if (n == 'subscribe') { subsFn = asResolverFn(spec.fn.name); } else if (n == 'onSubscription') { subsEvent = spec.fn.name; } else { methods.set(n, asResolverFn(spec.fn.name)); } }); const methodsObj = Object.fromEntries(methods.entries()) as GenericResolverMethods; const resolver = new GenericResolver(resolverName, methodsObj); registerResolver(resolverName, () => { return resolver; }); paths.forEach((path: string) => { setResolver(path, resolverName); }); if (subsFn) { resolver.subs = { subscribe: subsFn, }; if (subsEvent) SetSubscription(subsEvent, resolverName); resolver.subscribe(); } }); } function asResolverFn(fname: string): Function { let fn = getModuleFn(fname); if (fn) return fn; fn = eval(fname); if (!(fn instanceof Function)) { throw new Error(`${fname} is not a function`); } return fn as Function; } export async function addFromDef(def: Definition, moduleName: string) { if (isEntityDefinition(def)) addSchemaFromDef(def, moduleName); else if (isEventDefinition(def)) addSchemaFromDef(def, moduleName); else if (isPublicEventDefinition(def)) addSchemaFromDef(def.def, moduleName, true); else if (isRecordDefinition(def)) addSchemaFromDef(def, moduleName); else if (isRelationshipDefinition(def)) addRelationshipFromDef(def, moduleName); else if (isWorkflowDefinition(def)) addWorkflowFromDef(def, moduleName); else if (isPublicWorkflowDefinition(def)) addWorkflowFromDef(def.def, moduleName, true); else if (isAgentDefinition(def)) await addAgentDefinition(def, moduleName); else if (isPublicAgentDefinition(def)) await addAgentDefinition(def.def, moduleName, true); else if (isStandaloneStatement(def)) addStandaloneStatement(def.stmt, moduleName); else if (isResolverDefinition(def)) addResolverDefinition(def, moduleName); else if (isFlowDefinition(def)) addFlowDefinition(def, moduleName); else if (isDecisionDefinition(def)) addDecisionDefinition(def, moduleName); else if (isScenarioDefinition(def)) addScenarioDefintion(def, moduleName); else if (isDirectiveDefinition(def)) addDirectiveDefintion(def, moduleName); else if (isGlossaryEntryDefinition(def)) addGlossaryEntryDefintion(def, moduleName); else if (isRetryDefinition(def)) addRetryDefinition(def, moduleName); } export async function parseAndIntern(code: string, moduleName?: string) { if (moduleName && !isModule(moduleName)) { throw new Error(`Module not found - ${moduleName}`); } const r = await parse(moduleName ? `module ${moduleName} ${code}` : code); maybeRaiseParserErrors(r); await internModule(r.parseResult.value); } export async function internModule( module: ModuleDefinition, moduleFileName?: string ): Promise<Module> { const mn = module.name; const r = addModule(mn); module.imports.forEach(async (imp: Import) => { await importModule(imp.path, imp.name, moduleFileName); }); for (let i = 0; i < module.defs.length; ++i) { const def = module.defs[i]; await addFromDef(def, mn); } return r; } export async function loadRawConfig( configFileName: string, validate: boolean = true, fsOptions?: any ): Promise<any> { const fs = await getFileSystem(fsOptions); if (await fs.exists(configFileName)) { let rawConfig = preprocessRawConfig(JSON.parse(await fs.readFile(configFileName))); if (validate) { rawConfig = ConfigSchema.parse(rawConfig); } return rawConfig; } else { return { service: { port: 8080 } }; } } export function configFromObject(cfgObj: any, validate: boolean = true): any { const rawConfig = preprocessRawConfig(cfgObj); if (validate) { return ConfigSchema.parse(rawConfig); } return rawConfig; } export function generateRawConfig(configObj: any): string { return JSON.stringify(configObj); }