langchain/docs/use_cases/agents/characters.ipynb
vowelparrot 99c0382209
Generative Characters (#2859)
Add a time-weighted memory retriever and a notebook that approximates a
Generative Agent from https://arxiv.org/pdf/2304.03442.pdf


The "daily plan" components are removed for now since they are less
useful without a virtual world, but the memory is an interesting
component to build off.

---------

Co-authored-by: Harrison Chase <hw.chase.17@gmail.com>
2023-04-16 21:41:00 -07:00

1251 lines
51 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"id": "e9732067-71c7-46f7-ad09-381b3bf21a27",
"metadata": {},
"source": [
"# Generative Agents in LangChain\n",
"\n",
"This notebook implements a generative agent based on the paper [Generative Agents: Interactive Simulacra of Human Behavior](https://arxiv.org/abs/2304.03442) by Park, et. al.\n",
"\n",
"In it, we leverage a time-weighted Memory object backed by a LangChain Retriever."
]
},
{
"cell_type": "code",
"execution_count": 1,
"id": "53f81c37-db45-4fdc-843c-aa8fd2a9e99d",
"metadata": {},
"outputs": [],
"source": [
"# Use termcolor to make it easy to colorize the outputs.\n",
"!pip install termcolor > /dev/null"
]
},
{
"cell_type": "code",
"execution_count": 41,
"id": "8851c370-b395-4b80-a79d-486a38ffc244",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"import re\n",
"from datetime import datetime, timedelta\n",
"from typing import List, Optional, Tuple\n",
"from termcolor import colored\n",
"\n",
"from pydantic import BaseModel, Field\n",
"\n",
"from langchain import LLMChain\n",
"from langchain.chat_models import ChatOpenAI\n",
"from langchain.docstore import InMemoryDocstore\n",
"from langchain.embeddings import OpenAIEmbeddings\n",
"from langchain.prompts import PromptTemplate\n",
"from langchain.retrievers import TimeWeightedVectorStoreRetriever\n",
"from langchain.schema import BaseLanguageModel, Document\n",
"from langchain.vectorstores import FAISS\n"
]
},
{
"cell_type": "code",
"execution_count": 42,
"id": "81824e76",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"USER_NAME = \"Person A\" # The name you want to use when interviewing the agent.\n",
"LLM = ChatOpenAI(max_tokens=1500) # Can be any LLM you want."
]
},
{
"cell_type": "markdown",
"id": "c3da1649-d88f-4973-b655-7042975cde7e",
"metadata": {},
"source": [
"### Generative Agent Memory Components\n",
"\n",
"This tutorial highlights the memory of generative agents and its impact on their behavior. The memory varies from standard LangChain Chat memory in two aspects:\n",
"\n",
"1. **Memory Formation**\n",
"\n",
" Generative Agents have extended memories, stored in a single stream:\n",
" 1. Observations - from dialogues or interactions with the virtual world, about self or others\n",
" 2. Reflections - resurfaced and summarized core memories\n",
"\n",
"2. **Memory Recall**\n",
"\n",
" Memories are retrieved using a weighted sum of salience, recency, and importance.\n",
"\n",
"Review the definition below, focusing on `add_memory` and `summarize_related_memories` methods."
]
},
{
"cell_type": "code",
"execution_count": 43,
"id": "043e5203-6a41-431c-9efa-3e1743d7d25a",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"class GenerativeAgent(BaseModel):\n",
" \"\"\"A character with memory and innate characteristics.\"\"\"\n",
" \n",
" name: str\n",
" age: int\n",
" traits: str\n",
" \"\"\"The traits of the character you wish not to change.\"\"\"\n",
" status: str\n",
" \"\"\"Current activities of the character.\"\"\"\n",
" llm: BaseLanguageModel\n",
" memory_retriever: TimeWeightedVectorStoreRetriever\n",
" \"\"\"The retriever to fetch related memories.\"\"\"\n",
" verbose: bool = False\n",
" \n",
" reflection_threshold: Optional[float] = None\n",
" \"\"\"When the total 'importance' of memories exceeds the above threshold, stop to reflect.\"\"\"\n",
" \n",
" current_plan: List[str] = []\n",
" \"\"\"The current plan of the agent.\"\"\"\n",
" \n",
" summary: str = \"\" #: :meta private:\n",
" summary_refresh_seconds: int= 3600 #: :meta private:\n",
" last_refreshed: datetime =Field(default_factory=datetime.now) #: :meta private:\n",
" daily_summaries: List[str] #: :meta private:\n",
" memory_importance: float = 0.0 #: :meta private:\n",
" max_tokens_limit: int = 1200 #: :meta private:\n",
" \n",
" class Config:\n",
" \"\"\"Configuration for this pydantic object.\"\"\"\n",
"\n",
" arbitrary_types_allowed = True\n",
"\n",
" @staticmethod\n",
" def _parse_list(text: str) -> List[str]:\n",
" \"\"\"Parse a newline-separated string into a list of strings.\"\"\"\n",
" lines = re.split(r'\\n', text.strip())\n",
" return [re.sub(r'^\\s*\\d+\\.\\s*', '', line).strip() for line in lines]\n",
"\n",
"\n",
" def _compute_agent_summary(self):\n",
" \"\"\"\"\"\"\n",
" prompt = PromptTemplate.from_template(\n",
" \"How would you summarize {name}'s core characteristics given the\"\n",
" +\" following statements:\\n\"\n",
" +\"{related_memories}\"\n",
" + \"Do not embellish.\"\n",
" +\"\\n\\nSummary: \"\n",
" )\n",
" # The agent seeks to think about their core characteristics.\n",
" relevant_memories = self.fetch_memories(f\"{self.name}'s core characteristics\")\n",
" relevant_memories_str = \"\\n\".join([f\"{mem.page_content}\" for mem in relevant_memories])\n",
" chain = LLMChain(llm=self.llm, prompt=prompt, verbose=self.verbose)\n",
" return chain.run(name=self.name, related_memories=relevant_memories_str).strip()\n",
" \n",
" def _get_topics_of_reflection(self, last_k: int = 50) -> Tuple[str, str, str]:\n",
" \"\"\"Return the 3 most salient high-level questions about recent observations.\"\"\"\n",
" prompt = PromptTemplate.from_template(\n",
" \"{observations}\\n\\n\"\n",
" + \"Given only the information above, what are the 3 most salient\"\n",
" + \" high-level questions we can answer about the subjects in the statements?\"\n",
" + \" Provide each question on a new line.\\n\\n\"\n",
" )\n",
" reflection_chain = LLMChain(llm=self.llm, prompt=prompt, verbose=self.verbose)\n",
" observations = self.memory_retriever.memory_stream[-last_k:]\n",
" observation_str = \"\\n\".join([o.page_content for o in observations])\n",
" result = reflection_chain.run(observations=observation_str)\n",
" return self._parse_list(result)\n",
" \n",
" def _get_insights_on_topic(self, topic: str) -> List[str]:\n",
" \"\"\"Generate 'insights' on a topic of reflection, based on pertinent memories.\"\"\"\n",
" prompt = PromptTemplate.from_template(\n",
" \"Statements about {topic}\\n\"\n",
" +\"{related_statements}\\n\\n\"\n",
" + \"What 5 high-level insights can you infer from the above statements?\"\n",
" + \" (example format: insight (because of 1, 5, 3))\"\n",
" )\n",
" related_memories = self.fetch_memories(topic)\n",
" related_statements = \"\\n\".join([f\"{i+1}. {memory.page_content}\" \n",
" for i, memory in \n",
" enumerate(related_memories)])\n",
" reflection_chain = LLMChain(llm=self.llm, prompt=prompt, verbose=self.verbose)\n",
" result = reflection_chain.run(topic=topic, related_statements=related_statements)\n",
" # TODO: Parse the connections between memories and insights\n",
" return self._parse_list(result)\n",
" \n",
" def pause_to_reflect(self) -> List[str]:\n",
" \"\"\"Reflect on recent observations and generate 'insights'.\"\"\"\n",
" print(colored(f\"Character {self.name} is reflecting\", \"blue\"))\n",
" new_insights = []\n",
" topics = self._get_topics_of_reflection()\n",
" for topic in topics:\n",
" insights = self._get_insights_on_topic( topic)\n",
" for insight in insights:\n",
" self.add_memory(insight)\n",
" new_insights.extend(insights)\n",
" return new_insights\n",
" \n",
" def _score_memory_importance(self, memory_content: str, weight: float = 0.15) -> float:\n",
" \"\"\"Score the absolute importance of the given memory.\"\"\"\n",
" # A weight of 0.25 makes this less important than it\n",
" # would be otherwise, relative to salience and time\n",
" prompt = PromptTemplate.from_template(\n",
" \"On the scale of 1 to 10, where 1 is purely mundane\"\n",
" +\" (e.g., brushing teeth, making bed) and 10 is\"\n",
" + \" extremely poignant (e.g., a break up, college\"\n",
" + \" acceptance), rate the likely poignancy of the\"\n",
" + \" following piece of memory. Respond with a single integer.\"\n",
" + \"\\nMemory: {memory_content}\"\n",
" + \"\\nRating: \"\n",
" )\n",
" chain = LLMChain(llm=self.llm, prompt=prompt, verbose=self.verbose)\n",
" score = chain.run(memory_content=memory_content).strip()\n",
" match = re.search(r\"^\\D*(\\d+)\", score)\n",
" if match:\n",
" return (float(score[0]) / 10) * weight\n",
" else:\n",
" return 0.0\n",
"\n",
"\n",
" def add_memory(self, memory_content: str) -> List[str]:\n",
" \"\"\"Add an observation or memory to the agent's memory.\"\"\"\n",
" importance_score = self._score_memory_importance(memory_content)\n",
" self.memory_importance += importance_score\n",
" document = Document(page_content=memory_content, metadata={\"importance\": importance_score})\n",
" result = self.memory_retriever.add_documents([document])\n",
"\n",
" # After an agent has processed a certain amount of memories (as measured by\n",
" # aggregate importance), it is time to reflect on recent events to add\n",
" # more synthesized memories to the agent's memory stream.\n",
" if (self.reflection_threshold is not None \n",
" and self.memory_importance > self.reflection_threshold\n",
" and self.status != \"Reflecting\"):\n",
" old_status = self.status\n",
" self.status = \"Reflecting\"\n",
" self.pause_to_reflect()\n",
" # Hack to clear the importance from reflection\n",
" self.memory_importance = 0.0\n",
" self.status = old_status\n",
" return result\n",
" \n",
" def fetch_memories(self, observation: str) -> List[Document]:\n",
" \"\"\"Fetch related memories.\"\"\"\n",
" return self.memory_retriever.get_relevant_documents(observation)\n",
" \n",
" \n",
" def get_summary(self, force_refresh: bool = False) -> str:\n",
" \"\"\"Return a descriptive summary of the agent.\"\"\"\n",
" current_time = datetime.now()\n",
" since_refresh = (current_time - self.last_refreshed).seconds\n",
" if not self.summary or since_refresh >= self.summary_refresh_seconds or force_refresh:\n",
" self.summary = self._compute_agent_summary()\n",
" self.last_refreshed = current_time\n",
" return (\n",
" f\"Name: {self.name} (age: {self.age})\"\n",
" +f\"\\nInnate traits: {self.traits}\"\n",
" +f\"\\n{self.summary}\"\n",
" )\n",
" \n",
" def get_full_header(self, force_refresh: bool = False) -> str:\n",
" \"\"\"Return a full header of the agent's status, summary, and current time.\"\"\"\n",
" summary = self.get_summary(force_refresh=force_refresh)\n",
" current_time_str = datetime.now().strftime(\"%B %d, %Y, %I:%M %p\")\n",
" return f\"{summary}\\nIt is {current_time_str}.\\n{self.name}'s status: {self.status}\"\n",
"\n",
" \n",
" \n",
" def _get_entity_from_observation(self, observation: str) -> str:\n",
" prompt = PromptTemplate.from_template(\n",
" \"What is the observed entity in the following observation? {observation}\"\n",
" +\"\\nEntity=\"\n",
" )\n",
" chain = LLMChain(llm=self.llm, prompt=prompt, verbose=self.verbose)\n",
" return chain.run(observation=observation).strip()\n",
"\n",
" def _get_entity_action(self, observation: str, entity_name: str) -> str:\n",
" prompt = PromptTemplate.from_template(\n",
" \"What is the {entity} doing in the following observation? {observation}\"\n",
" +\"\\nThe {entity} is\"\n",
" )\n",
" chain = LLMChain(llm=self.llm, prompt=prompt, verbose=self.verbose)\n",
" return chain.run(entity=entity_name, observation=observation).strip()\n",
" \n",
" def _format_memories_to_summarize(self, relevant_memories: List[Document]) -> str:\n",
" content_strs = set()\n",
" content = []\n",
" for mem in relevant_memories:\n",
" if mem.page_content in content_strs:\n",
" continue\n",
" content_strs.add(mem.page_content)\n",
" created_time = mem.metadata[\"created_at\"].strftime(\"%B %d, %Y, %I:%M %p\")\n",
" content.append(f\"- {created_time}: {mem.page_content.strip()}\")\n",
" return \"\\n\".join([f\"{mem}\" for mem in content])\n",
" \n",
" def summarize_related_memories(self, observation: str) -> str:\n",
" \"\"\"Summarize memories that are most relevant to an observation.\"\"\"\n",
" entity_name = self._get_entity_from_observation(observation)\n",
" entity_action = self._get_entity_action(observation, entity_name)\n",
" q1 = f\"What is the relationship between {self.name} and {entity_name}\"\n",
" relevant_memories = self.fetch_memories(q1) # Fetch memories related to the agent's relationship with the entity\n",
" q2 = f\"{entity_name} is {entity_action}\"\n",
" relevant_memories += self.fetch_memories(q2) # Fetch things related to the entity-action pair\n",
" context_str = self._format_memories_to_summarize(relevant_memories)\n",
" prompt = PromptTemplate.from_template(\n",
" \"{q1}?\\nContext from memory:\\n{context_str}\\nRelevant context: \"\n",
" )\n",
" chain = LLMChain(llm=self.llm, prompt=prompt, verbose=self.verbose)\n",
" return chain.run(q1=q1, context_str=context_str.strip()).strip()\n",
" \n",
" def _get_memories_until_limit(self, consumed_tokens: int) -> str:\n",
" \"\"\"Reduce the number of tokens in the documents.\"\"\"\n",
" result = []\n",
" for doc in self.memory_retriever.memory_stream[::-1]:\n",
" if consumed_tokens >= self.max_tokens_limit:\n",
" break\n",
" consumed_tokens += self.llm.get_num_tokens(doc.page_content)\n",
" if consumed_tokens < self.max_tokens_limit:\n",
" result.append(doc.page_content) \n",
" return \"; \".join(result[::-1])\n",
" \n",
" def _generate_reaction(\n",
" self,\n",
" observation: str,\n",
" suffix: str\n",
" ) -> str:\n",
" \"\"\"React to a given observation.\"\"\"\n",
" prompt = PromptTemplate.from_template(\n",
" \"{agent_summary_description}\"\n",
" +\"\\nIt is {current_time}.\"\n",
" +\"\\n{agent_name}'s status: {agent_status}\"\n",
" + \"\\nSummary of relevant context from {agent_name}'s memory:\"\n",
" +\"\\n{relevant_memories}\"\n",
" +\"\\nMost recent observations: {recent_observations}\"\n",
" + \"\\nObservation: {observation}\"\n",
" + \"\\n\\n\" + suffix\n",
" )\n",
" agent_summary_description = self.get_summary()\n",
" relevant_memories_str = self.summarize_related_memories(observation)\n",
" current_time_str = datetime.now().strftime(\"%B %d, %Y, %I:%M %p\")\n",
" kwargs = dict(agent_summary_description=agent_summary_description,\n",
" current_time=current_time_str,\n",
" relevant_memories=relevant_memories_str,\n",
" agent_name=self.name,\n",
" observation=observation,\n",
" agent_status=self.status)\n",
" consumed_tokens = self.llm.get_num_tokens(prompt.format(recent_observations=\"\", **kwargs))\n",
" kwargs[\"recent_observations\"] = self._get_memories_until_limit(consumed_tokens)\n",
" action_prediction_chain = LLMChain(llm=self.llm, prompt=prompt)\n",
" result = action_prediction_chain.run(**kwargs)\n",
" return result.strip()\n",
" \n",
" def generate_reaction(self, observation: str) -> Tuple[bool, str]:\n",
" \"\"\"React to a given observation.\"\"\"\n",
" call_to_action_template = (\n",
" \"Should {agent_name} react to the observation, and if so,\"\n",
" +\" what would be an appropriate reaction? Respond in one line.\"\n",
" +' If the action is to engage in dialogue, write:\\nSAY: \"what to say\"'\n",
" +\"\\notherwise, write:\\nREACT: {agent_name}'s reaction (if anything).\"\n",
" + \"\\nEither do nothing, react, or say something but not both.\\n\\n\"\n",
" )\n",
" full_result = self._generate_reaction(observation, call_to_action_template)\n",
" result = full_result.strip().split('\\n')[0]\n",
" self.add_memory(f\"{self.name} observed {observation} and reacted by {result}\")\n",
" if \"REACT:\" in result:\n",
" reaction = result.split(\"REACT:\")[-1].strip()\n",
" return False, f\"{self.name} {reaction}\"\n",
" if \"SAY:\" in result:\n",
" said_value = result.split(\"SAY:\")[-1].strip()\n",
" return True, f\"{self.name} said {said_value}\"\n",
" else:\n",
" return False, result\n",
"\n",
" def generate_dialogue_response(self, observation: str) -> Tuple[bool, str]:\n",
" \"\"\"React to a given observation.\"\"\"\n",
" call_to_action_template = (\n",
" 'What would {agent_name} say? To end the conversation, write: GOODBYE: \"what to say\". Otherwise to continue the conversation, write: SAY: \"what to say next\"\\n\\n'\n",
" )\n",
" full_result = self._generate_reaction(observation, call_to_action_template)\n",
" result = full_result.strip().split('\\n')[0]\n",
" if \"GOODBYE:\" in result:\n",
" farewell = result.split(\"GOODBYE:\")[-1].strip()\n",
" self.add_memory(f\"{self.name} observed {observation} and said {farewell}\")\n",
" return False, f\"{self.name} said {farewell}\"\n",
" if \"SAY:\" in result:\n",
" response_text = result.split(\"SAY:\")[-1].strip()\n",
" self.add_memory(f\"{self.name} observed {observation} and said {response_text}\")\n",
" return True, f\"{self.name} said {response_text}\"\n",
" else:\n",
" return False, result"
]
},
{
"cell_type": "markdown",
"id": "361bd49e",
"metadata": {
"jp-MarkdownHeadingCollapsed": true,
"tags": []
},
"source": [
"## Memory Lifecycle\n",
"\n",
"Summarizing the above key methods: `add_memory` and `summarize_related_memories`.\n",
"\n",
"When an agent makes an observation, it stores the memory:\n",
" \n",
"1. Language model scores the memory's importance (1 for mundane, 10 for poignant)\n",
"2. Observation and importance are stored within a document by TimeWeightedVectorStoreRetriever, with a `last_accessed_time`.\n",
"\n",
"When an agent responds to an observation:\n",
"\n",
"1. Generates query(s) for retriever, which fetches documents based on salience, recency, and importance.\n",
"2. Summarizes the retrieved information\n",
"3. Updates the `last_accessed_time` for the used documents.\n"
]
},
{
"cell_type": "markdown",
"id": "2fa3ca02",
"metadata": {},
"source": [
"## Create a Generative Character\n",
"\n",
"\n",
"\n",
"Now that we've walked through the definition, we will create two characters named \"Tommie\" and \"Eve\"."
]
},
{
"cell_type": "code",
"execution_count": 44,
"id": "ee9c1a1d-c311-4f1c-8131-75fccd9025b1",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"import math\n",
"import faiss\n",
"\n",
"def normalize_score_fn(score: float) -> float:\n",
" \"\"\"Return a similarity score on a scale [0, 1].\"\"\"\n",
" # This will differ depending on a few things:\n",
" # - the distance / similarity metric used by the VectorStore\n",
" # - the scale of your embeddings (OpenAI's are unit norm. Many others are not!)\n",
" # This function converts the euclidean norm of normalized embeddings\n",
" # (0 is most similar, sqrt(2) most dissimilar)\n",
" # to a similarity function (0 to 1)\n",
" return 1.0 - score / math.sqrt(2)\n",
"\n",
"def create_new_memory_retriever():\n",
" \"\"\"Create a new vector store retriever unique to the agent.\"\"\"\n",
" # Define your embedding model\n",
" embeddings_model = OpenAIEmbeddings()\n",
" # Initialize the vectorstore as empty\n",
" embedding_size = 1536\n",
" index = faiss.IndexFlatL2(embedding_size)\n",
" vectorstore = FAISS(embeddings_model.embed_query, index, InMemoryDocstore({}), {}, normalize_score_fn=normalize_score_fn)\n",
" return TimeWeightedVectorStoreRetriever(vectorstore=vectorstore, other_score_keys=[\"importance\"], k=15) "
]
},
{
"cell_type": "code",
"execution_count": 45,
"id": "7884f9dd-c597-4c27-8c77-1402c71bc2f8",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"tommie = GenerativeAgent(name=\"Tommie\", \n",
" age=25,\n",
" traits=\"anxious, likes design\", # You can add more persistent traits here \n",
" status=\"looking for a job\", # When connected to a virtual world, we can have the characters update their status\n",
" memory_retriever=create_new_memory_retriever(),\n",
" llm=LLM,\n",
" daily_summaries = [\n",
" \"Drove across state to move to a new town but doesn't have a job yet.\"\n",
" ],\n",
" reflection_threshold = 8, # we will give this a relatively low number to show how reflection works\n",
" )"
]
},
{
"cell_type": "code",
"execution_count": 46,
"id": "c524d529",
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Name: Tommie (age: 25)\n",
"Innate traits: anxious, likes design\n",
"It is not possible to provide a summary without any statements or information about Tommie's characteristics.\n"
]
}
],
"source": [
"# We can see a current \"Summary\" of a character based on their own perception of self.\n",
"# It isn't very complete right now since the character doesn't have any memories.\n",
"print(tommie.get_summary())"
]
},
{
"cell_type": "code",
"execution_count": 47,
"id": "4be60979-d56e-4abf-a636-b34ffa8b7fba",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"# We can give the character memories directly\n",
"tommie_memories = [\n",
" \"Tommie remembers his dog, Bruno, from when he was a kid\",\n",
" \"Tommie feels tired from driving so far\",\n",
" \"Tommie sees the new home\",\n",
" \"The new neighbors have a cat\",\n",
" \"The road is noisy at night\",\n",
" \"Tommie is hungry\",\n",
" \"Tommie tries to get some rest.\",\n",
"]\n",
"for memory in tommie_memories:\n",
" tommie.add_memory(memory)"
]
},
{
"cell_type": "code",
"execution_count": 48,
"id": "6992b48b-697f-4973-9560-142ef85357d7",
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Name: Tommie (age: 25)\n",
"Innate traits: anxious, likes design\n",
"Tommie is observant, has a sentimental side, gets hungry, tries to rest when possible, gets tired easily, notices details about his surroundings, and is affected by noise.\n"
]
}
],
"source": [
"# Now that Tommie has 'memories', their self-summary is more descriptive\n",
"print(tommie.get_summary(force_refresh=True))"
]
},
{
"cell_type": "markdown",
"id": "40d39a32-838c-4a03-8b27-a52c76c402e7",
"metadata": {
"tags": []
},
"source": [
"## Pre-Interview with Character\n",
"\n",
"Before sending our character on their way, let's ask them a few questions."
]
},
{
"cell_type": "code",
"execution_count": 49,
"id": "eaf125d8-f54c-4c5f-b6af-32789b1f7d3a",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"def interview_agent(agent: GenerativeAgent, message: str) -> str:\n",
" \"\"\"Help the notebook user interact with the agent.\"\"\"\n",
" new_message = f\"{USER_NAME} says {message}\"\n",
" return agent.generate_dialogue_response(new_message)[1]\n"
]
},
{
"cell_type": "code",
"execution_count": 50,
"id": "54024d41-6e83-4914-91e5-73140e2dd9c8",
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"'Tommie said \"I really enjoy design, especially graphic design. It\\'s something I\\'ve been interested in for a while now.\"'"
]
},
"execution_count": 50,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"interview_agent(tommie, \"What do you like to do?\")"
]
},
{
"cell_type": "code",
"execution_count": 51,
"id": "71e2e8cc-921e-4816-82f1-66962b2c1055",
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"'Tommie said \"Well, I\\'m actually on the hunt for a job right now. So, I\\'m hoping to make some progress in that area today. How about you? Any exciting plans?\"'"
]
},
"execution_count": 51,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"interview_agent(tommie, \"What are you looking forward to doing today?\")"
]
},
{
"cell_type": "code",
"execution_count": 52,
"id": "a2521ffc-7050-4ac3-9a18-4cccfc798c31",
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"'Tommie said \"Honestly, I\\'m feeling pretty anxious about finding a job. It\\'s been a tough process so far. But I\\'m trying to stay positive and keep pushing forward. How about you, what are you worried about today?\"'"
]
},
"execution_count": 52,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"interview_agent(tommie, \"What are you most worried about today?\")"
]
},
{
"cell_type": "markdown",
"id": "e509c468-f7cd-4d72-9f3a-f4aba28b1eea",
"metadata": {},
"source": [
"## Step through the day's observations."
]
},
{
"cell_type": "code",
"execution_count": 53,
"id": "154dee3d-bfe0-4828-b963-ed7e885799b3",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"# Let's have Tommie start going through a day in the life.\n",
"observations = [\n",
" \"Tommie wakes up to the sound of a noisy construction site outside his window.\",\n",
" \"Tommie gets out of bed and heads to the kitchen to make himself some coffee.\",\n",
" \"Tommie realizes he forgot to buy coffee filters and starts rummaging through his moving boxes to find some.\",\n",
" \"Tommie finally finds the filters and makes himself a cup of coffee.\",\n",
" \"The coffee tastes bitter, and Tommie regrets not buying a better brand.\",\n",
" \"Tommie checks his email and sees that he has no job offers yet.\",\n",
" \"Tommie spends some time updating his resume and cover letter.\",\n",
" \"Tommie heads out to explore the city and look for job openings.\",\n",
" \"Tommie sees a sign for a job fair and decides to attend.\",\n",
" \"The line to get in is long, and Tommie has to wait for an hour.\",\n",
" \"Tommie meets several potential employers at the job fair but doesn't receive any offers.\",\n",
" \"Tommie leaves the job fair feeling disappointed.\",\n",
" \"Tommie stops by a local diner to grab some lunch.\",\n",
" \"The service is slow, and Tommie has to wait for 30 minutes to get his food.\",\n",
" \"Tommie overhears a conversation at the next table about a job opening.\",\n",
" \"Tommie asks the diners about the job opening and gets some information about the company.\",\n",
" \"Tommie decides to apply for the job and sends his resume and cover letter.\",\n",
" \"Tommie continues his search for job openings and drops off his resume at several local businesses.\",\n",
" \"Tommie takes a break from his job search to go for a walk in a nearby park.\",\n",
" \"A dog approaches and licks Tommie's feet, and he pets it for a few minutes.\",\n",
" \"Tommie sees a group of people playing frisbee and decides to join in.\",\n",
" \"Tommie has fun playing frisbee but gets hit in the face with the frisbee and hurts his nose.\",\n",
" \"Tommie goes back to his apartment to rest for a bit.\",\n",
" \"A raccoon tore open the trash bag outside his apartment, and the garbage is all over the floor.\",\n",
" \"Tommie starts to feel frustrated with his job search.\",\n",
" \"Tommie calls his best friend to vent about his struggles.\",\n",
" \"Tommie's friend offers some words of encouragement and tells him to keep trying.\",\n",
" \"Tommie feels slightly better after talking to his friend.\",\n",
"]\n"
]
},
{
"cell_type": "code",
"execution_count": 54,
"id": "238be49c-edb3-4e26-a2b6-98777ba8de86",
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\u001b[32mTommie wakes up to the sound of a noisy construction site outside his window.\u001b[0m Tommie Tommie may feel agitated or annoyed by the noise, but it depends on his tolerance level and mood at the moment.\n",
"\u001b[32mTommie gets out of bed and heads to the kitchen to make himself some coffee.\u001b[0m Tommie Tommie feels a sense of relief and comfort as he prepares his coffee.\n",
"\u001b[32mTommie realizes he forgot to buy coffee filters and starts rummaging through his moving boxes to find some.\u001b[0m Tommie Tommie may feel frustrated or annoyed with himself for forgetting to buy coffee filters.\n",
"\u001b[32mTommie finally finds the filters and makes himself a cup of coffee.\u001b[0m Tommie Tommie may feel relieved and satisfied as he sips his coffee.\n",
"\u001b[32mThe coffee tastes bitter, and Tommie regrets not buying a better brand.\u001b[0m Tommie Tommie may feel disappointed or regretful about the bitter coffee, but he may still finish it or consider buying a better brand next time.\n",
"\u001b[32mTommie checks his email and sees that he has no job offers yet.\u001b[0m Tommie Tommie may feel disappointed or discouraged, but he may also try to stay positive and continue his job search.\n",
"\u001b[32mTommie spends some time updating his resume and cover letter.\u001b[0m Tommie Tommie may feel productive or accomplished for updating his resume and cover letter.\n",
"\u001b[32mTommie heads out to explore the city and look for job openings.\u001b[0m Tommie Tommie may feel excited or optimistic about exploring the city and finding job openings.\n",
"\u001b[32mTommie sees a sign for a job fair and decides to attend.\u001b[0m Tommie Tommie may feel hopeful or motivated about attending the job fair.\n",
"\u001b[32mThe line to get in is long, and Tommie has to wait for an hour.\u001b[0m Tommie Tommie may feel impatient or frustrated with having to wait in line for an hour.\n",
"\u001b[32mTommie meets several potential employers at the job fair but doesn't receive any offers.\u001b[0m Tommie Tommie may feel disappointed or discouraged that he did not receive any job offers at the fair.\n",
"\u001b[32mTommie leaves the job fair feeling disappointed.\u001b[0m Tommie Tommie may feel discouraged or frustrated about not receiving any job offers at the fair.\n",
"\u001b[32mTommie stops by a local diner to grab some lunch.\u001b[0m Tommie Tommie may feel hungry and relieved to have found a place to eat lunch.\n",
"\u001b[32mThe service is slow, and Tommie has to wait for 30 minutes to get his food.\u001b[0m Tommie Tommie may feel frustrated or annoyed with the slow service, but he may try to be patient or find something else to do while waiting.\n",
"\u001b[32mTommie overhears a conversation at the next table about a job opening.\u001b[0m Tommie said \"Excuse me, I couldn't help but overhear about the job opening. Can you tell me more about it?\"\n",
"\u001b[32mTommie asks the diners about the job opening and gets some information about the company.\u001b[0m Tommie said \"Thank you for the information. Do you happen to know how I can apply for the job?\"\n",
"\u001b[32mTommie decides to apply for the job and sends his resume and cover letter.\u001b[0m Tommie said \"Thank you for the information. Do you happen to know how I can apply for the job?\"\n",
"\u001b[32mTommie continues his search for job openings and drops off his resume at several local businesses.\u001b[0m Tommie Tommie may feel hopeful or determined to continue his search for job openings.\n",
"\u001b[32mTommie takes a break from his job search to go for a walk in a nearby park.\u001b[0m Tommie Tommie may feel relaxed or refreshed by taking a break and going for a walk in the park.\n",
"\u001b[32mA dog approaches and licks Tommie's feet, and he pets it for a few minutes.\u001b[0m Tommie Tommie may feel surprised or amused by the dog's behavior, but he may also enjoy the brief interaction and continue on his walk.\n",
"****************************************\n",
"\u001b[34mAfter 20 observations, Tommie's summary is:\n",
"Name: Tommie (age: 25)\n",
"Innate traits: anxious, likes design\n",
"Tommie is optimistic and determined in his job search, but also experiences disappointment and discouragement. He enjoys design, has a sense of nostalgia for his past, and finds comfort in simple pleasures like making coffee and going for walks.\u001b[0m\n",
"****************************************\n",
"\u001b[32mTommie sees a group of people playing frisbee and decides to join in.\u001b[0m Tommie said \"Mind if I join in?\"\n",
"\u001b[32mTommie has fun playing frisbee but gets hit in the face with the frisbee and hurts his nose.\u001b[0m Tommie Tommie may feel embarrassed or in pain after getting hit in the face with the frisbee.\n",
"\u001b[32mTommie goes back to his apartment to rest for a bit.\u001b[0m Tommie Tommie may feel tired or in need of rest after a long day of job searching and activities.\n",
"\u001b[32mA raccoon tore open the trash bag outside his apartment, and the garbage is all over the floor.\u001b[0m Tommie Tommie may feel frustrated or annoyed with the mess and may need to clean it up.\n",
"\u001b[32mTommie starts to feel frustrated with his job search.\u001b[0m Tommie Tommie may feel discouraged or overwhelmed with his job search and may need to take a break or try a different approach.\n",
"\u001b[32mTommie calls his best friend to vent about his struggles.\u001b[0m Tommie said \"Hey, I just needed to vent about my job search. It's been a tough day.\"\n",
"\u001b[32mTommie's friend offers some words of encouragement and tells him to keep trying.\u001b[0m Tommie said \"Thanks for the encouragement, it means a lot to me.\"\n",
"\u001b[32mTommie feels slightly better after talking to his friend.\u001b[0m Tommie said \"Thanks for listening, I really needed to hear that.\"\n"
]
}
],
"source": [
"# Let's send Tommie on their way. We'll check in on their summary every few observations to watch it evolve\n",
"for i, observation in enumerate(observations):\n",
" _, reaction = tommie.generate_reaction(observation)\n",
" print(colored(observation, \"green\"), reaction)\n",
" if ((i+1) % 20) == 0:\n",
" print('*'*40)\n",
" print(colored(f\"After {i+1} observations, Tommie's summary is:\\n{tommie.get_summary(force_refresh=True)}\", \"blue\"))\n",
" print('*'*40)"
]
},
{
"cell_type": "markdown",
"id": "dd62a275-7290-43ca-aa0f-504f3a706d09",
"metadata": {},
"source": [
"## Interview after the day"
]
},
{
"cell_type": "code",
"execution_count": 55,
"id": "6336ab5d-3074-4831-951f-c9e2cba5dfb5",
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"'Tommie said \"It\\'s been a bit of a rollercoaster, to be honest. I went to a job fair but didn\\'t get any offers, but then I heard about a job opening at a diner and applied for it. I also went for a walk in the park and played frisbee, but got hit in the face with it. Overall, it\\'s been a mix of good and bad experiences.\"'"
]
},
"execution_count": 55,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"interview_agent(tommie, \"Tell me about how your day has been going\")"
]
},
{
"cell_type": "code",
"execution_count": 56,
"id": "809ac906-69b7-4326-99ec-af638d32bb20",
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"'Tommie said \"I actually really enjoy coffee, although I recently bought a brand that turned out to be too bitter for my taste. And I always seem to forget to buy coffee filters when I need them. But overall, I find making and drinking coffee to be a comforting ritual.\"'"
]
},
"execution_count": 56,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"interview_agent(tommie, \"How do you feel about coffee?\")"
]
},
{
"cell_type": "code",
"execution_count": 57,
"id": "f733a431-19ea-421a-9101-ae2593a8c626",
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"'Tommie said \"Oh, I had a dog when I was a kid named Bruno. He was a golden retriever and we were really close. I have a lot of happy memories with him.\"'"
]
},
"execution_count": 57,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"interview_agent(tommie, \"Tell me about your childhood dog!\")"
]
},
{
"cell_type": "markdown",
"id": "c9261428-778a-4c0b-b725-bc9e91b71391",
"metadata": {},
"source": [
"## Adding Multiple Characters\n",
"\n",
"Let's add a second character to have a conversation with Tommie. Feel free to configure different traits."
]
},
{
"cell_type": "code",
"execution_count": 58,
"id": "ec8bbe18-a021-419c-bf1f-23d34732cd99",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"eve = GenerativeAgent(name=\"Eve\", \n",
" age=34, \n",
" traits=\"curious, helpful\", # You can add more persistent traits here \n",
" status=\"N/A\", # When connected to a virtual world, we can have the characters update their status\n",
" memory_retriever=create_new_memory_retriever(),\n",
" llm=LLM,\n",
" daily_summaries = [\n",
" (\"Eve started her new job as a career counselor last week and received her first assignment, a client named Tommie.\")\n",
" ],\n",
" reflection_threshold = 5,\n",
" )"
]
},
{
"cell_type": "code",
"execution_count": 59,
"id": "1e2745f5-e0da-4abd-98b4-830802ce6698",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"yesterday = (datetime.now() - timedelta(days=1)).strftime(\"%A %B %d\")\n",
"eve_memories = [\n",
" \"Eve overhears her colleague say something about a new client being hard to work with\",\n",
" \"Eve wakes up and hear's the alarm\",\n",
" \"Eve eats a boal of porridge\",\n",
" \"Eve helps a coworker on a task\",\n",
" \"Eve plays tennis with her friend Xu before going to work\",\n",
" \"Eve overhears her colleague say something about Tommie being hard to work with\",\n",
" \n",
"]\n",
"for memory in eve_memories:\n",
" eve.add_memory(memory)"
]
},
{
"cell_type": "code",
"execution_count": 60,
"id": "de4726e3-4bb1-47da-8fd9-f317a036fe0f",
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Name: Eve (age: 34)\n",
"Innate traits: curious, helpful\n",
"Eve is helpful, active, attentive, routine-oriented, and aware of her colleagues.\n"
]
}
],
"source": [
"print(eve.get_summary())"
]
},
{
"cell_type": "markdown",
"id": "837524e9-7f7e-4e9f-b610-f454062f5915",
"metadata": {},
"source": [
"## Pre-conversation interviews\n",
"\n",
"\n",
"Let's \"Interview\" Eve before she speaks with Tommie."
]
},
{
"cell_type": "code",
"execution_count": 61,
"id": "6cda916d-800c-47bc-a7f9-6a2f19187472",
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"'Eve said \"I\\'m feeling curious about what the day has in store. How about you?\"'"
]
},
"execution_count": 61,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"interview_agent(eve, \"How are you feeling about today?\")"
]
},
{
"cell_type": "code",
"execution_count": 62,
"id": "448ae644-0a66-4eb2-a03a-319f36948b37",
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"'Eve said \"I overheard someone say that Tommie is hard to work with, but I don\\'t know much else. Have you worked with them before?\"'"
]
},
"execution_count": 62,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"interview_agent(eve, \"What do you know about Tommie?\")"
]
},
{
"cell_type": "code",
"execution_count": 63,
"id": "493fc5b8-8730-4ef8-9820-0f1769ce1691",
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"'Eve said \"That\\'s interesting. What kind of job is Tommie looking for?\"'"
]
},
"execution_count": 63,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"interview_agent(eve, \"Tommie is looking to find a job. What are are some things you'd like to ask him?\")"
]
},
{
"cell_type": "code",
"execution_count": 64,
"id": "4b46452a-6c54-4db2-9d87-18597f70fec8",
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"'Eve said \"Sure, I\\'ll do my best to make him comfortable and ask about his experiences and goals. Thanks for the heads up.\"'"
]
},
"execution_count": 64,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"interview_agent(eve, \"You'll have to ask him. He may be a bit shy, so I'd appreciate it if you keep the conversation go and ask as many questions as possible.\")"
]
},
{
"cell_type": "markdown",
"id": "dd780655-1d73-4fcb-a78d-79fd46a20636",
"metadata": {},
"source": [
"## Dialogue between Generative Agents\n",
"\n",
"Generative agents are much more complex when they interact with a virtual environment or with each other. Below, we run a simple conversation between Tommie and Eve."
]
},
{
"cell_type": "code",
"execution_count": 65,
"id": "042ea271-4bf1-4247-9082-239a6fea43b8",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"def run_conversation(agents: List[GenerativeAgent], initial_observation: str, max_turns:int = 25) -> None:\n",
" \"\"\"Runs a conversation between agents.\"\"\"\n",
" _, observation = agents[1].generate_reaction(initial_observation)\n",
" print(observation)\n",
" turns = 0\n",
" while True:\n",
" break_dialogue = False\n",
" for agent in agents:\n",
" stay_in_dialogue, observation = agent.generate_dialogue_response(observation)\n",
" print(observation)\n",
" # observation = f\"{agent.name} said {reaction}\"\n",
" if not stay_in_dialogue:\n",
" break_dialogue = True \n",
" if break_dialogue:\n",
" break\n",
" turns += 1\n"
]
},
{
"cell_type": "code",
"execution_count": 66,
"id": "d5462b14-218e-4d85-b035-df57ea8e0f80",
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Eve said \"Of course, Tommie. I'd be happy to share my story with you and offer any advice I can.\"\n",
"There is no mention of Eve in the given context, so it is unclear what the conversation is about.\n",
"Eve said \"That's a great place to start, Tommie. I actually majored in marketing in college and landed my first job in the industry as a marketing assistant. From there, I worked my way up through various roles and eventually ended up in my current position. What about you? What led you to pursue a career in this field?\"\n"
]
}
],
"source": [
"agents = [tommie, eve]\n",
"run_conversation(agents, \"Tommie said: Hi, Eve. Thanks for agreeing to share your story with me and give me advice. Maybe we can start with how you got into the industry.\")"
]
},
{
"cell_type": "markdown",
"id": "1b28fe80-03dc-4399-961d-6e9ee1980216",
"metadata": {
"tags": []
},
"source": [
"## Let's interview our agents after their conversation\n",
"\n",
"Since the generative agents retain their memories from the day, we can ask them about their plans, conversations, and other memoreis."
]
},
{
"cell_type": "code",
"execution_count": 67,
"id": "c4d252f3-fcc1-474c-846e-a7605a6b4ce7",
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Name: Tommie (age: 25)\n",
"Innate traits: anxious, likes design\n",
"Tommie is a hopeful and determined individual who is actively searching for job openings. He is also sentimental and remembers his childhood dog, Bruno. Tommie can become frustrated and discouraged at times during his job search but is productive and accomplished when updating his resume and cover letter. He takes breaks when needed and tries to stay positive, even when he receives no job offers. Tommie enjoys design, especially graphic design, and finds comfort in making coffee.\n"
]
}
],
"source": [
"# We can see how the agents have \n",
"print(tommie.get_summary(force_refresh=True))"
]
},
{
"cell_type": "code",
"execution_count": 68,
"id": "c04db9a4",
"metadata": {
"tags": []
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"Name: Eve (age: 34)\n",
"Innate traits: curious, helpful\n",
"Eve is a helpful and active individual who enjoys playing tennis and eating porridge. She is attentive and curious, often observing and engaging in conversations with colleagues. She is willing to share her experiences and offer advice, and is considerate of others' comfort levels in social situations.\n"
]
}
],
"source": [
"# We can see a current \"Summary\" of a character based on their own perception of self\n",
"print(eve.get_summary(force_refresh=True))"
]
},
{
"cell_type": "code",
"execution_count": 69,
"id": "71762558-8fb6-44d7-8483-f5b47fb2a862",
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"'Tommie said \"I\\'m sorry, I don\\'t know who Eve is. I haven\\'t had a conversation with her.\"'"
]
},
"execution_count": 69,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"interview_agent(tommie, \"How was your conversation with Eve?\")"
]
},
{
"cell_type": "code",
"execution_count": 70,
"id": "085af3d8-ac21-41ea-8f8b-055c56976a67",
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"'Eve said \"It was great! Tommie seems really interested in learning more about the industry and he\\'s asking all the right questions. I\\'m happy to help him out.\"'"
]
},
"execution_count": 70,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"interview_agent(eve, \"How was your conversation with Tommie?\")"
]
},
{
"cell_type": "code",
"execution_count": 71,
"id": "5b439f3c-7849-4432-a697-2bcc85b89dae",
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"'Eve said \"I think I covered most of what I wanted to share with Tommie, but if there\\'s anything else he needs advice on, I\\'m happy to help. Did you have any specific questions in mind, Person A?\"'"
]
},
"execution_count": 71,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"interview_agent(eve, \"What do you wish you would have said to Tommie?\")"
]
},
{
"cell_type": "code",
"execution_count": 72,
"id": "526e8863-8b32-4216-8e61-2dfe82e3fb47",
"metadata": {
"tags": []
},
"outputs": [
{
"data": {
"text/plain": [
"'Tommie said \"I actually forgot to buy coffee filters, so I had to improvise with a paper towel. It wasn\\'t the best cup of coffee, but it did the job.\"'"
]
},
"execution_count": 72,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"interview_agent(tommie, \"What happened with your coffee this morning?\")"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a17ff5bc-5ad9-4184-8f80-33643e06c589",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.9.1"
}
},
"nbformat": 4,
"nbformat_minor": 5
}