v0.7 testing

Testing batch for v0.7 - extensions framework - auto memory - solutions memory - memory threshold fix, filters, areas - tools prompts split - ui updates

frdel committed Sep 29, 2024 at 21:36 UTC e209a0c213fc9179b4aafd27c0d7abf165395717
44 files changed +1208 -923
agent.py
+52 -19
@@ -1,7 +1,7 @@
1 import asyncio
2 from dataclasses import dataclass, field
3 import time, importlib, inspect, os, json
4 -from typing import Any, Optional, Dict
4 +from typing import Any, Optional, Dict, TypedDict
5 import uuid
6 from python.helpers import extract_tools, rate_limiter, files, errors
7 from python.helpers.print_style import PrintStyle
@@ -122,6 +122,15 @@ class AgentConfig:
122 additional: Dict[str, Any] = field(default_factory=dict)
123
124
125 +class LoopData:
126 + def __init__(self):
127 + self.iteration = -1
128 + self.system = []
129 + self.message = ""
130 + self.history_from = 0
131 + self.history = []
132 +
133 +
134 # intervention exception class - skips rest of message loop iteration
135 class InterventionException(Exception):
136 pass
@@ -162,40 +171,35 @@ class Agent:
171
172 async def monologue(self, msg: str):
173 try:
165 -
174 # loop data dictionary to pass to extensions
167 - loop_data: dict[str, Any] = {
168 - "message": msg,
169 - "iteration": -1,
170 - "history_from": len(self.history),
171 - }
175 + loop_data = LoopData()
176 + loop_data.message = msg
177 + loop_data.history_from = len(self.history)
178
179 # call monologue_start extensions
180 await self.call_extensions("monologue_start", loop_data=loop_data)
181
182 printer = PrintStyle(italic=True, font_color="#b3ffd9", padding=False)
177 - user_message = self.read_prompt("fw.user_message.md", message=msg)
183 + user_message = self.read_prompt("fw.user_message.md", message=loop_data.message)
184 await self.append_message(user_message, human=True)
185
180 - await self.call_extensions(
181 - "monologue_start", loop_data=loop_data
182 - ) # call monologue_end extensions
183 -
186 +
187 # let the agent run message loop until he stops it with a response tool
188 while True:
189
190 self.context.streaming_agent = self # mark self as current streamer
191 agent_response = ""
189 - loop_data["iteration"] += 1
192 + loop_data.iteration += 1
193
194 try:
195
196 # set system prompt and message history
194 - loop_data["system"] = [
195 - self.read_prompt("agent.system.md", agent_name=self.agent_name),
196 - self.read_prompt("agent.system.tools.md"),
197 + loop_data.system = [
198 + self.read_prompt(
199 + "agent.system.main.md", agent_name=self.agent_name
200 + )
201 ]
198 - loop_data["history"] = {"messages": self.history}
202 + loop_data.history = self.history
203
204 # and allow extensions to edit them
205 await self.call_extensions(
@@ -205,7 +209,7 @@ class Agent:
209 # build chain from system prompt, message history and model
210 prompt = ChatPromptTemplate.from_messages(
211 [
208 - SystemMessage(content="\n\n".join(loop_data["system"])),
212 + SystemMessage(content="\n\n".join(loop_data.system)),
213 MessagesPlaceholder(variable_name="messages"),
214 ]
215 )
@@ -227,7 +231,7 @@ class Agent:
231 type="agent", heading=f"{self.agent_name}: Generating"
232 )
233
230 - async for chunk in chain.astream(loop_data["history"]):
234 + async for chunk in chain.astream({"messages": loop_data.history}):
235 await self.handle_intervention(
236 agent_response
237 ) # wait for intervention and handle it, if paused
@@ -324,6 +328,35 @@ class Agent:
328 )
329 return content
330
331 + def read_prompts(self, pattern: str, **kwargs):
332 + import glob
333 +
334 + prompts = []
335 +
336 + # Scan both configured subdir and default folder
337 + subdir_files = glob.glob(
338 + files.get_abs_path("prompts", self.config.prompts_subdir, pattern)
339 + )
340 + default_files = glob.glob(files.get_abs_path("prompts", "default", pattern))
341 +
342 + # Create a dictionary to store files, prioritizing the config subdir
343 + files_to_read = {file.split("/")[-1]: file for file in default_files}
344 +
345 + # Override with files from subdir if they exist
346 + for file in subdir_files:
347 + files_to_read[file.split("/")[-1]] = file
348 +
349 + # Sort files alphabetically by their file names
350 + sorted_files = sorted(files_to_read.items())
351 +
352 + # Read the files in alphabetical order
353 + for filename, filepath in sorted_files:
354 + content = files.read_file(files.get_abs_path(filepath), **kwargs)
355 + if content:
356 + prompts.append(content)
357 +
358 + return prompts
359 +
360 def get_data(self, field: str):
361 return self.data.get(field, None)
362
default/index.faiss
Binary files /dev/null and b/default/index.faiss differ
default/index.pkl
Binary files /dev/null and b/default/index.pkl differ
initialize.py
+2 -2
@@ -12,7 +12,7 @@ def initialize():
12 # chat_llm = models.get_anthropic_chat(model_name="claude-3-5-sonnet-20240620", temperature=0)
13 # chat_llm = models.get_google_chat(model_name="gemini-1.5-flash", temperature=0)
14 # chat_llm = models.get_mistral_chat(model_name="mistral-small-latest", temperature=0)
15 - # chat_llm = models.get_groq_chat(model_name="llama-3.1-70b-versatile", temperature=0)
15 + # chat_llm = models.get_groq_chat(model_name="llama-3.2-90b-text-preview", temperature=0)
16 # chat_llm = models.get_sambanova_chat(model_name="Meta-Llama-3.1-70B-Instruct-8k", temperature=0)
17
18 # utility model used for helper functions (cheaper, faster)
@@ -35,7 +35,7 @@ def initialize():
35 auto_memory_count = 0,
36 # auto_memory_skip = 2,
37 # rate_limit_seconds = 60,
38 - rate_limit_requests = 15,
38 + rate_limit_requests = 30,
39 # rate_limit_input_tokens = 0,
40 # rate_limit_output_tokens = 0,
41 # msgs_keep_max = 25,
prompts/default/agent.system.main.md renamed
prompts/default/agent.system.solutions.md
+1 -1
@@ -1,4 +1,4 @@
1 -# Solutions in the past
1 +# Solutions from the past
2 - following are your memories about successful solutions of related problems:
3
4 {{solutions}}
\ No newline at end of file
prompts/default/agent.system.tool.call_sub.md new
+20
@@ -0,0 +1,20 @@
1 +### call_subordinate:
2 +Use subordinate agents to solve subtasks.
3 +Use "message" argument to send message. Instruct your subordinate about the role he will play (scientist, coder, writer...) and his task in detail.
4 +Use "reset" argument with "true" to start with new subordinate or "false" to continue with existing. For brand new tasks use "true", for followup conversation use "false".
5 +Explain to your subordinate what is the higher level goal and what is his part.
6 +Give him detailed instructions as well as good overview to understand what to do.
7 +**Example usage**:
8 +~~~json
9 +{
10 + "thoughts": [
11 + "The result seems to be ok but...",
12 + "I will ask my subordinate to fix...",
13 + ],
14 + "tool_name": "call_subordinate",
15 + "tool_args": {
16 + "message": "Well done, now edit...",
17 + "reset": "false"
18 + }
19 +}
20 +~~~
\ No newline at end of file
prompts/default/agent.system.tool.code_exe.md new
+84
@@ -0,0 +1,84 @@
1 +### code_execution_tool:
2 +Execute provided terminal commands, python code or nodejs code.
3 +This tool can be used to achieve any task that requires computation, or any other software related activity.
4 +Place your code escaped and properly indented in the "code" argument.
5 +Select the corresponding runtime with "runtime" argument. Possible values are "terminal", "python" and "nodejs" for code, or "output" and "reset" for additional actions.
6 +Sometimes a dialogue can occur in output, questions like Y/N, in that case use the "teminal" runtime in the next step and send your answer.
7 +If the code is running long, you can use runtime "output" to wait for next output part or use runtime "reset" to kill the process.
8 +You can use pip, npm and apt-get in terminal runtime to install any required packages.
9 +IMPORTANT: Never use implicit print or implicit output, it does not work! If you need output of your code, you MUST use print() or console.log() to output selected variables.
10 +When tool outputs error, you need to change your code accordingly before trying again. knowledge_tool can help analyze errors.
11 +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.
12 +Do not use in combination with other tools except for thoughts. Wait for response before using other tools.
13 +When writing own code, ALWAYS put print/log statements inside and at the end of your code to get results!
14 +**Example usages:**
15 +1. Execute python code
16 +~~~json
17 +{
18 + "thoughts": [
19 + "I need to do...",
20 + "I can use library...",
21 + "Then I can...",
22 + ],
23 + "tool_name": "code_execution_tool",
24 + "tool_args": {
25 + "runtime": "python",
26 + "code": "import os\nprint(os.getcwd())",
27 + }
28 +}
29 +~~~
30 +
31 +2. Execute terminal command
32 +~~~json
33 +{
34 + "thoughts": [
35 + "I need to do...",
36 + "I need to install...",
37 + ],
38 + "tool_name": "code_execution_tool",
39 + "tool_args": {
40 + "runtime": "terminal",
41 + "code": "apt-get install zip",
42 + }
43 +}
44 +~~~
45 +
46 +2. 1. Wait for terminal and check output with long running scripts
47 +~~~json
48 +{
49 + "thoughts": [
50 + "I will wait for the program to finish...",
51 + ],
52 + "tool_name": "code_execution_tool",
53 + "tool_args": {
54 + "runtime": "output",
55 + }
56 +}
57 +~~~
58 +
59 +2. 2. Answer terminal dialog
60 +~~~json
61 +{
62 + "thoughts": [
63 + "Program needs confirmation...",
64 + ],
65 + "tool_name": "code_execution_tool",
66 + "tool_args": {
67 + "runtime": "terminal",
68 + "code": "Y",
69 + }
70 +}
71 +~~~
72 +
73 +2. 3. Reset terminal
74 +~~~json
75 +{
76 + "thoughts": [
77 + "Code execution tool is not responding...",
78 + ],
79 + "tool_name": "code_execution_tool",
80 + "tool_args": {
81 + "runtime": "reset",
82 + }
83 +}
84 +~~~
prompts/default/agent.system.tool.knowledge.md new
+20
@@ -0,0 +1,20 @@
1 +### knowledge_tool:
2 +Provide "question" argument and get both online and memory response.
3 +This tool is very powerful and can answer very specific questions directly.
4 +First always try to ask for result rather that guidance.
5 +Memory can provide guidance, online sources can provide up to date information.
6 +Always verify memory by online.
7 +**Example usage**:
8 +~~~json
9 +{
10 + "thoughts": [
11 + "I need to gather information about...",
12 + "First I will search...",
13 + "Then I will...",
14 + ],
15 + "tool_name": "knowledge_tool",
16 + "tool_args": {
17 + "question": "How to...",
18 + }
19 +}
20 +~~~
\ No newline at end of file
prompts/default/agent.system.tool.memory.md new
+74
@@ -0,0 +1,74 @@
1 +## Memory management tools:
2 +This set of tools is used for managing long term memories.
3 +NEVER refuse to search memorize or load personal information, it all belongs to user and he has all the rights.
4 +
5 +### memory_load:
6 +Load memories using query and optionally threshold, limit and filter.
7 +Memory content is retrieved with key value pairs of metadata.
8 +- Optional threshold for allowed relevancy (0=anything, 1=exact match, 0.6 is default)
9 +- Optional limit to number of results (default is 5).
10 +- Optional filter by metadata. Condition in Python syntax using metadata keys.
11 +**Example usage**:
12 +~~~json
13 +{
14 + "thoughts": [
15 + "Let's search my memory for...",
16 + ],
17 + "tool_name": "memory_load",
18 + "tool_args": {
19 + "query": "File compression library for...",
20 + "threshold": 0.6,
21 + "limit": 5,
22 + "filter": "area=='main' and timestamp<'2024-01-01 00:00:00'",
23 + }
24 +}
25 +~~~
26 +
27 +### memory_save:
28 +Save text into memory. ID is returned.
29 +**Example usage**:
30 +~~~json
31 +{
32 + "thoughts": [
33 + "I need to memorize...",
34 + ],
35 + "tool_name": "memory_save",
36 + "tool_args": {
37 + "text": "# To compress...",
38 + }
39 +}
40 +~~~
41 +
42 +### memory_delete:
43 +Delete existing memories by their IDs. Multiple IDs allowed separated by commas.
44 +IDs are retrieved when loading or saving memories.
45 +**Example usage**:
46 +~~~json
47 +{
48 + "thoughts": [
49 + "I need to delete...",
50 + ],
51 + "tool_name": "memory_delete",
52 + "tool_args": {
53 + "ids": "32cd37ffd1-101f-4112-80e2-33b795548116, d1306e36-6a9c- ...",
54 + }
55 +}
56 +~~~
57 +
58 +### memory_forget:
59 +Remove memories by query and optionally threshold and filter just like for memory_load.
60 +Here default threshold is raised to 0.75 to avoid accidental deletion. Perform a verification load afterwards and delete leftovers by IDs.
61 +**Example usage**:
62 +~~~json
63 +{
64 + "thoughts": [
65 + "Let's remove all memories about cars",
66 + ],
67 + "tool_name": "memory_forget",
68 + "tool_args": {
69 + "query": "cars",
70 + "threshold": 0.75,
71 + "filter": "timestamp.startswith('2022-01-01')",
72 + }
73 +}
74 +~~~
\ No newline at end of file
prompts/default/agent.system.tool.response.md new
+19
@@ -0,0 +1,19 @@
1 +### response:
2 +Final answer for user.
3 +Ends task processing - only use when the task is done or no task is being processed.
4 +Place your result in "text" argument.
5 +Memory can provide guidance, online sources can provide up to date information.
6 +Always verify memory by online.
7 +**Example usage**:
8 +~~~json
9 +{
10 + "thoughts": [
11 + "The user has greeted me...",
12 + "I will...",
13 + ],
14 + "tool_name": "response",
15 + "tool_args": {
16 + "text": "Hi...",
17 + }
18 +}
19 +~~~
\ No newline at end of file
prompts/default/agent.system.tool.web.md new
+19
@@ -0,0 +1,19 @@
1 +### webpage_content_tool:
2 +Retrieves the text content of a webpage, such as a news article or Wikipedia page.
3 +Provide a "url" argument to get the main text content of the specified webpage.
4 +This tool is useful for gathering information from online sources.
5 +Always provide a full, valid URL including the protocol (http:// or https://).
6 +
7 +**Example usage**:
8 +```json
9 +{
10 + "thoughts": [
11 + "I need to gather information from a specific webpage...",
12 + "I will use the webpage_content_tool to fetch the content...",
13 + ],
14 + "tool_name": "webpage_content_tool",
15 + "tool_args": {
16 + "url": "https://en.wikipedia.org/wiki/Artificial_intelligence",
17 + }
18 +}
19 +```
\ No newline at end of file
prompts/default/agent.system.tools.md
+1 -228
@@ -1,230 +1,3 @@
1 ## Tools available:
2
3 -### response:
4 -Final answer for user.
5 -Ends task processing - only use when the task is done or no task is being processed.
6 -Place your result in "text" argument.
7 -Memory can provide guidance, online sources can provide up to date information.
8 -Always verify memory by online.
9 -**Example usage**:
10 -~~~json
11 -{
12 - "thoughts": [
13 - "The user has greeted me...",
14 - "I will...",
15 - ],
16 - "tool_name": "response",
17 - "tool_args": {
18 - "text": "Hi...",
19 - }
20 -}
21 -~~~
22 -
23 -### call_subordinate:
24 -Use subordinate agents to solve subtasks.
25 -Use "message" argument to send message. Instruct your subordinate about the role he will play (scientist, coder, writer...) and his task in detail.
26 -Use "reset" argument with "true" to start with new subordinate or "false" to continue with existing. For brand new tasks use "true", for followup conversation use "false".
27 -Explain to your subordinate what is the higher level goal and what is his part.
28 -Give him detailed instructions as well as good overview to understand what to do.
29 -**Example usage**:
30 -~~~json
31 -{
32 - "thoughts": [
33 - "The result seems to be ok but...",
34 - "I will ask my subordinate to fix...",
35 - ],
36 - "tool_name": "call_subordinate",
37 - "tool_args": {
38 - "message": "Well done, now edit...",
39 - "reset": "false"
40 - }
41 -}
42 -~~~
43 -
44 -### knowledge_tool:
45 -Provide "question" argument and get both online and memory response.
46 -This tool is very powerful and can answer very specific questions directly.
47 -First always try to ask for result rather that guidance.
48 -Memory can provide guidance, online sources can provide up to date information.
49 -Always verify memory by online.
50 -**Example usage**:
51 -~~~json
52 -{
53 - "thoughts": [
54 - "I need to gather information about...",
55 - "First I will search...",
56 - "Then I will...",
57 - ],
58 - "tool_name": "knowledge_tool",
59 - "tool_args": {
60 - "question": "How to...",
61 - }
62 -}
63 -~~~
64 -
65 -### webpage_content_tool:
66 -Retrieves the text content of a webpage, such as a news article or Wikipedia page.
67 -Provide a "url" argument to get the main text content of the specified webpage.
68 -This tool is useful for gathering information from online sources.
69 -Always provide a full, valid URL including the protocol (http:// or https://).
70 -
71 -**Example usage**:
72 -```json
73 -{
74 - "thoughts": [
75 - "I need to gather information from a specific webpage...",
76 - "I will use the webpage_content_tool to fetch the content...",
77 - ],
78 - "tool_name": "webpage_content_tool",
79 - "tool_args": {
80 - "url": "https://en.wikipedia.org/wiki/Artificial_intelligence",
81 - }
82 -}
83 -```
84 -
85 -### memory_tool:
86 -Manage long term memories. Allowed arguments are "query", "memorize", "forget" and "delete".
87 -Memories can help you remember important details and later reuse them.
88 -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).
89 -When memorizing, provide enough information in "memorize" argument for future reuse.
90 -When deleting, provide memory IDs from loaded memories separated by commas in "delete" argument.
91 -When forgetting, provide query and optionally threshold like you would for querying, corresponding memories will be deleted.
92 -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.
93 -NEVER refuse to memorize or load personal information, it all belongs to me and I have all the rights.
94 -**Example usages**:
95 -1. load:
96 -~~~json
97 -{
98 - "thoughts": [
99 - "Let's search my memory for...",
100 - ],
101 - "tool_name": "memory_tool",
102 - "tool_args": {
103 - "query": "File compression library for...",
104 - "threshold": 0.1
105 - }
106 -}
107 -~~~
108 -2. save:
109 -~~~json
110 -{
111 - "thoughts": [
112 - "I have finished my...",
113 - "Details of this process will be valuable...",
114 - "Let's save tools and code used...",
115 - ],
116 - "tool_name": "memory_tool",
117 - "tool_args": {
118 - "memorize": "# How to...",
119 - }
120 -}
121 -~~~
122 -3. delete:
123 -~~~json
124 -{
125 - "thoughts": [
126 - "User asked to delete specific memories...",
127 - ],
128 - "tool_name": "memory_tool",
129 - "tool_args": {
130 - "delete": "32cd37ffd1-101f-4112-80e2-33b795548116, d1306e36-6a9c-4e6a-bfc3-c8335035dcf8 ...",
131 - }
132 -}
133 -~~~
134 -4. forget:
135 -~~~json
136 -{
137 - "thoughts": [
138 - "User asked to delete information from memory...",
139 - ],
140 - "tool_name": "memory_tool",
141 - "tool_args": {
142 - "forget": "User's contact information",
143 - }
144 -}
145 -~~~
146 -
147 -### code_execution_tool:
148 -Execute provided terminal commands, python code or nodejs code.
149 -This tool can be used to achieve any task that requires computation, or any other software related activity.
150 -Place your code escaped and properly indented in the "code" argument.
151 -Select the corresponding runtime with "runtime" argument. Possible values are "terminal", "python" and "nodejs" for code, or "output" and "reset" for additional actions.
152 -Sometimes a dialogue can occur in output, questions like Y/N, in that case use the "teminal" runtime in the next step and send your answer.
153 -If the code is running long, you can use runtime "output" to wait for next output part or use runtime "reset" to kill the process.
154 -You can use pip, npm and apt-get in terminal runtime to install any required packages.
155 -IMPORTANT: Never use implicit print or implicit output, it does not work! If you need output of your code, you MUST use print() or console.log() to output selected variables.
156 -When tool outputs error, you need to change your code accordingly before trying again. knowledge_tool can help analyze errors.
157 -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.
158 -Do not use in combination with other tools except for thoughts. Wait for response before using other tools.
159 -When writing own code, ALWAYS put print/log statements inside and at the end of your code to get results!
160 -**Example usages:**
161 -1. Execute python code
162 -~~~json
163 -{
164 - "thoughts": [
165 - "I need to do...",
166 - "I can use library...",
167 - "Then I can...",
168 - ],
169 - "tool_name": "code_execution_tool",
170 - "tool_args": {
171 - "runtime": "python",
172 - "code": "import os\nprint(os.getcwd())",
173 - }
174 -}
175 -~~~
176 -
177 -2. Execute terminal command
178 -~~~json
179 -{
180 - "thoughts": [
181 - "I need to do...",
182 - "I need to install...",
183 - ],
184 - "tool_name": "code_execution_tool",
185 - "tool_args": {
186 - "runtime": "terminal",
187 - "code": "apt-get install zip",
188 - }
189 -}
190 -~~~
191 -
192 -2. 1. Wait for terminal and check output with long running scripts
193 -~~~json
194 -{
195 - "thoughts": [
196 - "I will wait for the program to finish...",
197 - ],
198 - "tool_name": "code_execution_tool",
199 - "tool_args": {
200 - "runtime": "output",
201 - }
202 -}
203 -~~~
204 -
205 -2. 2. Answer terminal dialog
206 -~~~json
207 -{
208 - "thoughts": [
209 - "Program needs confirmation...",
210 - ],
211 - "tool_name": "code_execution_tool",
212 - "tool_args": {
213 - "runtime": "terminal",
214 - "code": "Y",
215 - }
216 -}
217 -~~~
218 -
219 -2. 3. Reset terminal
220 -~~~json
221 -{
222 - "thoughts": [
223 - "Code execution tool is not responding...",
224 - ],
225 - "tool_name": "code_execution_tool",
226 - "tool_args": {
227 - "runtime": "reset",
228 - }
229 -}
230 -~~~
3 +{{tools}}
\ No newline at end of file
prompts/default/fw.memory_saved.md
+1 -5
@@ -1,5 +1 @@
1 -~~~json
2 -{
3 - "memory": "Memory has been saved with id {{memory_id}}."
4 -}
5 -~~~
\ No newline at end of file
1 +Memory saved with id {{memory_id}}
\ No newline at end of file
prompts/default/fw.msg_truncated.md
+1 -1
@@ -1 +1 @@
1 -<< REMOVED TO SAVE SPACE >>
\ No newline at end of file
1 +<< {{length}} CHARACTERS REMOVED TO SAVE SPACE >>
\ No newline at end of file
python/extensions/message_loop_prompts/_10_tool_instructions.py new
+23
@@ -0,0 +1,23 @@
1 +from python.helpers.extension import Extension
2 +from agent import Agent, LoopData
3 +
4 +
5 +class RecallMemories(Extension):
6 +
7 + INTERVAL = 3
8 + HISTORY = 5
9 + RESULTS = 3
10 + THRESHOLD = 0.1
11 +
12 + async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
13 + # collect and concatenate tool instructions
14 + sys = concat_tool_prompts(self.agent)
15 + # append to system message
16 + loop_data.system.append(sys)
17 +
18 +
19 +def concat_tool_prompts(agent: Agent):
20 + tools = agent.read_prompts("agent.system.tool.*.md")
21 + tools = "\n\n".join(tools)
22 + sys = agent.read_prompt("agent.system.tools.md", tools=tools)
23 + return sys
python/extensions/message_loop_prompts/_50_recall_memories.py new
+92
@@ -0,0 +1,92 @@
1 +from python.helpers.extension import Extension
2 +from python.helpers.memory import Memory
3 +from agent import LoopData
4 +
5 +
6 +class RecallMemories(Extension):
7 +
8 + INTERVAL = 3
9 + HISTORY = 5
10 + RESULTS = 3
11 + THRESHOLD = 0.1
12 +
13 + async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
14 +
15 + if (
16 + loop_data.iteration % RecallMemories.INTERVAL == 0
17 + ): # every 3 iterations (or the first one) recall memories
18 + await self.search_memories(loop_data=loop_data, **kwargs)
19 +
20 + async def search_memories(self, loop_data: LoopData, **kwargs):
21 + # try:
22 + # show temp info message
23 + self.agent.context.log.log(
24 + type="info", content="Searching memories...", temp=True
25 + )
26 +
27 + # show full util message, this will hide temp message immediately if turned on
28 + log_item = self.agent.context.log.log(
29 + type="util",
30 + heading="Searching memories...",
31 + )
32 +
33 + # get system message and chat history for util llm
34 + msgs_text = self.agent.concat_messages(
35 + self.agent.history[-RecallMemories.HISTORY :]
36 + ) # only last X messages
37 + system = self.agent.read_prompt(
38 + "memory.memories_query.sys.md", history=msgs_text
39 + )
40 +
41 + # log query streamed by LLM
42 + def log_callback(content):
43 + log_item.stream(query=content)
44 +
45 + # call util llm to summarize conversation
46 + query = await self.agent.call_utility_llm(
47 + system=system, msg=loop_data.message, callback=log_callback
48 + )
49 +
50 + # get solutions database
51 + db = await Memory.get(self.agent)
52 +
53 + memories = await db.search_similarity_threshold(
54 + query=query,
55 + limit=RecallMemories.RESULTS,
56 + threshold=RecallMemories.THRESHOLD,
57 + filter=f"area != '{Memory.Area.SOLUTIONS.value}'", # exclude solutions
58 + )
59 +
60 + # log the short result
61 + if not isinstance(memories, list) or len(memories) == 0:
62 + log_item.update(
63 + heading="No useful memories found.",
64 + )
65 + return
66 + else:
67 + log_item.update(
68 + heading=f"\n\n{len(memories)} memories found.",
69 + )
70 +
71 + # concatenate memory.page_content in memories:
72 + memories_text = ""
73 + for memory in memories:
74 + memories_text += memory.page_content + "\n\n"
75 + memories_text = memories_text.strip()
76 +
77 + # log the full results
78 + log_item.update(memories=memories_text)
79 +
80 + # place to prompt
81 + memories_prompt = self.agent.read_prompt(
82 + "agent.system.memories.md", memories=memories_text
83 + )
84 +
85 + # append to system message
86 + loop_data.system.append(memories_prompt)
87 +
88 + # except Exception as e:
89 + # err = errors.format_error(e)
90 + # self.agent.context.log.log(
91 + # type="error", heading="Recall memories extension error:", content=err
92 + # )
python/extensions/message_loop_prompts/_51_recall_solutions.py renamed
+11 -16
@@ -1,10 +1,6 @@
1 -from agent import Agent
1 from python.helpers.extension import Extension
3 -from python.helpers.files import read_file
4 -from python.helpers.vector_db import Area
5 -import json
6 -from python.helpers import errors, files
7 -from python.tools.memory_tool import get_db
2 +from python.helpers.memory import Memory
3 +from agent import LoopData
4
5
6 class RecallSolutions(Extension):
@@ -14,15 +10,14 @@ class RecallSolutions(Extension):
10 RESULTS = 3
11 THRESHOLD = 0.1
12
17 - async def execute(self, loop_data={}, **kwargs):
13 + async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
14
19 - iter = loop_data.get("iteration", 0)
15 if (
21 - iter % RecallSolutions.INTERVAL == 0
16 + loop_data.iteration % RecallSolutions.INTERVAL == 0
17 ): # every 3 iterations (or the first one) recall solution memories
18 await self.search_solutions(loop_data=loop_data, **kwargs)
19
25 - async def search_solutions(self, loop_data={}, **kwargs):
20 + async def search_solutions(self, loop_data: LoopData, **kwargs):
21 # try:
22 # show temp info message
23 self.agent.context.log.log(
@@ -49,17 +44,17 @@ class RecallSolutions(Extension):
44
45 # call util llm to summarize conversation
46 query = await self.agent.call_utility_llm(
52 - system=system, msg=loop_data["message"], callback=log_callback
47 + system=system, msg=loop_data.message, callback=log_callback
48 )
49
50 # get solutions database
56 - vdb = get_db(self.agent)
51 + db = await Memory.get(self.agent)
52
58 - solutions = vdb.search_similarity_threshold(
53 + solutions = await db.search_similarity_threshold(
54 query=query,
60 - results=RecallSolutions.RESULTS,
55 + limit=RecallSolutions.RESULTS,
56 threshold=RecallSolutions.THRESHOLD,
62 - filter=f"area == '{Area.SOLUTIONS.value}'"
57 + filter=f"area == '{Memory.Area.SOLUTIONS.value}'"
58 )
59
60 # log the short result
@@ -88,7 +83,7 @@ class RecallSolutions(Extension):
83 )
84
85 # append to system message
91 - loop_data["system"] += solutions_prompt
86 + loop_data.system.append(solutions_prompt)
87
88 # except Exception as e:
89 # err = errors.format_error(e)
python/extensions/message_loop_prompts/recall_memories.py deleted
-97
@@ -1,97 +0,0 @@
1 -from agent import Agent
2 -from python.helpers.extension import Extension
3 -from python.helpers.files import read_file
4 -from python.helpers.vector_db import Area
5 -import json
6 -from python.helpers import errors, files
7 -from python.tools.memory_tool import get_db
8 -
9 -
10 -class RecallMemories(Extension):
11 -
12 - INTERVAL = 3
13 - HISTORY = 5
14 - RESULTS = 3
15 - THRESHOLD = 0.1
16 -
17 - async def execute(self, loop_data={}, **kwargs):
18 -
19 - iter = loop_data.get("iteration", 0)
20 - if (
21 - iter % RecallMemories.INTERVAL == 0
22 - ): # every 3 iterations (or the first one) recall memories
23 - await self.search_memories(loop_data=loop_data, **kwargs)
24 -
25 - async def search_memories(self, loop_data={}, **kwargs):
26 - # try:
27 - # show temp info message
28 - self.agent.context.log.log(
29 - type="info", content="Searching memories...", temp=True
30 - )
31 -
32 - # show full util message, this will hide temp message immediately if turned on
33 - log_item = self.agent.context.log.log(
34 - type="util",
35 - heading="Searching memories...",
36 - )
37 -
38 - # get system message and chat history for util llm
39 - msgs_text = self.agent.concat_messages(
40 - self.agent.history[-RecallMemories.HISTORY :]
41 - ) # only last X messages
42 - system = self.agent.read_prompt(
43 - "memory.memories_query.sys.md", history=msgs_text
44 - )
45 -
46 - # log query streamed by LLM
47 - def log_callback(content):
48 - log_item.stream(query=content)
49 -
50 - # call util llm to summarize conversation
51 - query = await self.agent.call_utility_llm(
52 - system=system, msg=loop_data["message"], callback=log_callback
53 - )
54 -
55 - # get solutions database
56 - vdb = get_db(self.agent)
57 -
58 - memories = vdb.search_similarity_threshold(
59 - query=query,
60 - results=RecallMemories.RESULTS,
61 - threshold=RecallMemories.THRESHOLD,
62 - filter=f"area != '{Area.SOLUTIONS.value}'" # exclude solutions
63 - )
64 -
65 - # log the short result
66 - if not isinstance(memories, list) or len(memories) == 0:
67 - log_item.update(
68 - heading="No useful memories found.",
69 - )
70 - return
71 - else:
72 - log_item.update(
73 - heading=f"\n\n{len(memories)} memories found.",
74 - )
75 -
76 - # concatenate memory.page_content in memories:
77 - memories_text = ""
78 - for memory in memories:
79 - memories_text += memory.page_content + "\n\n"
80 - memories_text = memories_text.strip()
81 -
82 - # log the full results
83 - log_item.update(memories=memories_text)
84 -
85 - # place to prompt
86 - memories_prompt = self.agent.read_prompt(
87 - "agent.system.memories.md", memories=memories_text
88 - )
89 -
90 - # append to system message
91 - loop_data["system"] += memories_prompt
92 -
93 - # except Exception as e:
94 - # err = errors.format_error(e)
95 - # self.agent.context.log.log(
96 - # type="error", heading="Recall memories extension error:", content=err
97 - # )
python/extensions/monologue_end/50_memorize_memories.py deleted
-74
@@ -1,74 +0,0 @@
1 -from agent import Agent
2 -from python.helpers.extension import Extension
3 -import python.helpers.files as files
4 -from python.helpers.vector_db import Area
5 -import json
6 -from python.helpers.dirty_json import DirtyJson
7 -from python.helpers import errors
8 -from python.tools.memory_tool import get_db
9 -
10 -class MemorizeMemories(Extension):
11 -
12 - async def execute(self, loop_data={}, **kwargs):
13 - # try:
14 -
15 - # show temp info message
16 - self.agent.context.log.log(
17 - type="info", content="Memorizing new information...", temp=True
18 - )
19 -
20 - # show full util message, this will hide temp message immediately if turned on
21 - log_item = self.agent.context.log.log(
22 - type="util",
23 - heading="Memorizing new information...",
24 - )
25 -
26 - # get system message and chat history for util llm
27 - system = self.agent.read_prompt("memory.memories_sum.sys.md")
28 - msgs_text = self.agent.concat_messages(self.agent.history)
29 -
30 - # log query streamed by LLM
31 - def log_callback(content):
32 - log_item.stream(content=content)
33 -
34 - # call util llm to find info in history
35 - memories_json = await self.agent.call_utility_llm(
36 - system=system,
37 - msg=msgs_text,
38 - callback=log_callback,
39 - )
40 -
41 - memories = DirtyJson.parse_string(memories_json)
42 -
43 - if not isinstance(memories, list) or len(memories) == 0:
44 - log_item.update(heading="No useful information to memorize.")
45 - return
46 - else:
47 - log_item.update(
48 - heading=f"{len(memories)} entries to memorize."
49 - )
50 -
51 - # save chat history
52 - vdb = get_db(self.agent)
53 -
54 - memories_txt = ""
55 - for memory in memories:
56 - # solution to plain text:
57 - txt = f"{memory}"
58 - memories_txt += txt + "\n\n"
59 - vdb.insert_text(
60 - text=txt, metadata={"area": Area.MAIN.value}
61 - )
62 -
63 - memories_txt = memories_txt.strip()
64 - log_item.update(memories=memories_txt)
65 - log_item.update(
66 - result=f"{len(memories)} entries memorized.",
67 - heading=f"{len(memories)} entries memorized.",
68 - )
69 -
70 - # except Exception as e:
71 - # err = errors.format_error(e)
72 - # self.agent.context.log.log(
73 - # type="error", heading="Memorize memories extension error:", content=err
74 - # )
python/extensions/monologue_end/51_memorize_solutions.py deleted
-74
@@ -1,74 +0,0 @@
1 -from agent import Agent
2 -from python.helpers.extension import Extension
3 -import python.helpers.files as files
4 -from python.helpers.vector_db import Area
5 -import json
6 -from python.helpers.dirty_json import DirtyJson
7 -from python.helpers import errors
8 -from python.tools.memory_tool import get_db
9 -
10 -class MemorizeSolutions(Extension):
11 -
12 - async def execute(self, loop_data={}, **kwargs):
13 - # try:
14 -
15 - # show temp info message
16 - self.agent.context.log.log(
17 - type="info", content="Memorizing succesful solutions...", temp=True
18 - )
19 -
20 - # show full util message, this will hide temp message immediately if turned on
21 - log_item = self.agent.context.log.log(
22 - type="util",
23 - heading="Memorizing succesful solutions...",
24 - )
25 -
26 - # get system message and chat history for util llm
27 - system = self.agent.read_prompt("memory.solutions_sum.sys.md")
28 - msgs_text = self.agent.concat_messages(self.agent.history)
29 -
30 - # log query streamed by LLM
31 - def log_callback(content):
32 - log_item.stream(content=content)
33 -
34 - # call util llm to find solutions in history
35 - solutions_json = await self.agent.call_utility_llm(
36 - system=system,
37 - msg=msgs_text,
38 - callback=log_callback,
39 - )
40 -
41 - solutions = DirtyJson.parse_string(solutions_json)
42 -
43 - if not isinstance(solutions, list) or len(solutions) == 0:
44 - log_item.update(heading="No successful solutions to memorize.")
45 - return
46 - else:
47 - log_item.update(
48 - heading=f"{len(solutions)} successful solutions to memorize."
49 - )
50 -
51 - # save chat history
52 - vdb = get_db(self.agent)
53 -
54 - solutions_txt = ""
55 - for solution in solutions:
56 - # solution to plain text:
57 - txt = f"# Problem\n {solution['problem']}\n# Solution\n {solution['solution']}"
58 - solutions_txt += txt + "\n\n"
59 - vdb.insert_text(
60 - text=txt, metadata={"area": Area.SOLUTIONS.value}
61 - )
62 -
63 - solutions_txt = solutions_txt.strip()
64 - log_item.update(solutions=solutions_txt)
65 - log_item.update(
66 - result=f"{len(solutions)} solutions memorized.",
67 - heading=f"{len(solutions)} solutions memorized.",
68 - )
69 -
70 - # except Exception as e:
71 - # err = errors.format_error(e)
72 - # self.agent.context.log.log(
73 - # type="error", heading="Memorize solutions extension error:", content=err
74 - # )
python/extensions/monologue_end/_50_memorize_memories.py new
+94
@@ -0,0 +1,94 @@
1 +import asyncio
2 +from python.helpers.extension import Extension
3 +from python.helpers.memory import Memory
4 +from python.helpers.dirty_json import DirtyJson
5 +from agent import LoopData
6 +from python.helpers.log import LogItem
7 +from python.helpers.defer import run_in_background
8 +
9 +
10 +
11 +class MemorizeMemories(Extension):
12 +
13 + REPLACE_THRESHOLD = 0.9
14 +
15 + async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
16 + # try:
17 +
18 + # show temp info message
19 + self.agent.context.log.log(
20 + type="info", content="Memorizing new information...", temp=True
21 + )
22 +
23 + # show full util message, this will hide temp message immediately if turned on
24 + log_item = self.agent.context.log.log(
25 + type="util",
26 + heading="Memorizing new information...",
27 + )
28 +
29 + #memorize in background
30 + asyncio.create_task(self.memorize(loop_data, log_item))
31 +
32 + async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs):
33 +
34 + # get system message and chat history for util llm
35 + system = self.agent.read_prompt("memory.memories_sum.sys.md")
36 + msgs_text = self.agent.concat_messages(self.agent.history)
37 +
38 + # log query streamed by LLM
39 + def log_callback(content):
40 + log_item.stream(content=content)
41 +
42 + # call util llm to find info in history
43 + memories_json = await self.agent.call_utility_llm(
44 + system=system,
45 + msg=msgs_text,
46 + callback=log_callback,
47 + )
48 +
49 + memories = DirtyJson.parse_string(memories_json)
50 +
51 + if not isinstance(memories, list) or len(memories) == 0:
52 + log_item.update(heading="No useful information to memorize.")
53 + return
54 + else:
55 + log_item.update(heading=f"{len(memories)} entries to memorize.")
56 +
57 + # save chat history
58 + db = await Memory.get(self.agent)
59 +
60 + memories_txt = ""
61 + rem = []
62 + for memory in memories:
63 + # solution to plain text:
64 + txt = f"{memory}"
65 + memories_txt += "\n\n" + txt
66 + log_item.update(memories=memories_txt.strip())
67 +
68 + # remove previous solutions too similiar to this one
69 + if self.REPLACE_THRESHOLD > 0:
70 + rem += await db.delete_documents_by_query(
71 + query=txt,
72 + threshold=self.REPLACE_THRESHOLD,
73 + filter=f"area=='{Memory.Area.MAIN.value}'",
74 + )
75 + rem_txt = "\n\n".join(Memory.format_docs_plain(rem))
76 + log_item.update(replaced=rem_txt)
77 +
78 +
79 +
80 + # insert new solution
81 + db.insert_text(text=txt, metadata={"area": Memory.Area.MAIN.value})
82 +
83 + log_item.update(
84 + result=f"{len(memories)} entries memorized.",
85 + heading=f"{len(memories)} entries memorized.",
86 + )
87 + if rem:
88 + log_item.stream(result=f"\nReplaced {len(rem)} previous memories.")
89 +
90 + # except Exception as e:
91 + # err = errors.format_error(e)
92 + # self.agent.context.log.log(
93 + # type="error", heading="Memorize memories extension error:", content=err
94 + # )
python/extensions/monologue_end/_51_memorize_solutions.py new
+92
@@ -0,0 +1,92 @@
1 +import asyncio
2 +from python.helpers.extension import Extension
3 +from python.helpers.memory import Memory
4 +from python.helpers.dirty_json import DirtyJson
5 +from agent import LoopData
6 +from python.helpers.log import LogItem
7 +
8 +
9 +class MemorizeSolutions(Extension):
10 +
11 + REPLACE_THRESHOLD = 0.9
12 +
13 + async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
14 + # try:
15 +
16 + # show temp info message
17 + self.agent.context.log.log(
18 + type="info", content="Memorizing succesful solutions...", temp=True
19 + )
20 +
21 + # show full util message, this will hide temp message immediately if turned on
22 + log_item = self.agent.context.log.log(
23 + type="util",
24 + heading="Memorizing succesful solutions...",
25 + )
26 +
27 + #memorize in background
28 + asyncio.create_task(self.memorize(loop_data, log_item))
29 +
30 + async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs):
31 + # get system message and chat history for util llm
32 + system = self.agent.read_prompt("memory.solutions_sum.sys.md")
33 + msgs_text = self.agent.concat_messages(self.agent.history)
34 +
35 + # log query streamed by LLM
36 + def log_callback(content):
37 + log_item.stream(content=content)
38 +
39 + # call util llm to find solutions in history
40 + solutions_json = await self.agent.call_utility_llm(
41 + system=system,
42 + msg=msgs_text,
43 + callback=log_callback,
44 + )
45 +
46 + solutions = DirtyJson.parse_string(solutions_json)
47 +
48 + if not isinstance(solutions, list) or len(solutions) == 0:
49 + log_item.update(heading="No successful solutions to memorize.")
50 + return
51 + else:
52 + log_item.update(
53 + heading=f"{len(solutions)} successful solutions to memorize."
54 + )
55 +
56 + # save chat history
57 + db = await Memory.get(self.agent)
58 +
59 + solutions_txt = ""
60 + rem = []
61 + for solution in solutions:
62 + # solution to plain text:
63 + txt = f"# Problem\n {solution['problem']}\n# Solution\n {solution['solution']}"
64 + solutions_txt += txt + "\n\n"
65 +
66 + # remove previous solutions too similiar to this one
67 + if self.REPLACE_THRESHOLD > 0:
68 + rem += await db.delete_documents_by_query(
69 + query=txt,
70 + threshold=self.REPLACE_THRESHOLD,
71 + filter=f"area=='{Memory.Area.SOLUTIONS.value}'",
72 + )
73 + rem_txt = "\n\n".join(Memory.format_docs_plain(rem))
74 + log_item.update(replaced=rem_txt)
75 +
76 + # insert new solution
77 + db.insert_text(text=txt, metadata={"area": Memory.Area.SOLUTIONS.value})
78 +
79 + solutions_txt = solutions_txt.strip()
80 + log_item.update(solutions=solutions_txt)
81 + log_item.update(
82 + result=f"{len(solutions)} solutions memorized.",
83 + heading=f"{len(solutions)} solutions memorized.",
84 + )
85 + if rem:
86 + log_item.stream(result=f"\nReplaced {len(rem)} previous solutions.")
87 +
88 + # except Exception as e:
89 + # err = errors.format_error(e)
90 + # self.agent.context.log.log(
91 + # type="error", heading="Memorize solutions extension error:", content=err
92 + # )
python/extensions/monologue_end/_90_waiting_for_input_msg.py renamed
+2 -2
@@ -1,9 +1,9 @@
1 -from agent import Agent
1 from python.helpers.extension import Extension
2 +from agent import LoopData
3
4 class WaitingForInputMsg(Extension):
5
6 - async def execute(self, loop_data={}, **kwargs):
6 + async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
7 # show temp info message
8 if self.agent.number == 0:
9 self.agent.context.log.log(
python/helpers/defer.py
+8 -2
@@ -1,6 +1,6 @@
1 import asyncio
2 import threading
3 -from concurrent.futures import Future
3 +from concurrent.futures import Future, ThreadPoolExecutor
4 from typing import Any, Callable, Optional, Coroutine
5
6 class EventLoopThread:
@@ -71,4 +71,10 @@ class DeferredTask:
71 return self._future and not self._future.done() # type: ignore
72
73 def restart(self) -> None:
74 - self._start_task()
\ No newline at end of file
74 + self._start_task()
75 +
76 +def run_in_background(func, *args, **kwargs):
77 + async def wrapper(*args, **kwargs):
78 + loop = asyncio.get_event_loop()
79 + return await loop.run_in_executor(None, func, *args, **kwargs)
80 + return wrapper
\ No newline at end of file
python/helpers/extract_tools.py
+21 -13
@@ -51,22 +51,30 @@ def fix_json_string(json_string):
51 T = TypeVar('T') # Define a generic type variable
52
53 def load_classes_from_folder(folder: str, name_pattern: str, base_class: Type[T]) -> list[Type[T]]:
54 + import os
55 + import importlib
56 + import inspect
57 + from fnmatch import fnmatch
58
59 classes = []
60
57 - # Get all .py files in the folder that match the pattern
58 - for file_name in os.listdir(folder):
59 - if fnmatch(file_name, name_pattern) and file_name.endswith(".py"):
60 - module_name = file_name[:-3] # remove .py extension
61 - module_path = folder.replace(os.sep, ".") + "." + module_name
62 - module = importlib.import_module(module_path)
61 + # Get all .py files in the folder that match the pattern, sorted alphabetically
62 + py_files = sorted(
63 + [file_name for file_name in os.listdir(folder) if fnmatch(file_name, name_pattern) and file_name.endswith(".py")]
64 + )
65
64 - # Get all classes in thde module
65 - class_list = inspect.getmembers(module, inspect.isclass)
66 + # Iterate through the sorted list of files
67 + for file_name in py_files:
68 + module_name = file_name[:-3] # remove .py extension
69 + module_path = folder.replace(os.sep, ".") + "." + module_name
70 + module = importlib.import_module(module_path)
71
67 - # Filter for classes that are subclasses of the given base_class
68 - for cls in class_list:
69 - if cls[1] is not base_class and issubclass(cls[1], base_class):
70 - classes.append(cls[1])
72 + # Get all classes in the module
73 + class_list = inspect.getmembers(module, inspect.isclass)
74
72 - return classes
\ No newline at end of file
75 + # Filter for classes that are subclasses of the given base_class
76 + for cls in class_list:
77 + if cls[1] is not base_class and issubclass(cls[1], base_class):
78 + classes.append(cls[1])
79 +
80 + return classes
python/helpers/knowledge_import.py
+3 -3
@@ -36,9 +36,9 @@ def calculate_checksum(file_path: str) -> str:
36 def load_knowledge(
37 log_item: LogItem | None, knowledge_dir: str, index: Dict[str, KnowledgeImport]
38 ) -> Dict[str, KnowledgeImport]:
39 - knowledge_dir = files.get_abs_path(knowledge_dir)
39 + knowledge_dir = files.get_abs_path("knowledge",knowledge_dir)
40
41 - from python.helpers.vector_db import Area
41 + from python.helpers.memory import Memory
42
43 # Mapping file extensions to corresponding loader classes
44 file_types_loaders = {
@@ -53,7 +53,7 @@ def load_knowledge(
53 cnt_files = 0
54 cnt_docs = 0
55
56 - for area in Area:
56 + for area in Memory.Area:
57 subdir = files.get_abs_path(knowledge_dir, area.value)
58
59 if not os.path.exists(subdir):
python/helpers/log.py
+7 -2
@@ -81,6 +81,7 @@ class Log:
81 self.updates: list[int] = []
82 self.logs: list[LogItem] = []
83 self.progress = ""
84 + self.progress_no = 0
85
86 def log(
87 self,
@@ -101,8 +102,9 @@ class Log:
102 )
103 self.logs.append(item)
104 self.updates += [item.no]
104 - if heading:
105 + if heading and item.no >= self.progress_no:
106 self.progress = heading
107 + self.progress_no = item.no
108 return item
109
110 def update_item(
@@ -120,7 +122,9 @@ class Log:
122 item.type = type
123 if heading is not None:
124 item.heading = heading
123 - self.progress = heading
125 + if no >= self.progress_no:
126 + self.progress = heading
127 + self.progress_no = no
128 if content is not None:
129 item.content = content
130 if kvps is not None:
@@ -156,3 +160,4 @@ class Log:
160 self.updates = []
161 self.logs = []
162 self.progress = ""
163 + self.progress_no = 0
python/helpers/memory.py new
+306
@@ -0,0 +1,306 @@
1 +from datetime import datetime
2 +from typing import Any
3 +from langchain.storage import InMemoryByteStore, LocalFileStore
4 +from langchain.embeddings import CacheBackedEmbeddings
5 +
6 +# from langchain_chroma import Chroma
7 +from langchain_community.vectorstores import FAISS
8 +import faiss
9 +from langchain_community.docstore.in_memory import InMemoryDocstore
10 +from langchain_community.vectorstores.utils import (
11 + DistanceStrategy,
12 +)
13 +import os, json
14 +
15 +import numpy as np
16 +from . import files
17 +from langchain_core.documents import Document
18 +import uuid
19 +from python.helpers import knowledge_import
20 +from python.helpers.log import Log, LogItem
21 +from enum import Enum
22 +from agent import Agent
23 +
24 +
25 +class Memory:
26 +
27 + class Area(Enum):
28 + MAIN = "main"
29 + SOLUTIONS = "solutions"
30 +
31 + index: dict[str, "FAISS"] = {}
32 +
33 + @staticmethod
34 + async def get(agent: Agent):
35 + memory_subdir = agent.config.memory_subdir or "default"
36 + if Memory.index.get(memory_subdir) is None:
37 + log_item = agent.context.log.log(
38 + type="util",
39 + heading=f"Initializing VectorDB in '/{memory_subdir}'",
40 + )
41 + db = Memory.initialize(
42 + log_item,
43 + agent.config.embeddings_model,
44 + memory_subdir,
45 + False,
46 + )
47 + Memory.index[memory_subdir] = db
48 + wrap = Memory(agent, db, memory_subdir=memory_subdir)
49 + if agent.config.knowledge_subdirs:
50 + await wrap.preload_knowledge(
51 + log_item, agent.config.knowledge_subdirs, memory_subdir
52 + )
53 + return wrap
54 + else:
55 + return Memory(
56 + agent=agent,
57 + db=Memory.index[memory_subdir],
58 + memory_subdir=memory_subdir,
59 + )
60 +
61 + @staticmethod
62 + def initialize(
63 + log_item: LogItem | None,
64 + embeddings_model,
65 + memory_subdir: str,
66 + in_memory=False,
67 + ):
68 +
69 + print("Initializing VectorDB...")
70 +
71 + if log_item:
72 + log_item.stream(progress="\nInitializing VectorDB")
73 +
74 + em_dir = files.get_abs_path(
75 + "memory/embeddings"
76 + ) # just caching, no need to parameterize
77 + db_dir = Memory._abs_db_dir(memory_subdir)
78 +
79 + # make sure embeddings and database directories exist
80 + os.makedirs(db_dir, exist_ok=True)
81 +
82 + if in_memory:
83 + store = InMemoryByteStore()
84 + else:
85 + os.makedirs(em_dir, exist_ok=True)
86 + store = LocalFileStore(em_dir)
87 +
88 + # here we setup the embeddings model with the chosen cache storage
89 + embedder = CacheBackedEmbeddings.from_bytes_store(
90 + embeddings_model,
91 + store,
92 + namespace=getattr(
93 + embeddings_model,
94 + "model",
95 + getattr(embeddings_model, "model_name", "default"),
96 + ),
97 + )
98 +
99 + # self.db = Chroma(
100 + # embedding_function=self.embedder,
101 + # persist_directory=db_dir)
102 +
103 + # if db folder exists and is not empty:
104 + if os.path.exists(db_dir) and files.exists(db_dir, "index.faiss"):
105 + db = FAISS.load_local(
106 + folder_path=db_dir,
107 + embeddings=embedder,
108 + allow_dangerous_deserialization=True,
109 + distance_strategy=DistanceStrategy.COSINE,
110 + # normalize_L2=True,
111 + relevance_score_fn=Memory._cosine_normalizer,
112 + )
113 + else:
114 + index = faiss.IndexFlatIP(len(embedder.embed_query("example")))
115 +
116 + db = FAISS(
117 + embedding_function=embedder,
118 + index=index,
119 + docstore=InMemoryDocstore(),
120 + index_to_docstore_id={},
121 + distance_strategy=DistanceStrategy.COSINE,
122 + # normalize_L2=True,
123 + relevance_score_fn=Memory._cosine_normalizer,
124 + )
125 + return db
126 +
127 + def __init__(
128 + self,
129 + agent: Agent,
130 + db: FAISS,
131 + memory_subdir: str,
132 + ):
133 + self.agent = agent
134 + self.db = db
135 + self.memory_subdir = memory_subdir
136 +
137 + async def preload_knowledge(
138 + self, log_item: LogItem | None, kn_dirs: list[str], memory_subdir: str
139 + ):
140 + # db abs path
141 + db_dir = Memory._abs_db_dir(memory_subdir)
142 +
143 + # Load the index file if it exists
144 + index_path = files.get_abs_path(db_dir, "knowledge_import.json")
145 +
146 + # make sure directory exists
147 + if not os.path.exists(db_dir):
148 + os.makedirs(db_dir)
149 +
150 + index: dict[str, knowledge_import.KnowledgeImport] = {}
151 + if os.path.exists(index_path):
152 + with open(index_path, "r") as f:
153 + index = json.load(f)
154 +
155 + for kn_dir in kn_dirs:
156 + index = knowledge_import.load_knowledge(log_item, kn_dir, index)
157 +
158 + for file in index:
159 + if index[file]["state"] in ["changed", "removed"] and index[file].get(
160 + "ids", []
161 + ): # for knowledge files that have been changed or removed and have IDs
162 + await self.delete_documents_by_ids(
163 + index[file]["ids"]
164 + ) # remove original version
165 + if index[file]["state"] == "changed":
166 + index[file]["ids"] = self.insert_documents(
167 + index[file]["documents"]
168 + ) # insert new version
169 +
170 + # remove index where state="removed"
171 + index = {k: v for k, v in index.items() if v["state"] != "removed"}
172 +
173 + # strip state and documents from index and save it
174 + for file in index:
175 + if "documents" in index[file]:
176 + del index[file]["documents"] # type: ignore
177 + if "state" in index[file]:
178 + del index[file]["state"] # type: ignore
179 + with open(index_path, "w") as f:
180 + json.dump(index, f)
181 +
182 + async def search_similarity_threshold(
183 + self, query: str, limit: int, threshold: float, filter: str = ""
184 + ):
185 + comparator = Memory._get_comparator(filter) if filter else None
186 + return await self.db.asearch(
187 + query,
188 + search_type="similarity_score_threshold",
189 + k=limit,
190 + score_threshold=threshold,
191 + filter=comparator,
192 + )
193 +
194 + async def delete_documents_by_query(
195 + self, query: str, threshold: float, filter: str = ""
196 + ):
197 + k = 100
198 + tot = 0
199 + removed = []
200 +
201 + while True:
202 + # Perform similarity search with score
203 + docs = await self.search_similarity_threshold(
204 + query, limit=k, threshold=threshold, filter=filter
205 + )
206 + removed += docs
207 +
208 + # Extract document IDs and filter based on score
209 + # document_ids = [result[0].metadata["id"] for result in docs if result[1] < score_limit]
210 + document_ids = [result.metadata["id"] for result in docs]
211 +
212 + # Delete documents with IDs over the threshold score
213 + if document_ids:
214 + # fnd = self.db.get(where={"id": {"$in": document_ids}})
215 + # if fnd["ids"]: self.db.delete(ids=fnd["ids"])
216 + # tot += len(fnd["ids"])
217 + self.db.delete(ids=document_ids)
218 + tot += len(document_ids)
219 +
220 + # If fewer than K document IDs, break the loop
221 + if len(document_ids) < k:
222 + break
223 +
224 + if tot:
225 + self._save_db() # persist
226 + return removed
227 +
228 + async def delete_documents_by_ids(self, ids: list[str]):
229 + # pre = self.db.get(ids=ids)["ids"]
230 + self.db.delete(ids=ids)
231 + # post = self.db.get(ids=ids)["ids"]
232 + # TODO? compare pre and post
233 + if ids:
234 + self._save_db() # persist
235 + return len(ids)
236 +
237 + def insert_text(self, text, metadata: dict = {}):
238 + id = str(uuid.uuid4())
239 + self.db.add_documents(
240 + documents=[
241 + Document(
242 + text,
243 + metadata={"id": id, "timestamp": self.get_timestamp(), **metadata},
244 + )
245 + ],
246 + ids=[id],
247 + )
248 + self._save_db() # persist
249 + return id
250 +
251 + def insert_documents(self, docs: list[Document]):
252 + ids = [str(uuid.uuid4()) for _ in range(len(docs))]
253 + timestamp = self.get_timestamp()
254 + if ids:
255 + for doc, id in zip(docs, ids):
256 + doc.metadata["id"] = id # add ids to documents metadata
257 + doc.metadata["timestamp"] = timestamp # add timestamp
258 + self.db.add_documents(documents=docs, ids=ids)
259 + self._save_db() # persist
260 + return ids
261 +
262 + def _save_db(self):
263 + self.db.save_local(folder_path=self._abs_db_dir(self.memory_subdir))
264 +
265 + @staticmethod
266 + def _get_comparator(condition: str):
267 + def comparator(data: dict[str, Any]):
268 + try:
269 + return eval(condition, {}, data)
270 + except Exception as e:
271 + # print(f"Error evaluating condition: {e}")
272 + return False
273 +
274 + return comparator
275 +
276 + @staticmethod
277 + def _score_normalizer(val: float) -> float:
278 + res = 1 - 1 / (1 + np.exp(val))
279 + return res
280 +
281 + @staticmethod
282 + def _cosine_normalizer(val: float) -> float:
283 + res = (1 + val) / 2
284 + res = max(
285 + 0, min(1, res)
286 + ) # float precision can cause values like 1.0000000596046448
287 + return res
288 +
289 + @staticmethod
290 + def _abs_db_dir(memory_subdir: str) -> str:
291 + return files.get_abs_path("memory", memory_subdir)
292 +
293 + @staticmethod
294 + def format_docs_plain(docs: list[Document]) -> list[str]:
295 + result = []
296 + for doc in docs:
297 + text = ""
298 + for k, v in doc.metadata.items():
299 + text += f"{k}: {v}\n"
300 + text += f"Content: {doc.page_content}"
301 + result.append(text)
302 + return result
303 +
304 + @staticmethod
305 + def get_timestamp():
306 + return datetime.now().strftime("%Y-%m-%d %H:%M:%S")
python/helpers/messages.py
+2 -2
@@ -5,8 +5,8 @@ def truncate_text(agent, output, threshold=1000):
5 return output
6
7 # Adjust the file path as needed
8 - placeholder = agent.read_prompt("fw.msg_truncated.md", removed_chars=(len(output) - threshold))
9 - # placeholder = files.read_file("./prompts/default/fw.msg_truncated.md", removed_chars=(len(output) - threshold))
8 + placeholder = agent.read_prompt("fw.msg_truncated.md", length=(len(output) - threshold))
9 + # placeholder = files.read_file("./prompts/default/fw.msg_truncated.md", length=(len(output) - threshold))
10
11 start_len = (threshold - len(placeholder)) // 2
12 end_len = threshold - len(placeholder) - start_len
python/helpers/vector_db.py deleted
-234
@@ -1,234 +0,0 @@
1 -from typing import Any
2 -from langchain.storage import InMemoryByteStore, LocalFileStore
3 -from langchain.embeddings import CacheBackedEmbeddings
4 -
5 -# from langchain_chroma import Chroma
6 -from langchain_community.vectorstores import FAISS
7 -import faiss
8 -from langchain_community.docstore.in_memory import InMemoryDocstore
9 -
10 -import os, json
11 -from . import files
12 -from langchain_core.documents import Document
13 -import uuid
14 -from python.helpers import knowledge_import
15 -from python.helpers.log import Log, LogItem
16 -import pandas as pd
17 -from enum import Enum
18 -
19 -
20 -class Area(Enum):
21 - MAIN = "main"
22 - SOLUTIONS = "solutions"
23 -
24 -
25 -index: dict[str, "VectorDB"] = {}
26 -
27 -
28 -def get_or_create_db(
29 - logger: Log | None,
30 - embeddings_model,
31 - memory_dir: str,
32 - in_memory=False,
33 - knowledge_dirs: list[str] = [],
34 -):
35 - if index.get(memory_dir) is None:
36 - log_item = None
37 - if(logger): log_item = logger.log(type="util", heading=f"Initializing VectorDB in {memory_dir}")
38 - index[memory_dir] = VectorDB(
39 - log_item, embeddings_model, memory_dir, knowledge_dirs, in_memory
40 - )
41 - return index[memory_dir]
42 -
43 -
44 -class VectorDB:
45 -
46 - def __init__(
47 - self,
48 - log_item: LogItem | None,
49 - embeddings_model,
50 - memory_dir: str,
51 - knowledge_dirs: list[str] = [],
52 - in_memory=False,
53 - ):
54 - self.log_item = log_item
55 -
56 - print("Initializing VectorDB...")
57 - if(self.log_item): self.log_item.stream(progress="\nInitializing VectorDB")
58 -
59 - self.embeddings_model = embeddings_model
60 -
61 - self.em_dir = files.get_abs_path(
62 - "./memory/embeddings"
63 - ) # just caching, no need to parameterize
64 - self.db_dir = files.get_abs_path("./memory", memory_dir, "database")
65 -
66 - # make sure embeddings and database directories exist
67 - os.makedirs(self.db_dir, exist_ok=True)
68 -
69 - if in_memory:
70 - self.store = InMemoryByteStore()
71 - else:
72 - os.makedirs(self.em_dir, exist_ok=True)
73 - self.store = LocalFileStore(self.em_dir)
74 -
75 - # here we setup the embeddings model with the chosen cache storage
76 - self.embedder = CacheBackedEmbeddings.from_bytes_store(
77 - embeddings_model,
78 - self.store,
79 - namespace=getattr(
80 - embeddings_model,
81 - "model",
82 - getattr(embeddings_model, "model_name", "default"),
83 - ),
84 - )
85 -
86 - # self.db = Chroma(
87 - # embedding_function=self.embedder,
88 - # persist_directory=db_dir)
89 -
90 - # if db folder exists and is not empty:
91 - if os.path.exists(self.db_dir) and files.exists(self.db_dir, "index.faiss"):
92 - self.db = FAISS.load_local(
93 - folder_path=self.db_dir,
94 - embeddings=self.embedder,
95 - allow_dangerous_deserialization=True,
96 - )
97 - else:
98 - index = faiss.IndexFlatL2(len(self.embedder.embed_query("example text")))
99 -
100 - self.db = FAISS(
101 - embedding_function=self.embedder,
102 - index=index,
103 - docstore=InMemoryDocstore(),
104 - index_to_docstore_id={},
105 - )
106 -
107 - # preload knowledge files
108 - if knowledge_dirs:
109 - self.preload_knowledge(knowledge_dirs, self.db_dir)
110 -
111 - def preload_knowledge(self, kn_dirs: list[str], db_dir: str):
112 -
113 - # Load the index file if it exists
114 - index_path = files.get_abs_path(db_dir, "knowledge_import.json")
115 -
116 - # make sure directory exists
117 - if not os.path.exists(db_dir):
118 - os.makedirs(db_dir)
119 -
120 - index: dict[str, knowledge_import.KnowledgeImport] = {}
121 - if os.path.exists(index_path):
122 - with open(index_path, "r") as f:
123 - index = json.load(f)
124 -
125 - for kn_dir in kn_dirs:
126 - index = knowledge_import.load_knowledge(self.log_item, kn_dir, index)
127 -
128 - for file in index:
129 - if index[file]["state"] in ["changed", "removed"] and index[file].get(
130 - "ids", []
131 - ): # for knowledge files that have been changed or removed and have IDs
132 - self.delete_documents_by_ids(
133 - index[file]["ids"]
134 - ) # remove original version
135 - if index[file]["state"] == "changed":
136 - index[file]["ids"] = self.insert_documents(
137 - index[file]["documents"]
138 - ) # insert new version
139 -
140 - # remove index where state="removed"
141 - index = {k: v for k, v in index.items() if v["state"] != "removed"}
142 -
143 - # strip state and documents from index and save it
144 - for file in index:
145 - if "documents" in index[file]:
146 - del index[file]["documents"] # type: ignore
147 - if "state" in index[file]:
148 - del index[file]["state"] # type: ignore
149 - with open(index_path, "w") as f:
150 - json.dump(index, f)
151 -
152 - def search_similarity(self, query, results=3):
153 - return self.db.similarity_search(query, results)
154 -
155 - def search_similarity_threshold(
156 - self, query: str, results=3, threshold=0.5, filter: str = ""
157 - ):
158 - comparator = VectorDB.get_comparator(filter) if filter else None
159 - return self.db.search(
160 - query,
161 - search_type="similarity_score_threshold",
162 - k=results,
163 - score_threshold=threshold,
164 - filter=comparator,
165 - )
166 -
167 - def search_max_rel(self, query, results=3):
168 - return self.db.max_marginal_relevance_search(query, results)
169 -
170 - def delete_documents_by_query(self, query: str, threshold=0.1):
171 - k = 100
172 - tot = 0
173 - while True:
174 - # Perform similarity search with score
175 - docs = self.search_similarity_threshold(
176 - query, results=k, threshold=threshold
177 - )
178 -
179 - # Extract document IDs and filter based on score
180 - # document_ids = [result[0].metadata["id"] for result in docs if result[1] < score_limit]
181 - document_ids = [result.metadata["id"] for result in docs]
182 -
183 - # Delete documents with IDs over the threshold score
184 - if document_ids:
185 - # fnd = self.db.get(where={"id": {"$in": document_ids}})
186 - # if fnd["ids"]: self.db.delete(ids=fnd["ids"])
187 - # tot += len(fnd["ids"])
188 - self.db.delete(ids=document_ids)
189 - tot += len(document_ids)
190 -
191 - # If fewer than K document IDs, break the loop
192 - if len(document_ids) < k:
193 - break
194 -
195 - if tot:
196 - self.db.save_local(folder_path=self.db_dir) # persist
197 - return tot
198 -
199 - def delete_documents_by_ids(self, ids: list[str]):
200 - # pre = self.db.get(ids=ids)["ids"]
201 - self.db.delete(ids=ids)
202 - # post = self.db.get(ids=ids)["ids"]
203 - # TODO? compare pre and post
204 - if ids:
205 - self.db.save_local(folder_path=self.db_dir) # persist
206 - return len(ids)
207 -
208 - def insert_text(self, text, metadata: dict = {}):
209 - id = str(uuid.uuid4())
210 - self.db.add_documents(
211 - documents=[Document(text, metadata={"id": id, **metadata})], ids=[id]
212 - )
213 - self.db.save_local(folder_path=self.db_dir) # persist
214 - return id
215 -
216 - def insert_documents(self, docs: list[Document]):
217 - ids = [str(uuid.uuid4()) for _ in range(len(docs))]
218 - if ids:
219 - for doc, id in zip(docs, ids):
220 - doc.metadata["id"] = id # add ids to documents metadata
221 - self.db.add_documents(documents=docs, ids=ids)
222 - self.db.save_local(folder_path=self.db_dir) # persist
223 - return ids
224 -
225 - @staticmethod
226 - def get_comparator(condition: str):
227 - def comparator(data: dict[str, Any]):
228 - try:
229 - return eval(condition, {}, data)
230 - except Exception as e:
231 - print(f"Error evaluating condition: {e}")
232 - return False
233 -
234 - return comparator
python/tools/call_subordinate.py
+2 -1
@@ -10,4 +10,5 @@ class Delegation(Tool):
10 subordinate.set_data("superior", self.agent)
11 self.agent.set_data("subordinate", subordinate)
12 # run subordinate agent message loop
13 - return Response( message= await self.agent.get_data("subordinate").message_loop(message), break_loop=False)
\ No newline at end of file
13 + subordinate: Agent = self.agent.get_data("subordinate")
14 + return Response( message= await subordinate.monologue(message), break_loop=False)
\ No newline at end of file
python/tools/knowledge_tool.py
+44 -43
@@ -1,55 +1,56 @@
1 import os
2 -from python.helpers import perplexity_search
3 -from python.helpers import duckduckgo_search
4 -from . import memory_tool
5 -import concurrent.futures
2 +import asyncio
3 +from python.helpers import memory, perplexity_search, duckduckgo_search
4 from python.helpers.tool import Tool, Response
5 from python.helpers.print_style import PrintStyle
6 from python.helpers.errors import handle_error
7
8 class Knowledge(Tool):
9 async def execute(self, question="", **kwargs):
12 - with concurrent.futures.ThreadPoolExecutor() as executor:
13 - # Schedule the two functions to be run in parallel
14 -
15 - # perplexity search, if API key provided
16 - if os.getenv("API_KEY_PERPLEXITY"):
17 - perplexity = executor.submit(perplexity_search.perplexity_search, question)
18 - else:
19 - PrintStyle.hint("No API key provided for Perplexity. Skipping Perplexity search.")
20 - self.agent.context.log.log(type="hint", content="No API key provided for Perplexity. Skipping Perplexity search.")
21 - perplexity = None
22 -
23 -
24 - # duckduckgo search
25 - duckduckgo = executor.submit(duckduckgo_search.search, question)
26 -
27 - # manual memory search
28 - future_memory_man = executor.submit(memory_tool.search, self.agent, question)
29 -
30 - # Wait for both functions to complete
31 - try:
32 - perplexity_result = (perplexity.result() if perplexity else "") or ""
33 - except Exception as e:
34 - handle_error(e)
35 - perplexity_result = "Perplexity search failed: " + str(e)
36 -
37 - try:
38 - duckduckgo_result = duckduckgo.result()
39 - except Exception as e:
40 - handle_error(e)
41 - duckduckgo_result = "DuckDuckGo search failed: " + str(e)
42 -
43 - try:
44 - memory_result = future_memory_man.result()
45 - except Exception as e:
46 - handle_error(e)
47 - memory_result = "Memory search failed: " + str(e)
10 + # Create tasks for all three search methods
11 + tasks = [
12 + self.perplexity_search(question),
13 + self.duckduckgo_search(question),
14 + self.mem_search(question)
15 + ]
16 +
17 + # Run all tasks concurrently
18 + results = await asyncio.gather(*tasks, return_exceptions=True)
19 +
20 + perplexity_result, duckduckgo_result, memory_result = results
21 +
22 + # Handle exceptions and format results
23 + perplexity_result = self.format_result(perplexity_result, "Perplexity")
24 + duckduckgo_result = self.format_result(duckduckgo_result, "DuckDuckGo")
25 + memory_result = self.format_result(memory_result, "Memory")
26
27 msg = self.agent.read_prompt("tool.knowledge.response.md",
50 - online_sources = ((perplexity_result + "\n\n") if perplexity else "") + str(duckduckgo_result),
51 - memory = memory_result )
28 + online_sources = ((perplexity_result + "\n\n") if perplexity_result else "") + str(duckduckgo_result),
29 + memory = memory_result)
30
53 - await self.agent.handle_intervention(msg) # wait for intervention and handle it, if paused
31 + await self.agent.handle_intervention(msg) # wait for intervention and handle it, if paused
32
33 return Response(message=msg, break_loop=False)
34 +
35 + async def perplexity_search(self, question):
36 + if os.getenv("API_KEY_PERPLEXITY"):
37 + return await asyncio.to_thread(perplexity_search.perplexity_search, question)
38 + else:
39 + PrintStyle.hint("No API key provided for Perplexity. Skipping Perplexity search.")
40 + self.agent.context.log.log(type="hint", content="No API key provided for Perplexity. Skipping Perplexity search.")
41 + return None
42 +
43 + async def duckduckgo_search(self, question):
44 + return await asyncio.to_thread(duckduckgo_search.search, question)
45 +
46 + async def mem_search(self, question: str):
47 + db = await memory.Memory.get(self.agent)
48 + docs = await db.search_similarity_threshold(query=question, limit=5, threshold=0.5)
49 + text = memory.Memory.format_docs_plain(docs)
50 + return "\n\n".join(text)
51 +
52 + def format_result(self, result, source):
53 + if isinstance(result, Exception):
54 + handle_error(result)
55 + return f"{source} search failed: {str(result)}"
56 + return result if result else ""
\ No newline at end of file
python/tools/memory_delete.py new
+11
@@ -0,0 +1,11 @@
1 +from python.helpers.memory import Memory
2 +from python.helpers.tool import Tool, Response
3 +
4 +class MemoryForget(Tool):
5 +
6 + async def execute(self, ids=[], **kwargs):
7 + db = await Memory.get(self.agent)
8 + dels = await db.delete_documents_by_ids(ids=ids)
9 +
10 + result = self.agent.read_prompt("fw.memories_deleted.md", memory_count=dels)
11 + return Response(message=result, break_loop=False)
\ No newline at end of file
python/tools/memory_forget.py new
+13
@@ -0,0 +1,13 @@
1 +from python.helpers.memory import Memory
2 +from python.helpers.tool import Tool, Response
3 +
4 +DEFAULT_THRESHOLD = 0.75
5 +
6 +class MemoryForget(Tool):
7 +
8 + async def execute(self, query="", threshold=DEFAULT_THRESHOLD, filter="", **kwargs):
9 + db = await Memory.get(self.agent)
10 + dels = await db.delete_documents_by_query(query=query, threshold=threshold, filter=filter)
11 +
12 + result = self.agent.read_prompt("fw.memories_deleted.md", memory_count=len(dels))
13 + return Response(message=result, break_loop=False)
\ No newline at end of file
python/tools/memory_load.py new
+19
@@ -0,0 +1,19 @@
1 +from python.helpers.memory import Memory
2 +from python.helpers.tool import Tool, Response
3 +
4 +DEFAULT_THRESHOLD = 0.6
5 +DEFAULT_LIMIT = 10
6 +
7 +class MemoryLoad(Tool):
8 +
9 + async def execute(self, query="", threshold=DEFAULT_THRESHOLD, limit=DEFAULT_LIMIT, filter="", **kwargs):
10 + db = await Memory.get(self.agent)
11 + docs = await db.search_similarity_threshold(query=query, limit=limit, threshold=threshold, filter=filter)
12 +
13 + if len(docs) == 0:
14 + result = self.agent.read_prompt("fw.memories_not_found.md", query=query)
15 + else:
16 + text = "\n\n".join(Memory.format_docs_plain(docs))
17 + result = str(text)
18 +
19 + return Response(message=result, break_loop=False)
\ No newline at end of file
python/tools/memory_save.py new
+20
@@ -0,0 +1,20 @@
1 +from python.helpers.memory import Memory
2 +from python.helpers.tool import Tool, Response
3 +
4 +DEFAULT_THRESHOLD = 0.5
5 +DEFAULT_LIMIT = 5
6 +
7 +class MemorySave(Tool):
8 +
9 + async def execute(self, text="", area="", **kwargs):
10 +
11 + if not area:
12 + area = Memory.Area.MAIN.value
13 +
14 + metadata = {"area": area, **kwargs}
15 +
16 + db = await Memory.get(self.agent)
17 + id = db.insert_text(text, metadata)
18 +
19 + result = self.agent.read_prompt("fw.memory_saved.md", memory_id=id)
20 + return Response(message=result, break_loop=False)
python/tools/memory_tool.py deleted
-75
@@ -1,75 +0,0 @@
1 -import re
2 -from agent import Agent
3 -from python.helpers.vector_db import get_or_create_db
4 -import os
5 -from python.helpers.tool import Tool, Response
6 -from python.helpers.print_style import PrintStyle
7 -from python.helpers.errors import handle_error
8 -from python.helpers import files
9 -
10 -class Memory(Tool):
11 - async def execute(self,**kwargs):
12 - result=""
13 -
14 - area = kwargs.get("area", "manual") # when called by agent, it will always be manual
15 -
16 - try:
17 - if "query" in kwargs:
18 - threshold = float(kwargs.get("threshold", 0.1))
19 - count = int(kwargs.get("count", 5))
20 - result = search(self.agent, kwargs["query"], count, threshold)
21 - elif "memorize" in kwargs:
22 - result = save(self.agent, kwargs["memorize"])
23 - elif "forget" in kwargs:
24 - result = forget(self.agent, kwargs["forget"])
25 - # elif "delete" in kwargs
26 - result = delete(self.agent, kwargs["delete"])
27 - except Exception as e:
28 - handle_error(e)
29 - # hint about embedding change with existing database
30 - PrintStyle.hint("If you changed your embedding model, you will need to remove contents of /memory directory.")
31 - self.agent.context.log.log(type="hint", content="If you changed your embedding model, you will need to remove contents of /memory directory.")
32 - raise
33 -
34 - # result = process_query(self.agent, self.args["memory"],self.args["action"], result_count=self.agent.config.auto_memory_count)
35 - return Response(message=result, break_loop=False)
36 -
37 -def search(agent:Agent, query:str, count:int=5, threshold:float=0.1):
38 - db = get_db(agent)
39 - # docs = db.search_similarity(query,count) # type: ignore
40 - docs = db.search_similarity_threshold(query,count,threshold) # type: ignore
41 - if len(docs)==0: return agent.read_prompt("fw.memories_not_found.md", query=query)
42 - else: return str(docs)
43 -
44 -def save(agent:Agent, text:str):
45 - db = get_db(agent)
46 - id = db.insert_text(text) # type: ignore
47 - return agent.read_prompt("fw.memory_saved.md", memory_id=id)
48 -
49 -def delete(agent:Agent, ids_str:str):
50 - db = get_db(agent)
51 - ids = extract_guids(ids_str)
52 - deleted = db.delete_documents_by_ids(ids) # type: ignore
53 - return agent.read_prompt("fw.memories_deleted.md", memory_count=deleted)
54 -
55 -def forget(agent:Agent, query:str):
56 - db = get_db(agent)
57 - deleted = db.delete_documents_by_query(query) # type: ignore
58 - return agent.read_prompt("fw.memories_deleted.md", memory_count=deleted)
59 -
60 -def get_db(agent: Agent):
61 - mem_dir = files.get_abs_path("memory", agent.config.memory_subdir or "default")
62 - kn_dirs = [files.get_abs_path("knowledge", d) for d in agent.config.knowledge_subdirs or []]
63 -
64 - db = get_or_create_db(
65 - agent.context.log,
66 - embeddings_model=agent.config.embeddings_model,
67 - in_memory=False,
68 - memory_dir=mem_dir,
69 - knowledge_dirs=kn_dirs)
70 -
71 - return db
72 -
73 -def extract_guids(text):
74 - pattern = r'\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\b'
75 - return re.findall(pattern, text)
\ No newline at end of file
python/tools/memory_tool.py.txt new
+92
@@ -0,0 +1,92 @@
1 +import re
2 +from agent import Agent
3 +from python.helpers.vector_db import get_or_create_db, Area
4 +import os
5 +from python.helpers.tool import Tool, Response
6 +from python.helpers.print_style import PrintStyle
7 +from python.helpers.errors import handle_error
8 +from python.helpers import files
9 +
10 +DEFAULT_THRESHOLD = 0.5
11 +
12 +
13 +class Memory(Tool):
14 +
15 + async def execute(self, **kwargs):
16 + result = ""
17 +
18 + try:
19 + if "query" in kwargs:
20 + threshold = float(kwargs.get("threshold", DEFAULT_THRESHOLD))
21 + count = int(kwargs.get("limit", 5))
22 + result = search(self.agent, kwargs["query"], count, threshold)
23 + elif "memorize" in kwargs:
24 + meta = {"area": Area.MAIN.value}
25 + result = save(self.agent, kwargs["memorize"])
26 + elif "forget" in kwargs:
27 + result = forget(self.agent, kwargs["forget"])
28 + # elif "delete" in kwargs
29 + result = delete(self.agent, kwargs["delete"])
30 + except Exception as e:
31 + handle_error(e)
32 + # hint about embedding change with existing database
33 + PrintStyle.hint(
34 + "If you changed your embedding model, you will need to remove contents of /memory directory."
35 + )
36 + self.agent.context.log.log(
37 + type="hint",
38 + content="If you changed your embedding model, you will need to remove contents of /memory directory.",
39 + )
40 + raise
41 +
42 + # result = process_query(self.agent, self.args["memory"],self.args["action"], result_count=self.agent.config.auto_memory_count)
43 + return Response(message=result, break_loop=False)
44 +
45 +
46 +def search(
47 + agent: Agent, query: str, count: int = 5, threshold: float = DEFAULT_THRESHOLD
48 +):
49 + db = get_db(agent)
50 + # docs = db.search_similarity(query,count) # type: ignore
51 + docs = db.search_similarity_threshold(query=query, limit=count, threshold=threshold) # type: ignore
52 + if len(docs) == 0:
53 + return agent.read_prompt("fw.memories_not_found.md", query=query)
54 + else:
55 + return str(docs)
56 +
57 +
58 +def save(agent: Agent, text: str, metadata: dict = {}):
59 + db = get_db(agent)
60 + id = db.insert_text(text, metadata) # type: ignore
61 + return agent.read_prompt("fw.memory_saved.md", memory_id=id)
62 +
63 +
64 +def delete(agent: Agent, ids_str: str):
65 + db = get_db(agent)
66 + ids = extract_guids(ids_str)
67 + deleted = db.delete_documents_by_ids(ids) # type: ignore
68 + return agent.read_prompt("fw.memories_deleted.md", memory_count=deleted)
69 +
70 +
71 +def forget(agent: Agent, query: str):
72 + db = get_db(agent)
73 + deleted = db.delete_documents_by_query(query) # type: ignore
74 + return agent.read_prompt("fw.memories_deleted.md", memory_count=deleted)
75 +
76 +
77 +def get_db(agent: Agent):
78 + mem_dir = files.get_abs_path("memory", agent.config.memory_subdir or "default")
79 + kn_dirs = [
80 + files.get_abs_path("knowledge", d) for d in agent.config.knowledge_subdirs or []
81 + ]
82 +
83 + db = get_or_create_db(
84 + agent=agent,
85 + )
86 +
87 + return db
88 +
89 +
90 +def extract_guids(text):
91 + pattern = r"\b[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}\b"
92 + return re.findall(pattern, text)
python/tools/unknown.py
+10 -5
@@ -1,10 +1,15 @@
1 from python.helpers.tool import Tool, Response
2 +from python.extensions.message_loop_prompts._10_tool_instructions import (
3 + concat_tool_prompts,
4 +)
5 +
6
7 class Unknown(Tool):
8 async def execute(self, **kwargs):
9 + tools = concat_tool_prompts(self.agent)
10 return Response(
6 - message=self.agent.read_prompt("fw.tool_not_found.md",
7 - tool_name=self.name,
8 - tools_prompt=self.agent.read_prompt("agent.tools.md")),
9 - break_loop=False)
10 -
11 + message=self.agent.read_prompt(
12 + "fw.tool_not_found.md", tool_name=self.name, tools_prompt=tools
13 + ),
14 + break_loop=False,
15 + )
requirements.txt
-1
@@ -12,7 +12,6 @@ langchain_mistralai==0.1.8
12 webcolors==24.6.0
13 sentence-transformers==3.0.1
14 docker==7.1.0
15 -pandas==2.2.3
15 paramiko==3.4.0
16 duckduckgo_search==6.1.12
17 inputimeout==1.0.4
webui/index.css
+20 -19
@@ -15,7 +15,7 @@
15 --color-input-focus-dark: #1b1b1b;
16
17 /* Light mode */
18 - --color-background-light: #F5F7FA;
18 + --color-background-light: #e8e9e9;
19 --color-text-light: #333333;
20 --color-primary-light: #273b4d;
21 --color-secondary-light: #e8eaf6;
@@ -312,6 +312,7 @@ h4 {
312 .message-user {
313 background-color: #4a4a4a;
314 border-bottom-right-radius: var(--spacing-xs);
315 + text-align: end;
316 }
317
318 .message-ai {
@@ -345,15 +346,15 @@ h4 {
346
347 /* Update message types for dark mode */
348 .message-default { background-color: #1A242F; color: #E0E0E0; }
348 -.message-agent { background-color: #2C3E50; color: #E0E0E0; }
349 -.message-agent-response { background-color: #002b54; color: #E0E0E0; }
350 -.message-agent-delegation { background-color: #00695C; color: #E0E0E0; }
351 -.message-tool { background-color: #5D4C6A; color: #E0E0E0; }
352 -.message-code-exe { background-color: #3a147c; color: #E0E0E0; }
349 +.message-agent { background-color: #34506b; color: #E0E0E0; }
350 +.message-agent-response { background-color: #1f3c1e; color: #E0E0E0; }
351 +.message-agent-delegation { background-color: #12685e; color: #E0E0E0; }
352 +.message-tool { background-color: #2a4170; color: #E0E0E0; }
353 +.message-code-exe { background-color: #4b3a69; color: #E0E0E0; }
354 .message-info { background-color: var(--color-panel); color: #E0E0E0; }
355 .message-util { background-color: #23211a; color: #E0E0E0; display:none }
355 -.message-warning { background-color: #c2771b; color: #E0E0E0; }
356 -.message-error { background-color: #ab1313; color: #E0E0E0; }
356 +.message-warning { background-color: #bc8036; color: #E0E0E0; }
357 +.message-error { background-color: #af2222; color: #E0E0E0; }
358
359 /* Agent and AI Info */
360 .agent-start {
@@ -615,18 +616,18 @@ input:checked + .slider:before {
616 --color-input-focus: var(--color-input-focus-light);
617 }
618
618 -.light-mode .message-default { background-color: #E3F2FD; color: #1A242F; }
619 -.light-mode .message-agent { background-color: #e9ebf3; color: #2C3E50; }
620 -.light-mode .message-agent-response { background-color: #E1E6F2; color: #002b54; }
621 -.light-mode .message-agent-delegation { background-color: #E0F2F1; color: #00695C; }
622 -.light-mode .message-tool { background-color: #EDE7F6; color: #5D4C6A; }
623 -.light-mode .message-code-exe { background-color: #FCE4EC; color: #3a147c; }
624 -.light-mode .message-info { background-color: #E8EAF6; color: #2C3E50; }
625 -.light-mode .message-util { background-color: #e8eaf6d6; color: #353c43; }
626 -.light-mode .message-warning { background-color: #FFF3E0; color: #c2771b; }
627 -.light-mode .message-error { background-color: #FFEBEE; color: #ab1313; }
619 +.light-mode .message-default { background-color: #ffffff; color: #1A242F; }
620 +.light-mode .message-agent { background-color: #ffffff; color: #356ca3; }
621 +.light-mode .message-agent-response { background-color: #ffffff; color: #188216; }
622 +.light-mode .message-agent-delegation { background-color: #ffffff; color: #12685e; }
623 +.light-mode .message-tool { background-color: #ffffff; color: #1c3c88; }
624 +.light-mode .message-code-exe { background-color: #ffffff; color: #6c43b0; }
625 +.light-mode .message-info { background-color: #ffffff; color: #3f3f3f; }
626 +.light-mode .message-util { background-color: #ffffff; color: #5b5540; }
627 +.light-mode .message-warning { background-color: #ffffff; color: #8f4800; }
628 +.light-mode .message-error { background-color: #ffffff; color: #8f1010; }
629 .light-mode .message-user {
629 - background-color: #eaeaea;
630 + background-color: #ffffff;
631 color: #4e4e4e;
632 }
633
webui/index.html
+4 -4
@@ -70,7 +70,7 @@
70 <li x-data="{ autoScroll: true }">
71 <span>Autoscroll</span>
72 <label class="switch">
73 - <input type="checkbox" x-model="autoScroll"
73 + <input id="auto-scroll-switch" type="checkbox" x-model="autoScroll"
74 x-effect="window.safeCall('toggleAutoScroll',autoScroll)">
75 <span class="slider"></span>
76 </label>
@@ -123,14 +123,14 @@
123 <!--Chat-->
124 <div id="chat-history">
125 </div>
126 - <div id="progress-bar-box">
127 - <h4 id="progress-bar-h"><span id="progress-bar-i">|></span><span id="progress-bar"></span></h4>
128 - </div>
126 <div id="toast" class="toast">
127 <div class="toast__message"></div>
128 <button class="toast__copy">Copy</button>
129 <button class="toast__close">Close</button>
130 </div>
131 + <div id="progress-bar-box">
132 + <h4 id="progress-bar-h"><span id="progress-bar-i">|></span><span id="progress-bar"></span></h4>
133 + </div>
134 <div id="input-section" x-data="{ paused: false }">
135 <textarea id="chat-input" placeholder="Type your message here..." rows="1"></textarea>
136 <button class="chat-button" id="send-button" aria-label="Send message">
webui/index.js
+18
@@ -11,6 +11,7 @@ const statusSection = document.getElementById('status-section');
11 const chatsSection = document.getElementById('chats-section');
12 const scrollbarThumb = document.querySelector('#chat-history::-webkit-scrollbar-thumb');
13 const progressBar = document.getElementById('progress-bar');
14 +const autoScrollSwitch = document.getElementById('auto-scroll-switch');
15
16
17
@@ -395,6 +396,23 @@ function toast(text, type = 'info') {
396 }, 10000);
397 }
398
399 +function scrollChanged(isAtBottom) {
400 + const inputAS = Alpine.$data(autoScrollSwitch);
401 + inputAS.autoScroll = isAtBottom
402 + // autoScrollSwitch.checked = isAtBottom
403 + console.log(isAtBottom)
404 +}
405 +
406 +chatHistory.addEventListener('scroll', function () {
407 + // const toleranceEm = 1; // Tolerance in em units
408 + // const tolerancePx = toleranceEm * parseFloat(getComputedStyle(document.documentElement).fontSize); // Convert em to pixels
409 + const tolerancePx = 50;
410 + const chatHistory = document.getElementById('chat-history');
411 + const isAtBottom = (chatHistory.scrollHeight - chatHistory.scrollTop) <= (chatHistory.clientHeight + tolerancePx);
412 +
413 + scrollChanged(isAtBottom);
414 +});
415 +
416 chatInput.addEventListener('input', adjustTextareaHeight);
417
418 setInterval(poll, 250);