393 lines
16 KiB
JavaScript
393 lines
16 KiB
JavaScript
import { PromptTemplate } from "@langchain/core/prompts";
|
|
import { LLMChain } from "../../chains/llm_chain.js";
|
|
import { BaseChain } from "../../chains/base.js";
|
|
/**
|
|
* Implementation of a generative agent that can learn and form new memories over
|
|
* time. It extends the BaseChain class, which is a generic
|
|
* sequence of calls to components, including other chains.
|
|
* @example
|
|
* ```typescript
|
|
* const tommie: GenerativeAgent = new GenerativeAgent(
|
|
* new OpenAI({ temperature: 0.9, maxTokens: 1500 }),
|
|
* new GenerativeAgentMemory(
|
|
* new ChatOpenAI(),
|
|
* new TimeWeightedVectorStoreRetriever({
|
|
* vectorStore: new MemoryVectorStore(new OpenAIEmbeddings()),
|
|
* otherScoreKeys: ["importance"],
|
|
* k: 15,
|
|
* }),
|
|
* { reflectionThreshold: 8 },
|
|
* ),
|
|
* {
|
|
* name: "Tommie",
|
|
* age: 25,
|
|
* traits: "anxious, likes design, talkative",
|
|
* status: "looking for a job",
|
|
* },
|
|
* );
|
|
*
|
|
* await tommie.addMemory(
|
|
* "Tommie remembers his dog, Bruno, from when he was a kid",
|
|
* new Date(),
|
|
* );
|
|
* const summary = await tommie.getSummary({ forceRefresh: true });
|
|
* const response = await tommie.generateDialogueResponse(
|
|
* "USER says Hello Tommie, how are you today?",
|
|
* );
|
|
* ```
|
|
*/
|
|
export class GenerativeAgent extends BaseChain {
|
|
static lc_name() {
|
|
return "GenerativeAgent";
|
|
}
|
|
// TODO: Add support for daily summaries
|
|
// private dailySummaries: string[] = []; // summary of the events in the plan that the agent took.
|
|
_chainType() {
|
|
return "generative_agent_executor";
|
|
}
|
|
get inputKeys() {
|
|
return ["observation", "suffix", "now"];
|
|
}
|
|
get outputKeys() {
|
|
return ["output", "continue_dialogue"];
|
|
}
|
|
constructor(llm, longTermMemory, config) {
|
|
super();
|
|
// a character with memory and innate characterisitics
|
|
Object.defineProperty(this, "name", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: void 0
|
|
}); // the character's name
|
|
Object.defineProperty(this, "age", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: void 0
|
|
}); // the optional age of the character
|
|
Object.defineProperty(this, "traits", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: void 0
|
|
}); // permanent traits to ascribe to the character
|
|
Object.defineProperty(this, "status", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: void 0
|
|
}); // the traits of the character you wish not to change
|
|
Object.defineProperty(this, "longTermMemory", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: void 0
|
|
});
|
|
Object.defineProperty(this, "llm", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: void 0
|
|
}); // the underlying language model
|
|
Object.defineProperty(this, "verbose", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: void 0
|
|
}); // false
|
|
Object.defineProperty(this, "summary", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: void 0
|
|
}); // stateful self-summary generated via reflection on the character's memory.
|
|
Object.defineProperty(this, "summaryRefreshSeconds", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: 3600
|
|
});
|
|
Object.defineProperty(this, "lastRefreshed", {
|
|
enumerable: true,
|
|
configurable: true,
|
|
writable: true,
|
|
value: void 0
|
|
}); // the last time the character's summary was regenerated
|
|
this.llm = llm;
|
|
this.longTermMemory = longTermMemory;
|
|
this.name = config.name;
|
|
this.age = config.age;
|
|
this.traits = config.traits;
|
|
this.status = config.status;
|
|
this.verbose = config.verbose ?? this.verbose;
|
|
this.summary = "";
|
|
this.summaryRefreshSeconds =
|
|
config.summaryRefreshSeconds ?? this.summaryRefreshSeconds;
|
|
this.lastRefreshed = new Date();
|
|
// this.dailySummaries = config.dailySummaries ?? this.dailySummaries;
|
|
}
|
|
// LLM methods
|
|
/**
|
|
* Parses a newline-separated string into a list of strings.
|
|
* @param text The string to parse.
|
|
* @returns An array of strings parsed from the input text.
|
|
*/
|
|
parseList(text) {
|
|
// parse a newline-seperated string into a list of strings
|
|
const lines = text.trim().split("\n");
|
|
const result = lines.map((line) => line.replace(/^\s*\d+\.\s*/, "").trim());
|
|
return result;
|
|
}
|
|
/**
|
|
* Creates a new LLMChain with the given prompt and the agent's language
|
|
* model, verbosity, output key, and memory.
|
|
* @param prompt The prompt to use for the LLMChain.
|
|
* @returns A new LLMChain instance.
|
|
*/
|
|
chain(prompt) {
|
|
const chain = new LLMChain({
|
|
llm: this.llm,
|
|
prompt,
|
|
verbose: this.verbose,
|
|
outputKey: "output",
|
|
memory: this.longTermMemory,
|
|
});
|
|
return chain;
|
|
}
|
|
/**
|
|
* Extracts the observed entity from the given observation.
|
|
* @param observation The observation to extract the entity from.
|
|
* @param runManager Optional CallbackManagerForChainRun instance.
|
|
* @returns The extracted entity as a string.
|
|
*/
|
|
async getEntityFromObservations(observation, runManager) {
|
|
const prompt = PromptTemplate.fromTemplate("What is the observed entity in the following observation? {observation}" +
|
|
"\nEntity=");
|
|
const result = await this.chain(prompt).call({
|
|
observation,
|
|
}, runManager?.getChild("entity_extractor"));
|
|
return result.output;
|
|
}
|
|
/**
|
|
* Extracts the action of the given entity from the given observation.
|
|
* @param observation The observation to extract the action from.
|
|
* @param entityName The name of the entity to extract the action for.
|
|
* @param runManager Optional CallbackManagerForChainRun instance.
|
|
* @returns The extracted action as a string.
|
|
*/
|
|
async getEntityAction(observation, entityName, runManager) {
|
|
const prompt = PromptTemplate.fromTemplate("What is the {entity} doing in the following observation? {observation}" +
|
|
"\nThe {entity} is");
|
|
const result = await this.chain(prompt).call({
|
|
entity: entityName,
|
|
observation,
|
|
}, runManager?.getChild("entity_action_extractor"));
|
|
const trimmedResult = result.output.trim();
|
|
return trimmedResult;
|
|
}
|
|
/**
|
|
* Summarizes memories that are most relevant to an observation.
|
|
* @param observation The observation to summarize related memories for.
|
|
* @param runManager Optional CallbackManagerForChainRun instance.
|
|
* @returns The summarized memories as a string.
|
|
*/
|
|
async summarizeRelatedMemories(observation, runManager) {
|
|
// summarize memories that are most relevant to an observation
|
|
const prompt = PromptTemplate.fromTemplate(`
|
|
{q1}?
|
|
Context from memory:
|
|
{relevant_memories}
|
|
Relevant context:`);
|
|
const entityName = await this.getEntityFromObservations(observation, runManager);
|
|
const entityAction = await this.getEntityAction(observation, entityName, runManager);
|
|
const q1 = `What is the relationship between ${this.name} and ${entityName}`;
|
|
const q2 = `${entityName} is ${entityAction}`;
|
|
const response = await this.chain(prompt).call({
|
|
q1,
|
|
queries: [q1, q2],
|
|
}, runManager?.getChild("entity_relationships"));
|
|
return response.output.trim(); // added output
|
|
}
|
|
async _call(values, runManager) {
|
|
const { observation, suffix, now } = values;
|
|
// react to a given observation or dialogue act
|
|
const prompt = PromptTemplate.fromTemplate(`{agent_summary_description}` +
|
|
`\nIt is {current_time}.` +
|
|
`\n{agent_name}'s status: {agent_status}` +
|
|
`\nSummary of relevant context from {agent_name}'s memory:` +
|
|
"\n{relevant_memories}" +
|
|
`\nMost recent observations: {most_recent_memories}` +
|
|
`\nObservation: {observation}` +
|
|
`\n\n${suffix}`);
|
|
const agentSummaryDescription = await this.getSummary({}, runManager); // now = now in param
|
|
const relevantMemoriesStr = await this.summarizeRelatedMemories(observation, runManager);
|
|
const currentTime = (now || new Date()).toLocaleString("en-US", {
|
|
month: "long",
|
|
day: "numeric",
|
|
year: "numeric",
|
|
hour: "numeric",
|
|
minute: "numeric",
|
|
hour12: true,
|
|
});
|
|
const chainInputs = {
|
|
agent_summary_description: agentSummaryDescription,
|
|
current_time: currentTime,
|
|
agent_name: this.name,
|
|
observation,
|
|
agent_status: this.status,
|
|
most_recent_memories: "",
|
|
};
|
|
chainInputs[this.longTermMemory.getRelevantMemoriesKey()] =
|
|
relevantMemoriesStr;
|
|
const consumedTokens = await this.llm.getNumTokens(await prompt.format({ ...chainInputs }));
|
|
chainInputs[this.longTermMemory.getMostRecentMemoriesTokenKey()] =
|
|
consumedTokens;
|
|
const response = await this.chain(prompt).call(chainInputs, runManager?.getChild("reaction_from_summary"));
|
|
const rawOutput = response.output;
|
|
let output = rawOutput;
|
|
let continue_dialogue = false;
|
|
if (rawOutput.includes("REACT:")) {
|
|
const reaction = this._cleanResponse(rawOutput.split("REACT:").pop());
|
|
await this.addMemory(`${this.name} observed ${observation} and reacted by ${reaction}`, now, {}, runManager?.getChild("memory"));
|
|
output = `${reaction}`;
|
|
continue_dialogue = false;
|
|
}
|
|
else if (rawOutput.includes("SAY:")) {
|
|
const saidValue = this._cleanResponse(rawOutput.split("SAY:").pop());
|
|
await this.addMemory(`${this.name} observed ${observation} and said ${saidValue}`, now, {}, runManager?.getChild("memory"));
|
|
output = `${this.name} said ${saidValue}`;
|
|
continue_dialogue = true;
|
|
}
|
|
else if (rawOutput.includes("GOODBYE:")) {
|
|
const farewell = this._cleanResponse(rawOutput.split("GOODBYE:").pop() ?? "");
|
|
await this.addMemory(`${this.name} observed ${observation} and said ${farewell}`, now, {}, runManager?.getChild("memory"));
|
|
output = `${this.name} said ${farewell}`;
|
|
continue_dialogue = false;
|
|
}
|
|
return { output, continue_dialogue };
|
|
}
|
|
_cleanResponse(text) {
|
|
if (text === undefined) {
|
|
return "";
|
|
}
|
|
const regex = new RegExp(`^${this.name} `);
|
|
return text.replace(regex, "").trim();
|
|
}
|
|
/**
|
|
* Generates a reaction to the given observation.
|
|
* @param observation The observation to generate a reaction for.
|
|
* @param now Optional current date.
|
|
* @returns A boolean indicating whether to continue the dialogue and the output string.
|
|
*/
|
|
async generateReaction(observation, now) {
|
|
const callToActionTemplate = `Should {agent_name} react to the observation, and if so,` +
|
|
` what would be an appropriate reaction? Respond in one line.` +
|
|
` If the action is to engage in dialogue, write:\nSAY: "what to say"` +
|
|
` \notherwise, write:\nREACT: {agent_name}'s reaction (if anything).` +
|
|
` \nEither do nothing, react, or say something but not both.\n\n`;
|
|
const { output, continue_dialogue } = await this.call({
|
|
observation,
|
|
suffix: callToActionTemplate,
|
|
now,
|
|
});
|
|
return [continue_dialogue, output];
|
|
}
|
|
/**
|
|
* Generates a dialogue response to the given observation.
|
|
* @param observation The observation to generate a dialogue response for.
|
|
* @param now Optional current date.
|
|
* @returns A boolean indicating whether to continue the dialogue and the output string.
|
|
*/
|
|
async generateDialogueResponse(observation, now) {
|
|
const callToActionTemplate = `What would ${this.name} say? To end the conversation, write: GOODBYE: "what to say". Otherwise to continue the conversation, write: SAY: "what to say next"\n\n`;
|
|
const { output, continue_dialogue } = await this.call({
|
|
observation,
|
|
suffix: callToActionTemplate,
|
|
now,
|
|
});
|
|
return [continue_dialogue, output];
|
|
}
|
|
// Agent stateful' summary methods
|
|
// Each dialog or response prompt includes a header
|
|
// summarizing the agent's self-description. This is
|
|
// updated periodically through probing it's memories
|
|
/**
|
|
* Gets the agent's summary, which includes the agent's name, age, traits,
|
|
* and a summary of the agent's core characteristics. The summary is
|
|
* updated periodically through probing the agent's memories.
|
|
* @param config Optional configuration object with current date and a boolean to force refresh.
|
|
* @param runManager Optional CallbackManagerForChainRun instance.
|
|
* @returns The agent's summary as a string.
|
|
*/
|
|
async getSummary(config, runManager) {
|
|
const { now = new Date(), forceRefresh = false } = config ?? {};
|
|
const sinceRefresh = Math.floor((now.getTime() - this.lastRefreshed.getTime()) / 1000);
|
|
if (!this.summary ||
|
|
sinceRefresh >= this.summaryRefreshSeconds ||
|
|
forceRefresh) {
|
|
this.summary = await this.computeAgentSummary(runManager);
|
|
this.lastRefreshed = now;
|
|
}
|
|
let age;
|
|
if (this.age) {
|
|
age = this.age;
|
|
}
|
|
else {
|
|
age = "N/A";
|
|
}
|
|
return `Name: ${this.name} (age: ${age})
|
|
Innate traits: ${this.traits}
|
|
${this.summary}`;
|
|
}
|
|
/**
|
|
* Computes the agent's summary by summarizing the agent's core
|
|
* characteristics given the agent's relevant memories.
|
|
* @param runManager Optional CallbackManagerForChainRun instance.
|
|
* @returns The computed summary as a string.
|
|
*/
|
|
async computeAgentSummary(runManager) {
|
|
const prompt = PromptTemplate.fromTemplate("How would you summarize {name}'s core characteristics given the following statements:\n" +
|
|
"----------" +
|
|
"{relevant_memories}" +
|
|
"----------" +
|
|
"Do not embellish." +
|
|
"\n\nSummary: ");
|
|
// the agent seeks to think about their core characterisitics
|
|
const result = await this.chain(prompt).call({
|
|
name: this.name,
|
|
queries: [`${this.name}'s core characteristics`],
|
|
}, runManager?.getChild("compute_agent_summary"));
|
|
return result.output.trim();
|
|
}
|
|
/**
|
|
* Returns a full header of the agent's status, summary, and current time.
|
|
* @param config Optional configuration object with current date and a boolean to force refresh.
|
|
* @returns The full header as a string.
|
|
*/
|
|
getFullHeader(config = {}) {
|
|
const { now = new Date(), forceRefresh = false } = config;
|
|
// return a full header of the agent's status, summary, and current time.
|
|
const summary = this.getSummary({ now, forceRefresh });
|
|
const currentTimeString = now.toLocaleString("en-US", {
|
|
month: "long",
|
|
day: "numeric",
|
|
year: "numeric",
|
|
hour: "numeric",
|
|
minute: "numeric",
|
|
hour12: true,
|
|
});
|
|
return `${summary}\nIt is ${currentTimeString}.\n${this.name}'s status: ${this.status}`;
|
|
}
|
|
/**
|
|
* Adds a memory to the agent's long-term memory.
|
|
* @param memoryContent The content of the memory to add.
|
|
* @param now Optional current date.
|
|
* @param metadata Optional metadata for the memory.
|
|
* @param callbacks Optional Callbacks instance.
|
|
* @returns The result of adding the memory to the agent's long-term memory.
|
|
*/
|
|
async addMemory(memoryContent, now, metadata, callbacks) {
|
|
return this.longTermMemory.addMemory(memoryContent, now, metadata, callbacks);
|
|
}
|
|
}
|