diff --git a/examples/st_game/memory/agent_memory.py b/examples/st_game/memory/agent_memory.py
new file mode 100644
index 000000000..a56100ee7
--- /dev/null
+++ b/examples/st_game/memory/agent_memory.py
@@ -0,0 +1,318 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# @Desc : BasicMemory,AgentMemory实现
+
+from metagpt.memory.memory import Memory
+from metagpt.schema import Message
+import json
+from datetime import datetime
+
+
+class BasicMemory(Message):
+
+ def __init__(self, memory_id: str, memory_count: int, type_count: int, memory_type: str, depth: int,
+ created: datetime, expiration: datetime,
+ subject: str, predicate: str, object: str,
+ content: str, embedding_key: str, poignancy: int, keywords: list, filling: list,
+ cause_by = ""):
+ """
+ BasicMemory继承于MG的Message类,其中content属性替代description属性
+ Message类中对于Chat类型支持的非常好,对于Agent个体的Perceive,Reflection,Plan支持的并不多
+ 在Type设计上,我们延续GA的三个种类,但是对于Chat种类的对话进行特别设计(具体怎么设计还没想好)
+ """
+ super().__init__(content,cause_by=cause_by)
+ """
+ 从父类中继承的属性
+ content: str # 记忆描述
+ cause_by: Type["Action"] = field(default="") # 触发动作,只在Type为chat时初始化
+ cause_by 接受一个Action类,在此项目中,每个Agent需要有一个基础动作[Receive] 用于接受假对话Message;而每个Agent需要有独一无二的动作类,用以接受真对话Message
+ """
+ self.memory_id: str = memory_id # 记忆ID
+ self.memory_count: int = memory_count # 第几个记忆,实际数值与Memory相等
+ self.type_count: int = type_count # 第几种记忆,类型为整数(具体不太理解如何生成的)
+ self.memory_type: str = memory_type # 记忆类型,包含 event,thought,chat三种类型
+ self.depth: str = depth # 记忆深度,类型为整数
+
+ self.created: datetime = created # 创建时间
+ self.expiration: datetime = expiration # 记忆失效时间,默认为空()
+ self.last_accessed: datetime = created # 上一次调用的时间,初始化时候与self.created一致
+
+ self.subject: str = subject # 主语
+ self.predicate: str = predicate # 谓语
+ self.object: str = object # 宾语
+
+ self.embedding_key: str = embedding_key # 内容与self.content一致
+ self.poignancy: int = poignancy # importance值
+ self.keywords: list = keywords # keywords
+ self.filling: list = filling # 装的与之相关联的memory_id的列表
+
+ def save_to_dict(self) -> dict:
+ """
+ 将MemoryBasic类转化为字典,用于存储json文件
+ 这里需要注意,cause_by跟GA不兼容,所以需要做一个格式转换
+ """
+ memory_dict = dict()
+ node_id = self.memory_id
+
+ memory_dict[node_id] = dict()
+ memory_dict[node_id]["node_count"] = self.memory_count
+ memory_dict[node_id]["type_count"] = self.type_count
+ memory_dict[node_id]["type"] = self.type
+ memory_dict[node_id]["depth"] = self.depth
+
+ memory_dict[node_id]["cmemory_dicteated"] = self.created.strftime('%Y-%m-%d %H:%M:%S')
+ memory_dict[node_id]["expiration"] = None
+ if self.expiration:
+ memory_dict[node_id]["expiration"] = (self.expiration
+ .strftime('%Y-%m-%d %H:%M:%S'))
+
+ memory_dict[node_id]["subject"] = self.subject
+ memory_dict[node_id]["predicate"] = self.predicate
+ memory_dict[node_id]["object"] = self.object
+
+ memory_dict[node_id]["description"] = self.description
+ memory_dict[node_id]["embedding_key"] = self.embedding_key
+ memory_dict[node_id]["poignancy"] = self.poignancy
+ memory_dict[node_id]["keywords"] = list(self.keywords)
+ memory_dict[node_id]["filling"] = self.filling
+ if self.cause_by:
+ memory_dict[node_id]["cause_by"] = self.cause_by
+
+ return memory_dict
+
+class AgentMemory(Memory):
+ """
+ GA中主要存储三种JSON
+ 1. embedding.json (Dict embedding_key:embedding)
+ 2. Node.json (Dict Node_id:Node)
+ 3. kw_strength.json
+ """
+ def __init__(self, memory_saved: str):
+ """
+ AgentMemory类继承自Memory类,重写storage替代GA中id_to_node,一方面存储所有信息,一方面作为JSON转化
+ index存储与不同Agent的chat信息
+ @李嵩@张凯 这里的storage是List,你们需要写一个JSON转化器,将List修改为node.json一致的格式
+ """
+ super.__init__()
+ self.storage: list[BasicMemory] = [] # 重写Stroage,存储BasicMemory所有节点
+ self.event_list = [] # 存储event记忆
+ self.thought_list = [] # 存储thought记忆
+
+ self.event_keywords = dict() # 存储keywords
+ self.thought_keywords = dict()
+ self.chat_keywords = dict()
+
+ self.kw_strength_event = dict() # 关键词影响存储
+ self.kw_strength_thought = dict()
+
+ self.load(memory_saved)
+
+
+ def save(self,memory_saved:str):
+ """
+ 将MemormyBasic类存储为Nodes.json形式。复现GA中的Kw Strength.json形式
+ 这里添加一个路径即可
+ """
+
+ memory_json = dict()
+ for i in range(len(self.storage)):
+ memory_node = self.storage[i]
+ memory_json.update(memory_node)
+ with open(memory_saved+"/nodes.json", "w") as outfile:
+ json.dump(memory_json, outfile)
+
+ with open(memory_saved+"/embeddings.json", "w") as outfile:
+ json.dump(self.embeddings, outfile)
+
+ strength_json = dict()
+ strength_json["kw_strength_event"] = self.kw_strength_event
+ strength_json["kw_strength_thought"] = self.kw_strength_thought
+ with open(memory_saved+"/kw_strength.json", "w") as outfile:
+ json.dump(strength_json, outfile)
+
+
+ def load(self,memory_saved:str):
+ """
+ 将GA的JSON解析,填充到AgentMemory类之中
+ """
+ self.embeddings = json.load(open(memory_saved + "/embeddings.json"))
+ memory_load = json.load(open(memory_saved + "/nodes.json"))
+ for count in range(len(memory_load.keys())):
+ node_id = f"node_{str(count+1)}"
+ node_details = memory_load[node_id]
+ node_type = node_details["type"]
+ created = datetime.datetime.strptime(node_details["created"],
+ '%Y-%m-%d %H:%M:%S')
+ expiration = None
+ if node_details["expiration"]:
+ expiration = datetime.datetime.strptime(node_details["expiration"],
+ '%Y-%m-%d %H:%M:%S')
+
+ if node_details["cause_by"]:
+ cause_by = node_details["cause_by"]
+
+ s = node_details["subject"]
+ p = node_details["predicate"]
+ o = node_details["object"]
+
+ description = node_details["description"]
+ embedding_pair = (node_details["embedding_key"],
+ self.embeddings[node_details["embedding_key"]])
+ poignancy =node_details["poignancy"]
+ keywords = set(node_details["keywords"])
+ filling = node_details["filling"]
+
+ if node_type == "event":
+ self.add_event(created, expiration, s, p, o,
+ description, keywords, poignancy, embedding_pair, filling)
+ elif node_type == "chat":
+ self.add_chat(created, expiration, s, p, o,
+ description, keywords, poignancy, embedding_pair, filling,cause_by)
+ elif node_type == "thought":
+ self.add_thought(created, expiration, s, p, o,
+ description, keywords, poignancy, embedding_pair, filling)
+
+ strength_keywords_load = json.load(open(memory_saved + "/kw_strength.json"))
+ if strength_keywords_load["kw_strength_event"]:
+ self.kw_strength_event = strength_keywords_load["kw_strength_event"]
+ if strength_keywords_load["kw_strength_thought"]:
+ self.kw_strength_thought = strength_keywords_load["kw_strength_thought"]
+
+
+ def add(self, memory_basic: BasicMemory):
+ """
+ Add a new message to storage, while updating the index
+ 重写add方法,修改原有的Message类为BasicMemory类,并添加不同的记忆类型添加方式
+ """
+ if memory_basic in self.storage:
+ return
+ self.storage.append(memory_basic)
+ if memory_basic.cause_by:
+ self.index[memory_basic.cause_by][0:0] = [memory_basic]
+ return
+ if memory_basic.type == "thought":
+ self.thought_list[0:0] = [memory_basic]
+ return
+ if memory_basic.type == "event":
+ self.event_list[0:0] = [memory_basic]
+
+
+ def add_chat(self, created, expiration, s, p, o,
+ content, keywords, poignancy,
+ embedding_pair, filling,
+ cause_by):
+ """
+ 调用add方法,初始化chat,在创建的时候就需要调用embeeding函数
+ """
+ memory_count = len(self.storage) + 1
+ type_count = len(self.thought_list) + 1
+ memory_type = "chat"
+ memory_id = f"memory_{str(memory_count)}"
+ depth = 1
+
+ memory_node = BasicMemory(memory_id, memory_count, type_count, memory_type, depth,
+ created, expiration,
+ s, p ,o,
+ content, embedding_pair[0],
+ poignancy, keywords, filling,
+ cause_by)
+
+ keywords = [i.lower() for i in keywords]
+ for kw in keywords:
+ if kw in self.chat_keywords:
+ self.chat_keywords[kw][0:0] = [memory_node]
+ else:
+ self.chat_keywords[kw] = [memory_node]
+
+ self.add(memory_node)
+
+ self.embeddings[embedding_pair[0]] = embedding_pair[1]
+ return memory_node
+
+
+ def add_thought(self, created, expiration, s, p, o,
+ content, keywords, poignancy,
+ embedding_pair, filling):
+ """
+ 调用add方法,初始化thought
+ """
+ memory_count = len(self.storage) + 1
+ type_count = len(self.thought_list) + 1
+ memory_type = "event"
+ memory_id = f"memory_{str(memory_count)}"
+ depth = 1
+
+ try:
+ if filling:
+ depth_list = [memory_node.depth for memory_node in self.storage if memory_node.memory_id in filling ]
+ depth += max(depth_list)
+ except:
+ pass
+
+ memory_node = BasicMemory(memory_id, memory_count, type_count, memory_type, depth,
+ created, expiration,
+ s, p ,o,
+ content, embedding_pair[0],
+ poignancy, keywords, filling)
+
+ keywords = [i.lower() for i in keywords]
+ for kw in keywords:
+ if kw in self.thought_keywords:
+ self.thought_keywords[kw][0:0] = [memory_node]
+ else:
+ self.thought_keywords[kw] = [memory_node]
+
+ self.add(memory_node)
+
+ if f"{p} {o}" != "is idle":
+ for kw in keywords:
+ if kw in self.kw_strength_thought:
+ self.kw_strength_thought[kw] += 1
+ else:
+ self.kw_strength_thought[kw] = 1
+
+ self.embeddings[embedding_pair[0]] = embedding_pair[1]
+ return memory_node
+
+
+ def add_event(self, created, expiration, s, p, o,
+ content, keywords, poignancy,
+ embedding_pair, filling):
+ """
+ 调用add方法,初始化event
+ """
+ memory_count = len(self.storage) + 1
+ type_count = len(self.event_list) + 1
+ memory_type = "event"
+ memory_id = f"memory_{str(memory_count)}"
+ depth = 0
+
+ if "(" in content:
+ content = (" ".join(content.split()[:3])
+ + " "
+ + content.split("(")[-1][:-1])
+
+ memory_node = BasicMemory(memory_id, memory_count, type_count, memory_type, depth,
+ created, expiration,
+ s, p ,o,
+ content, embedding_pair[0],
+ poignancy, keywords, filling)
+
+ keywords = [i.lower() for i in keywords]
+ for kw in keywords:
+ if kw in self.event_keywords:
+ self.event_keywords[kw][0:0] = [memory_node]
+ else:
+ self.event_keywords[kw] = [memory_node]
+
+ self.add(memory_node)
+
+ if f"{p} {o}" != "is idle":
+ for kw in keywords:
+ if kw in self.kw_strength_event:
+ self.kw_strength_event[kw] += 1
+ else:
+ self.kw_strength_event[kw] = 1
+
+ self.embeddings[embedding_pair[0]] = embedding_pair[1]
+ return memory_node
diff --git a/examples/st_game/memory/associative_memory.py b/examples/st_game/memory/associative_memory.py
deleted file mode 100644
index ef1514398..000000000
--- a/examples/st_game/memory/associative_memory.py
+++ /dev/null
@@ -1,9 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-# @Desc : associative_memory to store conversation、plan detail、reflection result and so on.
-
-from metagpt.memory.memory import Memory
-
-
-class AssociativeMemory(Memory):
- pass
diff --git a/examples/st_game/memory/retrieve.py b/examples/st_game/memory/retrieve.py
new file mode 100644
index 000000000..97eb3b6f0
--- /dev/null
+++ b/examples/st_game/memory/retrieve.py
@@ -0,0 +1,142 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# @Desc : Retrieve函数实现
+
+import datetime
+from numpy import dot
+from numpy.linalg import norm
+from examples.st_game.memory.agent_memory import AgentMemory, BasicMemory
+from utils.utils import embedding_tools
+
+
+def agent_retrieve(agent_memory: AgentMemory, curr_time: datetime.datetime, memory_forget: float, query: str, n: int = 30, topk: int = 4) -> list[BasicMemory]:
+ """
+ Retrieve需要集合Role使用,原因在于Role才具有AgentMemory,scratch
+ 逻辑:Role调用该函数,self._rc.AgentMemory,self._rc.scratch.curr_time,self._rc.scratch.memory_forget
+ 输入希望查询的内容与希望回顾的条数,返回TopK条高分记忆,即List[BasicMemory]
+
+ Score_lists示例
+ {
+ "memory": memories[i], BasicMemory类
+ "importance": memories[i].poignancy
+ "recency": 衰减因子计算结果
+ "relevance": 搜索结果
+ }
+ """
+ memories = agent_memory.storage
+ sorted_memories = sorted(memories, key=lambda memory_node: memory_node.last_accessed_time, reverse=True)
+ memories = sorted_memories[:n] if len(sorted_memories) >= n else sorted_memories
+
+ score_list = []
+ score_list = extract_importance(memories, score_list)
+ score_list = extract_recency(curr_time, memory_forget, score_list)
+ score_list = extract_relevance(query, score_list)
+ score_list = normalize_score_floats(score_list, 0, 1)
+
+ total_dict = {}
+ gw = [1, 1, 1] # 三个因素的权重,重要性,近因性,相关性
+ for i in range(len(score_list)):
+ total_score = (score_list[i]['importance'] * gw[0] +
+ score_list[i]['recency'] * gw[1] +
+ score_list[i]['relevance'] * gw[2]
+ )
+ total_dict[score_list[i]['memory']] = total_score
+
+ result = top_highest_x_values(total_dict, topk)
+
+ return result
+
+
+def top_highest_x_values(d, x):
+ """
+ 输入字典,Topx
+ 返回以字典值排序,字典键组成的List[BasicMemory]
+ """
+ top_v = [item[0] for item in sorted(d.items(), key=lambda item: item[1], reverse=True)[:x]]
+ return top_v
+
+
+def extract_importance(memories, score_list):
+ """
+ 抽取重要性
+ """
+ for i in range(len(memories)):
+ score = {"memory": memories[i],
+ "importance": memories[i].poignancy
+ }
+ score_list.append(score)
+ return score_list
+
+
+def extract_relevance(query, score_list):
+ """
+ 抽取相关性
+ """
+ query_embedding = embedding_tools(query)
+ # 进行
+ for i in range(len(score_list)):
+ result = cos_sim(score_list[i]["memory"].embedding_key, query_embedding)
+ score_list[i]['relevance'] = result
+
+ return score_list
+
+
+def extract_recency(curr_time, memory_forget, score_list):
+ """
+ 抽取近因性,目前使用的现实世界过一天走一个衰减因子
+ """
+ for i in range(len(score_list)):
+ day_count = (curr_time - score_list[i]['memory'].created).days
+ score_list[i]['recency'] = memory_forget**day_count
+ return score_list
+
+
+def cos_sim(a, b):
+ """
+ 计算余弦相似度
+ """
+ return dot(a, b) / (norm(a) * norm(b))
+
+
+def normalize_list_floats(single_list, target_min, target_max):
+ """
+ 单个列表归一化
+ """
+ min_val = min(single_list)
+ max_val = max(single_list)
+ range_val = max_val - min_val
+
+ if range_val == 0:
+ for i in range(len(single_list)):
+ single_list[i] = (target_max - target_min) / 2
+ else:
+ for i in range(len(single_list)):
+ single_list[i] = ((single_list[i] - min_val) * (target_max - target_min)
+ / range_val + target_min)
+ return single_list
+
+
+def normalize_score_floats(score_list, target_min, target_max):
+ """
+ 整体归一化
+ """
+ importance_list = []
+ relevance_list = []
+ recency_list = []
+
+ for i in range(len(score_list)):
+ importance_list.append(score_list[i]['importance'])
+ relevance_list.append(score_list[i]['relevance'])
+ recency_list.append(score_list[i]['recency'])
+
+ # 进行归一化操作
+ importance_list = normalize_list_floats(importance_list, target_min, target_max)
+ relevance_list = normalize_list_floats(relevance_list, target_min, target_max)
+ recency_list = normalize_list_floats(recency_list, target_min, target_max)
+
+ for i in range(len(score_list)):
+ score_list[i]['importance'] = importance_list[i]
+ score_list[i]['relevance'] = relevance_list[i]
+ score_list[i]['recency'] = recency_list[i]
+
+ return score_list
diff --git a/examples/st_game/memory/scratch.py b/examples/st_game/memory/scratch.py
new file mode 100644
index 000000000..d0d13002e
--- /dev/null
+++ b/examples/st_game/memory/scratch.py
@@ -0,0 +1,537 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# @Desc : Scratch类实现(角色信息类)
+
+import datetime
+import json
+import sys
+sys.path.append('../../')
+
+from ..utils.check import check_if_file_exists
+
+class Scratch:
+ def __init__(self, f_saved):
+ # 类别1:人物超参
+ self.vision_r = 4
+ self.att_bandwidth = 3
+ self.retention = 5
+
+ # 类别2:世界信息
+ self.curr_time = None
+ self.curr_tile = None
+ self.daily_plan_req = None
+
+ # 类别3:人物角色的核心身份
+ self.name = None
+ self.first_name = None
+ self.last_name = None
+ self.age = None
+ # L0 permanent core traits.
+ self.innate = None
+ # L1 stable traits.
+ self.learned = None
+ # L2 external implementation.
+ self.currently = None
+ self.lifestyle = None
+ self.living_area = None
+
+ # 类别4:旧反思变量
+ self.concept_forget = 100
+ self.daily_reflection_time = 60 * 3
+ self.daily_reflection_size = 5
+ self.overlap_reflect_th = 2
+ self.kw_strg_event_reflect_th = 4
+ self.kw_strg_thought_reflect_th = 4
+
+ # 类别5:新反思变量
+ self.recency_w = 1
+ self.relevance_w = 1
+ self.importance_w = 1
+ self.recency_decay = 0.99
+ self.importance_trigger_max = 150
+ self.importance_trigger_curr = self.importance_trigger_max
+ self.importance_ele_n = 0
+ self.thought_count = 5
+
+ # 类别6:个人计划
+ self.daily_req = []
+ self.f_daily_schedule = []
+ self.f_daily_schedule_hourly_org = []
+
+ # 类别7:当前动作
+ self.act_address = None
+ self.act_start_time = None
+ self.act_duration = None
+ self.act_description = None
+ self.act_pronunciatio = None
+ self.act_event = (self.name, None, None)
+
+ self.act_obj_description = None
+ self.act_obj_pronunciatio = None
+ self.act_obj_event = (self.name, None, None)
+
+ self.chatting_with = None
+ self.chat = None
+ self.chatting_with_buffer = dict()
+ self.chatting_end_time = None
+
+ self.act_path_set = False
+ self.planned_path = []
+
+ if check_if_file_exists(f_saved):
+ # If we have a bootstrap file, load that here.
+ scratch_load = json.load(open(f_saved))
+
+ self.vision_r = scratch_load["vision_r"]
+ self.att_bandwidth = scratch_load["att_bandwidth"]
+ self.retention = scratch_load["retention"]
+
+ if scratch_load["curr_time"]:
+ self.curr_time = datetime.datetime.strptime(scratch_load["curr_time"],
+ "%B %d, %Y, %H:%M:%S")
+ else:
+ self.curr_time = None
+ self.curr_tile = scratch_load["curr_tile"]
+ self.daily_plan_req = scratch_load["daily_plan_req"]
+
+ self.name = scratch_load["name"]
+ self.first_name = scratch_load["first_name"]
+ self.last_name = scratch_load["last_name"]
+ self.age = scratch_load["age"]
+ self.innate = scratch_load["innate"]
+ self.learned = scratch_load["learned"]
+ self.currently = scratch_load["currently"]
+ self.lifestyle = scratch_load["lifestyle"]
+ self.living_area = scratch_load["living_area"]
+
+ self.concept_forget = scratch_load["concept_forget"]
+ self.daily_reflection_time = scratch_load["daily_reflection_time"]
+ self.daily_reflection_size = scratch_load["daily_reflection_size"]
+ self.overlap_reflect_th = scratch_load["overlap_reflect_th"]
+ self.kw_strg_event_reflect_th = scratch_load["kw_strg_event_reflect_th"]
+ self.kw_strg_thought_reflect_th = scratch_load["kw_strg_thought_reflect_th"]
+
+ self.recency_w = scratch_load["recency_w"]
+ self.relevance_w = scratch_load["relevance_w"]
+ self.importance_w = scratch_load["importance_w"]
+ self.recency_decay = scratch_load["recency_decay"]
+ self.importance_trigger_max = scratch_load["importance_trigger_max"]
+ self.importance_trigger_curr = scratch_load["importance_trigger_curr"]
+ self.importance_ele_n = scratch_load["importance_ele_n"]
+ self.thought_count = scratch_load["thought_count"]
+
+ self.daily_req = scratch_load["daily_req"]
+ self.f_daily_schedule = scratch_load["f_daily_schedule"]
+ self.f_daily_schedule_hourly_org = scratch_load["f_daily_schedule_hourly_org"]
+
+ self.act_address = scratch_load["act_address"]
+ if scratch_load["act_start_time"]:
+ self.act_start_time = datetime.datetime.strptime(
+ scratch_load["act_start_time"],
+ "%B %d, %Y, %H:%M:%S")
+ else:
+ self.curr_time = None
+ self.act_duration = scratch_load["act_duration"]
+ self.act_description = scratch_load["act_description"]
+ self.act_pronunciatio = scratch_load["act_pronunciatio"]
+ self.act_event = tuple(scratch_load["act_event"])
+
+ self.act_obj_description = scratch_load["act_obj_description"]
+ self.act_obj_pronunciatio = scratch_load["act_obj_pronunciatio"]
+ self.act_obj_event = tuple(scratch_load["act_obj_event"])
+
+ self.chatting_with = scratch_load["chatting_with"]
+ self.chat = scratch_load["chat"]
+ self.chatting_with_buffer = scratch_load["chatting_with_buffer"]
+ if scratch_load["chatting_end_time"]:
+ self.chatting_end_time = datetime.datetime.strptime(
+ scratch_load["chatting_end_time"],
+ "%B %d, %Y, %H:%M:%S")
+ else:
+ self.chatting_end_time = None
+
+ self.act_path_set = scratch_load["act_path_set"]
+ self.planned_path = scratch_load["planned_path"]
+
+
+ def save(self, out_json):
+ """
+ Save persona's scratch.
+
+ INPUT:
+ out_json: The file where we wil be saving our persona's state.
+ OUTPUT:
+ None
+ """
+ scratch = dict()
+ scratch["vision_r"] = self.vision_r
+ scratch["att_bandwidth"] = self.att_bandwidth
+ scratch["retention"] = self.retention
+
+ scratch["curr_time"] = self.curr_time.strftime("%B %d, %Y, %H:%M:%S")
+ scratch["curr_tile"] = self.curr_tile
+ scratch["daily_plan_req"] = self.daily_plan_req
+
+ scratch["name"] = self.name
+ scratch["first_name"] = self.first_name
+ scratch["last_name"] = self.last_name
+ scratch["age"] = self.age
+ scratch["innate"] = self.innate
+ scratch["learned"] = self.learned
+ scratch["currently"] = self.currently
+ scratch["lifestyle"] = self.lifestyle
+ scratch["living_area"] = self.living_area
+
+ scratch["concept_forget"] = self.concept_forget
+ scratch["daily_reflection_time"] = self.daily_reflection_time
+ scratch["daily_reflection_size"] = self.daily_reflection_size
+ scratch["overlap_reflect_th"] = self.overlap_reflect_th
+ scratch["kw_strg_event_reflect_th"] = self.kw_strg_event_reflect_th
+ scratch["kw_strg_thought_reflect_th"] = self.kw_strg_thought_reflect_th
+
+ scratch["recency_w"] = self.recency_w
+ scratch["relevance_w"] = self.relevance_w
+ scratch["importance_w"] = self.importance_w
+ scratch["recency_decay"] = self.recency_decay
+ scratch["importance_trigger_max"] = self.importance_trigger_max
+ scratch["importance_trigger_curr"] = self.importance_trigger_curr
+ scratch["importance_ele_n"] = self.importance_ele_n
+ scratch["thought_count"] = self.thought_count
+
+ scratch["daily_req"] = self.daily_req
+ scratch["f_daily_schedule"] = self.f_daily_schedule
+ scratch["f_daily_schedule_hourly_org"] = self.f_daily_schedule_hourly_org
+
+ scratch["act_address"] = self.act_address
+ scratch["act_start_time"] = (self.act_start_time
+ .strftime("%B %d, %Y, %H:%M:%S"))
+ scratch["act_duration"] = self.act_duration
+ scratch["act_description"] = self.act_description
+ scratch["act_pronunciatio"] = self.act_pronunciatio
+ scratch["act_event"] = self.act_event
+
+ scratch["act_obj_description"] = self.act_obj_description
+ scratch["act_obj_pronunciatio"] = self.act_obj_pronunciatio
+ scratch["act_obj_event"] = self.act_obj_event
+
+ scratch["chatting_with"] = self.chatting_with
+ scratch["chat"] = self.chat
+ scratch["chatting_with_buffer"] = self.chatting_with_buffer
+ if self.chatting_end_time:
+ scratch["chatting_end_time"] = (self.chatting_end_time
+ .strftime("%B %d, %Y, %H:%M:%S"))
+ else:
+ scratch["chatting_end_time"] = None
+
+ scratch["act_path_set"] = self.act_path_set
+ scratch["planned_path"] = self.planned_path
+
+ with open(out_json, "w") as outfile:
+ json.dump(scratch, outfile, indent=2)
+
+
+ def get_f_daily_schedule_index(self, advance=0):
+ """
+ We get the current index of self.f_daily_schedule.
+
+ Recall that self.f_daily_schedule stores the decomposed action sequences
+ up until now, and the hourly sequences of the future action for the rest
+ of today. Given that self.f_daily_schedule is a list of list where the
+ inner list is composed of [task, duration], we continue to add up the
+ duration until we reach "if elapsed > today_min_elapsed" condition. The
+ index where we stop is the index we will return.
+
+ INPUT
+ advance: Integer value of the number minutes we want to look into the
+ future. This allows us to get the index of a future timeframe.
+ OUTPUT
+ an integer value for the current index of f_daily_schedule.
+ """
+ # We first calculate teh number of minutes elapsed today.
+ today_min_elapsed = 0
+ today_min_elapsed += self.curr_time.hour * 60
+ today_min_elapsed += self.curr_time.minute
+ today_min_elapsed += advance
+
+ x = 0
+ for task, duration in self.f_daily_schedule:
+ x += duration
+ x = 0
+ for task, duration in self.f_daily_schedule_hourly_org:
+ x += duration
+
+ # We then calculate the current index based on that.
+ curr_index = 0
+ elapsed = 0
+ for task, duration in self.f_daily_schedule:
+ elapsed += duration
+ if elapsed > today_min_elapsed:
+ return curr_index
+ curr_index += 1
+
+ return curr_index
+
+
+ def get_f_daily_schedule_hourly_org_index(self, advance=0):
+ """
+ We get the current index of self.f_daily_schedule_hourly_org.
+ It is otherwise the same as get_f_daily_schedule_index.
+
+ INPUT
+ advance: Integer value of the number minutes we want to look into the
+ future. This allows us to get the index of a future timeframe.
+ OUTPUT
+ an integer value for the current index of f_daily_schedule.
+ """
+ # We first calculate teh number of minutes elapsed today.
+ today_min_elapsed = 0
+ today_min_elapsed += self.curr_time.hour * 60
+ today_min_elapsed += self.curr_time.minute
+ today_min_elapsed += advance
+ # We then calculate the current index based on that.
+ curr_index = 0
+ elapsed = 0
+ for task, duration in self.f_daily_schedule_hourly_org:
+ elapsed += duration
+ if elapsed > today_min_elapsed:
+ return curr_index
+ curr_index += 1
+ return curr_index
+
+
+ def get_str_iss(self):
+ """
+ ISS stands for "identity stable set." This describes the commonset summary
+ of this persona -- basically, the bare minimum description of the persona
+ that gets used in almost all prompts that need to call on the persona.
+
+ INPUT
+ None
+ OUTPUT
+ the identity stable set summary of the persona in a string form.
+ EXAMPLE STR OUTPUT
+ "Name: Dolores Heitmiller
+ Age: 28
+ Innate traits: hard-edged, independent, loyal
+ Learned traits: Dolores is a painter who wants live quietly and paint
+ while enjoying her everyday life.
+ Currently: Dolores is preparing for her first solo show. She mostly
+ works from home.
+ Lifestyle: Dolores goes to bed around 11pm, sleeps for 7 hours, eats
+ dinner around 6pm.
+ Daily plan requirement: Dolores is planning to stay at home all day and
+ never go out."
+ """
+ commonset = ""
+ commonset += f"Name: {self.name}\n"
+ commonset += f"Age: {self.age}\n"
+ commonset += f"Innate traits: {self.innate}\n"
+ commonset += f"Learned traits: {self.learned}\n"
+ commonset += f"Currently: {self.currently}\n"
+ commonset += f"Lifestyle: {self.lifestyle}\n"
+ commonset += f"Daily plan requirement: {self.daily_plan_req}\n"
+ commonset += f"Current Date: {self.curr_time.strftime('%A %B %d')}\n"
+ return commonset
+
+
+ def get_str_name(self):
+ return self.name
+
+
+ def get_str_firstname(self):
+ return self.first_name
+
+
+ def get_str_lastname(self):
+ return self.last_name
+
+
+ def get_str_age(self):
+ return str(self.age)
+
+
+ def get_str_innate(self):
+ return self.innate
+
+
+ def get_str_learned(self):
+ return self.learned
+
+
+ def get_str_currently(self):
+ return self.currently
+
+
+ def get_str_lifestyle(self):
+ return self.lifestyle
+
+
+ def get_str_daily_plan_req(self):
+ return self.daily_plan_req
+
+
+ def get_str_curr_date_str(self):
+ return self.curr_time.strftime("%A %B %d")
+
+
+ def get_curr_event(self):
+ if not self.act_address:
+ return (self.name, None, None)
+ else:
+ return self.act_event
+
+
+ def get_curr_event_and_desc(self):
+ if not self.act_address:
+ return (self.name, None, None, None)
+ else:
+ return (self.act_event[0],
+ self.act_event[1],
+ self.act_event[2],
+ self.act_description)
+
+
+ def get_curr_obj_event_and_desc(self):
+ if not self.act_address:
+ return ("", None, None, None)
+ else:
+ return (self.act_address,
+ self.act_obj_event[1],
+ self.act_obj_event[2],
+ self.act_obj_description)
+
+
+ def add_new_action(self,
+ action_address,
+ action_duration,
+ action_description,
+ action_pronunciatio,
+ action_event,
+ chatting_with,
+ chat,
+ chatting_with_buffer,
+ chatting_end_time,
+ act_obj_description,
+ act_obj_pronunciatio,
+ act_obj_event,
+ act_start_time=None):
+ self.act_address = action_address
+ self.act_duration = action_duration
+ self.act_description = action_description
+ self.act_pronunciatio = action_pronunciatio
+ self.act_event = action_event
+
+ self.chatting_with = chatting_with
+ self.chat = chat
+ if chatting_with_buffer:
+ self.chatting_with_buffer.update(chatting_with_buffer)
+ self.chatting_end_time = chatting_end_time
+
+ self.act_obj_description = act_obj_description
+ self.act_obj_pronunciatio = act_obj_pronunciatio
+ self.act_obj_event = act_obj_event
+
+ self.act_start_time = self.curr_time
+
+ self.act_path_set = False
+
+
+ def act_time_str(self):
+ """
+ Returns a string output of the current time.
+
+ INPUT
+ None
+ OUTPUT
+ A string output of the current time.
+ EXAMPLE STR OUTPUT
+ "14:05 P.M."
+ """
+ return self.act_start_time.strftime("%H:%M %p")
+
+
+ def act_check_finished(self):
+ """
+ Checks whether the self.Action instance has finished.
+
+ INPUT
+ curr_datetime: Current time. If current time is later than the action's
+ start time + its duration, then the action has finished.
+ OUTPUT
+ Boolean [True]: Action has finished.
+ Boolean [False]: Action has not finished and is still ongoing.
+ """
+ if not self.act_address:
+ return True
+
+ if self.chatting_with:
+ end_time = self.chatting_end_time
+ else:
+ x = self.act_start_time
+ if x.second != 0:
+ x = x.replace(second=0)
+ x = (x + datetime.timedelta(minutes=1))
+ end_time = (x + datetime.timedelta(minutes=self.act_duration))
+
+ if end_time.strftime("%H:%M:%S") == self.curr_time.strftime("%H:%M:%S"):
+ return True
+ return False
+
+
+ def act_summarize(self):
+ """
+ Summarize the current action as a dictionary.
+
+ INPUT
+ None
+ OUTPUT
+ ret: A human readable summary of the action.
+ """
+ exp = dict()
+ exp["persona"] = self.name
+ exp["address"] = self.act_address
+ exp["start_datetime"] = self.act_start_time
+ exp["duration"] = self.act_duration
+ exp["description"] = self.act_description
+ exp["pronunciatio"] = self.act_pronunciatio
+ return exp
+
+
+ def act_summary_str(self):
+ """
+ Returns a string summary of the current action. Meant to be
+ human-readable.
+
+ INPUT
+ None
+ OUTPUT
+ ret: A human readable summary of the action.
+ """
+ start_datetime_str = self.act_start_time.strftime("%A %B %d -- %H:%M %p")
+ ret = f"[{start_datetime_str}]\n"
+ ret += f"Activity: {self.name} is {self.act_description}\n"
+ ret += f"Address: {self.act_address}\n"
+ ret += f"Duration in minutes (e.g., x min): {str(self.act_duration)} min\n"
+ return ret
+
+
+ def get_str_daily_schedule_summary(self):
+ ret = ""
+ curr_min_sum = 0
+ for row in self.f_daily_schedule:
+ curr_min_sum += row[1]
+ hour = int(curr_min_sum/60)
+ minute = curr_min_sum%60
+ ret += f"{hour:02}:{minute:02} || {row[0]}\n"
+ return ret
+
+
+ def get_str_daily_schedule_hourly_org_summary(self):
+ ret = ""
+ curr_min_sum = 0
+ for row in self.f_daily_schedule_hourly_org:
+ curr_min_sum += row[1]
+ hour = int(curr_min_sum/60)
+ minute = curr_min_sum%60
+ ret += f"{hour:02}:{minute:02} || {row[0]}\n"
+ return ret
diff --git a/examples/st_game/prompts/poignancy_chat_v1.txt b/examples/st_game/prompts/poignancy_chat_v1.txt
new file mode 100644
index 000000000..572dd8a05
--- /dev/null
+++ b/examples/st_game/prompts/poignancy_chat_v1.txt
@@ -0,0 +1,17 @@
+poignancy_chat_v1.txt
+
+!!: agent name
+!!: iss
+!!: name
+!!: event description
+
+###
+Here is a brief description of !!.
+!!
+
+On the scale of 1 to 10, where 1 is purely mundane (e.g., routine morning greetings) and 10 is extremely poignant (e.g., a conversation about breaking up, a fight), rate the likely poignancy of the following conversation for !!.
+
+Conversation:
+!!
+
+Rate (return a number between 1 to 10):
\ No newline at end of file
diff --git a/examples/st_game/prompts/run_gpt_prompts.py b/examples/st_game/prompts/run_gpt_prompts.py
new file mode 100644
index 000000000..14b699c15
--- /dev/null
+++ b/examples/st_game/prompts/run_gpt_prompts.py
@@ -0,0 +1,33 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# @Desc : 调用Prompts中模板,实现相关Action
+
+from wrapper_prompt import special_response_generate, prompt_generate
+from memory.scratch import Scratch
+from examples.st_game.memory.agent_memory import BasicMemory
+import json
+
+
+def get_poignancy_action(scratch: Scratch, content: BasicMemory.content) -> str:
+ """
+ 衡量事件心酸度
+ """
+ def create_prompt_input(scratch, content):
+ prompt_input = [scratch.name,
+ scratch.iss,
+ scratch.name,
+ content]
+ return prompt_input
+
+ # 1. Prompt构建
+ # 2. Instruction给出
+ prompt_template = "poignancy_chat_v1.txt" # 保留原来的注释
+ prompt_input = create_prompt_input(scratch, content) # 保留原来的注释
+ prompt = prompt_generate(prompt_input, prompt_template)
+ special_instruction = "The output should ONLY contain ONE integer value on the scale of 1 to 10."
+ poignancy = special_response_generate(prompt, special_instruction)
+ try:
+ poi_dict = json.loads(poignancy)
+ return str(poi_dict['poignancy']) # 将返回值强制转换为字符串
+ except json.JSONDecodeError as e:
+ return poignancy
diff --git a/examples/st_game/prompts/wrapper_prompt.py b/examples/st_game/prompts/wrapper_prompt.py
new file mode 100644
index 000000000..0950f99d1
--- /dev/null
+++ b/examples/st_game/prompts/wrapper_prompt.py
@@ -0,0 +1,50 @@
+#!/usr/bin/env python
+# -*- coding: utf-8 -*-
+# @Desc : 基于Prompt Templates 填充Prompt; 为Prompt包装与调用
+
+from metagpt import llm
+
+
+def prompt_generate(curr_input: list, prompt_path: str):
+ """
+ curr_input: 输入一个按照Prompt Template的要求的列表
+ prompt_path: 输入一个Prompt path
+ """
+ # 如果输入是字符串,将其转换为列表
+ if isinstance(curr_input, str):
+ curr_input = [curr_input]
+
+ # 将输入列表中的每个元素转换为字符串
+ curr_input = [str(i) for i in curr_input]
+
+ with open(prompt_path, "r") as f:
+ prompt = f.read()
+
+ for count, i in enumerate(curr_input):
+ prompt = prompt.replace(f"!!", i)
+
+ if "###" in prompt:
+ prompt = prompt.split("###")[1]
+
+ return prompt.strip()
+
+
+def response_generate(prompt: str):
+ """
+ 待完善,我没有找到MG中可以设置Temperature以及Maxtoken的位置
+ """
+ return llm.ai_func(prompt)
+
+
+def special_response_generate(prompt: str, special_instruction: str, example_output: str = None):
+ """
+ 当对于Prompt生成有特殊要求时,调用该函数增加special_instruction或example_output
+ """
+ prompt = '"""\n' + prompt + '\n"""\n'
+ prompt += f"Output the response to the prompt above in JSON. {special_instruction}\n"
+
+ if example_output:
+ prompt += "Example output JSON:\n"
+ prompt += '{"output": "' + str(example_output) + '"}'
+
+ return response_generate(prompt)
diff --git a/examples/st_game/roles/st_role.py b/examples/st_game/roles/st_role.py
index fa071f069..8f0c47f7f 100644
--- a/examples/st_game/roles/st_role.py
+++ b/examples/st_game/roles/st_role.py
@@ -17,19 +17,22 @@ from pathlib import Path
from metagpt.roles.role import Role, RoleContext
from metagpt.schema import Message
-from ..memory.associative_memory import AssociativeMemory
+from ..memory.agent_memory import AgentMemory
from ..actions.dummy_action import DummyAction
from ..actions.user_requirement import UserRequirement
from ..maze_environment import MazeEnvironment
+from ..memory.retrieve import agent_retrieve
+from ..memory.scratch import Scratch
class STRoleContext(RoleContext):
env: 'MazeEnvironment' = Field(default=None)
- memory: AssociativeMemory = Field(default=AssociativeMemory)
+ memory: AgentMemory = Field(default=AgentMemory)
+ scratch: Scratch = Field(default=Scratch)
class STRole(Role):
-
+ # 继承Role类,Role类继承RoleContext,这里的逻辑需要认真考虑
# add a role's property structure to store role's age and so on like GA's Scratch.
def __init__(self,
@@ -65,6 +68,12 @@ class STRole(Role):
# TODO observe info from maze_env
pass
+ async def retrieve(self, query, n = 30 ,topk = 4):
+ # TODO retrieve memories from agent_memory
+ retrieve_memories = agent_retrieve(self._rc.memory, self._rc.scratch.curr_time, self._rc.scratch.recency_decay, query, n, topk)
+ return retrieve_memories
+
+
async def plan(self):
# TODO make a plan
diff --git a/examples/st_game/utils/check.py b/examples/st_game/utils/check.py
new file mode 100644
index 000000000..0a806fe2d
--- /dev/null
+++ b/examples/st_game/utils/check.py
@@ -0,0 +1,14 @@
+def check_if_file_exists(curr_file):
+ """
+ Checks if a file exists
+ ARGS:
+ curr_file: path to the current csv file.
+ RETURNS:
+ True if the file exists
+ False if the file does not exist
+ """
+ try:
+ with open(curr_file) as f_analysis_file: pass
+ return True
+ except:
+ return False
\ No newline at end of file
diff --git a/examples/st_game/utils/utils.py b/examples/st_game/utils/utils.py
index a70f7606d..11cbabd8e 100644
--- a/examples/st_game/utils/utils.py
+++ b/examples/st_game/utils/utils.py
@@ -4,6 +4,7 @@
from typing import Any
import json
+import openai
from pathlib import Path
@@ -22,3 +23,11 @@ def read_json_file(json_file: str, encoding=None) -> list[Any]:
def write_json_file(json_file: str, data: list, encoding=None):
with open(json_file, "w", encoding=encoding) as fout:
json.dump(data, fout, ensure_ascii=False, indent=4)
+
+def embedding_tools(query):
+ embedding_result = openai.Embedding.create(
+ model="text-embedding-ada-002",
+ input=query
+ )
+ embedding_key = embedding_result['data'][0]["embedding"]
+ return embedding_key
\ No newline at end of file