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);