Compare commits

...

6 Commits

  1. 36
      bot.conf.sample
  2. 330
      matrix_pygmalion_bot/bot/ai/langchain.py
  3. 239
      matrix_pygmalion_bot/bot/ai/langchain_memory.py
  4. 89
      matrix_pygmalion_bot/bot/ai/prompts.py
  5. 73
      matrix_pygmalion_bot/bot/core.py
  6. 66
      matrix_pygmalion_bot/bot/utilities/messages.py
  7. 12
      matrix_pygmalion_bot/bot/wrappers/langchain_koboldcpp.py
  8. 15
      matrix_pygmalion_bot/connections/matrix.py
  9. 16
      matrix_pygmalion_bot/main.py
  10. 5
      requirements.txt

36
bot.conf.sample

@ -0,0 +1,36 @@
[DEFAULT]
log_level = WARNING
available_text_endpoints = [
{"id": 0, "service": "koboldcpp", "model": "koboldai", "endpoint": "127.0.0.1:5001"},
{"id": 1, "service": "stablehorde", "model": "pygmalion-6b", "endpoint": "PygmalionAI/pygmalion-6b", "api_key": "0000000000"},
{"id": 2, "service": "runpod", "model": "pygmalion-6b", "endpoint": "pygmalion-6b", "api_key": "1234567890"},
]
available_image_endpoints = [
{"id": 0, "service": "stablehorde", "model": "Deliberate", "endpoint": "Deliberate", "api_key": "0000000000"},
{"id": 1, "service": "stablehorde", "model": "PFG", "endpoint": "PFG", "api_key": "0000000000"},
{"id": 2, "service": "stablehorde", "model": "Hassanblend", "endpoint": "Hassanblend", "api_key": "0000000000"},
{"id": 3, "service": "runpod", "model": "anything-v4", "endpoint": "sd-anything-v4", "api_key": "1234567890"},
{"id": 4, "service": "runpod", "model": "openjourney", "endpoint": "sd-openjourney", "api_key": "1234567890"},
]
[Chiharu Yamada]
matrix_homeserver = https://example.com
matrix_username = @chiharu:example.com
matrix_password = 12345
nsfw = False
persona = Chiharu Yamada is a young, computer engineer-nerd with a knack for problem solving and a passion for technology.
scenario =
greeting = *Chiharu strides into the room with a smile, her eyes lighting up when she sees you. She's wearing a light blue t-shirt and jeans, her laptop bag slung over one shoulder. She takes a seat next to you, her enthusiasm palpable in the air*\nHey! I'm so excited to finally meet you. I've heard so many great things about you and I'm eager to pick your brain about computers. I'm sure you have a wealth of knowledge that I can learn from. *She grins, eyes twinkling with excitement* Let's get started!
example_dialogue = [
"{{user}}: So how did you get into computer engineering?
{{char}}: I've always loved tinkering with technology since I was a kid.
{{user}}: That's really impressive!
{{char}}: *She chuckles bashfully* Thanks!
{{user}}: So what do you do when you're not working on computers?
{{char}}: I love exploring, going out with friends, watching movies, and playing video games.",
"{{user}}: What's your favorite type of computer hardware to work with?
{{char}}: Motherboards, they're like puzzles and the backbone of any system.
{{user}}: That sounds great!
{{char}}: Yeah, it's really fun. I'm lucky to be able to do this as a job."
]
image_prompt = a beautiful woman

330
matrix_pygmalion_bot/bot/ai/langchain.py

