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

836 lines (707 loc) 20.9 kB
import { parseHelper } from 'langium/test'; import { escapeQueryName, trimQuotes } from '../runtime/util.js'; import { isDecisionDefinition, ModuleDefinition } from './generated/ast.js'; import { createAgentlangServices } from './agentlang-module.js'; import { EmptyFileSystem } from 'langium'; import { introspect, parseModule } from './parser.js'; export class BasePattern { alias: string | undefined; aliases: string[] | undefined; handlers: Map<string, BasePattern> | undefined; setAlias(alias: string) { this.alias = alias; return this; } unsetAlias() { this.alias = undefined; return this; } addAlias(alias: string) { if (this.aliases === undefined) { this.aliases = []; } this.aliases.push(alias); } setAliases(aliases: string[]) { this.aliases = aliases; } addHandler(k: 'not_found' | 'error', handler: BasePattern) { if (this.handlers === undefined) { this.handlers = new Map(); } this.handlers.set(k, handler); } private aliasesAsString(): string | undefined { if (this.alias) { return ` @as ${this.alias}`; } else if (this.aliases) { return ` @as [${this.aliases.join(',')}]`; } else { return undefined; } } private handlersAsString(): string | undefined { if (this.handlers) { let s = '{'; this.handlers.forEach((handler: BasePattern, k: string) => { s = `${s} ${k} ${handler.toString()}\n`; }); return s + '}'; } else { return undefined; } } hintsAsString(): string { const a = this.aliasesAsString(); const h = this.handlersAsString(); if (!a && !h) return ''; if (a && !h) return a; if (!a && h) return h; return `${a}\n${h}`; } toString(): string { return ''; } } export const EmptyBasePattern = new BasePattern(); export enum LiteralPatternType { ID, NUMBER, BOOLEAN, STRING, REFERENCE, MAP, ARRAY, } export type MapKey = { str?: string; num?: number; bool?: boolean; }; export class LiteralPattern extends BasePattern { type: LiteralPatternType; value: any; static EmptyArray = new LiteralPattern(LiteralPatternType.ARRAY, []); constructor(type: LiteralPatternType, value: any) { super(); this.type = type; this.value = value; } static Id(value: string): LiteralPattern { return new LiteralPattern(LiteralPatternType.ID, value); } static Number(value: number): LiteralPattern { return new LiteralPattern(LiteralPatternType.NUMBER, value); } static Boolean(value: boolean): LiteralPattern { return new LiteralPattern(LiteralPatternType.BOOLEAN, value); } static String(value: string): LiteralPattern { return new LiteralPattern(LiteralPatternType.STRING, value); } static Reference(value: string): LiteralPattern { if (value.indexOf('.') < 0) { throw new Error(`${value} does not look like a reference`); } return new LiteralPattern(LiteralPatternType.REFERENCE, value); } static Map(value: Map<MapKey, BasePattern>): LiteralPattern { return new LiteralPattern(LiteralPatternType.MAP, value); } static Array(value: Array<BasePattern>): LiteralPattern { return new LiteralPattern(LiteralPatternType.ARRAY, value); } override toString(): string { let s = ''; switch (this.type) { case LiteralPatternType.ARRAY: { const a = this.value as Array<BasePattern>; s = `[${a .map((v: BasePattern) => { return v.toString(); }) .join(', ')}]`; break; } case LiteralPatternType.MAP: { const m = this.value as Map<MapKey, BasePattern>; const arr = new Array<string>(); m.forEach((v: BasePattern, key: any) => { let k: any = key.str; if (k === undefined) { k = key.num; } else { k = `"${k}"`; } if (k === undefined) { k = key.bool; } arr.push(`${k}: ${v.toString()}`); }); s = `{${arr.join(', ')}}`; break; } case LiteralPatternType.STRING: { s = `"${this.value}"`; break; } default: s = this.value.toString(); } return s.concat(this.hintsAsString()); } } export function isLiteralPattern(p: BasePattern): boolean { return p instanceof LiteralPattern; } export function isReferenceLiteral(p: LiteralPattern): boolean { return p.type == LiteralPatternType.REFERENCE; } export function referenceParts(p: LiteralPattern): string[] | undefined { if (isReferenceLiteral(p)) { const s: string = p.value as string; return s.split('.'); } return undefined; } export function isStringLiteral(p: LiteralPattern): boolean { return p.type == LiteralPatternType.STRING; } export function isNumberLiteral(p: LiteralPattern): boolean { return p.type == LiteralPatternType.NUMBER; } export function isBooleanLiteral(p: LiteralPattern): boolean { return p.type == LiteralPatternType.BOOLEAN; } export function isIdentifierLiteral(p: LiteralPattern): boolean { return p.type == LiteralPatternType.ID; } export function isArrayLiteral(p: LiteralPattern): boolean { return p.type == LiteralPatternType.ARRAY; } export function isMapLiteral(p: LiteralPattern): boolean { return p.type == LiteralPatternType.MAP; } export class FunctionCallPattern extends BasePattern { fnName: string; arguments: BasePattern[]; isAsync: boolean = false; constructor(fnName: string, args: BasePattern[]) { super(); this.fnName = fnName; this.arguments = args; } asAsync(): FunctionCallPattern { this.isAsync = true; return this; } override toString(): string { let s = ''; if (this.arguments.length > 0) { const args: Array<string> = []; this.arguments.forEach((bp: BasePattern) => { args.push(bp.toString()); }); s = `${this.fnName}(${args.join(', ')})`; } else { s = `${this.fnName}()`; } s = s.concat(this.hintsAsString()); if (this.isAsync) { return `await ${s}`; } else { return s; } } } export function isFunctionCallPattern(p: BasePattern): boolean { return p instanceof FunctionCallPattern; } export class ExpressionPattern extends BasePattern { expression: any; private static services: ReturnType<typeof createAgentlangServices> = createAgentlangServices(EmptyFileSystem); private static doParse = parseHelper<ModuleDefinition>(this.services.Agentlang); private static parse: ReturnType<typeof parseHelper<ModuleDefinition>> = (input: string) => this.doParse(input, { validation: true }); constructor(expression: any) { super(); this.expression = expression; } static async Validated(exprString: string): Promise<ExpressionPattern> { const result = await ExpressionPattern.parse( `module Temp workflow Test { if (${exprString}) {} }` ); if (result.parseResult.lexerErrors.length > 0) { throw new Error(result.parseResult.lexerErrors.join('\n')); } if (result.parseResult.parserErrors.length > 0) { throw new Error(result.parseResult.parserErrors.join('\n')); } return new ExpressionPattern(exprString); } override toString(): string { const s = this.expression.toString(); return s.concat(this.hintsAsString()); } } export function isExpressionPattern(p: BasePattern): boolean { return p instanceof ExpressionPattern; } export class GroupExpressionPattern extends BasePattern { expression: ExpressionPattern; constructor(expr: ExpressionPattern) { super(); this.expression = expr; } override toString(): string { return `(${this.expression.toString()})`; } } export function isGroupExpressionPattern(p: BasePattern): boolean { return p instanceof GroupExpressionPattern; } export class NegExpressionPattern extends BasePattern { expression: ExpressionPattern; constructor(expr: ExpressionPattern) { super(); this.expression = expr; } override toString(): string { return `-${this.expression.toString}`; } } export class NotExpressionPattern extends BasePattern { expression: ExpressionPattern; constructor(expr: ExpressionPattern) { super(); this.expression = expr; } override toString(): string { return `not(${this.expression.toString})`; } } export function isNegExpressionPattern(p: BasePattern): boolean { return p instanceof NegExpressionPattern; } export function isNotExpressionPattern(p: BasePattern): boolean { return p instanceof NotExpressionPattern; } export class ReferencePattern extends BasePattern { record: string; member: string; constructor(record: string, member: string) { super(); this.record = record; this.member = member; } override toString(): string { return `${this.record}.${this.member}`.concat(this.hintsAsString()); } } export function isReferencePattern(p: BasePattern): boolean { return p instanceof ReferencePattern; } export type AttributePattern = { name: string; op: string | undefined; value: BasePattern; }; export class CrudPattern extends BasePattern { recordName: string; attributes: Array<AttributePattern>; relationships: Map<string, CrudPattern[] | CrudPattern> | undefined; into: Map<string, string> | undefined; isQuery: boolean = false; isQueryUpdate: boolean = false; isCreate: boolean = false; constructor(recordName: string) { super(); this.recordName = recordName; this.attributes = []; if (recordName.endsWith('?')) { this.isQuery = true; } else { this.isCreate = true; } } addAttribute(n: string, p: BasePattern, op?: string): CrudPattern { this.attributes.push({ name: n, op: op, value: p }); if (this.recordName.endsWith('?')) { this.recordName = this.recordName.substring(0, this.recordName.length - 1); } this.flagType(); return this; } removeAttribute(n: string): CrudPattern { const idx: number = this.attributes.findIndex((ap: AttributePattern) => { return n == ap.name; }); if (idx >= 0) { this.attributes.splice(idx, 1); } this.flagType(); return this; } addInto(alias: string, attr: string): CrudPattern { if (this.into === undefined) { this.into = new Map(); } this.into.set(alias, attr); return this; } removeInto(alias: string): CrudPattern { if (this.into) { this.into.delete(alias); } return this; } resetInto(into?: Map<string, string>): CrudPattern { this.into = into; return this; } hasInto(): boolean { if (this.into && this.into.size > 0) { return true; } else { return false; } } private flagType() { let hasq = false; let hasc = false; for (let i = 0; i < this.attributes.length; ++i) { if (hasq && hasc) break; const ap = this.attributes[i]; hasq = ap.name.endsWith('?'); if (!hasc) hasc = !hasq; } if (hasq && hasc) { this.isQueryUpdate = true; this.isQuery = false; this.isCreate = false; } else if (hasc) { this.isCreate = true; this.isQuery = false; this.isQueryUpdate = false; } else { this.isQuery = hasq; this.isCreate = false; this.isQueryUpdate = false; } } addRelationship(n: string, p: CrudPattern[] | CrudPattern) { if (this.relationships === undefined) { this.relationships = new Map(); } this.relationships.set(n, p); return this; } removeRelationship(n: string) { if (this.relationships) { this.relationships.delete(n); } return this; } private attributesAsString(): string { const result: Array<string> = []; this.attributes.forEach((ap: AttributePattern) => { result.push(`${ap.name}${ap.op ? ap.op : ''} ${ap.value.toString()}`); }); const s = result.join(', '); return `{${s}}`; } private relationshipsAsString(): string | undefined { if (this.relationships !== undefined) { const result: Array<string> = []; this.relationships.forEach((p: CrudPattern | CrudPattern[], n: string) => { const ps = p instanceof Array ? `[${patternsToString(p, ',')}]` : p.toString(); result.push(`${n} ${ps}`); }); return result.join(','); } else { return undefined; } } getNormalizedRecordName(): string { return escapeQueryName(this.recordName); } private intoAsString(): string | undefined { if (this.into) { const ss = new Array<string>(); this.into.forEach((attr: string, alias: string) => { ss.push(`${alias} ${attr}`); }); return `@into { ${ss.join(',\n')} }`; } return undefined; } override toString(): string { let s = `{${this.recordName} ${this.attributesAsString()}`; const rs = this.relationshipsAsString(); if (rs) { s = s.concat(`,${rs}`); } const ins = this.intoAsString(); if (ins) { s = s.concat(`,${ins}`); } return s.concat('}', this.hintsAsString()); } } export function isCrudPattern(p: BasePattern): boolean { return p instanceof CrudPattern; } export function isCreatePattern(p: BasePattern): boolean { return isCrudPattern(p) && (p as CrudPattern).isCreate; } export function isQueryPattern(p: BasePattern): boolean { return isCrudPattern(p) && (p as CrudPattern).isQuery; } export function isQueryUpdatePattern(p: BasePattern): boolean { return isCrudPattern(p) && (p as CrudPattern).isQueryUpdate; } export class ForEachPattern extends BasePattern { variable: string; source: BasePattern; body: BasePattern[]; constructor(variable?: string, source?: BasePattern) { super(); this.variable = variable ? variable : 'X'; this.source = source ? source : LiteralPattern.EmptyArray; this.body = []; } addPattern(p: BasePattern): ForEachPattern { this.body.push(p); return this; } removePattern(index: number): ForEachPattern { this.body.splice(index, 1); return this; } setPatternAt(p: BasePattern, index: number): ForEachPattern { this.body[index] = p; return this; } removePatternAt(index: number): ForEachPattern { this.body.splice(index, 1); return this; } getPatternAt(index: number): BasePattern { return this.body[index]; } setVariable(s: string): ForEachPattern { this.variable = s; return this; } setSourcePattern(p: BasePattern): ForEachPattern { this.source = p; return this; } override toString(): string { if (this.source === undefined || this.variable === undefined) { throw new Error('`for` requires variable and source-pattern'); } let s = `for ${this.variable} in ${this.source.toString()}`; s = s.concat(`{${patternsToString(this.body)}}`); return s.concat(this.hintsAsString()); } } export function isForEachPattern(p: BasePattern): boolean { return p instanceof ForEachPattern; } export class IfPattern extends BasePattern { condition: BasePattern; body: BasePattern[]; elseBody: BasePattern[] | undefined; private static True = new LiteralPattern(LiteralPatternType.BOOLEAN, true); constructor(condition?: BasePattern) { super(); this.condition = condition ? condition : IfPattern.True; this.body = []; } isEmpty(): boolean { if (this.condition === IfPattern.True && this.body.length === 0) { return true; } else { return false; } } addPattern(p: BasePattern): IfPattern { this.body.push(p); return this; } removePattern(index: number): IfPattern { this.body.splice(index, 1); return this; } setPatternAt(p: BasePattern, index: number): IfPattern { this.body[index] = p; return this; } removePatternAt(index: number): IfPattern { this.body.splice(index, 1); return this; } getPatternAt(index: number): BasePattern { return this.body[index]; } setConditionPattern(p: BasePattern): IfPattern { this.condition = p; return this; } setElse(elseBody?: BasePattern[]): IfPattern { this.elseBody = elseBody ? elseBody : new Array<BasePattern>(); return this; } removeElse(): IfPattern { this.elseBody = undefined; return this; } override toString(): string { let s = `if(${this.condition.toString()}) {`; s = s.concat(patternsToString(this.body), '}'); if (this.elseBody) { if (this.elseBody.length == 1 && this.elseBody[0] instanceof IfPattern) { s = s.concat(` else ${this.elseBody[0].toString()}`); } else { s = s.concat(` else {${patternsToString(this.elseBody)}}`); } } return s.concat(this.hintsAsString()); } } export function isIfPattern(p: BasePattern): boolean { return p instanceof IfPattern; } export class CasePattern extends BasePattern { condition: BasePattern; body: BasePattern; constructor(condition: BasePattern, body: BasePattern) { super(); this.condition = condition; this.body = body; } static async FromString(s: string): Promise<CasePattern> { const ss = s.trimStart(); if (ss.startsWith('case')) { const m = await parseModule(`module T\ndecision D {\n${ss}}`); const d = m.defs[0]; if (isDecisionDefinition(d) && d.body) { const c = d.body.cases[0]; const b = await introspect(c.statements[0].$cstNode?.text || ''); return new CasePattern(new ExpressionPattern(c.cond), b[0]); } else { throw new Error(`Failed to parse ${s}`); } } throw new Error(`Not a case expression - ${s}`); } override toString(): string { return `case (${this.condition.toString()}) { ${this.body.toString()} }`; } } export function isCasePattern(p: BasePattern): boolean { return p instanceof CasePattern; } export function newCreatePattern(recName: string): CrudPattern { const cp: CrudPattern = new CrudPattern(recName); cp.isCreate = true; return cp; } export function newQueryPattern(recName: string, forQueryUpdate: boolean = false): CrudPattern { recName = recName.charAt(recName.length - 1) == '?' ? recName : recName + '?'; const cp: CrudPattern = new CrudPattern(recName); cp.isCreate = false; if (forQueryUpdate) { cp.isQueryUpdate = true; } else { cp.isQuery = true; } return cp; } export function newQueryUpdatePattern(recName: string): CrudPattern { return newQueryPattern(recName, true); } export class DeletePattern extends BasePattern { pattern: BasePattern; constructor(pattern: BasePattern) { super(); this.pattern = pattern; } override toString(): string { return `delete ${this.pattern.toString()}`.concat(this.hintsAsString()); } } export class ReturnPattern extends BasePattern { pattern: BasePattern; constructor(pattern: BasePattern) { super(); this.pattern = pattern; } override toString(): string { return `return ${this.pattern.toString()}`.concat(this.hintsAsString()); } } export class FullTextSearchPattern extends BasePattern { name: string; query: BasePattern; options: BasePattern | undefined; constructor(name: string, query: BasePattern, options?: BasePattern) { super(); this.name = name; this.query = query; this.options = options; } override toString(): string { const ops = this.options ? this.options.toString() : ''; return `{${this.name} ${this.query.toString()} ${ops}}`; } } export function isDeletePattern(p: BasePattern): boolean { return p instanceof DeletePattern; } export function newDeletePattern(recName: string): DeletePattern { const qp: CrudPattern = newQueryPattern(recName); return new DeletePattern(qp); } function patternsToString(body: BasePattern[], sep = ';\n'): string { return body .map((p: BasePattern) => { return p.toString(); }) .join(sep); } export class FlowStepPattern extends BasePattern { first: string; next: string; condition?: string; constructor(first: string, next: string, condition?: string) { super(); this.first = first; this.next = next; this.condition = condition ? trimQuotes(condition) : undefined; } static Parse(s: string): FlowStepPattern { const parts = s.trim().split(' '); const first = parts[0]; if (parts[1] == '-->') { if (parts.length == 3) { return new FlowStepPattern(first, parts[2]); } else { return new FlowStepPattern(first, parts[3], parts[2]); } } else { throw new Error(`Invalid flow-step format in ${s}`); } } override toString(): string { if (this.condition) { return `${this.first} --> "${this.condition}" ${this.next}`; } else { return `${this.first} --> ${this.next}`; } } }