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

711 lines (704 loc) 26.2 kB
import { escapeSpecialChars, isFqName, isString, makeCoreModuleName, makeFqName, nameToPath, sleepMilliseconds, splitFqName, } from '../util.js'; import { GlobalEnvironment, makeEventEvaluator, parseAndEvaluateStatement, } from '../interpreter.js'; import { asJSONSchema, fetchModule, getDecision, Instance, instanceToObject, isAgent, isInstanceOfType, isModule, makeInstance, newInstanceAttributes, Record, } from '../module.js'; import { provider } from '../agents/registry.js'; import { assistantMessage, humanMessage, systemMessage, } from '../agents/provider.js'; import { AIMessage, HumanMessage } from '@langchain/core/messages'; import { DecisionAgentInstructions, FlowExecInstructions, getAgentDirectives, getAgentGlossary, getAgentResponseSchema, getAgentScenarios, getAgentScratchNames, PlannerInstructions, } from '../agents/common.js'; import { PathAttributeNameQuery } from '../defs.js'; import { logger } from '../logger.js'; import Handlebars from 'handlebars'; import { isMonitoringEnabled } from '../state.js'; export const CoreAIModuleName = makeCoreModuleName('ai'); export const AgentEntityName = 'Agent'; export const LlmEntityName = 'LLM'; export default `module ${CoreAIModuleName} entity ${LlmEntityName} { name String @id, service String @default("openai"), config Map @optional } entity ${AgentEntityName} { name String @id, moduleName String @default("${CoreAIModuleName}"), type @enum("chat", "planner", "flow-exec") @default("chat"), runWorkflows Boolean @default(true), instruction String @optional, tools String @optional, // comma-separated list of tool names documents String @optional, // comma-separated list of document names channels String @optional, // comma-separated list of channel names role String @optional, flows String @optional, validate String @optional, retry String @optional, llm String } entity agentChatSession { id String @id, messages String } workflow findAgentChatSession { {agentChatSession {id? findAgentChatSession.id}} @as [sess]; sess } workflow saveAgentChatSession { {agentChatSession {id saveAgentChatSession.id, messages saveAgentChatSession.messages}, @upsert} } entity Document { title String @id, content String, @meta {"fullTextSearch": "*"} } event doc { title String, url String } `; export const AgentFqName = makeFqName(CoreAIModuleName, AgentEntityName); const ProviderDb = new Map(); export class AgentInstance { constructor() { this.llm = ''; this.name = ''; this.moduleName = CoreAIModuleName; this.instruction = ''; this.type = 'chat'; this.runWorkflows = true; this.toolsArray = undefined; this.hasModuleTools = false; this.withSession = true; this.decisionExecutor = false; this.addContext = false; } static FromInstance(agentInstance) { const agent = instanceToObject(agentInstance, new AgentInstance()); let finalTools = undefined; if (agent.tools) finalTools = agent.tools; if (agent.channels) { if (finalTools) { finalTools = `${finalTools},${agent.channels}`; } else { finalTools = agent.channels; } } if (finalTools) { agent.toolsArray = finalTools.split(','); } if (agent.toolsArray) { for (let i = 0; i < agent.toolsArray.length; ++i) { const n = agent.toolsArray[i]; if (isFqName(n)) { const parts = nameToPath(n); agent.hasModuleTools = isModule(parts.getModuleName()); } else { agent.hasModuleTools = isModule(n); } if (agent.hasModuleTools) break; } } if (agent.retry) { let n = agent.retry; if (!isFqName(n)) { n = `${agent.moduleName}/${n}`; } const parts = splitFqName(n); const m = fetchModule(parts[0]); agent.retryObj = m.getRetry(parts[1]); } return agent; } static FromFlowStep(step, flowAgent, context) { const desc = getDecision(step, flowAgent.moduleName); if (desc) { return AgentInstance.FromDecision(desc, flowAgent, context); } const fqs = isFqName(step) ? step : `${flowAgent.moduleName}/${step}`; const isagent = isAgent(fqs); const i0 = `Analyse the context and generate the pattern required to invoke ${fqs}. Never include references in the pattern. All attribute values must be literals derived from the context.`; const instruction = isagent ? `${i0} ${fqs} is an agent, so generate the message as a text instruction, if possible.` : i0; const inst = makeInstance(CoreAIModuleName, AgentEntityName, newInstanceAttributes() .set('llm', flowAgent.llm) .set('name', `${step}_agent`) .set('moduleName', flowAgent.moduleName) .set('instruction', instruction) .set('tools', fqs) .set('type', 'planner')); return AgentInstance.FromInstance(inst).disableSession(); } static FromDecision(desc, flowAgent, context) { const instruction = `${DecisionAgentInstructions}\n${context}\n\n${desc.joinedCases()}`; const inst = makeInstance(CoreAIModuleName, AgentEntityName, newInstanceAttributes() .set('llm', flowAgent.llm) .set('name', `${desc.name}_agent`) .set('moduleName', flowAgent.moduleName) .set('instruction', instruction)); return AgentInstance.FromInstance(inst).disableSession().markAsDecisionExecutor(); } swapInstruction(newIns) { const s = this.instruction; this.instruction = newIns; return s; } disableSession() { this.withSession = false; return this; } enableSession() { this.withSession = true; return this; } hasSession() { return this.withSession; } isPlanner() { return this.hasModuleTools || this.type == 'planner'; } isFlowExecutor() { return this.type == 'flow-exec'; } markAsDecisionExecutor() { this.decisionExecutor = true; return this; } isDecisionExecutor() { return this.decisionExecutor; } directivesAsString(fqName) { const conds = getAgentDirectives(fqName); if (conds) { const ss = new Array(); ss.push('\nUse the following guidelines to take more accurate decisions in relevant scenarios.\n'); conds.forEach((ac) => { if (ac.ifPattern) { ss.push(ac.if); } else { ss.push(`if ${ac.if}, then ${ac.then}`); } }); return `${ss.join('\n')}\n`; } return ''; } getFullInstructions(env) { const fqName = this.getFqName(); const ins = this.role ? `${this.role}\n${this.instruction || ''}` : this.instruction || ''; let finalInstruction = `${ins} ${this.directivesAsString(fqName)}`; const gls = getAgentGlossary(fqName); if (gls) { const glss = new Array(); gls.forEach((age) => { glss.push(`${age.name}: ${age.meaning}. ${age.synonyms ? `These words are synonyms for ${age.name}: ${age.synonyms}` : ''}`); }); finalInstruction = `${finalInstruction}\nThe following glossary will be helpful for understanding user requests. ${glss.join('\n')}\n`; } const scenarios = getAgentScenarios(fqName); if (scenarios) { const scs = new Array(); scenarios.forEach((sc) => { try { const aiResp = processScenarioResponse(sc.ai); scs.push(`User: ${sc.user}\nAI: ${aiResp}\n`); } catch (error) { logger.error(`Unable to process scenario ${fqName}: ${error.message}`); } }); finalInstruction = `${finalInstruction}\nHere are some example user requests and the corresponding responses you are supposed to produce:\n${scs.join('\n')}`; } const responseSchema = getAgentResponseSchema(fqName); if (responseSchema) { finalInstruction = `${finalInstruction}\nReturn your response in the following JSON schema:\n${asJSONSchema(responseSchema)} Only return a pure JSON object with no extra text, annotations etc.`; } const spad = env.getScratchPad(); if (spad !== undefined) { if (finalInstruction.indexOf('{{') > 0) { return AgentInstance.maybeRewriteTemplatePatterns(spad, finalInstruction, env); } else { const ctx = JSON.stringify(spad); return `${finalInstruction}\nSome additional context:\n${ctx}`; } } else { this.addContext = true; return finalInstruction; } } static maybeRewriteTemplatePatterns(scratchPad, instruction, env) { const templ = Handlebars.compile(env.rewriteTemplateMappings(instruction)); return templ(scratchPad); } maybeValidateJsonResponse(response) { if (response) { const responseSchema = getAgentResponseSchema(this.getFqName()); if (responseSchema) { const attrs = JSON.parse(trimGeneratedCode(response)); const parts = nameToPath(responseSchema); const moduleName = parts.getModuleName(); const entryName = parts.getEntryName(); const attrsMap = new Map(Object.entries(attrs)); const scm = fetchModule(moduleName).getRecord(entryName).schema; const recAttrs = new Map(); attrsMap.forEach((v, k) => { if (scm.has(k)) { recAttrs.set(k, v); } }); makeInstance(moduleName, entryName, recAttrs); return attrs; } } return undefined; } getFqName() { if (this.fqName === undefined) { this.fqName = makeFqName(this.moduleName, this.name); } return this.fqName; } markAsFlowExecutor() { this.type = 'flow-exec'; return this; } getScratchNames() { return getAgentScratchNames(this.getFqName()); } maybeAddScratchData(env) { const obj = env.getLastResult(); if (obj === null || obj === undefined) return this; let r = undefined; if (Instance.IsInstance(obj) || (obj instanceof Array && obj.length > 0 && Instance.IsInstance(obj[0]))) { r = obj; } else { env.addToScratchPad(this.name, obj); return this; } const scratchNames = this.getScratchNames(); let data = undefined; let n = ''; if (r instanceof Array) { data = r.map((inst) => { return extractScratchData(scratchNames, inst); }); n = r[0].getFqName(); } else { const i = r; data = extractScratchData(scratchNames, i); n = i.getFqName(); } if (data) env.addToScratchPad(n, data); return this; } async invoke(message, env) { const p = await findProviderForLLM(this.llm, env); const agentName = this.name; const chatId = env.getAgentChatId() || agentName; let isplnr = this.isPlanner(); const isflow = !isplnr && this.isFlowExecutor(); if (isplnr && this.withSession) { this.withSession = false; } if (isflow) { this.withSession = false; } if (this.withSession && env.getFlowContext()) { this.withSession = false; } if (!this.withSession && env.isAgentModeSet()) { this.withSession = true; if (env.isInAgentChatMode()) { isplnr = false; } } const monitoringEnabled = isMonitoringEnabled(); const sess = this.withSession ? await findAgentChatSession(chatId, env) : null; let msgs; let cachedMsg = undefined; if (sess) { msgs = sess.lookup('messages'); } else { cachedMsg = this.getFullInstructions(env); msgs = [systemMessage(cachedMsg || '')]; } if (msgs) { try { const sysMsg = msgs[0]; if (isplnr || isflow) { const s = isplnr ? PlannerInstructions : FlowExecInstructions; const ts = this.toolsAsString(); const msg = `${s}\n${ts}\n${cachedMsg || this.getFullInstructions(env)}`; const newSysMsg = systemMessage(msg); msgs[0] = newSysMsg; } const hmsg = await this.maybeAddRelevantDocuments(this.maybeAddFlowContext(message, env), env); if (hmsg.length > 0) { msgs.push(humanMessage(hmsg)); } const externalToolSpecs = this.getExternalToolSpecs(); const msgsContent = msgs //.slice(1) .map((bm) => { return bm.content; }) .join('\n'); if (monitoringEnabled) { env.setMonitorEntryLlmPrompt(msgsContent); if (this.isPlanner()) { env.flagMonitorEntryAsPlanner(); } if (this.isFlowExecutor()) { env.flagMonitorEntryAsFlowStep(); } if (this.isDecisionExecutor()) { env.flagMonitorEntryAsDecision(); } } logger.debug(`Invoking LLM ${this.llm} via agent ${this.fqName} with messages:\n${msgsContent}`); let response = await p.invoke(msgs, externalToolSpecs); const v = this.getValidationEvent(); if (v) { response = await this.handleValidation(response, v, msgs, p); } msgs.push(assistantMessage(response.content)); if (isplnr) { msgs[0] = sysMsg; } if (this.withSession) { await saveAgentChatSession(chatId, msgs, env); } if (monitoringEnabled) env.setMonitorEntryLlmResponse(response.content); env.setLastResult(response.content); } catch (err) { logger.error(`Error while invoking ${agentName} - ${err}`); if (monitoringEnabled) env.setMonitorEntryError(`${err}`); env.setLastResult(undefined); } } else { throw new Error(`failed to initialize messages for agent ${agentName}`); } } maybeAddFlowContext(message, env) { if (this.addContext) { this.addContext = false; const fctx = env.getFlowContext(); if (fctx) { return `${message}\nContext: ${fctx}`; } return message; } return message; } async invokeValidator(response, validationEventName) { let isstr = true; const content = trimGeneratedCode(response.content); try { const c = JSON.parse(content); isstr = isString(c); } catch (reason) { logger.debug(`invokeValidator json/parse - ${reason}`); } const d = isstr ? `"${escapeSpecialChars(content)}"` : content; const r = await parseAndEvaluateStatement(`{${validationEventName} {data ${d}}}`); if (r instanceof Array) { const i = r.find((inst) => { return isInstanceOfType(inst, 'agentlang/ValidationResult'); }); if (i) { return i; } else { throw new Error('Validation failed to produce result'); } } else { if (!isInstanceOfType(r, 'agentlang/ValidationResult')) { throw new Error('Invalid validation result'); } return r; } } async handleValidation(response, validationEventName, msgs, provider) { let r = await this.invokeValidator(response, validationEventName); const status = r.lookup('status'); if (status === 'ok') { return response; } else { if (this.retryObj) { let resp = response; let attempt = 0; let delay = this.retryObj.getNextDelayMs(attempt); while (delay) { msgs.push(assistantMessage(resp.content)); const vs = JSON.stringify(r.asSerializableObject()); msgs.push(humanMessage(`Validation for your last response failed with this result: \n${vs}\n\nFix the errors.`)); await sleepMilliseconds(delay); resp = await provider.invoke(msgs, undefined); r = await this.invokeValidator(resp, validationEventName); if (r.lookup('status') === 'ok') { return resp; } delay = this.retryObj.getNextDelayMs(++attempt); } throw new Error(`Agent ${this.name} failed to generate a valid response after ${attempt} attempts`); } else { return response; } } } getValidationEvent() { if (this.validate) { if (isFqName(this.validate)) { return this.validate; } else { return `${this.moduleName}/${this.validate}`; } } return undefined; } getExternalToolSpecs() { let result = undefined; if (this.toolsArray) { this.toolsArray.forEach((n) => { const v = GlobalEnvironment.lookup(n); if (v) { if (result === undefined) { result = new Array(); } result.push(v); } }); } return result; } async maybeAddRelevantDocuments(message, env) { if (this.documents && this.documents.length > 0) { const s = `${message}. Relevant documents are: ${this.documents}`; const result = await parseHelper(`{${CoreAIModuleName}/Document? "${s}"}`, env); if (result && result.length > 0) { const docs = []; for (let i = 0; i < result.length; ++i) { const v = result[i]; const r = await parseHelper(`{${CoreAIModuleName}/Document {${PathAttributeNameQuery} "${v.id}"}}`, env); if (r && r.length > 0) { docs.push(r[0]); } } if (docs.length > 0) { message = message.concat('\nUse the additional information given below:\n').concat(docs .map((v) => { return v.lookup('content'); }) .join('\n')); } } } return message; } toolsAsString() { const cachedTools = AgentInstance.ToolsCache.get(this.name); if (cachedTools) { return cachedTools; } if (this.toolsArray) { const tooldefs = new Array(); const slimModules = new Map(); this.toolsArray.forEach((n) => { let moduleName; let entryName; if (isFqName(n)) { const parts = nameToPath(n); moduleName = parts.getModuleName(); entryName = parts.getEntryName(); } else { moduleName = n; } if (isModule(moduleName)) { const m = fetchModule(moduleName); if (entryName) { const hasmod = slimModules.has(moduleName); const defs = hasmod ? slimModules.get(moduleName) : new Array(); const entry = m.getEntry(entryName); const s = entry instanceof Record ? entry.toString_(true) : entry.toString(); defs === null || defs === void 0 ? void 0 : defs.push(s); if (!hasmod && defs) { slimModules.set(moduleName, defs); } } else { tooldefs.push(fetchModule(moduleName).toString()); } } }); slimModules.forEach((defs, modName) => { tooldefs.push(`module ${modName}\n${defs.join('\n')}`); }); const agentTools = tooldefs.join('\n'); AgentInstance.ToolsCache.set(this.name, agentTools); return agentTools; } else { return ''; } } } AgentInstance.ToolsCache = new Map(); function extractScratchData(scratchNames, inst) { const data = {}; inst.attributes.forEach((v, k) => { if (scratchNames) { if (scratchNames.has(k)) { data[k] = v; } } else { data[k] = v; } }); return data; } async function parseHelper(stmt, env) { await parseAndEvaluateStatement(stmt, undefined, env); return env.getLastResult(); } export async function findAgentByName(name, env) { const result = await parseHelper(`{${AgentFqName} {name? "${name}"}}`, env); if (result instanceof Array && result.length > 0) { const agentInstance = result[0]; return AgentInstance.FromInstance(agentInstance); } else { throw new Error(`Failed to find agent ${name}`); } } export async function findProviderForLLM(llmName, env) { let p = ProviderDb.get(llmName); if (p === undefined) { const result = await parseAndEvaluateStatement(`{${CoreAIModuleName}/${LlmEntityName} {name? "${llmName}"}}`, undefined, env); if (result.length > 0) { const llm = result[0]; const service = llm.lookup('service'); const pclass = provider(service); const configValue = llm.lookup('config'); const providerConfig = configValue ? configValue instanceof Map ? configValue : new Map(Object.entries(configValue)) : new Map().set('service', service); p = new pclass(providerConfig); if (p) ProviderDb.set(llmName, p); } } if (p) { return p; } else { throw new Error(`Failed to load provider for ${llmName}`); } } const evalEvent = makeEventEvaluator(CoreAIModuleName); function asBaseMessages(gms) { return gms.map((gm) => { switch (gm.role) { case 'user': { return humanMessage(gm.content); } case 'assistant': { return assistantMessage(gm.content); } default: { return systemMessage(gm.content); } } }); } function asGenericMessages(bms) { return bms.map((bm) => { if (bm instanceof HumanMessage) { return { role: 'user', content: bm.text }; } else if (bm instanceof AIMessage) { return { role: 'assistant', content: bm.text }; } else { return { role: 'system', content: bm.text }; } }); } export async function findAgentChatSession(chatId, env) { const result = await evalEvent('findAgentChatSession', { id: chatId }, env); if (result) { result.attributes.set('messages', asBaseMessages(JSON.parse(result.lookup('messages')))); } return result; } export async function saveAgentChatSession(chatId, messages, env) { await evalEvent('saveAgentChatSession', { id: chatId, messages: JSON.stringify(asGenericMessages(messages)) }, env); } export function agentName(agentInstance) { return agentInstance.lookup('name'); } function processScenarioResponse(resp) { const r = resp.trimStart(); if (r.startsWith('[') || r.startsWith('{')) { return resp; } if (isFqName(r)) { const parts = splitFqName(r); const m = fetchModule(parts[0]); const wf = m.getWorkflowForEvent(parts[1]); if (wf) { const ss = wf.statements.map((stmt) => { var _a; return (_a = stmt.$cstNode) === null || _a === void 0 ? void 0 : _a.text; }); return `[${ss.join(';\n')}]`; } else { return resp; } } return resp; } export function trimGeneratedCode(code) { if (code !== undefined) { let s = code.trim(); if (s.startsWith('```')) { const idx = s.indexOf('\n'); s = s.substring(idx).trimStart(); } if (s.endsWith('```')) { s = s.substring(0, s.length - 3); } return s; } else { return ''; } } //# sourceMappingURL=ai.js.map