LangChainによるGenerative Agents: 生成エージェントの実装 その1
LangChain0.0.42で実装された、TimeWeightedVectorStoreRetrieverのチュートリアルの前半部分をgoogl colabで試してみました。行動を性格に反映することで、より生き生きとした会話が楽しめるようになっています。設定やストーリーを色々かえて試してみると楽しそうです。お試しあれ
# ライブラリーのインストール
!pip install openai > /dev/null
!pip install langchain > /dev/null
!pip install faiss-cpu > /dev/null
!pip install termcolor > /dev/null
!pip install tiktoken > /dev/null
import os
os.environ["OPENAI_API_KEY"] = "YOUR OPENAI_API_KEY"
USER_NAME = "hamachi" # エージェントにインタビューするときに使う名前
LLM = ChatOpenAI(max_tokens=1500, model="gpt-4") # Can be any LLM you want.
Generative Agentのメモリ コンポーネント
生成エージェントの記憶とその動作への影響について説明します。メモリは、次の 2 つの点で標準の LangChain チャット メモリとは異なります。
1.メモリ形成
観察 - 仮想世界との対話、相互作用から、自己または他者について観察
振り返り - 核となる記憶を再浮上させ、要約する
2.メモリの取得
メモリは、重要度、新しさ、重要度の加重合計を使用して取得されます。
メモリのライフサイクル
エージェントによる観察時:
言語モデルは、記憶の重要性をスコア付けします (平凡な場合は 1、心に訴えるものは 10)
観察結果と重要性は、TimeWeightedVectorStoreRetriever によってlast_accessed_timeを使ってドキュメント内に保存される
エージェントが観察に応答する場合:
顕著性、新しさ、および重要性に基づいてドキュメントをフェッチするレトリバー用のクエリを生成
取得した情報を要約
使用したドキュメントのlast_accessed_timeを更新。
class GenerativeAgent(BaseModel):
"""記憶力と生来の性格を持つキャラクター"""
name: str
age: int
traits: str
"""変えたくないキャラクターの特徴"""
status: str
"""Current activities of the character."""
llm: BaseLanguageModel
memory_retriever: TimeWeightedVectorStoreRetriever
"""キャラクターの現在の活動状況"""
verbose: bool = False
reflection_threshold: Optional[float] = None
"""記憶の「importance:重要度」の合計が上記の閾値を超えたら、立ち止まって振り返る。"""
current_plan: List[str] = []
"""エージェントの現在の方針"""
# メタ情報
summary: str = "" #: :meta private:
summary_refresh_seconds: int= 3600 #: :meta private:
last_refreshed: datetime =Field(default_factory=datetime.now) #: :meta private:
daily_summaries: List[str] #: :meta private:
memory_importance: float = 0.0 #: :meta private:
max_tokens_limit: int = 1200 #: :meta private:
class Config:
"""Configuration for this pydantic object."""
arbitrary_types_allowed = True
@staticmethod
def _parse_list(text: str) -> List[str]:
"""改行で区切られた文字列をリストに分割"""
lines = re.split(r'\n', text.strip())
return [re.sub(r'^\s*\d+\.\s*', '', line).strip() for line in lines]
def _compute_agent_summary(self):
""""""
prompt = PromptTemplate.from_template(
"How would you summarize {name}'s core characteristics given the"
+" following statements:\n"
+"{related_memories}"
+ "Do not embellish."
+"\n\nSummary: "
)
# 登場人物の核となる特徴を考察
relevant_memories = self.fetch_memories(f"{self.name}'s core characteristics")
relevant_memories_str = "\n".join([f"{mem.page_content}" for mem in relevant_memories])
chain = LLMChain(llm=self.llm, prompt=prompt, verbose=self.verbose)
return chain.run(name=self.name, related_memories=relevant_memories_str).strip()
def _get_topics_of_reflection(self, last_k: int = 50) -> Tuple[str, str, str]:
"""Return the 3 most salient high-level questions about recent observations."""
prompt = PromptTemplate.from_template(
"{observations}\n\n"
+ "Given only the information above, what are the 3 most salient in Japanese"
+ " high-level questions we can answer about the subjects in the statements?"
+ " Provide each question on a new line.\n\n"
)
reflection_chain = LLMChain(llm=self.llm, prompt=prompt, verbose=self.verbose)
observations = self.memory_retriever.memory_stream[-last_k:]
observation_str = "\n".join([o.page_content for o in observations])
result = reflection_chain.run(observations=observation_str)
return self._parse_list(result)
def _get_insights_on_topic(self, topic: str) -> List[str]:
"""Generate 'insights' on a topic of reflection, based on pertinent memories."""
prompt = PromptTemplate.from_template(
"Statements about {topic}\n"
+"{related_statements}\n\n"
+ "What 5 high-level insights can you infer from the above statements?"
+ " (example format: insight in Japanese(because of 1, 5, 3))"
)
related_memories = self.fetch_memories(topic)
related_statements = "\n".join([f"{i+1}. {memory.page_content}"
for i, memory in
enumerate(related_memories)])
reflection_chain = LLMChain(llm=self.llm, prompt=prompt, verbose=self.verbose)
result = reflection_chain.run(topic=topic, related_statements=related_statements)
# TODO: Parse the connections between memories and insights
return self._parse_list(result)
def pause_to_reflect(self) -> List[str]:
"""Reflect on recent observations and generate 'insights'."""
print(colored(f"Character {self.name} is reflecting", "blue"))
new_insights = []
topics = self._get_topics_of_reflection()
for topic in topics:
insights = self._get_insights_on_topic( topic)
for insight in insights:
self.add_memory(insight)
new_insights.extend(insights)
return new_insights
def _score_memory_importance(self, memory_content: str, weight: float = 0.15) -> float:
"""Score the absolute importance of the given memory."""
# A weight of 0.25 makes this less important than it
# would be otherwise, relative to salience and time
prompt = PromptTemplate.from_template(
"On the scale of 1 to 10, where 1 is purely mundane"
+" (e.g., 歯磨き, ベッドメーキング) and 10 is"
+ " extremely poignant (e.g., 別れ話, 大学入学試験の合否"
+ " ), rate the likely poignancy of the"
+ " following piece of memory. Respond with a single integer."
+ "\nMemory: {memory_content}"
+ "\nRating: "
)
chain = LLMChain(llm=self.llm, prompt=prompt, verbose=self.verbose)
score = chain.run(memory_content=memory_content).strip()
match = re.search(r"^\D*(\d+)", score)
if match:
return (float(score[0]) / 10) * weight
else:
return 0.0
def add_memory(self, memory_content: str) -> List[str]:
"""Add an observation or memory to the agent's memory."""
importance_score = self._score_memory_importance(memory_content)
self.memory_importance += importance_score
document = Document(page_content=memory_content, metadata={"importance": importance_score})
result = self.memory_retriever.add_documents([document])
# After an agent has processed a certain amount of memories (as measured by
# aggregate importance), it is time to reflect on recent events to add
# more synthesized memories to the agent's memory stream.
if (self.reflection_threshold is not None
and self.memory_importance > self.reflection_threshold
and self.status != "Reflecting"):
old_status = self.status
self.status = "Reflecting"
self.pause_to_reflect()
# Hack to clear the importance from reflection
self.memory_importance = 0.0
self.status = old_status
return result
def fetch_memories(self, observation: str) -> List[Document]:
"""Fetch related memories."""
return self.memory_retriever.get_relevant_documents(observation)
def get_summary(self, force_refresh: bool = False) -> str:
"""Return a descriptive summary of the agent."""
current_time = datetime.now()
since_refresh = (current_time - self.last_refreshed).seconds
if not self.summary or since_refresh >= self.summary_refresh_seconds or force_refresh:
self.summary = self._compute_agent_summary()
self.last_refreshed = current_time
return (
f"Name: {self.name} (age: {self.age})"
+f"\nInnate traits: {self.traits}"
+f"\n{self.summary}"
)
def get_full_header(self, force_refresh: bool = False) -> str:
"""Return a full header of the agent's status, summary, and current time."""
summary = self.get_summary(force_refresh=force_refresh)
current_time_str = datetime.now().strftime("%B %d, %Y, %I:%M %p")
return f"{summary}\nIt is {current_time_str}.\n{self.name}'s status: {self.status}"
def _get_entity_from_observation(self, observation: str) -> str:
prompt = PromptTemplate.from_template(
"What is the observed entity in the following observation? {observation}"
+"\nEntity="
)
chain = LLMChain(llm=self.llm, prompt=prompt, verbose=self.verbose)
return chain.run(observation=observation).strip()
def _get_entity_action(self, observation: str, entity_name: str) -> str:
prompt = PromptTemplate.from_template(
"What is the {entity} doing in the following observation? {observation}"
+"\nThe {entity} is"
)
chain = LLMChain(llm=self.llm, prompt=prompt, verbose=self.verbose)
return chain.run(entity=entity_name, observation=observation).strip()
def _format_memories_to_summarize(self, relevant_memories: List[Document]) -> str:
content_strs = set()
content = []
for mem in relevant_memories:
if mem.page_content in content_strs:
continue
content_strs.add(mem.page_content)
created_time = mem.metadata["created_at"].strftime("%B %d, %Y, %I:%M %p")
content.append(f"- {created_time}: {mem.page_content.strip()}")
return "\n".join([f"{mem}" for mem in content])
def summarize_related_memories(self, observation: str) -> str:
"""Summarize memories that are most relevant to an observation."""
entity_name = self._get_entity_from_observation(observation)
entity_action = self._get_entity_action(observation, entity_name)
q1 = f"What is the relationship between {self.name} and {entity_name} in Japanese"
relevant_memories = self.fetch_memories(q1) # Fetch memories related to the agent's relationship with the entity
q2 = f"{entity_name} is {entity_action}"
relevant_memories += self.fetch_memories(q2) # Fetch things related to the entity-action pair
context_str = self._format_memories_to_summarize(relevant_memories)
prompt = PromptTemplate.from_template(
"{q1}?\nContext from memory:\n{context_str}\nRelevant context: "
)
chain = LLMChain(llm=self.llm, prompt=prompt, verbose=self.verbose)
return chain.run(q1=q1, context_str=context_str.strip()).strip()
def _get_memories_until_limit(self, consumed_tokens: int) -> str:
"""Reduce the number of tokens in the documents."""
result = []
for doc in self.memory_retriever.memory_stream[::-1]:
if consumed_tokens >= self.max_tokens_limit:
break
consumed_tokens += self.llm.get_num_tokens(doc.page_content)
if consumed_tokens < self.max_tokens_limit:
result.append(doc.page_content)
return "; ".join(result[::-1])
def _generate_reaction(
self,
observation: str,
suffix: str
) -> str:
"""React to a given observation."""
prompt = PromptTemplate.from_template(
"{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: {recent_observations}"
+ "\nObservation: {observation}"
+ "\n\n" + suffix
)
agent_summary_description = self.get_summary()
relevant_memories_str = self.summarize_related_memories(observation)
current_time_str = datetime.now().strftime("%B %d, %Y, %I:%M %p")
kwargs = dict(agent_summary_description=agent_summary_description,
current_time=current_time_str,
relevant_memories=relevant_memories_str,
agent_name=self.name,
observation=observation,
agent_status=self.status)
consumed_tokens = self.llm.get_num_tokens(prompt.format(recent_observations="", **kwargs))
kwargs["recent_observations"] = self._get_memories_until_limit(consumed_tokens)
action_prediction_chain = LLMChain(llm=self.llm, prompt=prompt)
result = action_prediction_chain.run(**kwargs)
return result.strip()
def generate_reaction(self, observation: str) -> Tuple[bool, str]:
"""React to a given observation."""
call_to_action_template = (
"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"
)
full_result = self._generate_reaction(observation, call_to_action_template)
result = full_result.strip().split('\n')[0]
self.add_memory(f"{self.name} observed {observation} and reacted by {result}")
if "REACT:" in result:
reaction = result.split("REACT:")[-1].strip()
return False, f"{self.name} {reaction}"
if "SAY:" in result:
said_value = result.split("SAY:")[-1].strip()
return True, f"{self.name} said {said_value}"
else:
return False, result
def generate_dialogue_response(self, observation: str) -> Tuple[bool, str]:
"""React to a given observation."""
# 会話を終わらせる場合は GOODBYE:会話を続ける場合は: SAY: "what to say next"(次に何を言うか)
call_to_action_template = (
'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'
)
full_result = self._generate_reaction(observation, call_to_action_template)
result = full_result.strip().split('\n')[0]
if "GOODBYE:" in result:
farewell = result.split("GOODBYE:")[-1].strip()
self.add_memory(f"{self.name} observed {observation} and said {farewell}")
return False, f"{self.name} said {farewell}"
if "SAY:" in result:
response_text = result.split("SAY:")[-1].strip()
self.add_memory(f"{self.name} observed {observation} and said {response_text}")
return True, f"{self.name} said {response_text}"
else:
return False, result
生成エージェントを作成
import math
import faiss
def relevance_score_fn(score: float) -> float:
"""Return a similarity score on a scale [0, 1]."""
# This will differ depending on a few things:
# - the distance / similarity metric used by the VectorStore
# - the scale of your embeddings (OpenAI's are unit norm. Many others are not!)
# This function converts the euclidean norm of normalized embeddings
# (0 is most similar, sqrt(2) most dissimilar)
# to a similarity function (0 to 1)
return 1.0 - score / math.sqrt(2)
def create_new_memory_retriever():
"""Create a new vector store retriever unique to the agent."""
# Define your embedding model
embeddings_model = OpenAIEmbeddings()
# Initialize the vectorstore as empty
embedding_size = 1536
index = faiss.IndexFlatL2(embedding_size)
vectorstore = FAISS(embeddings_model.embed_query, index, InMemoryDocstore({}), {}, relevance_score_fn=relevance_score_fn)
return TimeWeightedVectorStoreRetriever(vectorstore=vectorstore, other_score_keys=["importance"], k=15)
taroh = GenerativeAgent(name="Taroh",
age=25,
traits="心配性, デザインが好き", # You can add more persistent traits here
status="就職活動中", # When connected to a virtual world, we can have the characters update their status
memory_retriever=create_new_memory_retriever(),
llm=LLM,
daily_summaries = [
"新しい街に県をまたいで車でやってきたが、まだ仕事がない。."
],
reflection_threshold = 8, # we will give this a relatively low number to show how reflection works
)
print(taroh.get_summary())
太郎のメモリーに直接に書き込む
# We can give the character memories directly
taroh_memories = [
"Tarohは、子供の頃に飼っていた犬のハチを覚えています",
"Tarohは、長距離運転で疲れている",
"Tarohは、新居を見つけた",
"隣人は、猫を飼っている",
"夜は、道がうるさい",
"Tarohは、空腹だ",
"Tarohは、休息をとろうとしている",
]
for memory in taroh_memories:
taroh.add_memory(memory)
print(taroh.get_summary(force_refresh=True))
事前インタビュー
def interview_agent(agent: GenerativeAgent, message: str) -> str:
"""Help the notebook user interact with the agent."""
new_message = f"{USER_NAME} says {message}"
return agent.generate_dialogue_response(new_message)[1]
interview_agent(taroh, "'好きなこと'を教えてください")
interview_agent(taroh, "いま、一番気になることは何ですか")
interview_agent(taroh, "いま、一番気になっていることは何ですか")
それでは、太郎さんに一日の生活を始めてもらいましょう。
observations = [
"Tarohは、窓の外の騒がしい工事現場の音で目を覚ました。",
"Tarohは、ベッドから起き上がり,コーヒーを淹れに台所へ向かいます",
"Tarohは、コーヒーのフィルターを買い忘れたことに気づき、引っ越しの段ボール箱をあさって探し始める。",
"Tarohは、やっとフィルターを見つけ、コーヒーを淹れる。",
"コーヒーの味は苦く、Tarohは、もっと良い銘柄を買わなかったことを後悔する。",
"Tarohは、メールをチェックし,まだ仕事の依頼がないことを確認する。",
"Tarohは、履歴書とカバーレターの更新に時間を費やす。",
"Tarohは、街に出て、求人情報を探すことにする。",
"Tarohは、就職説明会の看板を見て,参加することにした。",
"Tarohは、就職説明会に参加することにした。",
"就職説明会場に入場するための列は長く,Tarohは、1時間待つことになる",
"Tarohは、就職説明会で何人かの雇用主候補に会うが,内定はもらえない",
"Tarohは、がっかりした気持ちで就職説明会場を後にする。",
"Tarohは、昼食を取るために地元の食堂に立ち寄ります。",
"食堂のサービスが遅く,Tarohは食事にありつくのに30分も待たされた。",
"Tarohは、隣のテーブルで求人の会話をしているのを耳にする。",
"Tarohは、隣のテーブルの人に求人について尋ね、その会社についての情報を得る。",
"Tarohは、その仕事に応募することを決め、履歴書とカバーレターを送る。",
"Tarohは、求人情報の検索を続け、いくつかの地元の企業に履歴書を郵送する",
"Tarohは、仕事探しの合間を縫って,近くの公園に散歩に行く",
"犬が近づいてきてTarohの足を舐めるので、数分間撫でてやる",
"Tarohは、フリスビーで遊んでいるグループを見つけて、一緒に遊ぶことにした。",
"Tarohは、フリスビーで遊んでいる",
"Tarohは、フリスビーで楽しく遊ぶが,フリスビーに顔をぶつけて,鼻をけがしてしまう。",
"Tarohは、フリスビーで遊んでいる人たちと友達になる。",
"Tarohは、自分のアパートに戻って少し休む。",
"カラスがアパートの外のゴミ袋を破ってしまい,ゴミが床一面に散らばっている。",
"Tarohは、就職活動に焦りを感じ始める",
"Tarohは、親友に苦悩を吐き出すために電話をかける。",
"Tarohの友人は、励ましの言葉をかけ、努力を続けるように言う。",
"Tarohは、友人と話した後、少し気分が良くなった。",
]
# それでは、タローを送り出して、その変化を観察します。
for i, observation in enumerate(observations):
_, reaction = taroh.generate_reaction(observation)
print(colored(observation, "green"), reaction)
if ((i+1) % 20) == 0:
print('*'*40)
print(colored(f"After {i+1} observations, Taroh's summary is:\n{taroh.get_summary(force_refresh=True)}", "blue"))
print('*'*40)
一日を終えてのインタビュー
interview_agent(taroh, "あれ、鼻を怪我していませんか?")
interview_agent(taroh, "今日はどんな一日だったか教えてください")
interview_agent(taroh, "就職活動の進捗はどうですか?")
次回の予定
生成エージェント1、インタビュー1の簡単な構成で例題通り動作を見てみました。プロンプトだけでロールプレイをしてもらうよりも、より生き生きとした感じの会話になっているのではないでしょうか。
次回は、生成エージェントを複数人にして、設定も工夫しながら楽しんでみたいと思います。
この記事が参加している募集
この記事が気に入ったらサポートをしてみませんか?