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
JavaScript
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 ,
service String ,
config Map
}
entity ${AgentEntityName} {
name String ,
moduleName String ,
type ,
runWorkflows Boolean ,
instruction String ,
tools String , // comma-separated list of tool names
documents String , // comma-separated list of document names
channels String , // comma-separated list of channel names
role String ,
flows String ,
validate String ,
retry String ,
llm String
}
entity agentChatSession {
id String ,
messages String
}
workflow findAgentChatSession {
{agentChatSession {id? findAgentChatSession.id}} [sess];
sess
}
workflow saveAgentChatSession {
{agentChatSession {id saveAgentChatSession.id, messages saveAgentChatSession.messages}, }
}
entity Document {
title String ,
content String,
{"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