memory tool update, minor cleanup, utility models

frdel committed Jul 12, 2024 at 10:19 UTC b3adcc7bca8eaff90093a9c6d631e656ddf2b80a
11 files changed +204 -55
agent.py
+6 -7
@@ -1,7 +1,6 @@
1 from dataclasses import dataclass, field
2 import time, importlib, inspect, os, json
3 -import traceback
4 -from typing import Any, Optional, Dict, TypedDict
3 +from typing import Any, Optional, Dict
4 from python.helpers import extract_tools, rate_limiter, files, errors
5 from python.helpers.print_style import PrintStyle
6 from langchain.schema import AIMessage
@@ -9,20 +8,20 @@ from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
8 from langchain_core.messages import HumanMessage, SystemMessage
9 from langchain_core.language_models.chat_models import BaseChatModel
10 from langchain_core.embeddings import Embeddings
12 -from python.helpers.rate_limiter import RateLimiter
11
12 # rate_limit = rate_limiter.rate_limiter(30,160000) #TODO! implement properly
13
14 @dataclass
15 class AgentConfig:
16 chat_model:BaseChatModel
17 + utility_model: BaseChatModel
18 embeddings_model:Embeddings
19 memory_subdir: str = ""
20 auto_memory_count: int = 3
21 auto_memory_skip: int = 2
22 rate_limit_seconds: int = 60
24 - rate_limit_requests: int = 30
25 - rate_limit_input_tokens: int = 0
23 + rate_limit_requests: int = 15
24 + rate_limit_input_tokens: int = 1000000
25 rate_limit_output_tokens: int = 0
26 msgs_keep_max: int = 25
27 msgs_keep_start: int = 5
@@ -62,7 +61,7 @@ class Agent:
61 self.last_message = ""
62 self.intervention_message = ""
63 self.intervention_status = False
65 - self.rate_limiter = RateLimiter(max_calls=self.config.rate_limit_requests,max_input_tokens=self.config.rate_limit_input_tokens,max_output_tokens=self.config.rate_limit_output_tokens,window_seconds=self.config.rate_limit_seconds)
64 + self.rate_limiter = rate_limiter.RateLimiter(max_calls=self.config.rate_limit_requests,max_input_tokens=self.config.rate_limit_input_tokens,max_output_tokens=self.config.rate_limit_output_tokens,window_seconds=self.config.rate_limit_seconds)
65 self.data = {} # free data object all the tools can use
66
67 os.chdir(files.get_abs_path("./work_dir")) #change CWD to work_dir
@@ -160,7 +159,7 @@ class Agent:
159 SystemMessage(content=system),
160 HumanMessage(content=msg)])
161
163 - chain = prompt | self.config.chat_model
162 + chain = prompt | self.config.utility_model
163 response = ""
164 printer = None
165
example.env
+1 -1
@@ -2,7 +2,7 @@ API_KEY_OPENAI=
2 API_KEY_ANTHROPIC=
3 API_KEY_GROQ=
4 API_KEY_PERPLEXITY=
5 -
5 +API_KEY_GOOGLE=
6
7 TOKENIZERS_PARALLELISM=true
8 PYDEVD_DISABLE_FILE_VALIDATION=1
\ No newline at end of file
main.py
+10 -9
@@ -1,8 +1,5 @@
1 import signal
2 -import sys
3 -import termios
2 import threading, time, models, os
5 -import tty
3 from ansio import application_keypad, mouse_input, raw_input
4 from ansio.input import InputEvent, get_input_event
5 from agent import Agent, AgentConfig
@@ -33,6 +30,9 @@ def initialize():
30 # chat_llm = models.get_ollama(model_name="qwen:14b")
31 chat_llm = models.get_google_chat()
32
33 + utility_llm = models.get_anthropic_haiku(temperature=0)
34 +
35 +
36 # embedding model used for memory
37 # embedding_llm = models.get_embedding_openai()
38 embedding_llm = models.get_embedding_hf()
@@ -40,6 +40,7 @@ def initialize():
40 # agent configuration
41 config = AgentConfig(
42 chat_model = chat_llm,
43 + utility_model = utility_llm,
44 embeddings_model = embedding_llm,
45 # memory_subdir = "",
46 auto_memory_count = 0,
@@ -81,13 +82,13 @@ def chat(agent:Agent):
82 with input_lock:
83 timeout = agent.get_data("timeout") # how long the agent is willing to wait
84 if not timeout: # if agent wants to wait for user input forever
84 - PrintStyle(background_color="#6C3483", font_color="white", bold=True, padding=True).print(f"User message ('exit' to leave):")
85 + PrintStyle(background_color="#6C3483", font_color="white", bold=True, padding=True).print(f"User message ('e' to leave):")
86 import readline # this fixes arrow keys in terminal
87 user_input = input("> ")
88 PrintStyle(font_color="white", padding=False, log_only=True).print(f"> {user_input}")
89
90 else: # otherwise wait for user input with a timeout
90 - PrintStyle(background_color="#6C3483", font_color="white", bold=True, padding=True).print(f"User message ({timeout}s timeout, 'wait' to wait, 'exit' to leave):")
91 + PrintStyle(background_color="#6C3483", font_color="white", bold=True, padding=True).print(f"User message ({timeout}s timeout, 'w' to wait, 'e' to leave):")
92 import readline # this fixes arrow keys in terminal
93 # user_input = timed_input("> ", timeout=timeout)
94 user_input = timeout_input("> ", timeout=timeout)
@@ -97,14 +98,14 @@ def chat(agent:Agent):
98 PrintStyle(font_color="white", padding=False).stream(f"{user_input}")
99 else:
100 user_input = user_input.strip()
100 - if user_input.lower()=="wait": # the user needs more time
101 + if user_input.lower()=="w": # the user needs more time
102 user_input = input("> ").strip()
103 PrintStyle(font_color="white", padding=False, log_only=True).print(f"> {user_input}")
104
105
106
107 # exit the conversation when the user types 'exit'
107 - if user_input.lower() == 'exit': break
108 + if user_input.lower() == 'e': break
109
110 # send message to agent0,
111 assistant_response = agent.message_loop(user_input)
@@ -118,13 +119,13 @@ def chat(agent:Agent):
119 def intervention():
120 if Agent.streaming_agent and not Agent.paused:
121 Agent.paused = True # stop agent streaming
121 - PrintStyle(background_color="#6C3483", font_color="white", bold=True, padding=True).print(f"User intervention ('exit' to leave, empty to continue):")
122 + PrintStyle(background_color="#6C3483", font_color="white", bold=True, padding=True).print(f"User intervention ('e' to leave, empty to continue):")
123
124 import readline # this fixes arrow keys in terminal
125 user_input = input("> ").strip()
126 PrintStyle(font_color="white", padding=False, log_only=True).print(f"> {user_input}")
127
127 - if user_input.lower() == 'exit': os._exit(0) # exit the conversation when the user types 'exit'
128 + if user_input.lower() == 'e': os._exit(0) # exit the conversation when the user types 'exit'
129 if user_input: Agent.streaming_agent.intervention_message = user_input # set intervention message if non-empty
130 Agent.paused = False # continue agent streaming
131
models.py
+5 -4
@@ -5,7 +5,7 @@ from langchain_openai import ChatOpenAI, OpenAI, OpenAIEmbeddings
5 from langchain_anthropic import ChatAnthropic
6 from langchain_groq import ChatGroq
7 from langchain_huggingface import HuggingFaceEmbeddings
8 -from langchain_google_genai import ChatGoogleGenerativeAI
8 +from langchain_google_genai import ChatGoogleGenerativeAI, HarmBlockThreshold, HarmCategory
9
10
11 # Load environment variables
@@ -76,15 +76,16 @@ def get_groq_gemma(api_key=None, temperature=DEFAULT_TEMPERATURE):
76 api_key = api_key or get_api_key("groq")
77 return ChatGroq(model_name="gemma-7b-it", temperature=temperature, api_key=api_key) # type: ignore
78
79 -def get_ollama_dolphin(api_key=None, temperature=DEFAULT_TEMPERATURE):
79 +def get_ollama_dolphin(temperature=DEFAULT_TEMPERATURE):
80 return Ollama(model="dolphin-llama3:8b-256k-v2.9-fp16", temperature=temperature)
81
82 -def get_ollama_phi(api_key=None, temperature=DEFAULT_TEMPERATURE):
82 +def get_ollama_phi(temperature=DEFAULT_TEMPERATURE):
83 return Ollama(model="phi3:3.8b-mini-instruct-4k-fp16",temperature=temperature)
84
85 def get_google_chat(model_name="gemini-1.5-flash-latest", api_key=None, temperature=DEFAULT_TEMPERATURE):
86 api_key = api_key or get_api_key("google")
87 - return ChatGoogleGenerativeAI(model=model_name, temperature=temperature, google_api_key=api_key) # type: ignore
87 + return ChatGoogleGenerativeAI(model=model_name, temperature=temperature, google_api_key=api_key,
88 + safety_settings={HarmCategory.HARM_CATEGORY_DANGEROUS_CONTENT: HarmBlockThreshold.BLOCK_NONE }) # type: ignore
89
90 def get_embedding_hf(model_name="sentence-transformers/all-MiniLM-L6-v2"):
91 return HuggingFaceEmbeddings(model_name=model_name)
prompts/agent.tools.md
+78 -6
@@ -62,11 +62,28 @@ Always verify memory by online.
62 }
63 ~~~
64
65 -### memorize:
66 -Save information to persistent memory.
65 +### memory_tool:
66 +Manage long term memories. Allowed arguments are "query", "memorize" and "forget".
67 Memories can help you remember important details and later reuse them.
68 +When querying, provide a "query" argument to search for. You will retrieve IDs and contents of relevant memories. Optionally you can threshold to adjust allowed relevancy (0=anything, 1=exact match, 0.1 is default).
69 +When memorizing, provide enough information in "memorize" argument for future reuse.
70 +When forgetting, provide memory IDs from loaded memories separated by commas in "forget" argument.
71 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.
72 **Example usages**:
73 +1. load:
74 +~~~json
75 +{
76 + "thoughts": [
77 + "Let's search my memory for...",
78 + ],
79 + "tool_name": "memory_tool",
80 + "tool_args": {
81 + "query": "File compression library for...",
82 + "threshold": 0.1
83 + }
84 +}
85 +~~~
86 +2. save:
87 ~~~json
88 {
89 "thoughts": [
@@ -74,9 +91,21 @@ Provide a title, short summary and and all the necessary information to help you
91 "Details of this process will be valuable...",
92 "Let's save tools and code used...",
93 ],
77 - "tool_name": "memorize",
94 + "tool_name": "memory_tool",
95 "tool_args": {
79 - "memory": "# How to...",
96 + "memorize": "# How to...",
97 + }
98 +}
99 +~~~
100 +3. delete:
101 +~~~json
102 +{
103 + "thoughts": [
104 + "User asked to delete memories...",
105 + ],
106 + "tool_name": "memory_tool",
107 + "tool_args": {
108 + "forget": "32cd37ffd1-101f-4112-80e2-33b795548116, d1306e36-6a9c-4e6a-bfc3-c8335035dcf8 ...",
109 }
110 }
111 ~~~
@@ -93,7 +122,8 @@ When tool outputs error, you need to change your code accordingly before trying
122 Keep in mind that current working directory CWD automatically resets before every tool call.
123 IMPORTANT!: Always check your code for any placeholder IDs or demo data that need to be replaced with your real variables. Do not simply reuse code snippets from tutorials.
124 Do not use in combination with other tools except for thoughts. Wait for response before using other tools.
96 -**Example usage**:
125 +**Example usages:**
126 +1. Execute python code
127 ~~~json
128 {
129 "thoughts": [
@@ -101,10 +131,52 @@ Do not use in combination with other tools except for thoughts. Wait for respons
131 "I can use library...",
132 "Then I can...",
133 ],
104 - "tool_name": "memory_tool",
134 + "tool_name": "code_execution_tool",
135 "tool_args": {
136 "runtime": "python",
137 "code": "import os\nreturn os.getcwd()",
138 }
139 }
140 +~~~
141 +
142 +2. Execute terminal command
143 +~~~json
144 +{
145 + "thoughts": [
146 + "I need to do...",
147 + "I need to install...",
148 + ],
149 + "tool_name": "code_execution_tool",
150 + "tool_args": {
151 + "runtime": "terminal",
152 + "code": "apt-get install zip",
153 + }
154 +}
155 +~~~
156 +
157 +2. 1. Wait for terminal and check output with long running scripts
158 +~~~json
159 +{
160 + "thoughts": [
161 + "I will wait for the program to finish...",
162 + ],
163 + "tool_name": "code_execution_tool",
164 + "tool_args": {
165 + "runtime": "output",
166 + }
167 +}
168 +~~~
169 +
170 +2. 2. Answer terminal dialog
171 +~~~json
172 +{
173 + "thoughts": [
174 + "Program needs confirmation...",
175 + ],
176 + "tool_name": "code_execution_tool",
177 + "tool_args": {
178 + "runtime": "terminal",
179 + "code": "Y",
180 + }
181 +}
182 ~~~
\ No newline at end of file
python/helpers/vdb.py new
+69
@@ -0,0 +1,69 @@
1 +from langchain.storage import InMemoryByteStore, LocalFileStore
2 +from langchain.embeddings import CacheBackedEmbeddings
3 +from langchain_core.embeddings import Embeddings
4 +
5 +from langchain_chroma import Chroma
6 +import chromadb
7 +from chromadb.config import Settings
8 +
9 +from . import files
10 +from langchain_core.documents import Document
11 +import uuid
12 +
13 +
14 +class VectorDB:
15 +
16 + def __init__(self, embeddings_model:Embeddings, in_memory=False, cache_dir="./cache"):
17 + print("Initializing VectorDB...")
18 + self.embeddings_model = embeddings_model
19 +
20 + db_cache = files.get_abs_path(cache_dir,"database")
21 +
22 + self.client =chromadb.PersistentClient(path=db_cache)
23 + self.collection = self.client.create_collection("my_collection")
24 + self.collection
25 +
26 +
27 + def search(self, query:str, results=2):
28 + emb = self.embeddings_model.embed_query(query)
29 + res = self.collection.query(query_embeddings=[emb],n_results=results)
30 + best = res["documents"][0][0] # type: ignore
31 +
32 + # def delete_documents(self, query):
33 + # score_limit = 1
34 + # k = 2
35 + # tot = 0
36 + # while True:
37 + # # Perform similarity search with score
38 + # docs = self.db.similarity_search_with_score(query, k=k)
39 +
40 + # # Extract document IDs and filter based on score
41 + # document_ids = [result[0].metadata["id"] for result in docs if result[1] < score_limit]
42 +
43 + # # Delete documents with IDs over the threshold score
44 + # if document_ids:
45 + # fnd = self.db.get(where={"id": {"$in": document_ids}})
46 + # if fnd["ids"]: self.db.delete(ids=fnd["ids"])
47 + # tot += len(fnd["ids"])
48 +
49 + # # If fewer than K document IDs, break the loop
50 + # if len(document_ids) < k:
51 + # break
52 +
53 + # return tot
54 +
55 + def insert(self, data:str):
56 +
57 + id = str(uuid.uuid4())
58 + emb = self.embeddings_model.embed_documents([data])[0]
59 +
60 + self.collection.add(
61 + ids=[id],
62 + embeddings=[emb],
63 + documents=[data],
64 + )
65 +
66 + return id
67 +
68 +
69 +
python/helpers/vector_db.py
+7 -1
@@ -1,6 +1,7 @@
1 from langchain.storage import InMemoryByteStore, LocalFileStore
2 from langchain.embeddings import CacheBackedEmbeddings
3 from langchain_chroma import Chroma
4 +
5 from . import files
6 from langchain_core.documents import Document
7 import uuid
@@ -29,8 +30,12 @@ class VectorDB:
30
31 self.db = Chroma(embedding_function=self.embedder,persist_directory=db_cache)
32
33 +
34 def search_similarity(self, query, results=3):
35 return self.db.similarity_search(query,results)
36 +
37 + def search_similarity_threshold(self, query, results=3, threshold=0.5):
38 + return self.db.search(query,search_type="similarity_score_threshold",score_threshold=threshold)
39
40 def search_max_rel(self, query, results=3):
41 return self.db.max_marginal_relevance_search(query,results)
@@ -60,7 +65,8 @@ class VectorDB:
65
66 def insert_document(self, data):
67 id = str(uuid.uuid4())
63 - self.db.add_documents(documents=[ Document(data, metadata={"id": id}) ])
68 + self.db.add_documents(documents=[ Document(data, metadata={"id": id}) ], ids=[id])
69 +
70 return id
71
72
python/tools/code_execution_tool.py
+4
@@ -32,6 +32,8 @@ class CodeExecution(Tool):
32 response = self.execute_nodejs_code(self.args["code"])
33 elif runtime == "terminal":
34 response = self.execute_terminal_command(self.args["code"])
35 + elif runtime == "output":
36 + response = self.get_terminal_output()
37 else:
38 response = files.read_file("./prompts/fw.code_runtime_wrong.md", runtime=runtime)
39
@@ -78,7 +80,9 @@ class CodeExecution(Tool):
80 self.state.shell.send_command(command)
81
82 PrintStyle(background_color="white",font_color="#85C1E9",bold=True).print(f"{self.agent.agent_name} code execution output:")
83 + return self.get_terminal_output()
84
85 + def get_terminal_output(self):
86 idle=0
87 while True:
88 time.sleep(0.1) # Wait for some output to be generated
python/tools/memorize.py deleted
-14
@@ -1,14 +0,0 @@
1 -from agent import Agent
2 -from python.helpers import files
3 -from python.helpers.tool import Tool, Response
4 -import memory_tool
5 -
6 -class Memorize(Tool):
7 - def execute(self,**kwargs):
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
python/tools/memory_tool.py
+17 -6
@@ -9,7 +9,18 @@ db: VectorDB | None = None
9
10 class Memory(Tool):
11 def execute(self,**kwargs):
12 - result = process_query(self.agent, self.args["memory"],self.args["action"], result_count=self.agent.config.auto_memory_count)
12 + result=[]
13 +
14 + if "query" in kwargs:
15 + if "threshold" in kwargs: threshold = float(kwargs["threshold"])
16 + else: threshold = 0.1
17 + result = process_query(self.agent, kwargs["query"], action="load", threshold=threshold, result_count=3)
18 + elif "memorize" in kwargs:
19 + result = process_query(self.agent, kwargs["memorize"], action="save")
20 + elif "forget" in kwargs:
21 + result = process_query(self.agent, kwargs["forget"], action="delete")
22 +
23 + # result = process_query(self.agent, self.args["memory"],self.args["action"], result_count=self.agent.config.auto_memory_count)
24 return Response(message="\n\n".join(result), break_loop=False)
25
26
@@ -19,21 +30,21 @@ def initialize(embeddings_model, subdir=""):
30 db = VectorDB(embeddings_model=embeddings_model, in_memory=False, cache_dir=dir)
31
32
22 -def process_query(agent:Agent, message: str, action: str = "load", result_count: int = 3, **kwargs):
33 +def process_query(agent:Agent, message: str, action: str = "load", result_count: int = 3, threshold: float = 0.1, **kwargs):
34 if not db: initialize(agent.config.embeddings_model, subdir=agent.config.memory_subdir)
35
36 if action.strip().lower() == "save":
37 id = db.insert_document(str(message)) # type: ignore
27 - return files.read_file("./prompts/fw.memory_saved.md")
38 + return [files.read_file("./prompts/fw.memory_saved.md")]
39
40 elif action.strip().lower() == "delete":
41 deleted = db.delete_documents(message) # type: ignore
31 - return files.read_file("./prompts/fw.memories_deleted.md", count=deleted)
42 + return [files.read_file("./prompts/fw.memories_deleted.md", count=deleted)]
43
44 else:
45 results=[]
35 - docs = db.search_max_rel(message,result_count) # type: ignore
36 - if len(docs)==0: return files.read_file("./prompts/fw.memories_not_found.md", query=message)
46 + docs = db.search_similarity_threshold(message,result_count,threshold) # type: ignore
47 + if len(docs)==0: return [files.read_file("./prompts/fw.memories_not_found.md", query=message)]
48 for doc in docs:
49 results.append(doc.page_content)
50 return results
requirements.txt
+7 -7
@@ -1,14 +1,14 @@
1 ansio==0.0.1
2 python-dotenv==1.0.1
3 -langchain-groq==0.1.5
3 +langchain-groq==0.1.6
4 langchain-huggingface==0.0.3
5 -langchain-openai==0.1.8
6 -langchain-community==0.2.4
7 -langchain-anthropic==0.1.15
8 -langchain-chroma==0.1.1
9 -google-generativeai==0.5.4
5 +langchain-openai==0.1.15
6 +langchain-community==0.2.7
7 +langchain-anthropic==0.1.19
8 +langchain-chroma==0.1.2
9 +langchain-google-genai==1.0.7
10 webcolors==24.6.0
11 sentence-transformers==3.0.1
12 pytimedinput==2.0.1
13 docker==7.1.0
14 -paramiko==3.4.0
14 +paramiko==3.4.0
\ No newline at end of file