memory injection with filter, mid messages summary

frdel committed Jun 24, 2024 at 23:18 UTC 422d5ca983785c56dc0d07caae5c34e9fd6258ab
25 files changed +666 -161
agent.py
+157 -68
@@ -1,41 +1,57 @@
1 +import json
2 import time, importlib, inspect
2 -from typing import Optional, Dict
3 -from tools.helpers import extract_tools, rate_limiter, files
3 +import traceback
4 +from typing import Optional, Dict, TypedDict
5 +from tools.helpers import extract_tools, rate_limiter, files, errors
6 from tools.helpers.print_style import PrintStyle
7 from langchain.schema import AIMessage
8 from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
7 -from langchain_core.messages import HumanMessage
9 +from langchain_core.messages import HumanMessage, SystemMessage
10 from langchain_core.language_models.chat_models import BaseChatModel
11 +from langchain_core.embeddings import Embeddings
12 +
13 +# rate_limit = rate_limiter.rate_limiter(30,160000) #TODO! implement properly
14
10 -rate_limit = rate_limiter.rate_limiter(30,160000) #TODO! implement properly
15
16 class Agent:
17
18 paused=False
19 streaming_agent=None
20 +
21 + def __init__(self,
22 + agent_number: int,
23 + chat_llm:BaseChatModel,
24 + embeddings_model:Embeddings,
25 + memory_subdir: str = "",
26 + auto_memory_count: int = 3,
27 + auto_memory_skip: int = 2,
28 + rate_limit_seconds: int = 60,
29 + rate_limit_input_tokens: int = 0,
30 + rate_limit_output_tokens: int = 0,
31 + msgs_keep_max: int =25,
32 + msgs_keep_start: int =5,
33 + msgs_keep_end: int =10,
34 + **kwargs):
35
17 - @staticmethod
18 - def configure(model_chat, model_embedding, memory_subdir="", memory_results=3):
36 + # agent config
37 + self.agent_number = agent_number
38 + self.chat_model = chat_llm
39 + self.embeddings_model = embeddings_model
40 + self.memory_subdir = memory_subdir
41 + self.auto_memory_count = auto_memory_count
42 + self.auto_memory_skip = auto_memory_skip
43 + self.rate_limit_seconds = rate_limit_seconds
44 + self.rate_limit_input_tokens = rate_limit_input_tokens
45 + self.rate_limit_output_tokens = rate_limit_output_tokens
46 + self.msgs_keep_max = msgs_keep_max
47 + self.msgs_keep_start = msgs_keep_start
48 + self.msgs_keep_end = msgs_keep_end
49
20 - #save configuration
21 - Agent.model_chat = model_chat
50 + # non-config vars
51 + self.agent_name = f"Agent {self.agent_number}"
52
23 - # initialize memory tool
24 - from tools import memory_tool
25 - memory_tool.initialize(
26 - embeddings_model=model_embedding,
27 - messages_returned=memory_results,
28 - subdir=memory_subdir )
29 -
30 - def __init__(self, system_prompt:Optional[str]=None, tools_prompt:Optional[str]=None, number=0):
31 -
32 - self.number = number
33 - self.name = f"Agent {self.number}"
34 -
35 - if system_prompt is None: system_prompt = files.read_file("./prompts/agent.system.md")
36 - if tools_prompt is None: tools_prompt = files.read_file("./prompts/agent.tools.md")
37 - self.system_prompt = system_prompt.replace("{", "{{").replace("}", "}}")
38 - self.tools_prompt = tools_prompt.replace("{", "{{").replace("}", "}}")
53 + self.system_prompt = files.read_file("./prompts/agent.system.md").replace("{", "{{").replace("}", "}}")
54 + self.tools_prompt = files.read_file("./prompts/agent.tools.md").replace("{", "{{").replace("}", "}}")
55
56 self.history = []
57 self.last_message = ""
@@ -43,64 +59,67 @@ class Agent:
59 self.intervention_status = False
60
61 self.data = {} # free data object all the tools can use
46 -
47 - self.prompt = ChatPromptTemplate.from_messages([
48 - ("system", self.system_prompt + "\n\n" + self.tools_prompt),
49 - MessagesPlaceholder(variable_name="messages") ])
62
63 def message_loop(self, msg: str):
64 try:
65 printer = PrintStyle(italic=True, font_color="#b3ffd9", padding=False)
66 user_message = files.read_file("./prompts/fw.user_message.md", message=msg)
67 self.append_message(user_message, human=True) # Append the user's input to the history
56 -
57 - self.stop_loop = False
58 - self.loop_result = []
59 -
68 + memories = self.fetch_memories(True)
69 +
70 while True: # let the agent iterate on his thoughts until he stops by using a tool
71 Agent.streaming_agent = self #mark self as current streamer
72 agent_response = ""
73 self.intervention_status = False # reset interventon status
74 +
75 try:
76
77 + system = self.system_prompt + "\n\n" + self.tools_prompt
78 + memories = self.fetch_memories()
79 + if memories: system+= "\n\n"+memories
80 +
81 + prompt = ChatPromptTemplate.from_messages([
82 + SystemMessage(content=system),
83 + MessagesPlaceholder(variable_name="messages") ])
84 +
85 inputs = {"messages": self.history}
67 - chain = self.prompt | Agent.model_chat
68 - formatted_inputs = self.prompt.format(**inputs)
86 + chain = prompt | self.chat_model
87 + formatted_inputs = prompt.format(messages=self.history)
88
70 - rate_limit(len(formatted_inputs)/4) #wait for rate limiter - A helpful rule of thumb is that one token generally corresponds to ~4 characters of text for common English text. This translates to roughly ¾ of a word (so 100 tokens ~= 75 words).
89 + # rate_limit(len(formatted_inputs)/4) #wait for rate limiter - A helpful rule of thumb is that one token generally corresponds to ~4 characters of text for common English text. This translates to roughly ¾ of a word (so 100 tokens ~= 75 words).
90
91 # output that the agent is starting
73 - PrintStyle(bold=True, font_color="green", padding=True, background_color="white").print(f"{self.name}: Starting a message:")
92 + PrintStyle(bold=True, font_color="green", padding=True, background_color="white").print(f"{self.agent_name}: Starting a message:")
93
94 for chunk in chain.stream(inputs):
76 -
95 if self.handle_intervention(agent_response): break # wait for intervention and handle it, if paused
78 -
79 - if chunk.content is not None and chunk.content != '':
80 - printer.stream(chunk.content) # output the agent response stream
81 - agent_response += chunk.content # type: ignore | concatenate stream into the response
96 +
97 + if isinstance(chunk, str): content = chunk
98 + elif hasattr(chunk, "content"): content = str(chunk.content)
99 + else: content = str(chunk)
100 +
101 + if content:
102 + printer.stream(content) # output the agent response stream
103 + agent_response += content # concatenate stream into the response
104
105 if not self.handle_intervention(agent_response):
84 - #if assistant_response is the same as last message in history, let him know
85 - if self.last_message == agent_response:
86 - agent_response = files.read_file("./prompts/fw.msg_repeat.md")
87 - PrintStyle(font_color="orange", padding=True).print(agent_response)
88 - self.last_message = agent_response
89 -
90 - self.append_message(agent_response) # Append the assistant's response to the history
106 + if self.last_message == agent_response: #if assistant_response is the same as last message in history, let him know
107 + self.append_message(agent_response) # Append the assistant's response to the history
108 + warning_msg = files.read_file("./prompts/fw.msg_repeat.md")
109 + self.append_message(warning_msg, human=True) # Append warning message to the history
110 + PrintStyle(font_color="orange", padding=True).print(warning_msg)
111
92 - tools_result = self.process_tools(agent_response) # process tools requested in agent message
93 - if tools_result: return tools_result #break the execution if the task is done
112 + else: #otherwise proceed with tool
113 + self.append_message(agent_response) # Append the assistant's response to the history
114 + tools_result = self.process_tools(agent_response) # process tools requested in agent message
115 + if tools_result: return tools_result #break the execution if the task is done
116
117 # Forward errors to the LLM, maybe he can fix them
118 except Exception as e:
97 - msg_response = files.read_file("./prompts/fw.error.md", error=str(e)) # error message template
119 + error_message = errors.format_error(e)
120 + msg_response = files.read_file("./prompts/fw.error.md", error=error_message) # error message template
121 self.append_message(msg_response, human=True)
122 PrintStyle(font_color="red", padding=True).print(msg_response)
100 - finally:
101 - if self.get_last_message().type=="ai": #type: ignore
102 - user_message = files.read_file("./prompts/fw.msg_continue.md")
103 - PrintStyle(font_color="yellow", padding=False).print(user_message)
123
124 finally:
125 Agent.streaming_agent = None # unset current streamer
@@ -118,25 +137,74 @@ class Agent:
137 else:
138 new_message = HumanMessage(content=msg) if human else AIMessage(content=msg)
139 self.history.append(new_message)
121 - self.cleanup_history(5, 10)
140 + self.cleanup_history(self.msgs_keep_max, self.msgs_keep_start, self.msgs_keep_end)
141 if message_type=="ai":
142 self.last_message = msg
143
144 + def concat_messages(self,messages):
145 + return "\n".join([f"{msg.type}: {msg.content}" for msg in messages])
146 +
147 + def send_adhoc_message(self, system: str, msg: str, output_label:str):
148 + prompt = ChatPromptTemplate.from_messages([
149 + SystemMessage(content=system),
150 + HumanMessage(content=msg)])
151 +
152 + chain = prompt | self.chat_model
153 + response = ""
154 + printer = None
155 +
156 + if output_label:
157 + PrintStyle(bold=True, font_color="orange", padding=True, background_color="white").print(f"{self.agent_name}: {output_label}:")
158 + printer = PrintStyle(italic=True, font_color="orange", padding=False)
159 +
160 + for chunk in chain.stream({}):
161 + if self.handle_intervention(response): break # wait for intervention and handle it, if paused
162 +
163 + if isinstance(chunk, str): content = chunk
164 + elif hasattr(chunk, "content"): content = str(chunk.content)
165 + else: content = str(chunk)
166 +
167 + if printer: printer.stream(content)
168 + response+=content
169 +
170 + return response
171 +
172 def get_last_message(self):
173 if self.history:
174 return self.history[-1]
175
129 - def cleanup_history(self,x, y):
130 - if len(self.history) <= x + y:
176 + def replace_middle_messages(self,middle_messages):
177 + cleanup_prompt = files.read_file("./prompts/fw.msg_cleanup.md")
178 + summary = self.send_adhoc_message(system=cleanup_prompt,msg=self.concat_messages(middle_messages), output_label="Mid messages cleanup summary")
179 + new_human_message = HumanMessage(content=summary)
180 + return [new_human_message]
181 +
182 + def cleanup_history(self, max:int, keep_start:int, keep_end:int):
183 + if len(self.history) <= max:
184 return self.history
132 -
133 - first_x = self.history[:x]
134 - last_y = self.history[-y:]
185
136 - cleanup_prompt = files.read_file("./prompts/fw.msg_cleanup.md")
137 - middle_values = [AIMessage(content=cleanup_prompt)]
138 -
139 - self.history = first_x + middle_values + last_y
186 + first_x = self.history[:keep_start]
187 + last_y = self.history[-keep_end:]
188 +
189 + # Identify the middle part
190 + middle_part = self.history[keep_start:-keep_end]
191 +
192 + # Ensure the first message in the middle is "human", if not, move one message back
193 + if middle_part and middle_part[0].type != "human":
194 + if len(first_x) > 0:
195 + middle_part.insert(0, first_x.pop())
196 +
197 + # Ensure the middle part has an odd number of messages
198 + if len(middle_part) % 2 == 0:
199 + middle_part = middle_part[:-1]
200 +
201 + # Replace the middle part using the replacement function
202 + new_middle_part = self.replace_middle_messages(middle_part)
203 +
204 + self.history = first_x + new_middle_part + last_y
205 +
206 + return self.history
207 +
208
209 def handle_intervention(self, progress:str="") -> bool:
210 while self.paused: time.sleep(0.1) # wait if paused
@@ -151,10 +219,12 @@ class Agent:
219 def process_tools(self, msg: str):
220 # search for tool usage requests in agent message
221 tool_request = extract_tools.json_parse_dirty(msg)
222 + tool_name = tool_request.get("tool_name", "")
223 + tool_args = tool_request.get("tool_args", {})
224
225 tool = self.get_tool(
156 - tool_request["tool_name"],
157 - tool_request["tool_args"],
226 + tool_name,
227 + tool_args,
228 msg)
229
230 if self.handle_intervention(): return # wait if paused and handle intervention message if needed
@@ -179,4 +249,23 @@ class Agent:
249 tool_class = cls[1]
250 break
251
182 - return tool_class(agent=self, name=name, args=args, message=message, **kwargs)
\ No newline at end of file
252 + return tool_class(agent=self, name=name, args=args, message=message, **kwargs)
253 +
254 + def fetch_memories(self,reset_skip=False):
255 + if reset_skip: self.memory_skip_counter = 0
256 +
257 + if self.memory_skip_counter > 0:
258 + self.memory_skip_counter-=1
259 + return ""
260 + else:
261 + self.memory_skip_counter = self.auto_memory_skip
262 + from tools import memory_tool
263 + messages = self.concat_messages(self.history)
264 + memories = memory_tool.process_query(self,messages,"load")
265 + input = {
266 + "conversation_history" : messages,
267 + "raw_memories": memories
268 + }
269 + cleanup_prompt = files.read_file("./prompts/msg.memory_cleanup.md").replace("{", "{{")
270 + clean_memories = self.send_adhoc_message(cleanup_prompt,json.dumps(input), output_label="Memory cleanup summary")
271 + return clean_memories
\ No newline at end of file
main.py
+6 -11
@@ -23,23 +23,18 @@ def chat():
23 # chat_llm = models.get_openai_gpt4o(temperature=0)
24 # chat_llm = models.get_anthropic_opus(temperature=0)
25 # chat_llm = models.get_anthropic_sonnet(temperature=0)
26 - chat_llm = models.get_anthropic_haiku(temperature=0)
26 + chat_llm = models.get_anthropic_sonnet_35(temperature=0)
27 + # chat_llm = models.get_anthropic_haiku(temperature=0)
28 # chat_llm = models.get_ollama_dolphin()
29
30 # embedding model used for memory
31 # embedding_llm = models.get_embedding_openai()
32 embedding_llm = models.get_embedding_hf()
32 -
33 - # initial configuration
34 - Agent.configure(
35 - model_chat = chat_llm,
36 - model_embedding = embedding_llm,
37 - #memory_subdir=""
38 - #memory_results=3
39 - )
33
34 # create the first agent
42 - agent0 = Agent()
35 + agent0 = Agent(agent_number=0,
36 + chat_llm=chat_llm,
37 + embeddings_model=embedding_llm)
38
39 # start the conversation loop
40 while True:
@@ -73,7 +68,7 @@ def chat():
68 assistant_response = agent0.message_loop(user_input)
69
70 # print agent0 response
76 - PrintStyle(font_color="white",background_color="#1D8348", bold=True, padding=True).print(f"{agent0.name}: reponse:")
71 + PrintStyle(font_color="white",background_color="#1D8348", bold=True, padding=True).print(f"{agent0.agent_name}: reponse:")
72 PrintStyle(font_color="white").print(f"{assistant_response}")
73
74
models.py
+7 -2
@@ -22,6 +22,11 @@ def get_anthropic_haiku(api_key=None, temperature=DEFAULT_TEMPERATURE):
22 api_key = api_key or get_api_key("anthropic")
23 return ChatAnthropic(model_name="claude-3-haiku-20240307", temperature=temperature, api_key=api_key) # type: ignore
24
25 +def get_anthropic_sonnet_35(api_key=None, temperature=DEFAULT_TEMPERATURE):
26 + api_key = api_key or get_api_key("anthropic")
27 + return ChatAnthropic(model_name="claude-3-5-sonnet-20240620", temperature=temperature, api_key=api_key) # type: ignore
28 +
29 +
30 def get_anthropic_sonnet(api_key=None, temperature=DEFAULT_TEMPERATURE):
31 api_key = api_key or get_api_key("anthropic")
32 return ChatAnthropic(model_name="claude-3-sonnet-20240229", temperature=temperature, api_key=api_key) # type: ignore
@@ -68,10 +73,10 @@ def get_groq_gemma(api_key=None, temperature=DEFAULT_TEMPERATURE):
73 return ChatGroq(model_name="gemma-7b-it", temperature=temperature, api_key=api_key) # type: ignore
74
75 def get_ollama_dolphin(api_key=None, temperature=DEFAULT_TEMPERATURE):
71 - return Ollama(model="dolphin-llama3:8b-256k-v2.9-fp16")
76 + return Ollama(model="dolphin-llama3:8b-256k-v2.9-fp16", temperature=temperature)
77
78 def get_ollama_phi(api_key=None, temperature=DEFAULT_TEMPERATURE):
74 - return Ollama(model="phi3:3.8b-mini-instruct-4k-fp16")
79 + return Ollama(model="phi3:3.8b-mini-instruct-4k-fp16",temperature=temperature)
80
81 def get_embedding_hf(model_name="sentence-transformers/all-MiniLM-L6-v2"):
82 return HuggingFaceEmbeddings(model_name=model_name)
prompts/agent.memory.md new
+5
@@ -0,0 +1,5 @@
1 +# Memories
2 +- following are your memories on the current topic
3 +- you may find some of them helpful to solve the current task
4 +
5 +{{memories}}
\ No newline at end of file
prompts/agent.system.md
+1
@@ -9,6 +9,7 @@
9 - Tools help you gather knowledge and execute actions
10 3. **tool_args**: Object of arguments that are passed to the tool
11 - Each tool has specific arguments listed in Available tools section
12 +- No text before or after the JSON object. End message there.
13
14 ## Response example that must be used every time
15 ~~~json
prompts/agent.tools.md
+9 -16
@@ -41,28 +41,21 @@ Always verify memory by online.
41 }
42 ~~~
43
44 -### memory_tool:
45 -Access your persistent memory to load or save memories.
46 -Memories can help you to remember important information and later reuse it.
47 -With this you are able to learn and improve.
48 -Use argument "action" with value "load", "save" or "delete", based on what you want to do.
49 -Use argument "memory" for content to load or save.
50 -When loading memories using action "load", provide keywords or question relevant to your current task.
51 -When saving memories using action "save", provide a title, short summary and and all the necessary information to help you later solve similiar tasks including details like code executed, libraries used etc.
52 -When deleting memories using action "delete", provide a prompt to search memories to delete.
53 -Be specific with your question, do not input vague queries.
44 +### memorize:
45 +Save information to persistent memory.
46 +Memories can help you remember important details and later reuse them.
47 +Provide a title, short summary and and all the necessary information to help you later solve similiar tasks including details like code executed, libraries used etc.
48 **Example usages**:
49 ~~~json
50 {
51 "thoughts": [
58 - "I need to do...",
59 - "Maybe I have done it in the past...",
60 - "Let me check the memory...",
52 + "I have finished my...",
53 + "Details of this process will be valuable...",
54 + "Let's save tools and code used...",
55 ],
62 - "tool_name": "memory_tool",
56 + "tool_name": "memorize",
57 "tool_args": {
64 - "action": "load",
65 - "question": "How to...",
58 + "memory": "# How to...",
59 }
60 }
61 ~~~
prompts/fw.memorized.md new
+1
@@ -0,0 +1 @@
1 +Information saved to memory.
\ No newline at end of file
prompts/fw.msg_cleanup.md
+11 -1
@@ -1 +1,11 @@
1 -NOTICE: Some messages here have been removed to save memory.
\ No newline at end of file
1 +# Provide a JSON summary of given messages
2 +- From the messages you are given, write a summary of key points in the conversation.
3 +- Include important aspects and remove unnecessary details.
4 +
5 +# Expected output format
6 +~~~json
7 +{
8 + "system_info": "Messages have been summarized to save space.",
9 + "messages_summary": ["Key point 1...", "Key point 2..."]
10 +}
11 +~~~
\ No newline at end of file
prompts/fw.msg_continue.md deleted
-1
@@ -1 +0,0 @@
1 -Continue with your thoughts and use tool or message when ready.
\ No newline at end of file
prompts/fw.msg_info_sent.md deleted
-2
@@ -1,2 +0,0 @@
1 -Information sent, the user will not respond to info messages.
2 -If you required user interaction use response_required="true" instead.
\ No newline at end of file
prompts/fw.msg_repeat.md
+5 -1
@@ -1 +1,5 @@
1 -I tried the same response twice. I have to do something else.
\ No newline at end of file
1 +~~~json
2 +{
3 + "system_warning": "You have sent the same message again. You have to do something else!"
4 +}
5 +~~~
\ No newline at end of file
prompts/fw.msg_sent.md deleted
-1
@@ -1 +0,0 @@
1 -Message sent, wait for response.
\ No newline at end of file
prompts/fw.tool_not_found.md
+5 -2
@@ -1,2 +1,5 @@
1 -Tool {{tool_name}} not found. Available tools:
2 -{{tools_prompt}}
\ No newline at end of file
1 +~~~json
2 +{
3 + "system_warning": "Tool {{tool_name}} not found. Available tools: \n{{tools_prompt}}"
4 +}
5 +~~~
\ No newline at end of file
prompts/msg.memory_cleanup.md new
+13
@@ -0,0 +1,13 @@
1 +# Cleanup raw memories from database
2 +- You will receive two data collections:
3 + 1. Conversation history of AI agent.
4 + 2. Raw memories from vector database based on similarity score.
5 +- Your job is to remove all memories from the database that are not relevant to the topic of the conversation history and only return memories that are relevant and helpful for future of the conversation.
6 +- Database can sometimes produce results very different from the conversation, these have to be remove.
7 +- Focus on the end of the conversation history, that is where the most current topic is.
8 +
9 +# Expected output format
10 +- Return filtered list of bullet points of key elements in the memories
11 +- Include every important detail relevant to conversation
12 +- Include code snippets if relevant
13 +- Omit any unrelevant information
\ No newline at end of file
test.py new
+26
@@ -0,0 +1,26 @@
1 +def extract_json_string(content):
2 + start = content.find('{')
3 + if start == -1:
4 + print("No JSON content found.")
5 + return ""
6 +
7 + # Find the first '{'
8 + end = content.rfind('}')
9 + if end == -1:
10 + # If there's no closing '}', return from start to the end
11 + return content[start:]
12 + else:
13 + # If there's a closing '}', return the substring from start to end
14 + return content[start:end+1]
15 +
16 +# Test cases
17 +test_cases = [
18 + 'Some text before {"key1": "value1", "key2": 123, "key3": true, "key4": null} some text after',
19 + '{"key1": "value1", "key2": 123, "key3": true, "key4": null', # Incomplete JSON
20 + '{"nested": {"key": "value"}, "list": [1, 2, 3], "bool": true}',
21 + 'text without json',
22 +]
23 +
24 +# Run the test cases
25 +results = [extract_json_string(tc) for tc in test_cases]
26 +print(results)
tools/delegation.py
+3 -2
@@ -5,10 +5,11 @@ from tools.helpers.print_style import PrintStyle
5
6 class Delegation(Tool):
7
8 - def execute(self):
8 + def execute(self, **kwargs):
9 # create subordinate agent using the data object on this agent and set superior agent to his data object
10 if self.agent.get_data("subordinate") is None or self.args["reset"].lower().strip() == "true":
11 - subordinate = Agent(system_prompt=self.agent.system_prompt, tools_prompt=self.agent.tools_prompt, number=self.agent.number+1)
11 + # subordinate = Agent(system_prompt=self.agent.system_prompt, tools_prompt=self.agent.tools_prompt, number=self.agent.number+1)
12 + subordinate = Agent(**self.agent.__dict__, agent_number=self.agent.agent_number+1)
13 subordinate.set_data("superior", self.agent)
14 self.agent.set_data("subordinate", subordinate)
15 # run subordinate agent message loop
tools/helpers/dirty_json.py new
+276
@@ -0,0 +1,276 @@
1 +
2 +# work in progress, but quite good already
3 +# able to parse json like this, even when cut in half:
4 +
5 +# {
6 +# name: John Doe,
7 +# 'age': 30,
8 +# 'some': undefined,
9 +# other: tRue,
10 +# city: "New York",
11 +# "hobbies": ["reading", 'cycling'],
12 +# married: false,
13 +# children: null,
14 +# "bio": """A multi-line
15 +# biography that
16 +# spans several lines""",
17 +# 'quote': """Another
18 +# multi-line quote
19 +# using single quotes"""
20 +# }
21 +
22 +
23 +class DirtyJson:
24 + def __init__(self):
25 + self._reset()
26 +
27 + def _reset(self):
28 + self.json_string = ""
29 + self.index = 0
30 + self.current_char = None
31 + self.result = None
32 + self.stack = []
33 +
34 + @staticmethod
35 + def parse_string(json_string):
36 + parser = DirtyJson()
37 + return parser.parse(json_string)
38 +
39 + def parse(self, json_string):
40 + self._reset()
41 + self.json_string = json_string
42 + self.current_char = self.json_string[0]
43 + self._parse()
44 + return self.result
45 +
46 + def feed(self, chunk):
47 + self.json_string += chunk
48 + if not self.current_char and self.json_string:
49 + self.current_char = self.json_string[0]
50 + self._parse()
51 + return self.result
52 +
53 + def _advance(self,count=1):
54 + self.index += count
55 + if self.index < len(self.json_string):
56 + self.current_char = self.json_string[self.index]
57 + else:
58 + self.current_char = None
59 +
60 + def _skip_whitespace(self):
61 + while self.current_char is not None and self.current_char.isspace():
62 + self._advance()
63 +
64 + def _parse(self):
65 + if self.result is None:
66 + self.result = self._parse_value()
67 + else:
68 + self._continue_parsing()
69 +
70 + def _continue_parsing(self):
71 + while self.current_char is not None:
72 + if isinstance(self.result, dict):
73 + self._parse_object_content()
74 + elif isinstance(self.result, list):
75 + self._parse_array_content()
76 + elif isinstance(self.result, str):
77 + self.result = self._parse_string()
78 + else:
79 + break
80 +
81 + def _parse_value(self):
82 + self._skip_whitespace()
83 + if self.current_char == '{':
84 + return self._parse_object()
85 + elif self.current_char == '[':
86 + return self._parse_array()
87 + elif self.current_char in ['"', "'"]:
88 + if self._peek(2) == self.current_char * 2: # type: ignore
89 + return self._parse_multiline_string()
90 + return self._parse_string()
91 + elif self.current_char and (self.current_char.isdigit() or self.current_char in ['-', '+']):
92 + return self._parse_number()
93 + elif self._match("true"):
94 + return True
95 + elif self._match('false'):
96 + return False
97 + elif self._match('null') or self._match("undefined"):
98 + return None
99 + elif self.current_char:
100 + return self._parse_unquoted_string()
101 + return None
102 +
103 + def _match(self, text:str) -> bool:
104 + cnt = len(text)
105 + if self._peek(cnt).lower() == text.lower():
106 + self._advance(cnt)
107 + return True
108 + return False
109 +
110 + def _parse_object(self):
111 + obj = {}
112 + self._advance() # Skip opening brace
113 + self.stack.append(obj)
114 + self._parse_object_content()
115 + return obj
116 +
117 + def _parse_object_content(self):
118 + while self.current_char is not None:
119 + self._skip_whitespace()
120 + if self.current_char == '}':
121 + self._advance()
122 + self.stack.pop()
123 + return
124 + if self.current_char is None:
125 + return # End of input reached while parsing object
126 +
127 + key = self._parse_key()
128 + value = None
129 + self._skip_whitespace()
130 +
131 + if self.current_char == ':':
132 + self._advance()
133 + value = self._parse_value()
134 + elif self.current_char is None:
135 + value = None # End of input reached after key
136 + else:
137 + value = self._parse_value()
138 +
139 + self.stack[-1][key] = value
140 +
141 + self._skip_whitespace()
142 + if self.current_char == ',':
143 + self._advance()
144 + continue
145 + elif self.current_char != '}':
146 + if self.current_char is None:
147 + return # End of input reached after value
148 + # Allow missing comma between key-value pairs
149 + continue
150 +
151 + def _parse_key(self):
152 + self._skip_whitespace()
153 + if self.current_char in ['"', "'"]:
154 + return self._parse_string()
155 + else:
156 + return self._parse_unquoted_key()
157 +
158 + def _parse_unquoted_key(self):
159 + result = ""
160 + while self.current_char is not None and not self.current_char.isspace() and self.current_char not in [':', ',', '}', ']']:
161 + result += self.current_char
162 + self._advance()
163 + return result
164 +
165 + def _parse_array(self):
166 + arr = []
167 + self._advance() # Skip opening bracket
168 + self.stack.append(arr)
169 + self._parse_array_content()
170 + return arr
171 +
172 + def _parse_array_content(self):
173 + while self.current_char is not None:
174 + self._skip_whitespace()
175 + if self.current_char == ']':
176 + self._advance()
177 + self.stack.pop()
178 + return
179 + value = self._parse_value()
180 + self.stack[-1].append(value)
181 + self._skip_whitespace()
182 + if self.current_char == ',':
183 + self._advance()
184 + elif self.current_char != ']':
185 + self.stack.pop()
186 + return
187 +
188 + def _parse_string(self):
189 + result = ""
190 + quote_char = self.current_char
191 + self._advance() # Skip opening quote
192 + while self.current_char is not None and self.current_char != quote_char:
193 + if self.current_char == '\\':
194 + self._advance()
195 + if self.current_char in ['"', "'", '\\', '/', 'b', 'f', 'n', 'r', 't']:
196 + result += {'b': '\b', 'f': '\f', 'n': '\n', 'r': '\r', 't': '\t'}.get(self.current_char, self.current_char)
197 + elif self.current_char == 'u':
198 + unicode_char = ""
199 + for _ in range(4):
200 + if self.current_char is None:
201 + return result
202 + unicode_char += self.current_char
203 + self._advance()
204 + result += chr(int(unicode_char, 16))
205 + continue
206 + else:
207 + result += self.current_char
208 + self._advance()
209 + if self.current_char == quote_char:
210 + self._advance() # Skip closing quote
211 + return result
212 +
213 + def _parse_multiline_string(self):
214 + result = ""
215 + quote_char = self.current_char
216 + self._advance(3) # Skip first quote
217 + while self.current_char is not None:
218 + if self.current_char == quote_char and self._peek(2) == quote_char * 2: # type: ignore
219 + self._advance(3) # Skip first quote
220 + break
221 + result += self.current_char
222 + self._advance()
223 + return result.strip()
224 +
225 + def _parse_number(self):
226 + number_str = ""
227 + while self.current_char is not None and (self.current_char.isdigit() or self.current_char in ['-', '+', '.', 'e', 'E']):
228 + number_str += self.current_char
229 + self._advance()
230 + try:
231 + return int(number_str)
232 + except ValueError:
233 + return float(number_str)
234 +
235 + def _parse_true(self):
236 + self._advance()
237 + for char in 'rue':
238 + if self.current_char != char:
239 + return None
240 + self._advance()
241 + return True
242 +
243 + def _parse_false(self):
244 + self._advance()
245 + for char in 'alse':
246 + if self.current_char != char:
247 + return None
248 + self._advance()
249 + return False
250 +
251 + def _parse_null(self):
252 + self._advance()
253 + for char in 'ull':
254 + if self.current_char != char:
255 + return None
256 + self._advance()
257 + return None
258 +
259 + def _parse_unquoted_string(self):
260 + result = ""
261 + # while self.current_char is not None and not self.current_char.isspace() and self.current_char not in [':', ',', '}', ']']:
262 + while self.current_char is not None and self.current_char not in [':', ',', '}', ']']:
263 + result += self.current_char
264 + self._advance()
265 + return result.strip()
266 +
267 + def _peek(self, n):
268 + peek_index = self.index
269 + result = ''
270 + for _ in range(n):
271 + if peek_index < len(self.json_string):
272 + result += self.json_string[peek_index]
273 + peek_index += 1
274 + else:
275 + break
276 + return result
\ No newline at end of file
tools/helpers/errors.py new
+30
@@ -0,0 +1,30 @@
1 +
2 +def format_error(e: Exception, max_entries=2):
3 + traceback_text = str(e.with_traceback(None))
4 + # Split the traceback into lines
5 + lines = traceback_text.split('\n')
6 +
7 + # Find all "File" lines
8 + file_indices = [i for i, line in enumerate(lines) if line.strip().startswith("File ")]
9 +
10 + # If we found at least one "File" line, keep up to max_entries
11 + if file_indices:
12 + start_index = max(0, len(file_indices) - max_entries)
13 + trimmed_lines = lines[file_indices[start_index]:]
14 + else:
15 + # If no "File" lines found, just return the original traceback
16 + return traceback_text
17 +
18 + # Find the error message at the end
19 + error_message = ""
20 + for line in reversed(trimmed_lines):
21 + if re.match(r'\w+Error:', line):
22 + error_message = line
23 + break
24 +
25 + # Combine the trimmed traceback with the error message
26 + result = "Traceback (most recent call last):\n" + '\n'.join(trimmed_lines)
27 + if error_message:
28 + result += f"\n\n{error_message}"
29 +
30 + return result
\ No newline at end of file
tools/helpers/extract_tools.py
+23 -6
@@ -1,15 +1,32 @@
1 import re, os
2 from typing import Any
3 from . import files
4 -import dirtyjson
4 +# import dirtyjson
5 +from .dirty_json import DirtyJson
6 import regex
7
8
8 -def json_parse_dirty(json:str) -> Any:
9 - ext_json = extract_json_string(json)
10 - ext_json = fix_json_string(ext_json)
11 - data = dirtyjson.loads(ext_json)
12 - return data
9 +def json_parse_dirty(json:str) -> dict[str,Any]:
10 + ext_json = extract_json_object_string(json)
11 + # ext_json = fix_json_string(ext_json)
12 + data = DirtyJson.parse_string(ext_json)
13 + if isinstance(data,dict): return data
14 + return {}
15 +
16 +def extract_json_object_string(content):
17 + start = content.find('{')
18 + if start == -1:
19 + print("No JSON content found.")
20 + return ""
21 +
22 + # Find the first '{'
23 + end = content.rfind('}')
24 + if end == -1:
25 + # If there's no closing '}', return from start to the end
26 + return content[start:]
27 + else:
28 + # If there's a closing '}', return the substring from start to end
29 + return content[start:end+1]
30
31 def extract_json_string(content):
32 # Regular expression pattern to match a JSON object
tools/helpers/files.py
+1 -1
@@ -14,7 +14,7 @@ def read_file(relative_path, **kwargs):
14 # content = re.sub(re.escape(placeholder), strval, content)
15 content = content.replace(placeholder, strval)
16
17 - return content
17 + return content
18
19 def remove_code_fences(text):
20 return re.sub(r'~~~\w*\n|~~~', '', text)
tools/helpers/rate_limiter.py
+40 -28
@@ -1,36 +1,48 @@
1 import time
2 from collections import deque
3 -from .print_style import PrintStyle
3 +from dataclasses import dataclass
4 +from typing import List, Tuple
5
5 -def rate_limiter(max_requests_per_minute, max_tokens_per_minute):
6 - execution_times = deque()
7 - token_counts = deque()
6 +@dataclass
7 +class CallRecord:
8 + timestamp: float
9 + input_tokens: int
10 + output_tokens: int
11
9 - def limit(tokens):
10 - if tokens > max_tokens_per_minute:
11 - raise ValueError("Number of tokens exceeds the maximum allowed per minute.")
12 +class RateLimiter:
13 + def __init__(self, max_calls: int, max_input_tokens: int, max_output_tokens: int, window_seconds: int = 60):
14 + self.max_calls = max_calls
15 + self.max_input_tokens = max_input_tokens
16 + self.max_output_tokens = max_output_tokens
17 + self.window_seconds = window_seconds
18 + self.call_records: deque = deque()
19
13 - current_time = time.time()
14 -
15 - # Cleanup old execution times and token counts
16 - while execution_times and current_time - execution_times[0] > 60:
17 - execution_times.popleft()
18 - token_counts.popleft()
20 + def _clean_old_records(self, current_time: float):
21 + while self.call_records and current_time - self.call_records[0].timestamp > self.window_seconds:
22 + self.call_records.popleft()
23 +
24 + def _get_counts(self) -> Tuple[int, int, int]:
25 + calls = len(self.call_records)
26 + input_tokens = sum(record.input_tokens for record in self.call_records)
27 + output_tokens = sum(record.output_tokens for record in self.call_records)
28 + return calls, input_tokens, output_tokens
29
20 - total_tokens = sum(token_counts)
21 -
22 - if len(execution_times) < max_requests_per_minute and total_tokens + tokens <= max_tokens_per_minute:
23 - execution_times.append(current_time)
24 - token_counts.append(tokens)
25 - else:
26 - sleep_time = max(
27 - 60 - (current_time - execution_times[0]),
28 - 60 - (current_time - execution_times[0]) if total_tokens + tokens > max_tokens_per_minute else 0
29 - )
30 - PrintStyle(font_color="yellow", padding=True).print(f"Rate limiter: sleeping for {sleep_time} seconds...")
31 - time.sleep(sleep_time)
30 + def _wait_if_needed(self, current_time: float):
31 + while True:
32 + self._clean_old_records(current_time)
33 + calls, input_tokens, output_tokens = self._get_counts()
34 +
35 + if calls < self.max_calls and input_tokens < self.max_input_tokens and output_tokens < self.max_output_tokens:
36 + break
37 +
38 + oldest_record = self.call_records[0]
39 + wait_time = oldest_record.timestamp + self.window_seconds - current_time
40 + if wait_time > 0:
41 + time.sleep(wait_time)
42 current_time = time.time()
33 - execution_times.append(current_time)
34 - token_counts.append(tokens)
43
36 - return limit
\ No newline at end of file
44 + def limit(self, input_token_count: int, output_token_count: int):
45 + current_time = time.time()
46 + self._wait_if_needed(current_time)
47 + self.call_records.append(CallRecord(current_time, input_token_count, output_token_count))
48 +
tools/helpers/tool.py
+16 -6
@@ -11,7 +11,7 @@ class Response:
11
12 class Tool:
13
14 - def __init__(self, agent: Agent, name: str, args: dict, message: str, **kwargs) -> None:
14 + def __init__(self, agent: Agent, name: str, args: dict[str,str], message: str, **kwargs) -> None:
15 self.agent = agent
16 self.name = name
17 self.args = args
@@ -22,11 +22,21 @@ class Tool:
22 pass
23
24 def before_execution(self):
25 - PrintStyle(font_color="#1B4F72", padding=True, background_color="white", bold=True).print(f"{self.agent.name}: Using tool {self.name}:")
26 - PrintStyle(font_color="#85C1E9").print(self.args)
27 -
25 + PrintStyle(font_color="#1B4F72", padding=True, background_color="white", bold=True).print(f"{self.agent.agent_name}: Using tool '{self.name}':")
26 + if self.args and isinstance(self.args, dict):
27 + for key, value in self.args.items():
28 + PrintStyle(font_color="#85C1E9", bold=True).stream(self.nice_key(key)+": ")
29 + PrintStyle(font_color="#85C1E9", padding="\n" in value).stream(value)
30 + PrintStyle().print()
31 +
32 def after_execution(self, response: Response):
33 msg_response = files.read_file("./prompts/fw.tool_response.md", tool_name=self.name, tool_response=response.message)
34 self.agent.append_message(msg_response, human=True)
31 - PrintStyle(font_color="#1B4F72", background_color="white", padding=True, bold=True).print(f"{self.agent.name}: Response from {self.name}:")
32 - PrintStyle(font_color="#85C1E9").print(response.message)
\ No newline at end of file
35 + PrintStyle(font_color="#1B4F72", background_color="white", padding=True, bold=True).print(f"{self.agent.agent_name}: Response from tool '{self.name}':")
36 + PrintStyle(font_color="#85C1E9").print(response.message)
37 +
38 + def nice_key(self, key:str):
39 + words = key.split('_')
40 + words = [words[0].capitalize()] + [word.lower() for word in words[1:]]
41 + result = ' '.join(words)
42 + return result
\ No newline at end of file
tools/memorize.py new
+14
@@ -0,0 +1,14 @@
1 +from agent import Agent
2 +from tools.helpers import files
3 +from tools.helpers.tool import Tool, Response
4 +from tools import memory_tool
5 +
6 +class Memorize(Tool):
7 + def execute(self):
8 +
9 + memory_tool.process_query(self.agent, str(self.args), "save")
10 +
11 + return Response(
12 + message=files.read_file("prompts/fw.memorized.md"),
13 + break_loop=False,
14 + )
\ No newline at end of file
tools/memory_tool.py
+14 -13
@@ -5,38 +5,39 @@ import os, json
5 from tools.helpers.tool import Tool, Response
6 from tools.helpers.print_style import PrintStyle
7
8 -db: VectorDB
9 -result_count = 3 #TODO parametrize better
10 -
8 +db: VectorDB | None = None
9
10 class Memory(Tool):
11 def execute(self):
14 - result = process_query(self.agent, self.args["memory"],self.args["action"])
15 - return Response(message=result, break_loop=False)
12 + #TODO separate param for memory tool result count
13 + result = process_query(self.agent, self.args["memory"],self.args["action"], result_count=self.agent.auto_memory_count)
14 + return Response(message="\n\n".join(result), break_loop=False)
15
16
18 -def initialize(embeddings_model,messages_returned=3, subdir=""):
19 - global db, result_count
17 +def initialize(embeddings_model, subdir=""):
18 + global db
19 dir = os.path.join("memory",subdir)
20 db = VectorDB(embeddings_model=embeddings_model, in_memory=False, cache_dir=dir)
22 - result_count = messages_returned
21
22
25 -def process_query(agent:Agent, message: str, action: str = "load", **kwargs):
23 +def process_query(agent:Agent, message: str, action: str = "load", result_count: int = 3, **kwargs):
24 + if not db: initialize(agent.embeddings_model, subdir=agent.memory_subdir)
25 +
26 if action.strip().lower() == "save":
27 - id = db.insert_document(str(message))
27 + id = db.insert_document(str(message)) # type: ignore
28 return files.read_file("./prompts/fw.memory_saved.md")
29
30 elif action.strip().lower() == "delete":
31 - deleted = db.delete_documents(message)
31 + deleted = db.delete_documents(message) # type: ignore
32 return files.read_file("./prompts/fw.memories_deleted.md", count=deleted)
33
34 else:
35 results=[]
36 - docs = db.search_max_rel(message,result_count)
36 + docs = db.search_max_rel(message,result_count) # type: ignore
37 if len(docs)==0: return files.read_file("./prompts/fw.memories_not_found.md", query=message)
38 for doc in docs:
39 results.append(doc.page_content)
40 - return "\n\n".join(results)
40 + return results
41 + # return "\n\n".join(results)
42
43
tools/response.py
+3
@@ -15,6 +15,9 @@ class ResponseTool(Tool):
15 return Response(message=self.args["text"], break_loop=True)
16 # else:
17
18 + def after_execution(self, response):
19 + pass # do add anything to the history or output
20 +
21
22 # def execute(agent:Agent, message: str, _tools, _tool_index, timeout=15, **kwargs):
23