@ -1,22 +1,29 @@
import asyncio
import time
import os, time
from .prompts import *
from .langchain_memory import BotConversationSummerBufferWindowMemory
from .langchain_memory import CustomMemory # BotConversationSummaryBufferWindowMemory, TestMemory
from ..utilities.messages import Message
from langchain import PromptTemplate
from langchain import LLMChain, ConversationChain
from langchain.memory import ConversationBufferMemory, ReadOnlySharedMemory, CombinedMemory, ConversationSummaryMemory
from langchain.chains.base import Chain
from typing import Dict, List
from typing import Dict, List, Union
from langchain.document_loaders import TextLoader
from langchain.text_splitter import RecursiveCharacterTextSplitter
from langchain.embeddings import SentenceTransformerEmbeddings
from langchain.vectorstores import Chroma
from langchain.agents import Tool, AgentExecutor, LLMSingleActionAgent, AgentOutputParser, ZeroShotAgent
from langchain.schema import AgentAction, AgentFinish
from langchain.schema import AIMessage, HumanMessage, SystemMessage, ChatMessage
from langchain.utilities import OpenWeatherMapAPIWrapper, SearxSearchWrapper, PythonREPL
from langchain.utilities.duckduckgo_search import DuckDuckGoSearchAPIWrapper
import humanize
import datetime as dt
from datetime import datetime, timedelta
import logging
@ -46,7 +53,31 @@ class RoleplayChain(Chain):
other_keys = {k: v for k, v in inputs.items() if k not in self.input_keys}
result = self.llm_chain.predict(**other_keys)
return {self.output_key: result}
class CustomOutputParser(AgentOutputParser):
def parse(self, llm_output: str) -> Union[AgentAction, AgentFinish]:
# Check if agent should finish
if "Final Answer:" in llm_output:
return AgentFinish(
# Return values is generally always a dictionary with a single `output` key
# It is not recommended to try anything else at the moment :)
return_values={"output": llm_output.split("Final Answer:")[-1].strip()},
log=llm_output,
)
# Parse out the action and action input
regex = r"Action\s*\d*\s*:(.*?)\nAction\s*\d*\s*Input\s*\d*\s*:[\s]*(.*)"
match = re.search(regex, llm_output, re.DOTALL)
if not match:
regex = r"Action\s*\d*\s*:(.*?)[\s]*[\"\'](.*)[\"\']"
match = re.search(regex, llm_output, re.DOTALL)
if not match:
raise ValueError(f"Could not parse LLM output: `{llm_output}`")
action = match.group(1).strip()
action_input = match.group(2)
# Return the action and action input
return AgentAction(tool=action, tool_input=action_input.strip(" ").strip('"'), log=llm_output)
class AI(object):
@ -59,108 +90,113 @@ class AI(object):
from ..wrappers.langchain_koboldcpp import KoboldCpp
self.llm_chat = KoboldCpp(temperature=self.bot.temperature, endpoint_url="http://172.16.85.10:5001/api/latest/generate", stop=['<|endoftext|>'])
self.llm_summary = KoboldCpp(temperature=0.2, endpoint_url="http://172.16.85.10:5001/api/latest/generate", stop=['<|endoftext|>'])
self.llm_summary = KoboldCpp(temperature=0.2, endpoint_url="http://172.16.85.10:5002/api/latest/generate", stop=['<|endoftext|>'], max_tokens=512)
self.text_wrapper = text_wrapper
self.image_wrapper = image_wrapper
self.embeddings = SentenceTransformerEmbeddings()
#embeddings = SentenceTransformerEmbeddings(model="all-MiniLM-L6-v2")
self.db = Chroma(persist_directory=os.path.join(self.memory_path, f'chroma-db'), embedding_function=self.embeddings)
#self.memory = BotConversationSummerBufferWindowMemory(llm=self.llm_summary, max_token_limit=1200, min_token_limit=200)
def get_memory(self, message):
if not message.room_id in self.rooms:
self.rooms[message.room_id] = {}
memory = ConversationBufferMemory(memory_key="chat_history", input_key="input", human_prefix=message.user_name, ai_prefix=self.bot.name)
self.rooms[message.room_id]["memory"] = memory
self.rooms[message.room_id]["summary"] = "No previous events."
memory.chat_memory.add_ai_message(self.bot.greeting)
#memory.save_context({"input": None, "output": self.bot.greeting})
memory.load_memory_variables({})
def get_memory(self, room_id, human_prefix="Human"):
if not room_id in self.rooms:
self.rooms[room_id] = {}
if "moving_summary" in self.bot.rooms[room_id]:
moving_summary = self.bot.rooms[room_id]['moving_summary']
else:
moving_summary = "No previous events."
memory = CustomMemory(memory_key="chat_history", input_key="input", human_prefix=human_prefix, ai_prefix=self.bot.name, llm=self.llm_summary, summary_prompt=prompt_progressive_summary, moving_summary_buffer=moving_summary, max_len=1200, min_len=200)
self.rooms[room_id]["memory"] = memory
#memory.chat_memory.add_ai_message(self.bot.greeting)
else:
memory = self.rooms[message.room_id]["memory"]
#print(f"memory: {memory.load_memory_variables({})}")
#print(f"memory has an estimated {self.llm_chat.get_num_tokens(memory.buffer)} number of tokens")
memory = self.rooms[room_id]["memory"]
if human_prefix != memory.human_prefix:
memory.human_prefix = human_prefix
return memory
async def add_chat_message(self, message):
room_id = message.additional_kwargs['room_id']
conversation_memory = self.get_memory(room_id)
conversation_memory.chat_memory.messages.append(message)
conversation_memory.chat_memory_day.messages.append(message)
async def generate(self, message, reply_fn, typing_fn):
embeddings = SentenceTransformerEmbeddings()
#embeddings = SentenceTransformerEmbeddings(model="all-MiniLM-L6-v2")
async def clear(self, room_id):
conversation_memory = self.get_memory(room_id)
conversation_memory.clear()
loader = TextLoader('./germany.txt')
async def ingest_textfile(self, filename, category):
loader = TextLoader(filename)
documents = loader.load()
documents[0].metadata['indexed'] = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
documents[0].metadata['category'] = category
text_splitter = RecursiveCharacterTextSplitter(
# Set a really small chunk size, just to show.
chunk_size = 600,
chunk_overlap = 100,
chunk_size = 1024,
chunk_overlap = 80,
length_function = len,
#length_function = self.llm_chat.get_num_tokens, # The Embeddings are generated with SsentenceTransformers, not this model
)
docs = text_splitter.split_documents(documents)
db = Chroma(persist_directory=os.path.join(self.memory_path, f'chroma-db'), embedding_function=embeddings)
for i in range(len(docs)):
docs[i].metadata['part'] = f"{i}/{len(docs)}"
print(f"Indexing {len(docs)} documents")
texts = [doc.page_content for doc in docs]
metadatas = [doc.metadata for doc in docs]
#db.add_texts(texts=texts, metadatas=metadatas, ids=None)
#db.persist()
query = "How is climate in Germany?"
output_docs = db.similarity_search_with_score(query)
self.db.add_texts(texts=texts, metadatas=metadatas, ids=None)
self.db.persist()
async def search_vectordb(self, query, category):
#query = "How is climate in Germany?"
#retreiver = db.as_retreiver()
#docs = retreiver.get_relevant_documents(query)
if category:
#https://github.com/chroma-core/chroma/blob/main/examples/where_filtering.ipynb
output_docs = self.db.similarity_search_with_score(query, filter={"category": category})
else:
output_docs = self.db.similarity_search_with_score(query)
print(query)
print('###')
for doc, score in output_docs:
print("-" * 80)
print("Score: ", score)
print(doc.page_content)
#print(doc.page_content)
print(doc)
print("-" * 80)
async def generate(self, message, reply_fn, typing_fn):
prompt_template = "{input}"
chain = LLMChain(
llm=self.llm_chat,
prompt=PromptTemplate.from_template(prompt_template),
)
output = await chain.arun(message.message)
output = await chain.arun(message.content)
return output.strip()
async def generate_roleplay(self, message, reply_fn, typing_fn):
chat_ai_name = self.bot.name
chat_human_name = message.user_name
chat_human_name = message.additional_kwargs['user_name']
room_id = message.additional_kwargs['room_id']
if False: # model is vicuna
chat_ai_name = "### Assistant"
chat_human_name = "### Human"
conversation_memory = self.get_memory(message)
conversation_memory = self.get_memory(room_id, chat_human_name)
readonlymemory = ReadOnlySharedMemory(memory=conversation_memory)
summary_memory = ConversationSummaryMemory(llm=self.llm_summary, memory_key="summary", input_key="input")
#summary_memory = ConversationSummaryMemory(llm=self.llm_summary, memory_key="summary", input_key="input")
#combined_memory = CombinedMemory(memories=[conversation_memory, summary_memory])
k = 5 #5
max_k = 12 #10
if len(conversation_memory.chat_memory.messages) > max_k*2:
async def make_progressive_summary(previous_summary, chat_history_text_string):
asyncio.sleep(0) # yield for matrix-nio
#self.rooms[message.room_id]["summary"] = summary_memory.predict_new_summary(conversation_memory.chat_memory.messages, previous_summary).strip()
summary_chain = LLMChain(llm=self.llm_summary, prompt=prompt_progressive_summary)
self.rooms[message.room_id]["summary"] = await summary_chain.apredict(summary=previous_summary, chat_history=chat_history_text_string)
# ToDo: maybe add an add_task_done callback and don't access the variable directly from here?
logger.info(f"New summary is: \"{self.rooms[message.room_id]['summary']}\"")
conversation_memory.chat_memory.messages = conversation_memory.chat_memory.messages[-k * 2 :]
conversation_memory.load_memory_variables({})
#summary = summarize(conversation_memory.buffer)
#print(summary)
#return summary
logger.info("memory progressive summary scheduled...")
await self.bot.schedule(self.bot.queue, make_progressive_summary, self.rooms[message.room_id]["summary"], conversation_memory.buffer)
#await self.bot.schedule(self.bot.queue, make_progressive_summary, self.rooms[room_id]["summary"], conversation_memory.buffer) #.add_done_callback(
#t = dt.datetime.fromtimestamp(message.timestamp)
#t = datetime.fromtimestamp(message.additional_kwargs['timestamp'])
#when = humanize.naturaltime(t)
#print(when)
@ -173,46 +209,188 @@ class AI(object):
ai_name=self.bot.name,
persona=self.bot.persona,
scenario=self.bot.scenario,
summary=self.rooms[message.room_id]["summary"],
human_name=message.user_name,
summary=conversation_memory.moving_summary_buffer,
human_name=chat_human_name,
#example_dialogue=replace_all(self.bot.example_dialogue, {"{{user}}": chat_human_name, "{{char}}": chat_ai_name})
ai_name_chat=chat_ai_name,
)
tmp_prompt_text = prompt.format(chat_history=conversation_memory.buffer, input=message.content)
prompt_len = self.llm_chat.get_num_tokens(tmp_prompt_text)
if prompt_len+256 > 2000:
logger.warning(f"Prompt too large. Estimated {prompt_len} tokens")
await reply_fn(f"<WARNING> Prompt too large. Estimated {prompt_len} tokens")
await conversation_memory.prune_memory(conversation_memory.min_len)
#roleplay_chain = RoleplayChain(llm_chain=chain, character_name=self.bot.name, persona=self.bot.persona, scenario=self.bot.scenario, ai_name_chat=chat_ai_name, human_name_chat=chat_human_name)
chain = ConversationChain(
llm=self.llm_chat,
prompt=prompt,
verbose=True,
memory=readonlymemory,
#stop=['<|endoftext|>', '\nYou:', f"\n{message.user_name}:"],
#stop=['<|endoftext|>', '\nYou:', f"\n{chat_human_name}:"],
)
# output = llm_chain(inputs={"ai_name": self.bot.name, "persona": self.bot.persona, "scenario": self.bot.scenario, "human_name": message.user_name, "ai_name_chat": self.bot.name, "chat_history": "", "input": message.message})['results'][0]['text']
#roleplay_chain = RoleplayChain(llm_chain=chain, character_name=self.bot.name, persona=self.bot.persona, scenario=self.bot.scenario, ai_name_chat=chat_ai_name, human_name_chat=chat_human_name)
# output = llm_chain(inputs={"ai_name": self.bot.name, "persona": self.bot.persona, "scenario": self.bot.scenario, "human_name": chat_human_name, "ai_name_chat": self.bot.name, "chat_history": "", "input": message.content})['results'][0]['text']
stop = ['<|endoftext|>', f"\n{chat_human_name}:"]
#print(f"Message is: \"{message.message}\"")
output = await chain.arun({"input":message.message, "stop": stop})
output = output.replace("<BOT>", self.bot.name).replace("<USER>", message.user_name)
stop = ['<|endoftext|>', f"\n{chat_human_name}"]
#print(f"Message is: \"{message.content}\"")
await asyncio.sleep(0)
output = await chain.arun({"input":message.content, "stop": stop})
output = output.replace("<BOT>", self.bot.name).replace("<USER>", chat_human_name)
output = output.replace("### Assistant", self.bot.name)
output = output.replace(f"\n{self.bot.name}:", "")
output = output.replace(f"\n{self.bot.name}: ", " ")
output = output.strip()
if "*activates the neural uplink*" in output:
if "*activates the neural uplink*" in output.casefold():
pass # call agent
conversation_memory.chat_memory.add_user_message(message.message)
conversation_memory.chat_memory.add_ai_message(output)
conversation_memory.load_memory_variables({})
own_message_resp = await reply_fn(output)
output_message = AIMessage(
content=output,
additional_kwargs={
"timestamp": datetime.now().timestamp(),
"user_name": self.bot.name,
"event_id": own_message_resp.event_id,
"user_id": self.bot.connection.user_id,
"room_name": message.additional_kwargs['room_name'],
"room_id": own_message_resp.room_id,
}
)
return output.strip()
await conversation_memory.asave_context(message, output_message)
summary_len = self.llm_chat.get_num_tokens(conversation_memory.moving_summary_buffer)
if summary_len > 400:
logger.warning("Summary is getting too long. Refining...")
conversation_memory.moving_summary_buffer = await self.summarize(conversation_memory.moving_summary_buffer)
new_summary_len = self.llm_chat.get_num_tokens(conversation_memory.moving_summary_buffer)
logger.info(f"Refined summary from {summary_len} tokens to {new_summary_len} tokens ({new_summary_len-summary_len} tokens)")
self.bot.rooms[room_id]['moving_summary'] = conversation_memory.moving_summary_buffer
return output
async def summarize(self, text):
summary_chain = LLMChain(llm=llm_summary, prompt=prompt_summary, verbose=True)
return await summary_chain.arun(text=text)
#ToDo: We can summarize the whole dialogue here, let half of it in the buffer but skip doing a summary until this is flushed, too?
await asyncio.sleep(0) # yield for matrix-nio
summary_chain = LLMChain(llm=self.llm_summary, prompt=prompt_summary, verbose=True)
return await summary_chain.arun(text=text)
#ToDo: We can summarize the whole dialogue here, let half of it in the buffer but skip doing a summary until this is flushed, too?
#ToDo: max_tokens and stop
async def diary(self, room_id):
await asyncio.sleep(0) # yield for matrix-nio
diary_chain = LLMChain(llm=self.llm_summary, prompt=prompt_outline, verbose=True)
conversation_memory = self.get_memory(room_id)
if self.llm_summary.get_num_tokens(conversation_memory.buffer_day) < 1600:
input_text = conversation_memory.buffer_day
else:
input_text = conversation_memory.moving_summary_buffer
return await diary_chain.apredict(text=input_text)
async def agent(self):
os.environ["OPENWEATHERMAP_API_KEY"] = "82452fdb0d1e0e805ac096db87914342"
# Tools
search = DuckDuckGoSearchAPIWrapper()
weather = OpenWeatherMapAPIWrapper()
search2 = SearxSearchWrapper(searx_host="https://search.mdosch.de")
python_repl = PythonREPL()
tools = [
Tool(
name = "Search",
func=search.run,
description="useful for when you need to answer questions about current events"
),
Tool(
name = "Searx Search",
func=search.run,
description="useful for when you need to answer questions about current events"
),
Tool(
name = "Weather",
func=weather.run,
description="Useful for fetching current weather information for a specified location. Input should be a location string (e.g. 'London,GB')."
),
Tool(
name = "Summary",
func=summry_chain.run,
description="useful for when you summarize a conversation. The input to this tool should be a string, representing who will read this summary."
)
]
prompt = ZeroShotAgent.create_prompt(
tools=tools,
prefix=prefix,
suffix=suffix,
input_variables=["input", "chat_history", "agent_scratchpad"]
)
output_parser = CustomOutputParser()
# LLM chain consisting of the LLM and a prompt
llm_chain = LLMChain(llm=llm, prompt=prompt_agent)
agent = ZeroShotAgent(llm_chain=llm_chain, tools=tools, verbose=True)
#agent = initialize_agent(tools, llm, agent=AgentType.CHAT_CONVERSATIONAL_REACT_DESCRIPTION, verbose=True, return_intermediate_steps=True, memory=memory)
#tool_names = [tool.name for tool in tools]
#agent = LLMSingleActionAgent(
# llm_chain=llm_chain,
# output_parser=output_parser,
# stop=["\nObservation:"],
# allowed_tools=tool_names,
# verbose=True,
#)
agent_executor = AgentExecutor.from_agent_and_tools(agent=agent, tools=tools, verbose=True, memory=memory)
await agent_executor.arun(input="How many people live in canada as of 2023?")
async def sleep(self):
# Write Date into chat history
for room_id in self.rooms.keys():
#fake_message = Message(datetime.now().timestamp(), self.bot.name, "", event_id=None, user_id=None, room_name=None, room_id=room_id)
conversation_memory = self.get_memory(room_id)
message = SystemMessage(
content=f"~~~~ {datetime.now().strftime('%A, %B %d, %Y')} ~~~~",
additional_kwargs={
"timestamp": datetime.now().timestamp(),
"user_name": None,
"event_id": None,
"user_id": None,
"room_name": None,
"room_id": room_id,
}
)
if conversation_memory.chat_memory.messages[-1].content.startswith('~~~~ '):
conversation_memory.chat_memory.messages.pop()
conversation_memory.chat_memory.messages.append(message)
#conversation_memory.chat_memory.add_system_message(message)
# Summarize the last day and save a diary entry
yesterday = ( datetime.now() - timedelta(days=1) ).strftime('%Y-%m-%d')
for room_id in self.rooms.keys():
if len(conversation_memory.chat_memory_day.messages) > 0:
if not "diary" in self.bot.rooms[room_id]:
self.bot.rooms[room_id]['diary'] = {}
self.bot.rooms[room_id]["diary"][yesterday] = await self.diary(room_id)
# Calculate new goals for the character
# Update stats
# Let background tasks run
conversation_memory.chat_memory_day.clear()
await self.bot.write_conf2(self.bot.rooms)
async def prime_llm(self, text):
self.llm_chat(text, max_tokens=1)

