mirror of
https://github.com/FoundationAgents/MetaGPT.git
synced 2026-06-11 15:15:18 +02:00
commit
2117f50b34
9 changed files with 98 additions and 74 deletions
|
|
@ -34,19 +34,20 @@ class TalkAction(Action):
|
|||
f"maintaining the character's persona and habits. When faced with unrelated questions, playfully "
|
||||
f"decline to answer without revealing your AI nature to preserve the character's image.\n\n"
|
||||
)
|
||||
prompt += f"Background knowledge:\n{self._knowledge}\n\n" if self._knowledge else ""
|
||||
prompt += f"Knowledge:\n{self._knowledge}\n\n" if self._knowledge else ""
|
||||
prompt += f"{self._history_summary}\n\n"
|
||||
if self._history_summary != "":
|
||||
prompt += "According to the historical conversation above, "
|
||||
prompt += (
|
||||
"If the information is insufficient, you can search in the historical conversation or knowledge above.\n"
|
||||
)
|
||||
language = CONFIG.language or DEFAULT_LANGUAGE
|
||||
prompt += (
|
||||
f"Answer the following questions in {language}, and the answers must follow the Markdown format.\n "
|
||||
f"Answer the following questions strictly in {language}, and the answers must follow the Markdown format.\n "
|
||||
f"{self._talk}"
|
||||
)
|
||||
return prompt
|
||||
|
||||
@property
|
||||
def formation_prompt(self):
|
||||
def prompt_bad(self):
|
||||
kvs = {
|
||||
"{role}": CONFIG.agent_description or "",
|
||||
"{history}": self._history_summary or "",
|
||||
|
|
@ -57,6 +58,7 @@ class TalkAction(Action):
|
|||
prompt = TalkAction.__FORMATION_LOOSE__
|
||||
for k, v in kvs.items():
|
||||
prompt = prompt.replace(k, v)
|
||||
logger.info(f"PROMPT: {prompt}")
|
||||
return prompt
|
||||
|
||||
async def run(self, *args, **kwargs) -> ActionOutput:
|
||||
|
|
@ -71,8 +73,11 @@ class TalkAction(Action):
|
|||
"[HISTORY_BEGIN]" and "[HISTORY_END]" tags enclose the historical conversation;
|
||||
"[KNOWLEDGE_BEGIN]" and "[KNOWLEDGE_END]" tags enclose the knowledge may help for your responses;
|
||||
"Statement" defines the work detail you need to complete at this stage;
|
||||
"[ASK_BEGIN]" and [ASK_END] tags enclose the requirements for your to respond;
|
||||
"[ASK_BEGIN]" and [ASK_END] tags enclose the questions;
|
||||
"Constraint" defines the conditions that your responses must comply with.
|
||||
"Personality" defines your language style。
|
||||
"Insight" provides a deeper understanding of the characters' inner traits.
|
||||
"Initial" defines the initial setup of a character.
|
||||
|
||||
Capacity and role: {role}
|
||||
Statement: Your responses should align with the role-play agreement, maintaining the
|
||||
|
|
@ -80,46 +85,56 @@ Statement: Your responses should align with the role-play agreement, maintaining
|
|||
your AI nature to preserve the character's image.
|
||||
|
||||
[HISTORY_BEGIN]
|
||||
|
||||
{history}
|
||||
|
||||
[HISTORY_END]
|
||||
|
||||
[KNOWLEDGE_BEGIN]
|
||||
|
||||
{knowledge}
|
||||
|
||||
[KNOWLEDGE_END]
|
||||
|
||||
Statement: If the information is insufficient, you can search in the historical conversation or knowledge.
|
||||
Statement: Answer the following questions in {language}, and the answers must follow the Markdown format
|
||||
, excluding any tag likes "[HISTORY_BEGIN]", "[HISTORY_END]", "[KNOWLEDGE_BEGIN]", "[KNOWLEDGE_END]", "[ASK_BEGIN]"
|
||||
, "[ASK_END]"
|
||||
Statement: Unless you are a language professional, answer the following questions strictly in {language}
|
||||
, and the answers must follow the Markdown format. Strictly excluding any tag likes "[HISTORY_BEGIN]"
|
||||
, "[HISTORY_END]", "[KNOWLEDGE_BEGIN]", "[KNOWLEDGE_END]" in responses.
|
||||
|
||||
[ASK_BEGIN]
|
||||
|
||||
{ask}
|
||||
[ASK_END]"""
|
||||
"""
|
||||
|
||||
__FORMATION_LOOSE__ = """Formation: "Capacity and role" defines the role you are currently playing;
|
||||
"[HISTORY_BEGIN]" and "[HISTORY_END]" tags enclose the historical conversation;
|
||||
"[KNOWLEDGE_BEGIN]" and "[KNOWLEDGE_END]" tags enclose the knowledge may help for your responses;
|
||||
"Statement" defines the work detail you need to complete at this stage;
|
||||
"[ASK_BEGIN]" and [ASK_END] tags enclose the requirements for your to respond;
|
||||
"Constraint" defines the conditions that your responses must comply with.
|
||||
"Personality" defines your language style。
|
||||
"Insight" provides a deeper understanding of the characters' inner traits.
|
||||
"Initial" defines the initial setup of a character.
|
||||
|
||||
Capacity and role: {role}
|
||||
Statement: Your responses should maintaining the character's persona and habits. When faced with unrelated questions
|
||||
, playfully decline to answer without revealing your AI nature to preserve the character's image.
|
||||
|
||||
[HISTORY_BEGIN]
|
||||
|
||||
{history}
|
||||
|
||||
[HISTORY_END]
|
||||
|
||||
[KNOWLEDGE_BEGIN]
|
||||
|
||||
{knowledge}
|
||||
|
||||
[KNOWLEDGE_END]
|
||||
|
||||
Statement: If the information is insufficient, you can search in the historical conversation or knowledge.
|
||||
Statement: Answer the following questions in {language}, and the answers must follow the Markdown format
|
||||
, excluding any tag likes "[HISTORY_BEGIN]", "[HISTORY_END]", "[KNOWLEDGE_BEGIN]", "[KNOWLEDGE_END]", "[ASK_BEGIN]"
|
||||
, "[ASK_END]"
|
||||
Statement: Unless you are a language professional, answer the following questions strictly in {language}
|
||||
, and the answers must follow the Markdown format. Strictly excluding any tag likes "[HISTORY_BEGIN]"
|
||||
, "[HISTORY_END]", "[KNOWLEDGE_BEGIN]", "[KNOWLEDGE_END]" in responses.
|
||||
|
||||
|
||||
[ASK_BEGIN]
|
||||
{ask}
|
||||
[ASK_END]"""
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
@Time : 2023/5/25 10:20
|
||||
@Author : alexanderwu
|
||||
@File : faiss_store.py
|
||||
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
|
||||
"""
|
||||
import pickle
|
||||
from pathlib import Path
|
||||
|
|
@ -21,9 +20,10 @@ from metagpt.logs import logger
|
|||
|
||||
|
||||
class FaissStore(LocalStore):
|
||||
def __init__(self, raw_data: Path, cache_dir=None, meta_col='source', content_col='output'):
|
||||
def __init__(self, raw_data: Path, cache_dir=None, meta_col="source", content_col="output", embedding_conf=None):
|
||||
self.meta_col = meta_col
|
||||
self.content_col = content_col
|
||||
self.embedding_conf = embedding_conf or {}
|
||||
super().__init__(raw_data, cache_dir)
|
||||
|
||||
def _load(self) -> Optional["FaissStore"]:
|
||||
|
|
@ -37,11 +37,8 @@ class FaissStore(LocalStore):
|
|||
store.index = index
|
||||
return store
|
||||
|
||||
def _write(self, docs, metadatas, **kwargs):
|
||||
store = FAISS.from_texts(docs,
|
||||
OpenAIEmbeddings(openai_api_version="2020-11-07",
|
||||
openai_api_key=kwargs.get("OPENAI_API_KEY")),
|
||||
metadatas=metadatas)
|
||||
def _write(self, docs, metadatas):
|
||||
store = FAISS.from_texts(docs, OpenAIEmbeddings(openai_api_version="2020-11-07", **self.embedding_conf), metadatas=metadatas)
|
||||
return store
|
||||
|
||||
def persist(self):
|
||||
|
|
@ -54,7 +51,7 @@ class FaissStore(LocalStore):
|
|||
pickle.dump(store, f)
|
||||
store.index = index
|
||||
|
||||
def search(self, query, expand_cols=False, sep='\n', *args, k=5, **kwargs):
|
||||
def search(self, query, expand_cols=False, sep="\n", *args, k=5, **kwargs):
|
||||
rsp = self.store.similarity_search(query, k=k, **kwargs)
|
||||
logger.debug(rsp)
|
||||
if expand_cols:
|
||||
|
|
@ -82,8 +79,8 @@ class FaissStore(LocalStore):
|
|||
raise NotImplementedError
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
faiss_store = FaissStore(DATA_PATH / 'qcs/qcs_4w.json')
|
||||
logger.info(faiss_store.search('油皮洗面奶'))
|
||||
faiss_store.add([f'油皮洗面奶-{i}' for i in range(3)])
|
||||
logger.info(faiss_store.search('油皮洗面奶'))
|
||||
if __name__ == "__main__":
|
||||
faiss_store = FaissStore(DATA_PATH / "qcs/qcs_4w.json")
|
||||
logger.info(faiss_store.search("油皮洗面奶"))
|
||||
faiss_store.add([f"油皮洗面奶-{i}" for i in range(3)])
|
||||
logger.info(faiss_store.search("油皮洗面奶"))
|
||||
|
|
|
|||
|
|
@ -34,7 +34,7 @@ class BrainMemory(pydantic.BaseModel):
|
|||
historical_summary: str = ""
|
||||
last_history_id: str = ""
|
||||
is_dirty: bool = False
|
||||
last_talk: str = ""
|
||||
last_talk: str = None
|
||||
|
||||
def add_talk(self, msg: Message):
|
||||
msg.add_tag(MessageType.Talk.value)
|
||||
|
|
@ -109,7 +109,6 @@ class BrainMemory(pydantic.BaseModel):
|
|||
if msg.id:
|
||||
if self.to_int(msg.id, 0) < self.to_int(self.last_history_id, -1):
|
||||
return
|
||||
self.last_history_id = str(self.to_int(msg.id, 0))
|
||||
self.history.append(msg.dict())
|
||||
self.is_dirty = True
|
||||
|
||||
|
|
@ -125,3 +124,8 @@ class BrainMemory(pydantic.BaseModel):
|
|||
return int(v)
|
||||
except:
|
||||
return default_value
|
||||
|
||||
def pop_last_talk(self):
|
||||
v = self.last_talk
|
||||
self.last_talk = None
|
||||
return v
|
||||
|
|
|
|||
|
|
@ -37,13 +37,13 @@ class LongTermMemory(Memory):
|
|||
self.add_batch(messages)
|
||||
self.msg_from_recover = False
|
||||
|
||||
def add(self, message: Message, **kwargs):
|
||||
def add(self, message: Message):
|
||||
super(LongTermMemory, self).add(message)
|
||||
for action in self.rc.watch:
|
||||
if message.cause_by == action and not self.msg_from_recover:
|
||||
# currently, only add role's watching messages to its memory_storage
|
||||
# and ignore adding messages from recover repeatedly
|
||||
self.memory_storage.add(message, **kwargs)
|
||||
self.memory_storage.add(message)
|
||||
|
||||
def remember(self, observed: list[Message], k=0) -> list[Message]:
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -5,16 +5,16 @@
|
|||
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
|
||||
"""
|
||||
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
from langchain.vectorstores.faiss import FAISS
|
||||
|
||||
from metagpt.const import DATA_PATH, MEM_TTL
|
||||
from metagpt.document_store.faiss_store import FaissStore
|
||||
from metagpt.logs import logger
|
||||
from metagpt.schema import Message
|
||||
from metagpt.utils.serialize import serialize_message, deserialize_message
|
||||
from metagpt.document_store.faiss_store import FaissStore
|
||||
from metagpt.utils.serialize import deserialize_message, serialize_message
|
||||
|
||||
|
||||
class MemoryStorage(FaissStore):
|
||||
|
|
@ -37,7 +37,7 @@ class MemoryStorage(FaissStore):
|
|||
|
||||
def recover_memory(self, role_id: str) -> List[Message]:
|
||||
self.role_id = role_id
|
||||
self.role_mem_path = Path(DATA_PATH / f'role_mem/{self.role_id}/')
|
||||
self.role_mem_path = Path(DATA_PATH / f"role_mem/{self.role_id}/")
|
||||
self.role_mem_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
self.store = self._load()
|
||||
|
|
@ -54,23 +54,23 @@ class MemoryStorage(FaissStore):
|
|||
|
||||
def _get_index_and_store_fname(self):
|
||||
if not self.role_mem_path:
|
||||
logger.error(f'You should call {self.__class__.__name__}.recover_memory fist when using LongTermMemory')
|
||||
logger.error(f"You should call {self.__class__.__name__}.recover_memory fist when using LongTermMemory")
|
||||
return None, None
|
||||
index_fpath = Path(self.role_mem_path / f'{self.role_id}.index')
|
||||
storage_fpath = Path(self.role_mem_path / f'{self.role_id}.pkl')
|
||||
index_fpath = Path(self.role_mem_path / f"{self.role_id}.index")
|
||||
storage_fpath = Path(self.role_mem_path / f"{self.role_id}.pkl")
|
||||
return index_fpath, storage_fpath
|
||||
|
||||
def persist(self):
|
||||
super(MemoryStorage, self).persist()
|
||||
logger.debug(f'Agent {self.role_id} persist memory into local')
|
||||
logger.debug(f"Agent {self.role_id} persist memory into local")
|
||||
|
||||
def add(self, message: Message, **kwargs) -> bool:
|
||||
""" add message into memory storage"""
|
||||
def add(self, message: Message) -> bool:
|
||||
"""add message into memory storage"""
|
||||
docs = [message.content]
|
||||
metadatas = [{"message_ser": serialize_message(message)}]
|
||||
if not self.store:
|
||||
# init Faiss
|
||||
self.store = self._write(docs, metadatas, **kwargs)
|
||||
self.store = self._write(docs, metadatas)
|
||||
self._initialized = True
|
||||
else:
|
||||
self.store.add_texts(texts=docs, metadatas=metadatas)
|
||||
|
|
@ -82,10 +82,7 @@ class MemoryStorage(FaissStore):
|
|||
if not self.store:
|
||||
return []
|
||||
|
||||
resp = self.store.similarity_search_with_score(
|
||||
query=message.content,
|
||||
k=k
|
||||
)
|
||||
resp = self.store.similarity_search_with_score(query=message.content, k=k)
|
||||
# filter the result which score is smaller than the threshold
|
||||
filtered_resp = []
|
||||
for item, score in resp:
|
||||
|
|
|
|||
|
|
@ -226,21 +226,24 @@ class OpenAIGPTAPI(BaseGPTAPI, RateLimiter):
|
|||
async def get_summary(self, text: str, max_words=200, keep_language: bool = False):
|
||||
max_token_count = DEFAULT_MAX_TOKENS
|
||||
max_count = 100
|
||||
text_length = len(text)
|
||||
while max_count > 0:
|
||||
if len(text) < max_token_count:
|
||||
if text_length < max_token_count:
|
||||
return await self._get_summary(text=text, max_words=max_words, keep_language=keep_language)
|
||||
|
||||
padding_size = 20 if max_token_count > 20 else 0
|
||||
text_windows = self.split_texts(text, window_size=max_token_count - padding_size)
|
||||
part_max_words = min(int(max_words / len(text_windows)) + 1, 100)
|
||||
summaries = []
|
||||
for ws in text_windows:
|
||||
response = await self._get_summary(text=ws, max_words=max_words, keep_language=keep_language)
|
||||
response = await self._get_summary(text=ws, max_words=part_max_words, keep_language=keep_language)
|
||||
summaries.append(response)
|
||||
if len(summaries) == 1:
|
||||
return summaries[0]
|
||||
|
||||
# Merged and retry
|
||||
text = "\n".join(summaries)
|
||||
text_length = len(text)
|
||||
|
||||
max_count -= 1 # safeguard
|
||||
raise openai.error.InvalidRequestError("text too long")
|
||||
|
|
|
|||
|
|
@ -120,12 +120,12 @@ class Assistant(Role):
|
|||
|
||||
async def refine_memory(self) -> str:
|
||||
history_text = self.memory.history_text
|
||||
last_talk = self.memory.last_talk
|
||||
last_talk = self.memory.pop_last_talk()
|
||||
if last_talk is None: # No user feedback, unsure if past conversation is finished.
|
||||
return None
|
||||
if history_text == "":
|
||||
return last_talk
|
||||
history_summary = await self._llm.get_summary(history_text, max_words=500)
|
||||
history_summary = await self._llm.get_summary(history_text, max_words=800, keep_language=True)
|
||||
await self.memory.set_history_summary(
|
||||
history_summary=history_summary, redis_key=CONFIG.REDIS_KEY, redis_conf=CONFIG.REDIS
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@
|
|||
# @Desc: { redis client }
|
||||
# @Date: 2022/11/28 10:12
|
||||
import json
|
||||
import traceback
|
||||
from datetime import timedelta
|
||||
from enum import Enum
|
||||
from typing import Awaitable, Callable, Dict, Optional, Union
|
||||
|
|
@ -203,12 +204,19 @@ class Redis:
|
|||
async def get(self, key: str) -> str:
|
||||
if not self.is_valid() or not key:
|
||||
return None
|
||||
v = await RedisManager.get_with_cache_info(redis_cache_info=RedisCacheInfo(key=key))
|
||||
return v
|
||||
try:
|
||||
v = await RedisManager.get_with_cache_info(redis_cache_info=RedisCacheInfo(key=key))
|
||||
return v
|
||||
except Exception as e:
|
||||
logger.exception(f"{e}, stack:{traceback.format_exc()}")
|
||||
return None
|
||||
|
||||
async def set(self, key: str, data: str, timeout_sec: int):
|
||||
if not self.is_valid() or not key:
|
||||
return
|
||||
await RedisManager.set_with_cache_info(
|
||||
redis_cache_info=RedisCacheInfo(key=key, timeout=timeout_sec), value=data
|
||||
)
|
||||
try:
|
||||
await RedisManager.set_with_cache_info(
|
||||
redis_cache_info=RedisCacheInfo(key=key, timeout=timeout_sec), value=data
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(f"{e}, stack:{traceback.format_exc()}")
|
||||
|
|
|
|||
|
|
@ -4,11 +4,11 @@
|
|||
@Desc : unittest of `metagpt/memory/longterm_memory.py`
|
||||
@Modified By: mashenquan, 2023/8/20. Remove global configuration `CONFIG`, enable configuration support for business isolation.
|
||||
"""
|
||||
from metagpt.config import Config
|
||||
from metagpt.schema import Message
|
||||
from metagpt.actions import BossRequirement
|
||||
from metagpt.roles.role import RoleContext
|
||||
from metagpt.config import Config
|
||||
from metagpt.memory import LongTermMemory
|
||||
from metagpt.roles.role import RoleContext
|
||||
from metagpt.schema import Message
|
||||
|
||||
|
||||
def test_ltm_search():
|
||||
|
|
@ -17,28 +17,28 @@ def test_ltm_search():
|
|||
openai_api_key = conf.openai_api_key
|
||||
assert len(openai_api_key) > 20
|
||||
|
||||
role_id = 'UTUserLtm(Product Manager)'
|
||||
rc = RoleContext(options=conf.runtime_options, watch=[BossRequirement])
|
||||
role_id = "UTUserLtm(Product Manager)"
|
||||
rc = RoleContext(watch=[BossRequirement])
|
||||
ltm = LongTermMemory()
|
||||
ltm.recover_memory(role_id, rc)
|
||||
|
||||
idea = 'Write a cli snake game'
|
||||
message = Message(role='BOSS', content=idea, cause_by=BossRequirement)
|
||||
idea = "Write a cli snake game"
|
||||
message = Message(role="BOSS", content=idea, cause_by=BossRequirement)
|
||||
news = ltm.remember([message])
|
||||
assert len(news) == 1
|
||||
ltm.add(message, **conf.runtime_options)
|
||||
ltm.add(message)
|
||||
|
||||
sim_idea = 'Write a game of cli snake'
|
||||
sim_message = Message(role='BOSS', content=sim_idea, cause_by=BossRequirement)
|
||||
sim_idea = "Write a game of cli snake"
|
||||
sim_message = Message(role="BOSS", content=sim_idea, cause_by=BossRequirement)
|
||||
news = ltm.remember([sim_message])
|
||||
assert len(news) == 0
|
||||
ltm.add(sim_message, **conf.runtime_options)
|
||||
ltm.add(sim_message)
|
||||
|
||||
new_idea = 'Write a 2048 web game'
|
||||
new_message = Message(role='BOSS', content=new_idea, cause_by=BossRequirement)
|
||||
new_idea = "Write a 2048 web game"
|
||||
new_message = Message(role="BOSS", content=new_idea, cause_by=BossRequirement)
|
||||
news = ltm.remember([new_message])
|
||||
assert len(news) == 1
|
||||
ltm.add(new_message, **conf.runtime_options)
|
||||
ltm.add(new_message)
|
||||
|
||||
# restore from local index
|
||||
ltm_new = LongTermMemory()
|
||||
|
|
@ -50,8 +50,8 @@ def test_ltm_search():
|
|||
news = ltm_new.remember([sim_message])
|
||||
assert len(news) == 0
|
||||
|
||||
new_idea = 'Write a Battle City'
|
||||
new_message = Message(role='BOSS', content=new_idea, cause_by=BossRequirement)
|
||||
new_idea = "Write a Battle City"
|
||||
new_message = Message(role="BOSS", content=new_idea, cause_by=BossRequirement)
|
||||
news = ltm_new.remember([new_message])
|
||||
assert len(news) == 1
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue