Merge pull request #6 from send18/dev

Dev
This commit is contained in:
Guess 2023-09-06 12:01:35 +08:00 committed by GitHub
commit 2117f50b34
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 98 additions and 74 deletions

View file

@ -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]"""
"""

View file

@ -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("油皮洗面奶"))

View file

@ -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

View file

@ -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]:
"""

View file

@ -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:

View file

@ -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")

View file

@ -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
)

View file

@ -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()}")

View file

@ -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