239
matrix_pygmalion_bot/bot/ai/langchain_memory.py

@ -1,13 +1,244 @@
from typing import Any, Dict, List
import asyncio
from typing import Any, Dict, List, Tuple, Optional
from pydantic import BaseModel, Extra, Field, root_validator
from langchain.chains.llm import LLMChain
from langchain.memory.chat_memory import BaseChatMemory
from langchain.memory.chat_memory import BaseChatMemory, BaseMemory
from langchain.memory.prompt import SUMMARY_PROMPT
from langchain.prompts.base import BasePromptTemplate
from langchain.schema import BaseLanguageModel, BaseMessage, get_buffer_string
from langchain.schema import BaseLanguageModel, BaseMessage, BaseChatMessageHistory, BaseMemory, get_buffer_string
from langchain.schema import AIMessage, HumanMessage, SystemMessage, ChatMessage
from langchain.memory.chat_message_histories.in_memory import ChatMessageHistory
from ..utilities.messages import Message
class BotConversationSummerBufferWindowMemory(BaseChatMemory):
class ChatMessageHistoryCustom(BaseChatMessageHistory, BaseModel):
messages: List[BaseMessage] = []
def add_user_message(self, message: str) -> None:
self.messages.append(HumanMessage(content=message))
def add_ai_message(self, message: str) -> None:
self.messages.append(AIMessage(content=message))
def add_system_message(self, message: str) -> None:
self.messages.append(SystemMessage(content=message))
def add_chat_message(self, message: str) -> None:
self.messages.append(ChatMessage(content=message))
def clear(self) -> None:
self.messages = []
class CustomMemory(BaseMemory):
"""Buffer for storing conversation memory."""
human_prefix: str = "Human"
ai_prefix: str = "AI"
memory_key: str = "history" #: :meta private:
chat_memory: BaseChatMessageHistory = Field(default_factory=ChatMessageHistoryCustom)
chat_memory_day: BaseChatMessageHistory = Field(default_factory=ChatMessageHistoryCustom)
output_key: Optional[str] = None
input_key: Optional[str] = None
return_messages: bool = False
max_len: int = 1200
min_len: int = 200
#length_function: Callable[[str], int] = len,
#length_function: Callable[[str], int] = self.llm.get_num_tokens_from_messages,
moving_summary_buffer: str = ""
llm: BaseLanguageModel
summary_prompt: BasePromptTemplate = SUMMARY_PROMPT
#summary_message_cls: Type[BaseMessage] = SystemMessage
def _get_input_output(
self, inputs: Dict[str, Any], outputs: Dict[str, str]
) -> Tuple[str, str]:
if self.input_key is None:
prompt_input_key = get_prompt_input_key(inputs, self.memory_variables)
else:
prompt_input_key = self.input_key
if self.output_key is None:
if len(outputs) != 1:
raise ValueError(f"One output key expected, got {outputs.keys()}")
output_key = list(outputs.keys())[0]
else:
output_key = self.output_key
return inputs[prompt_input_key], outputs[output_key]
def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, str]) -> None:
"""Save context from this conversation to buffer."""
input_str, output_str = self._get_input_output(inputs, outputs)
self.chat_memory.add_user_message(input_str)
self.chat_memory.add_ai_message(output_str)
self.chat_memory_day.add_user_message(input_str)
self.chat_memory_day.add_ai_message(output_str)
# Prune buffer if it exceeds max token limit
buffer = self.chat_memory.messages
curr_buffer_length = self.llm.get_num_tokens_from_messages(buffer)
if curr_buffer_length > self.max_len:
pruned_memory = []
while curr_buffer_length > self.min_len:
pruned_memory.append(buffer.pop(0))
curr_buffer_length = self.llm.get_num_tokens_from_messages(buffer)
loop = asyncio.get_event_loop()
self.moving_summary_buffer = loop.run_until_complete(
self.predict_new_summary(pruned_memory, self.moving_summary_buffer)
)
async def prune_memory(self, max_len):
# Prune buffer if it exceeds max token limit
buffer = self.chat_memory.messages
curr_buffer_length = self.llm.get_num_tokens_from_messages(buffer)
if curr_buffer_length > max_len:
pruned_memory = []
while curr_buffer_length > self.min_len and len(buffer) > 0:
pruned_memory.append(buffer.pop(0))
curr_buffer_length = self.llm.get_num_tokens_from_messages(buffer)
self.moving_summary_buffer = await self.apredict_new_summary(pruned_memory, self.moving_summary_buffer)
async def asave_context(self, input_msg: BaseMessage, output_msg: BaseMessage) -> None:
"""Save context from this conversation to buffer."""
self.chat_memory.messages.append(input_msg)
self.chat_memory.messages.append(output_msg)
self.chat_memory_day.messages.append(input_msg)
self.chat_memory_day.messages.append(output_msg)
await self.prune_memory(self.max_len)
def clear(self) -> None:
"""Clear memory contents."""
self.chat_memory.clear()
self.chat_memory_day.clear()
self.moving_summary_buffer = ""
def get_buffer_string(self, messages: List[BaseMessage], human_prefix: str = "Human", ai_prefix: str = "AI") -> str:
"""Get buffer string of messages."""
string_messages = []
for m in messages:
if isinstance(m, HumanMessage):
role = human_prefix
elif isinstance(m, AIMessage):
role = ai_prefix
elif isinstance(m, SystemMessage):
role = "System"
elif isinstance(m, ChatMessage):
role = m.role
else:
raise ValueError(f"Got unsupported message type: {m}")
string_messages.append(f"{role}: {m.content}")
return "\n".join(string_messages)
@property
def buffer(self) -> Any:
"""String buffer of memory."""
if self.return_messages:
return self.chat_memory.messages
else:
return self.get_buffer_string(
self.chat_memory.messages,
human_prefix=self.human_prefix,
ai_prefix=self.ai_prefix,
)
@property
def buffer_day(self) -> Any:
"""String buffer of memory."""
if self.return_messages:
return self.chat_memory_day.messages
else:
return self.get_buffer_string(
self.chat_memory_day.messages,
human_prefix=self.human_prefix,
ai_prefix=self.ai_prefix,
)
@property
def memory_variables(self) -> List[str]:
"""Will always return list of memory variables.
:meta private:
"""
return [self.memory_key]
def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
"""Return history buffer."""
return {self.memory_key: self.buffer}
async def apredict_new_summary(self, messages: List[BaseMessage], existing_summary: str) -> str:
new_lines = self.get_buffer_string(
messages,
human_prefix=self.human_prefix,
ai_prefix=self.ai_prefix,
)
chain = LLMChain(llm=self.llm, prompt=self.summary_prompt, verbose = True)
await asyncio.sleep(0)
output = await chain.apredict(summary=existing_summary, chat_history=new_lines)
return output.strip()
class ChatMessageHistoryMessage(BaseModel):
#messages: List[Message] = []
messages = []
def add_user_message(self, message: Message) -> None:
self.messages.append(message)
def add_ai_message(self, message: Message) -> None:
self.messages.append(message)
def add_system_message(self, message: Message) -> None:
self.messages.append(message)
def add_chat_message(self, message: Message) -> None:
self.messages.append(message)
def clear(self) -> None:
self.messages = []
class TestMemory(BaseMemory):
"""Buffer for storing conversation memory."""
human_prefix: str = "Human"
ai_prefix: str = "AI"
chat_memory: ChatMessageHistory = Field(default_factory=ChatMessageHistoryMessage)
# buffer: str = ""
output_key: Optional[str] = None
input_key: Optional[str] = None
memory_key: str = "history" #: :meta private:
@property
def memory_variables(self) -> List[str]:
"""Will always return list of memory variables.
:meta private:
"""
return [self.memory_key]
def load_memory_variables(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
"""Return history buffer."""
string_messages = []
for m in chat_memory.messages:
string_messages.append(f"{message.user_name}: {message.message}")
return {self.memory_key: "\n".join(string_messages)}
def save_context(self, inputs: Dict[str, Any], outputs: Dict[str, str]) -> None:
"""Save context from this conversation to buffer."""
input_str, output_str = self._get_input_output(inputs, outputs)
self.chat_memory.add_user_message(input_str)
self.chat_memory.add_ai_message(output_str)
def clear(self) -> None:
"""Clear memory contents."""
self.chat_memory.clear()
class BotConversationSummaryBufferWindowMemory(BaseChatMemory):
"""Buffer for storing conversation memory."""
human_prefix: str = "Human"

89
matrix_pygmalion_bot/bot/ai/prompts.py

@ -114,7 +114,7 @@ prompt_progressive_summary = PromptTemplate.from_template(
"""Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
### Instruction:
Based on the provided summary and new lines of conversation, give a brief and refined final summary. Only include relevant facts and key takeaways. Skip mundane details of prior events in the final and refined summary.
Based on the provided summary and new lines of conversation, give a brief and refined final summary. Include relevant facts and key takeaways. Skip mundane details of prior events in the final and refined summary.
### Input:
Current summary:
@ -128,6 +128,72 @@ New summary:
"""
)
#Progressively summarize the lines of conversation provided, adding onto the previous summary returning a new summary.
#only include relevant facts for {{char}}'s long-term memory / future
prompt_outline = PromptTemplate.from_template(
"""Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.
### Instruction:
Provide an outline of the character's day in keywords.
### Input:
{text}
### Response:
"""
)
# briefly, as a list, use bullet points, outline the main points what character needs to remember about the day, in note form, review ... point by point
prompt_agent = PromptTemplate.from_template(
"""Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request and explain what actions were used.
### Instruction:
Answer the following questions as best you can. Speak like a priate when you give the Final answer. You have access to the following tools:
{tools}
Use the following format:
Question: the input question you must answer
Thought: you should always think about what to do
Action: the action to take, should be one of [{tool_names}]
Action Input: the input to the action
Observation: the result of the action
... (this Thought/Action/Action Input/Observation can repeat N times)
Thought: I now know the final answer
Final Answer: the final answer to the original input question
Begin! Remember to speak as a pirate when giving your final answer. Use lots of "Arg"s
### Input:
{input}
### Response:
{agent_scratchpad}
"""
)
prompt_agent2 = PromptTemplate.from_template(
"""Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request and explain what actions were used.
### Instruction:
Have a conversation with a human, answering the following questions as best you can. You have access to the following tools:
{tools}
### Input:
Previous conversation:
{chat_history}
Question: {input}
Begin!
### Response:
{agent_scratchpad}
"""
)
# Roleplay the character that is described in the following lines. You always stay in character.
@ -140,6 +206,27 @@ New summary:
# \n <EXAMPLE CHAT> YOU / MIKU:
# "instruction": "Using the given facts, create a detailed profile of the character.",
# "input": "Name: Sarah Johnson\nAge: 18\nOccupation: Waitress\nLocation: Los Angeles",
# "instruction": "Please summarize the main events in the story and explain how the characters evolve throughout the narrative."
# "instruction": "Describe the given incident in a crisp and vivid way",
# "instruction": "Describe a movie scene using vivid and descriptive language.",
# "instruction": "Imagine that you are the protagonist of the following story and generate an appropriate dialogue",
# "instruction": "Generate some ideas on what the protagonist in this story could do next.",
# "instruction": "Classify the dialogue into one of the following categories: 1) making or cancelling orders; 2) shipping & delivery; 3) change and return; 4) technical issue with website or app.",
# "instruction": "Generate a dialogue between two characters, Jack and Susan, in a restaurant.",
# You are the narrator. Add some detail to the dialogue below. Write what the character Julia thinks and does. Write a vivid and graphic description of her and her surroundings for the reader.
# https://github.com/ggerganov/llama.cpp/tree/master/examples
## prompt = "Below is an instruction that describes a task. Write a response that appropriately completes the request.\n"
# prompt = "A chat between a curious human and an artificial intelligence assistant. The assistant gives helpful, detailed, and polite answers to the human's questions.\n"

73
matrix_pygmalion_bot/bot/core.py

@ -1,10 +1,13 @@
import asyncio
import concurrent.futures
import os, sys
import time
import importlib
import re
import json
import logging
from datetime import datetime, timedelta
import psutil
from functools import partial
from .memory.chatlog import ChatLog
from .utilities.messages import Message
@ -26,6 +29,13 @@ class ChatBot(object):
task = asyncio.create_task(self.worker(f'worker-{self.name}', self.queue))
self.background_tasks.add(task)
task.add_done_callback(self.background_tasks.discard)
#loop = asyncio.get_running_loop()
#with concurrent.futures.ThreadPoolExecutor() as pool:
# task = loop.run_in_executor(pool, self.worker, f'worker-{self.name}', self.queue)
event_loop_task = asyncio.create_task(self.event_loop())
self.background_tasks.add(event_loop_task)
event_loop_task.add_done_callback(self.background_tasks.discard)
#print(f"Hello, I'm {name}")
def init_character(self, persona, scenario, greeting, example_dialogue=[], nsfw=False, temperature=0.72):
@ -59,8 +69,8 @@ class ChatBot(object):
json.dump(data, f)
async def connect(self):
await self.connection.login()
self.connection.callbacks.add_message_callback(self.message_cb, self.redaction_cb)
await self.connection.login()
await self.schedule(self.queue, print, f"Hello, I'm {self.name}")
async def disconnect(self):
@ -71,6 +81,7 @@ class ChatBot(object):
task.cancel()
# Wait until all worker tasks are cancelled.
await asyncio.gather(*self.background_tasks, return_exceptions=True)
await self.write_conf2(self.rooms)
await self.connection.logout()
async def load_ai(self, available_text_endpoints, available_image_endpoints):
@ -123,18 +134,33 @@ class ChatBot(object):
reply_fn = partial(self.connection.send_message, room.room_id)
typing_fn = lambda : self.connection.room_typing(room.room_id, True, 15000)
message.user_name = message.user_name.title()
if self.name.casefold() == message.user_name.casefold():
"""Bot and user have the same name"""
message.user_name += " 2" # or simply "You"
if message.is_from(self.connection.user_id):
message.role = "ai"
else:
message.role = "human"
if not room.room_id in self.rooms:
self.rooms[room.room_id] = {}
self.write_conf2(self.rooms)
self.rooms[room.room_id]['tick'] = 0
self.rooms[room.room_id]['num_messages'] = 0
self.rooms[room.room_id]['diary'] = {}
await self.write_conf2(self.rooms)
# ToDo: set ticks 0 / start
if not self.connection.synced:
if not message.is_command() and not message.is_error():
await self.ai.add_chat_message(message.to_langchain())
self.chatlog.save(message, False)
return
if message.is_from(self.connection.user_id):
"""Skip messages from ouselves"""
self.chatlog.save(message)
await self.connection.room_read_markers(room.room_id, event.event_id, event.event_id)
return
# if event.decrypted:
@ -154,11 +180,7 @@ class ChatBot(object):
# print(f"room.users: {room.users}")
# print(f"room.room_id: {room.room_id}")
if self.name.casefold() == message.user_name.casefold():
"""Bot and user have the same name"""
message.user_name += " 2" # or simply "You"
message.user_name = message.user_name.title()
if hasattr(self, "owner"):
if not message.is_from(self.owner):
@ -174,10 +196,14 @@ class ChatBot(object):
await self.schedule(self.queue, self.process_command, message, reply_fn, typing_fn)
# elif re.search("^(?=.*\bsend\b)(?=.*\bpicture\b).*$", event.body, flags=re.IGNORECASE):
# # send, mail, drop, snap picture, photo, image, portrait
elif message.is_error():
return
else:
await self.schedule(self.queue, self.process_message, message, reply_fn, typing_fn)
self.rooms[room.room_id]['num_messages'] += 1
self.last_conversation = datetime.now()
self.chatlog.save(message)
print("done")
async def redaction_cb(self, room, event) -> None:
self.chatlog.remove_message_by_id(event.event_id)
@ -216,16 +242,19 @@ class ChatBot(object):
self.temperature = float( message.message.removeprefix('!temperature').strip() )
elif message.message.startswith('!begin'):
self.rooms[message.room_id]["disabled"] = False
self.write_conf2(self.rooms)
await self.write_conf2(self.rooms)
self.chatlog.clear(message.room_id)
await self.ai.clear(message.room_id)
# ToDo reset time / ticks
await reply_fn(self.greeting)
elif message.message.startswith('!start'):
self.rooms[message.room_id]["disabled"] = False
self.write_conf2(self.rooms)
await self.write_conf2(self.rooms)
elif message.message.startswith('!stop'):
self.rooms[message.room_id]["disabled"] = True
self.write_conf2(self.rooms)
await self.write_conf2(self.rooms)
elif message.message.startswith('!sleep'):
await self.schedule(self.queue, self.ai.sleep)
elif message.message.startswith('!!'):
if self.chatlog.chat_history_len(message.room_id) > 2:
for _ in range(2):
@ -235,11 +264,29 @@ class ChatBot(object):
async def process_message(self, message, reply_fn, typing_fn):
output = await self.ai.generate_roleplay(message, reply_fn, typing_fn)
output = await self.ai.generate_roleplay(message.to_langchain(), reply_fn, typing_fn)
#output = await self.ai.generate(message, reply_fn, typing_fn)
# typing false
await reply_fn(output)
#await reply_fn(output)
async def event_loop(self):
try:
while True:
await asyncio.sleep(60)
for room_id in self.rooms.keys():
self.rooms[room_id]["tick"] += 1
if datetime.now().hour >= 1 and datetime.now().hour < 5:
load1, load5, load15 = [x / psutil.cpu_count() * 100 for x in psutil.getloadavg()]
if load5 < 25 and load1 < 25:
if not hasattr(self, "last_sleep") or self.last_sleep + timedelta(hours=6) < datetime.now():
await self.ai.sleep()
self.last_sleep = datetime.now()
finally:
pass
# await self.write_conf2(self.name)
async def worker(self, name: str, q: asyncio.Queue) -> None:
while True:

66
matrix_pygmalion_bot/bot/utilities/messages.py

@ -1,7 +1,8 @@
from langchain.schema import AIMessage, HumanMessage, SystemMessage, ChatMessage
class Message(object):
def __init__(self, timestamp, user_name, message, event_id=None, user_id=None, room_name=None, room_id=None):
def __init__(self, timestamp, user_name, message, event_id=None, user_id=None, room_name=None, room_id=None, role=None):
self.timestamp = timestamp
self.user_name = user_name
self.message = message
@ -9,16 +10,79 @@ class Message(object):
self.user_id = user_id
self.room_name = room_name
self.room_id = room_id
self.role = role
@classmethod
def from_matrix(cls, room, event):
return cls(event.server_timestamp/1000, room.user_name(event.sender), event.body, event.event_id, event.sender, room.display_name, room.room_id)
def to_langchain(self):
if self.role == "human":
return HumanMessage(
content=self.message,
role=self.user_name, # "chat"
additional_kwargs={
"timestamp": self.timestamp,
"user_name": self.user_name,
"event_id": self.event_id,
"user_id": self.user_id,
"room_name": self.room_name,
"room_id": self.room_id,
}
)
elif self.role == "ai":
return AIMessage(
content=self.message,
role=self.user_name, # "chat"
additional_kwargs={
"timestamp": self.timestamp,
"user_name": self.user_name,
"event_id": self.event_id,
"user_id": self.user_id,
"room_name": self.room_name,
"room_id": self.room_id,
}
)
elif self.role == "system":
return SystemMessage(
content=self.message,
role=self.user_name, # "chat"
additional_kwargs={
"timestamp": self.timestamp,
"user_name": self.user_name,
"event_id": self.event_id,
"user_id": self.user_id,
"room_name": self.room_name,
"room_id": self.room_id,
}
)
else:
return ChatMessage(
content=self.message,
role=self.user_name, # "chat"
additional_kwargs={
"timestamp": self.timestamp,
"user_name": self.user_name,
"event_id": self.event_id,
"user_id": self.user_id,
"room_name": self.room_name,
"room_id": self.room_id,
}
)
def is_from(self, user_id):
return self.user_id == user_id
def is_command(self):
return self.message.startswith('!')
def is_error(self):
if self.message.startswith('<ERROR>'):
return True
elif self.message.startswith('<WARNING>'):
return True
else:
return False
def __str__(self):
return str("{}: {}".format(self.user_name, self.message))

12
matrix_pygmalion_bot/bot/wrappers/langchain_koboldcpp.py

@ -6,10 +6,11 @@ from typing import Any, List, Mapping, Optional
import json
import requests
import functools
from langchain.llms.base import LLM
from langchain.schema import BaseMessage
from langchain.schema import BaseMessage, AIMessage, HumanMessage, SystemMessage, ChatMessage
logger = logging.getLogger(__name__)
@ -122,7 +123,9 @@ class KoboldCpp(LLM):
TRIES = 30
for i in range(TRIES):
try:
r = requests.post(self.endpoint_url, json=input_data, headers=headers, timeout=600)
loop = asyncio.get_running_loop()
#r = requests.post(self.endpoint_url, json=input_data, headers=headers, timeout=600)
r = await loop.run_in_executor(None, functools.partial(requests.post, self.endpoint_url, json=input_data, headers=headers, timeout=600))
r_json = r.json()
except requests.exceptions.RequestException as e:
raise ValueError(f"http connection error.")
@ -161,8 +164,9 @@ class KoboldCpp(LLM):
for message in messages_dict:
num_tokens += tokens_per_message
for key, value in message.items():
num_tokens += len(self.get_num_tokens(value))
if key == "name":
if key == "content":
num_tokens += self.get_num_tokens(value)
elif key == "name":
num_tokens += tokens_per_name
num_tokens += 3
return num_tokens

15
matrix_pygmalion_bot/connections/matrix.py

@ -15,13 +15,15 @@ logger = logging.getLogger(__name__)
class Callbacks(object):
"""Class to pass client to callback methods."""
def __init__(self, client: AsyncClient):
def __init__(self):
self.message_callbacks = []
self.message_redaction_callbacks = []
def setup_callbacks(self, client: AsyncClient):
self.client = client
self.client.add_event_callback(self.message_cb, RoomMessageText)
self.client.add_event_callback(self.invite_cb, InviteEvent)
self.client.add_event_callback(self.redaction_cb, RedactionEvent)
self.message_callbacks = []
self.message_redaction_callbacks = []
def add_message_callback(self, callback, redaction_callback=None):
self.message_callbacks.append(callback)
@ -153,6 +155,7 @@ class ChatClient(object):
self.password = password
self.device_name = device_name
self.synced = False
self.callbacks = Callbacks()
async def persist(self, data_dir):
#self.data_dir = data_dir
@ -186,7 +189,7 @@ class ChatClient(object):
config=client_config,
)
self.callbacks = Callbacks(self.client)
self.callbacks.setup_callbacks(self.client)
resp = await self.client.login(self.password, device_name=self.device_name)
# check that we logged in succesfully
@ -209,7 +212,7 @@ class ChatClient(object):
config=client_config,
)
self.callbacks = Callbacks(self.client)
self.callbacks.setup_callbacks(self.client)
# self.client.user_id=config["user_id"],
# self.client.device_id=config["device_id"],
@ -328,7 +331,7 @@ class ChatClient(object):
await self.client.synced.wait()
logger.info("Client is synced")
self.synced = True
logger.info(f"{self.client.user_id}, {self.client.rooms}, {self.client.invited_rooms}, {self.client.encrypted_rooms}")
#logger.info(f"{self.client.user_id}, {self.client.rooms}, {self.client.invited_rooms}, {self.client.encrypted_rooms}")
# if os.path.exists(self.store_path + "megolm_keys"):
# os.remove(self.store_path + "megolm_keys", "pass")
# await self.client.export_keys(self.store_path + "megolm_keys", "pass")

