Skip to content

Commit

Permalink
Merge pull request #462 from cheshire-cat-ai/445-hook-refactoring
Browse files Browse the repository at this point in the history
445 hook refactoring
  • Loading branch information
Pingdred authored Sep 19, 2023
2 parents 8926196 + ca116dd commit 520f940
Show file tree
Hide file tree
Showing 11 changed files with 543 additions and 616 deletions.
214 changes: 196 additions & 18 deletions core/cat/looking_glass/agent_manager.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,20 @@
from datetime import timedelta
import time
from typing import List, Dict

from langchain.docstore.document import Document
from langchain.prompts import PromptTemplate
from langchain.chains import LLMChain
from langchain.agents import AgentExecutor, LLMSingleActionAgent

from cat.looking_glass.prompts import ToolPromptTemplate
from cat.looking_glass import prompts
from cat.looking_glass.output_parser import ToolOutputParser
from cat.utils import verbal_timedelta
from cat.log import log




class AgentManager:
"""Manager of Langchain Agent.
Expand All @@ -26,9 +34,10 @@ def __init__(self, cat):
def execute_tool_agent(self, agent_input, allowed_tools):

allowed_tools_names = [t.name for t in allowed_tools]
# TODO: dynamic input_variables as in the main prompt

prompt = ToolPromptTemplate(
template = self.cat.mad_hatter.execute_hook("agent_prompt_instructions"),
prompt = prompts.ToolPromptTemplate(
template = self.cat.mad_hatter.execute_hook("agent_prompt_instructions", prompts.TOOL_PROMPT),
tools=allowed_tools,
# This omits the `agent_scratchpad`, `tools`, and `tool_names` variables because those are generated dynamically
# This includes the `intermediate_steps` variable because it is needed to fill the scratchpad
Expand Down Expand Up @@ -60,17 +69,13 @@ def execute_tool_agent(self, agent_input, allowed_tools):


def execute_memory_chain(self, agent_input, prompt_prefix, prompt_suffix):

input_variables = [i for i in agent_input.keys() if i in prompt_prefix + prompt_suffix]

# memory chain (second step)
memory_prompt = PromptTemplate(
template = prompt_prefix + prompt_suffix,
input_variables=[
"input",
"chat_history",
"episodic_memory",
"declarative_memory",
"tools_output"
]
input_variables=input_variables
)

memory_chain = LLMChain(
Expand All @@ -85,7 +90,7 @@ def execute_memory_chain(self, agent_input, prompt_prefix, prompt_suffix):
return out


def execute_agent(self, agent_input):
def execute_agent(self):
"""Instantiate the Agent with tools.
The method formats the main prompt and gather the allowed tools. It also instantiates a conversational Agent
Expand All @@ -97,16 +102,28 @@ def execute_agent(self, agent_input):
Instance of the Agent provided with a set of tools.
"""
mad_hatter = self.cat.mad_hatter

# this hook allows to reply without executing the agent (for example canned responses, out-of-topic barriers etc.)
fast_reply = mad_hatter.execute_hook("before_agent_starts", agent_input)
if fast_reply:
working_memory = self.cat.working_memory

# prepare input to be passed to the agent.
# Info will be extracted from working memory
agent_input = self.format_agent_input()
agent_input = mad_hatter.execute_hook("before_agent_starts", agent_input)
# should we ran the default agent?
fast_reply = {}
fast_reply = mad_hatter.execute_hook("agent_fast_reply", fast_reply)
if len(fast_reply.keys()) > 0:
return fast_reply
prompt_prefix = mad_hatter.execute_hook("agent_prompt_prefix", prompts.MAIN_PROMPT_PREFIX)
prompt_suffix = mad_hatter.execute_hook("agent_prompt_suffix", prompts.MAIN_PROMPT_SUFFIX)

prompt_prefix = mad_hatter.execute_hook("agent_prompt_prefix")
prompt_suffix = mad_hatter.execute_hook("agent_prompt_suffix")

allowed_tools = mad_hatter.execute_hook("agent_allowed_tools")
# tools currently recalled in working memory
recalled_tools = working_memory["procedural_memories"]
# Get the tools names only
tools_names = [t[0].metadata["name"] for t in recalled_tools]
tools_names = mad_hatter.execute_hook("agent_allowed_tools", tools_names)
# Get tools with that name from mad_hatter
allowed_tools = [i for i in mad_hatter.tools if i.name in tools_names]

# Try to get information from tools if there is some allowed
if len(allowed_tools) > 0:
Expand Down Expand Up @@ -160,3 +177,164 @@ def execute_agent(self, agent_input):
out = self.execute_memory_chain(agent_input, prompt_prefix, prompt_suffix)

return out

def format_agent_input(self):
"""Format the input for the Agent.
The method formats the strings of recalled memories and chat history that will be provided to the Langchain
Agent and inserted in the prompt.
Returns
-------
dict
Formatted output to be parsed by the Agent executor.
Notes
-----
The context of memories and conversation history is properly formatted before being parsed by the and, hence,
information are inserted in the main prompt.
All the formatting pipeline is hookable and memories can be edited.
See Also
--------
agent_prompt_episodic_memories
agent_prompt_declarative_memories
agent_prompt_chat_history
"""

working_memory = self.cat.working_memory

# format memories to be inserted in the prompt
episodic_memory_formatted_content = self.agent_prompt_episodic_memories(
working_memory["episodic_memories"]
)
declarative_memory_formatted_content = self.agent_prompt_declarative_memories(
working_memory["declarative_memories"]
)

# format conversation history to be inserted in the prompt
conversation_history_formatted_content = self.agent_prompt_chat_history(
working_memory["history"]
)

return {
"input": working_memory["user_message_json"]["text"],
"episodic_memory": episodic_memory_formatted_content,
"declarative_memory": declarative_memory_formatted_content,
"chat_history": conversation_history_formatted_content,
}

def agent_prompt_episodic_memories(self, memory_docs: List[Document]) -> str:
"""Formats episodic memories to be inserted into the prompt.
Parameters
----------
memory_docs : List[Document]
List of Langchain `Document` retrieved from the episodic memory.
Returns
-------
memory_content : str
String of retrieved context from the episodic memory.
"""

# convert docs to simple text
memory_texts = [m[0].page_content.replace("\n", ". ") for m in memory_docs]

# add time information (e.g. "2 days ago")
memory_timestamps = []
for m in memory_docs:

# Get Time information in the Document metadata
timestamp = m[0].metadata["when"]

# Get Current Time - Time when memory was stored
delta = timedelta(seconds=(time.time() - timestamp))

# Convert and Save timestamps to Verbal (e.g. "2 days ago")
memory_timestamps.append(f" ({verbal_timedelta(delta)})")

# Join Document text content with related temporal information
memory_texts = [a + b for a, b in zip(memory_texts, memory_timestamps)]

# Format the memories for the output
memories_separator = "\n - "
memory_content = "## Context of things the Human said in the past: " + \
memories_separator + memories_separator.join(memory_texts)

# if no data is retrieved from memory don't erite anithing in the prompt
if len(memory_texts) == 0:
memory_content = ""

return memory_content

def agent_prompt_declarative_memories(self, memory_docs: List[Document]) -> str:
"""Formats the declarative memories for the prompt context.
Such context is placed in the `agent_prompt_prefix` in the place held by {declarative_memory}.
Parameters
----------
memory_docs : List[Document]
list of Langchain `Document` retrieved from the declarative memory.
Returns
-------
memory_content : str
String of retrieved context from the declarative memory.
"""

# convert docs to simple text
memory_texts = [m[0].page_content.replace("\n", ". ") for m in memory_docs]

# add source information (e.g. "extracted from file.txt")
memory_sources = []
for m in memory_docs:

# Get and save the source of the memory
source = m[0].metadata["source"]
memory_sources.append(f" (extracted from {source})")

# Join Document text content with related source information
memory_texts = [a + b for a, b in zip(memory_texts, memory_sources)]

# Format the memories for the output
memories_separator = "\n - "

memory_content = "## Context of documents containing relevant information: " + \
memories_separator + memories_separator.join(memory_texts)

# if no data is retrieved from memory don't erite anithing in the prompt
if len(memory_texts) == 0:
memory_content = ""

return memory_content

def agent_prompt_chat_history(self, chat_history: List[Dict]) -> str:
"""Serialize chat history for the agent input.
Converts to text the recent conversation turns fed to the *Agent*.
Parameters
----------
chat_history : List[Dict]
List of dictionaries collecting speaking turns.
Returns
-------
history : str
String with recent conversation turns to be provided as context to the *Agent*.
Notes
-----
Such context is placed in the `agent_prompt_suffix` in the place held by {chat_history}.
The chat history is a dictionary with keys::
'who': the name of who said the utterance;
'message': the utterance.
"""
history = ""
for turn in chat_history:
history += f"\n - {turn['who']}: {turn['message']}"

return history

Loading

0 comments on commit 520f940

Please sign in to comment.