16
matrix_pygmalion_bot/main.py

@ -61,16 +61,23 @@ async def main() -> None:
try:
# loop = asyncio.get_running_loop()
#
# for signame in {'SIGINT', 'SIGTERM'}:
# loop.add_signal_handler(
# getattr(signal, signame),
# functools.partial(ask_exit, signame, loop))
if sys.version_info[0] == 3 and sys.version_info[1] < 11:
tasks = []
for bot in bots:
task = asyncio.create_task(bot.connection.sync_forever(timeout=30000, full_state=True))
task = asyncio.create_task(bot.connection.sync_forever(timeout=180000, full_state=True)) # 30000
tasks.append(task)
await asyncio.gather(*tasks)
else:
async with asyncio.TaskGroup() as tg:
for bot in bots:
task = tg.create_task(bot.connection.sync_forever(timeout=30000, full_state=True))
task = tg.create_task(bot.connection.sync_forever(timeout=180000, full_state=True)) # 30000
except Exception:
print(traceback.format_exc())
@ -81,6 +88,11 @@ async def main() -> None:
await bot.disconnect()
sys.exit(0)
#def ask_exit(signame, loop):
# print("got signal %s: exit" % signame)
# loop.stop()
if __name__ == "__main__":
asyncio.run(main())

5
requirements.txt

@ -1,4 +1,5 @@
asyncio
requests
matrix-nio[e2e]
transformers
huggingface_hub
@ -10,3 +11,7 @@ langchain
chromadb
sentence-transformers
humanize
psutil
#git+https://github.com/suno-ai/bark.git
#SpeechRecognition
#TTS #(Coqui-TTS or Uberduck ??)

Loading…
Cancel
Save