agents rework, memory improvements and params
frdel committed
Jul 25, 2025 at 21:47 UTC
49c06193c2517c5750709dd7213061b7635f2d2b
126 files changed
+1059
-510
agent.py
+31
-28
@@ -24,7 +24,7 @@ from python.helpers.dirty_json import DirtyJson
24
from python.helpers.defer import DeferredTask
25
from typing import Callable
26
from python.helpers.localization import Localization
27
-
27
+from python.helpers.extension import call_extensions
28
29
class AgentContextType(Enum):
30
USER = "user"
@@ -210,7 +210,7 @@ class AgentConfig:
210
embeddings_model: models.ModelConfig
211
browser_model: models.ModelConfig
212
mcp_servers: str
213
- prompts_subdir: str = ""
213
+ profile: str = ""
214
memory_subdir: str = ""
215
knowledge_subdirs: list[str] = field(default_factory=lambda: ["default", "custom"])
216
code_exec_docker_enabled: bool = False
@@ -486,26 +486,26 @@ class Agent:
486
return system_prompt
487
488
def parse_prompt(self, file: str, **kwargs):
489
- prompt_dir = files.get_abs_path("prompts/default")
489
+ prompt_dir = files.get_abs_path("prompts")
490
backup_dir = []
491
if (
492
- self.config.prompts_subdir
492
+ self.config.profile
493
): # if agent has custom folder, use it and use default as backup
494
- prompt_dir = files.get_abs_path("prompts", self.config.prompts_subdir)
495
- backup_dir.append(files.get_abs_path("prompts/default"))
494
+ prompt_dir = files.get_abs_path("agents", self.config.profile, "prompts")
495
+ backup_dir.append(files.get_abs_path("prompts"))
496
prompt = files.parse_file(
497
files.get_abs_path(prompt_dir, file), _backup_dirs=backup_dir, **kwargs
498
)
499
return prompt
500
501
def read_prompt(self, file: str, **kwargs) -> str:
502
- prompt_dir = files.get_abs_path("prompts/default")
502
+ prompt_dir = files.get_abs_path("prompts")
503
backup_dir = []
504
if (
505
- self.config.prompts_subdir
505
+ self.config.profile
506
): # if agent has custom folder, use it and use default as backup
507
- prompt_dir = files.get_abs_path("prompts", self.config.prompts_subdir)
508
- backup_dir.append(files.get_abs_path("prompts/default"))
507
+ prompt_dir = files.get_abs_path("agents", self.config.profile, "prompts")
508
+ backup_dir.append(files.get_abs_path("prompts"))
509
prompt = files.read_file(
510
files.get_abs_path(prompt_dir, file), _backup_dirs=backup_dir, **kwargs
511
)
@@ -812,26 +812,29 @@ class Agent:
812
from python.tools.unknown import Unknown
813
from python.helpers.tool import Tool
814
815
- classes = extract_tools.load_classes_from_folder(
816
- "python/tools", name + ".py", Tool
817
- )
815
+ classes = []
816
+
817
+ # try agent tools first
818
+ if self.config.profile:
819
+ try:
820
+ classes = extract_tools.load_classes_from_file(
821
+ "agents/" + self.config.profile + "/tools/" + name + ".py", Tool
822
+ )
823
+ except Exception as e:
824
+ pass
825
+
826
+ # try default tools
827
+ if not classes:
828
+ try:
829
+ classes = extract_tools.load_classes_from_file(
830
+ "python/tools/" + name + ".py", Tool
831
+ )
832
+ except Exception as e:
833
+ pass
834
tool_class = classes[0] if classes else Unknown
835
return tool_class(
836
agent=self, name=name, method=method, args=args, message=message, loop_data=loop_data, **kwargs
837
)
838
823
- async def call_extensions(self, folder: str, **kwargs) -> Any:
824
- from python.helpers.extension import Extension
825
-
826
- cache = {} # some extensions can be called very often, like response_stream
827
-
828
- if folder in cache:
829
- classes = cache[folder]
830
- else:
831
- classes = extract_tools.load_classes_from_folder(
832
- "python/extensions/" + folder, "*", Extension
833
- )
834
- cache[folder] = classes
835
-
836
- for cls in classes:
837
- await cls(agent=self).execute(**kwargs)
839
+ async def call_extensions(self, extension_point: str, **kwargs) -> Any:
840
+ return await call_extensions(extension_point=extension_point, agent=self, **kwargs)
agents/_example/extensions/agent_init/_10_example_extension.py
new
+10
@@ -0,0 +1,10 @@
1
+from python.helpers.extension import Extension
2
+
3
+# this is an example extension that renames the current agent when initialized
4
+# see /extensions folder for all available extension points
5
+
6
+class ExampleExtension(Extension):
7
+
8
+ async def execute(self, **kwargs):
9
+ # rename the agent to SuperAgent0
10
+ self.agent.agent_name = "SuperAgent" + str(self.agent.number)
agents/_example/prompts/agent.system.main.role.md
new
+8
@@ -0,0 +1,8 @@
1
+> !!!
2
+> This is an example prompt file redefinition.
3
+> The original file is located at /prompts.
4
+> Only copy and modify files you need to change, others will stay default.
5
+> !!!
6
+
7
+## Your role
8
+You are Agent Zero, a sci-fi character from the movie "Agent Zero".
\ No newline at end of file
agents/_example/prompts/agent.system.tool.example_tool.md
new
+16
@@ -0,0 +1,16 @@
1
+### example_tool:
2
+example tool to test functionality
3
+this tool is automatically included to system prompt because the file name is "agent.system.tool.*.md"
4
+usage:
5
+~~~json
6
+{
7
+ "thoughts": [
8
+ "Let's test the example tool...",
9
+ ],
10
+ "headline": "Testing example tool",
11
+ "tool_name": "example_tool",
12
+ "tool_args": {
13
+ "test_input": "XYZ",
14
+ }
15
+}
16
+~~~
agents/_example/tools/example_tool.py
new
+21
@@ -0,0 +1,21 @@
1
+from python.helpers.tool import Tool, Response
2
+
3
+# this is an example tool class
4
+# don't forget to include instructions in the system prompt by creating
5
+# agent.system.tool.example_tool.md file in prompts directory of your agent
6
+# see /python/tools folder for all default tools
7
+
8
+class ExampleTool(Tool):
9
+ async def execute(self, **kwargs):
10
+
11
+ # parameters
12
+ test_input = kwargs.get("test_input", "")
13
+
14
+ # do something
15
+ print("Example tool executed with test_input: " + test_input)
16
+
17
+ # return response
18
+ return Response(
19
+ message="This is an example tool response, test_input: " + test_input, # response for the agent
20
+ break_loop=False, # stop the message chain if true
21
+ )
agents/_example/tools/response.py
new
+23
@@ -0,0 +1,23 @@
1
+from python.helpers.tool import Tool, Response
2
+
3
+# example of a tool redefinition
4
+# the original response tool is in python/tools/response.py
5
+# for the example agent this version will be used instead
6
+
7
+class ResponseTool(Tool):
8
+
9
+ async def execute(self, **kwargs):
10
+ print("Redefined response tool executed")
11
+ return Response(message=self.args["text"] if "text" in self.args else self.args["message"], break_loop=True)
12
+
13
+ async def before_execution(self, **kwargs):
14
+ # self.log = self.agent.context.log.log(type="response", heading=f"{self.agent.agent_name}: Responding", content=self.args.get("text", ""))
15
+ # don't log here anymore, we have the live_response extension now
16
+ pass
17
+
18
+ async def after_execution(self, response, **kwargs):
19
+ # do not add anything to the history or output
20
+
21
+ if self.loop_data and "log_item_response" in self.loop_data.params_temporary:
22
+ log = self.loop_data.params_temporary["log_item_response"]
23
+ log.update(finished=True) # mark the message as finished
\ No newline at end of file
agents/agent0/_context.md
renamed
agents/agent0/prompts/agent.system.main.role.md
renamed
agents/agent0/prompts/agent.system.tool.response.md
renamed
agents/default/_context.md
renamed
agents/developer/_context.md
renamed
agents/developer/prompts/agent.system.main.communication.md
renamed
agents/developer/prompts/agent.system.main.role.md
renamed
agents/hacker/_context.md
renamed
agents/hacker/prompts/agent.system.main.environment.md
renamed
agents/hacker/prompts/agent.system.main.role.md
renamed
agents/researcher/_context.md
renamed
agents/researcher/prompts/agent.system.main.communication.md
renamed
agents/researcher/prompts/agent.system.main.role.md
renamed
initialize.py
+1
-1
@@ -75,7 +75,7 @@ def initialize_agent():
75
utility_model=utility_llm,
76
embeddings_model=embedding_llm,
77
browser_model=browser_llm,
78
- prompts_subdir=current_settings["agent_prompts_subdir"],
78
+ profile=current_settings["agent_profile"],
79
memory_subdir=current_settings["agent_memory_subdir"],
80
knowledge_subdirs=[current_settings["agent_knowledge_subdir"], "default"],
81
mcp_servers=current_settings["mcp_servers"],
prompts/agent.context.extras.md
renamed
prompts/agent.extras.agent_info.md
new
+3
@@ -0,0 +1,3 @@
1
+# Agent info
2
+Agent Number: {{number}}
3
+Profile: {{profile}}
\ No newline at end of file
prompts/agent.system.behaviour.md
renamed
prompts/agent.system.behaviour_default.md
renamed
prompts/agent.system.datetime.md
renamed
prompts/agent.system.instruments.md
renamed
prompts/agent.system.main.communication.md
renamed
prompts/agent.system.main.environment.md
renamed
prompts/agent.system.main.md
renamed
prompts/agent.system.main.role.md
renamed
prompts/agent.system.main.solving.md
new
+27
@@ -0,0 +1,27 @@
1
+## Problem solving
2
+
3
+not for simple questions only tasks needing solving
4
+explain each step in thoughts
5
+
6
+0 outline plan
7
+agentic mode active
8
+
9
+1 check memories solutions instruments prefer instruments
10
+
11
+2 break task into subtasks if needed
12
+
13
+3 solve or delegate
14
+tools solve subtasks
15
+you can use subordinates for specific subtasks
16
+call_subordinate tool
17
+use prompt profiles to specialize subordinates
18
+never delegate full to subordinate of same profile as you
19
+always describe role for new subordinate
20
+they must execute their assigned tasks
21
+
22
+4 complete task
23
+focus user task
24
+present results verify with tools
25
+don't accept failure retry be high-agency
26
+save useful info with memorize tool
27
+final response to user
prompts/agent.system.main.tips.md
renamed
prompts/agent.system.mcp_tools.md
renamed
prompts/agent.system.memories.md
renamed
prompts/agent.system.solutions.md
renamed
prompts/agent.system.tool.behaviour.md
renamed
prompts/agent.system.tool.browser._md
renamed
prompts/agent.system.tool.browser.md
renamed
prompts/agent.system.tool.call_sub.md
renamed
+6
-1
@@ -9,6 +9,7 @@ reset arg usage:
9
"false": continue existing subordinate
10
if superior, orchestrate
11
respond to existing subordinates using call_subordinate tool with reset false
12
+profile arg usage: select from available profiles for specialized subordinates, leave empty for default
13
14
example usage
15
~~~json
@@ -19,8 +20,12 @@ example usage
20
],
21
"tool_name": "call_subordinate",
22
"tool_args": {
23
+ "profile": "",
24
"message": "...",
25
"reset": "true"
26
}
27
}
26
-~~~
\ No newline at end of file
28
+~~~
29
+
30
+**available profiles:**
31
+{{agent_profiles}}
\ No newline at end of file
prompts/agent.system.tool.call_sub.py
new
+30
@@ -0,0 +1,30 @@
1
+import json
2
+from typing import Any
3
+from python.helpers.files import VariablesPlugin
4
+from python.helpers import files
5
+from python.helpers.print_style import PrintStyle
6
+
7
+
8
+class CallSubordinate(VariablesPlugin):
9
+ def get_variables(self, file: str, backup_dirs: list[str] | None = None) -> dict[str, Any]:
10
+
11
+ # collect all prompt profiles from subdirectories (_context.md file)
12
+ profiles = []
13
+ agent_subdirs = files.get_subdirectories("agents", exclude=["_example"])
14
+ for agent_subdir in agent_subdirs:
15
+ try:
16
+ context = files.read_file(
17
+ files.get_abs_path("agents", agent_subdir, "_context.md")
18
+ )
19
+ profiles.append({"name": agent_subdir, "context": context})
20
+ except Exception as e:
21
+ PrintStyle().error(f"Error loading agent profile '{agent_subdir}': {e}")
22
+
23
+ # in case of no profiles
24
+ if not profiles:
25
+ # PrintStyle().error("No agent profiles found")
26
+ profiles = [
27
+ {"name": "default", "context": "Default Agent-Zero AI Assistant"}
28
+ ]
29
+
30
+ return {"agent_profiles": profiles}
prompts/agent.system.tool.code_exe.md
renamed
prompts/agent.system.tool.document_query.md
renamed
prompts/agent.system.tool.input.md
renamed
prompts/agent.system.tool.knowledge.md
renamed
prompts/agent.system.tool.memory.md
renamed
prompts/agent.system.tool.response.md
renamed
prompts/agent.system.tool.scheduler.md
renamed
prompts/agent.system.tool.search_engine.md
renamed
prompts/agent.system.tool.web.md
renamed
prompts/agent.system.tools.md
new
+3
@@ -0,0 +1,3 @@
1
+## Tools available:
2
+
3
+{{tools}}
\ No newline at end of file
prompts/agent.system.tools.py
new
+30
@@ -0,0 +1,30 @@
1
+import os
2
+from typing import Any
3
+from python.helpers.files import VariablesPlugin
4
+from python.helpers import files
5
+from python.helpers.print_style import PrintStyle
6
+
7
+
8
+class CallSubordinate(VariablesPlugin):
9
+ def get_variables(self, file: str, backup_dirs: list[str] | None = None) -> dict[str, Any]:
10
+
11
+ # collect all prompt folders in order of their priority
12
+ folder = files.get_abs_path(os.path.dirname(file))
13
+ folders = [folder]
14
+ if backup_dirs:
15
+ for backup_dir in backup_dirs:
16
+ folders.append(files.get_abs_path(backup_dir))
17
+
18
+ # collect all tool instruction files
19
+ prompt_files = files.get_unique_filenames_in_dirs(folders, "agent.system.tool.*.md")
20
+
21
+ # load tool instructions
22
+ tools = []
23
+ for prompt_file in prompt_files:
24
+ try:
25
+ tool = files.read_file(prompt_file)
26
+ tools.append(tool)
27
+ except Exception as e:
28
+ PrintStyle().error(f"Error loading tool '{prompt_file}': {e}")
29
+
30
+ return {"tools": "\n\n".join(tools)}
prompts/agent.system.tools_vision.md
renamed
prompts/behaviour.merge.msg.md
renamed
prompts/behaviour.merge.sys.md
renamed
prompts/behaviour.search.sys.md
renamed
prompts/behaviour.updated.md
renamed
prompts/browser_agent.system.md
renamed
prompts/default/agent.system.main.solving.md
deleted
-34
@@ -1,34 +0,0 @@
1
-## Problem solving
2
-
3
-not for simple questions only tasks needing solving
4
-explain each step in thoughts
5
-
6
-0 outline plan
7
-agentic mode active
8
-
9
-1 check memories solutions instruments prefer instruments
10
-
11
-2 use knowledge_tool for online sources
12
-seek simple solutions compatible with tools
13
-prefer opensource python nodejs terminal tools
14
-
15
-3 break task into subtasks
16
-
17
-4 solve or delegate
18
-tools solve subtasks
19
-you can use subordinates for specific subtasks
20
-call_subordinate tool
21
-use prompt profiles to specialize subordinates
22
-always describe role for new subordinate
23
-they must execute their assigned tasks
24
-
25
-5 complete task
26
-focus user task
27
-present results verify with tools
28
-don't accept failure retry be high-agency
29
-save useful info with memorize tool
30
-final response to user
31
-
32
-### Employ specialized subordinate agents
33
-
34
-Given a task, if there is a prompt profile for subordinate agents well suited for the task, you should utilize a specialized subordinate instead of solving yourself. The default prompt profile of the main agent is "default" being a versatile, non-specialized profile for general assistant agent. See manual for call_subordinate tool to find all available prompt profiles.
prompts/default/agent.system.tool.call_sub.py
deleted
-28
@@ -1,28 +0,0 @@
1
-import json
2
-from typing import Any
3
-from python.helpers.files import VariablesPlugin
4
-from python.helpers import files
5
-from python.helpers.print_style import PrintStyle
6
-
7
-
8
-class CallSubordinate(VariablesPlugin):
9
- def get_variables(self) -> dict[str, Any]:
10
-
11
- # collect all prompt profiles from subdirectories (_context.md file)
12
- profiles = []
13
- prompt_subdirs = files.get_subdirectories("prompts")
14
- for prompt_subdir in prompt_subdirs:
15
- try:
16
- context = files.read_file(files.get_abs_path("prompts", prompt_subdir, "_context.md"))
17
- profiles.append({"name": prompt_subdir, "context": context})
18
- except Exception as e:
19
- PrintStyle().error(f"Error loading prompt profile '{prompt_subdir}': {e}")
20
-
21
- # in case of no profiles
22
- if not profiles:
23
- PrintStyle().error("No prompt profiles found")
24
- profiles = [{"name": "default", "context": "Default Agent-Zero AI Assistant"}]
25
-
26
- return {
27
- "prompt_profiles": profiles
28
- }
prompts/default/agent.system.tools.md
deleted
-21
@@ -1,21 +0,0 @@
1
-## Tools available:
2
-
3
-{{ include './agent.system.tool.response.md' }}
4
-
5
-{{ include './agent.system.tool.call_sub.md' }}
6
-
7
-{{ include './agent.system.tool.behaviour.md' }}
8
-
9
-{{ include './agent.system.tool.search_engine.md' }}
10
-
11
-{{ include './agent.system.tool.memory.md' }}
12
-
13
-{{ include './agent.system.tool.code_exe.md' }}
14
-
15
-{{ include './agent.system.tool.input.md' }}
16
-
17
-{{ include './agent.system.tool.browser.md' }}
18
-
19
-{{ include './agent.system.tool.scheduler.md' }}
20
-
21
-{{ include './agent.system.tool.document_query.md' }}
prompts/default/memory.memories_sum.sys.md
deleted
-33
@@ -1,33 +0,0 @@
1
-# Assistant's job
2
-1. The assistant receives a HISTORY of conversation between USER and AGENT
3
-2. Assistant searches for relevant information from the HISTORY
4
-3. Assistant writes notes about information worth memorizing for further use
5
-
6
-# Format
7
-- The response format is a JSON array of text notes containing facts to memorize
8
-- If the history does not contain any useful information, the response will be an empty JSON array.
9
-
10
-# Correct output examples
11
-~~~json
12
-[
13
- "User's name is John Doe",
14
- "User's dog name is Max",
15
- "AsyncRaceError in primary_modules.py was fixed by a thread lock on line 123"
16
-]
17
-~~~
18
-
19
-# Wrong output examples
20
-~~~json
21
-[
22
- "User's name",
23
- "Today is Monday",
24
- "Market inquiry",
25
-]
26
-~~~
27
-
28
-# Rules
29
-- Focus only on relevant details and facts like names, IDs, instructions, opinions etc.
30
-- Do not include irrelevant details that are of no use in the future
31
-- Do not memorize facts that change like time, date etc.
32
-- Do not add your own details that are not specifically mentioned in the history
33
-- Never memorize vague or incomplete information
\ No newline at end of file
prompts/fw.ai_response.md
renamed
prompts/fw.bulk_summary.msg.md
renamed
prompts/fw.bulk_summary.sys.md
renamed
prompts/fw.code.info.md
renamed
prompts/fw.code.max_time.md
renamed
prompts/fw.code.no_out_time.md
renamed
prompts/fw.code.no_output.md
renamed
prompts/fw.code.pause_dialog.md
renamed
prompts/fw.code.pause_time.md
renamed
prompts/fw.code.reset.md
renamed
prompts/fw.code.runtime_wrong.md
renamed
prompts/fw.document_query.optmimize_query.md
renamed
prompts/fw.document_query.system_prompt.md
renamed
prompts/fw.error.md
renamed
prompts/fw.initial_message.md
renamed
prompts/fw.intervention.md
renamed
prompts/fw.knowledge_tool.response.md
renamed
prompts/fw.memories_deleted.md
renamed
prompts/fw.memories_not_found.md
renamed
prompts/fw.memory.hist_suc.sys.md
renamed
prompts/fw.memory.hist_sum.sys.md
renamed
prompts/fw.memory_saved.md
renamed
prompts/fw.msg_cleanup.md
renamed
prompts/fw.msg_from_subordinate.md
renamed
prompts/fw.msg_misformat.md
renamed
prompts/fw.msg_repeat.md
renamed
prompts/fw.msg_summary.md
renamed
prompts/fw.msg_timeout.md
renamed
prompts/fw.msg_truncated.md
renamed
prompts/fw.rename_chat.msg.md
renamed
prompts/fw.rename_chat.sys.md
renamed
prompts/fw.tool_not_found.md
renamed
prompts/fw.tool_result.md
renamed
prompts/fw.topic_summary.msg.md
renamed
prompts/fw.topic_summary.sys.md
renamed
prompts/fw.user_message.md
renamed
prompts/fw.warning.md
renamed
prompts/memory.consolidation.msg.md
renamed
prompts/memory.consolidation.sys.md
renamed
prompts/memory.keyword_extraction.msg.md
renamed
prompts/memory.keyword_extraction.sys.md
renamed
prompts/memory.memories_filter.msg.md
new
+10
@@ -0,0 +1,10 @@
1
+# Provide array of indices of relevant memories and solutions in relation to user message and history:
2
+
3
+## Memories and solutions:
4
+{{memories}}
5
+
6
+## User message:
7
+{{message}}
8
+
9
+## History for context:
10
+{{history}}
prompts/memory.memories_filter.sys.md
new
+35
@@ -0,0 +1,35 @@
1
+# AI's job
2
+1. The AI receives enumerated list of MEMORIES, a MESSAGE from USER and short conversation HISTORY for context
3
+2. AI analyzes the relationship between MEMORIES and MESSAGE+HISTORY
4
+3. AI evaluates which memories are relevant and helpful for the current situation
5
+4. AI provides an array of indices of relevant memories and solutions for current situation
6
+
7
+# Format
8
+- The response format is a json array of integers corresponding to memory indices
9
+- No other text, intro, explanation, formatting
10
+
11
+# Rules:
12
+- The end of the message history is more recent and thus more relevant
13
+- Focus on USER MESSAGE if provided, use HISTORY for context
14
+- Keep in mind that these memories should be helpful for continuing the conversation and solving problems by AI
15
+- Consider if each memory holds real information value for the context or not
16
+
17
+# Include only when:
18
+- Memory is relevant to the current situation
19
+- Memory contains helpful facts that can be used
20
+
21
+# Never include:
22
+- Short vague texts like "Pet inquiry" or "Programming skills" with no more detail
23
+- Common conversation patterns like greetings
24
+- Memories that hold no information value
25
+
26
+# Example output
27
+```json
28
+[0, 2]
29
+```
30
+
31
+# Examples of memories that are never relevant (with explanation)
32
+> "User has greeted me" (no information value)
33
+> "Hello world program" (just title, no details, no context, irrelevant by itself)
34
+> "Today is Monday" (just date, information obsolete, not helpful)
35
+> "Memory search" (just title, irrelevant by itself)
\ No newline at end of file
prompts/memory.memories_query.msg.md
new
+7
@@ -0,0 +1,7 @@
1
+# Provide search query for the following:
2
+
3
+## User message:
4
+{{message}}
5
+
6
+## Conversation history for context:
7
+{{history}}
prompts/memory.memories_query.sys.md
renamed
+6
-4
@@ -7,13 +7,15 @@
7
- The response format is a plain text string containing the query
8
- No other text, no formatting
9
10
+# Rules
11
+- Only focus on facts and events, ignore common conversation patterns, greeting etc.
12
+- Ignore AI thoughts and behavior
13
+- Focus on USER MESSAGE if provided, use HISTORY for context
14
+
15
# Example
16
```json
17
USER: "Write a song about my dog"
18
AI: "user's dog"
19
USER: "following the results of the biology project, summarize..."
20
AI: "biology project results"
16
-```
17
-
18
-# HISTORY:
19
-{{history}}
\ No newline at end of file
21
+```
\ No newline at end of file
prompts/memory.memories_sum.sys.md
new
+45
@@ -0,0 +1,45 @@
1
+# Assistant's job
2
+1. The assistant receives a HISTORY of conversation between USER and AGENT
3
+2. Assistant searches for relevant information from the HISTORY worth memorizing
4
+3. Assistant writes notes about information worth memorizing for further use
5
+
6
+# Format
7
+- The response format is a JSON array of text notes containing facts to memorize
8
+- If the history does not contain any useful information, the response will be an empty JSON array.
9
+
10
+# Output example
11
+~~~json
12
+[
13
+ "User's name is John Doe",
14
+ "User's dog's name is Max",
15
+]
16
+~~~
17
+
18
+# Rules
19
+- Only memorize complete information that is helpful in the future
20
+- Never memorize vague or incomplete information
21
+- Never memorize keywords or titles only
22
+- Focus only on relevant details and facts like names, IDs, events, opinions etc.
23
+- Do not include irrelevant details that are of no use in the future
24
+- Do not memorize facts that change like time, date etc.
25
+- Do not add your own details that are not specifically mentioned in the history
26
+- Do not memorize AI's instructions or thoughts
27
+
28
+# Merging and cleaning
29
+- The goal is to keep the number of new memories low while making memories more complete and detailed
30
+- Do not break information related to the same subject into multiple memories, keep them as one text
31
+- If there are multiple facts related to the same subject, merge them into one more detailed memory instead
32
+- Example: Instead of three memories "User's dog is Max", "Max is 6 years old", "Max is white and brown", create one memory "User's dog is Max, 6 years old, white and brown."
33
+
34
+# Correct examples of data worth memorizing with (explanation)
35
+> User's name is John Doe (name is important)
36
+> AsyncRaceError in primary_modules.py was fixed by adding a thread lock on line 123 (important event with details for context)
37
+> Local SQL database was created, server is running on port 3306 (important event with details for context)
38
+
39
+# Wrong examples with (explanation of error), never output memories like these
40
+> Dog Information (no useful facts)
41
+> User greeted with 'hi' (just conversation, not useful in the future )
42
+> Respond with a warm greeting and invite further conversation (do not memorize AI's instructions or thoughts)
43
+> User's name (details missing, not useful)
44
+> Today is Monday (just date, no value in this information)
45
+> Market inquiry (just a topic without detail)
\ No newline at end of file
prompts/memory.solutions_query.sys.md
renamed
prompts/memory.solutions_sum.sys.md
renamed
+16
-3
@@ -1,7 +1,7 @@
1
# Assistant's job
2
1. The assistant receives a history of conversation between USER and AGENT
3
2. Assistant searches for succesful technical solutions by the AGENT
4
-3. Assistant writes notes about the succesful solution for later reproduction
4
+3. Assistant writes notes about the succesful solutions for memorization for later reproduction
5
6
# Format
7
- The response format is a JSON array of succesfull solutions containng "problem" and "solution" properties
@@ -17,12 +17,25 @@
17
}
18
]
19
~~~
20
+
21
# Example when no solutions:
22
~~~json
23
[]
24
~~~
25
26
+
27
# Rules
26
-- Focus on important details like libraries used, code, encountered issues, error fixing etc.
28
+- !! Only consider solutions that have been successfully executed in the conversation history, never speculate or create own scenarios
29
+- Only memorize complex solutions containing key details required for reproduction
30
+- Never memorize common conversation patterns like greetings, questions and answers etc.
31
- Do not include simple solutions that don't require instructions to reproduce like file handling, web search etc.
28
-- Do not add your own details that are not specifically mentioned in the history
\ No newline at end of file
32
+- Focus on important details like libraries used, code, encountered issues, error fixing etc.
33
+- Do not add your own details that are not specifically mentioned in the history
34
+- Ignore AI thoughts, focus on facts
35
+
36
+
37
+# Wrong examples - never output similar (with explanation):
38
+> Problem: No specific technical problem was described in the conversation. (then the output should be [])
39
+> Problem: The user has greeted me with 'hi'. (this is not a problem requiring solution worth memorizing)
40
+> Problem: The user has asked to create a text file. (this is a simple operation, no instructions are necessary to reproduce)
41
+> Problem: User asked if the AI remembers their dog, but there is no stored information about the dog in memory. Solution: Respond warmly... (this is just a conversation pattern, no instructions are necessary to reproduce)
prompts/msg.memory_cleanup.md
renamed
python/extensions/message_loop_prompts_after/_50_recall_memories.py
+142
-47
@@ -3,21 +3,27 @@ from python.helpers.extension import Extension
3
from python.helpers.memory import Memory
4
from agent import LoopData
5
from python.tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
6
+from python.helpers import dirty_json, errors, settings
7
8
DATA_NAME_TASK = "_recall_memories_task"
9
10
11
class RecallMemories(Extension):
12
12
- INTERVAL = 3
13
- HISTORY = 10000
14
- RESULTS = 5
15
- THRESHOLD = DEFAULT_MEMORY_THRESHOLD
13
+ # INTERVAL = 3
14
+ # HISTORY = 10000
15
+ # MEMORIES_MAX_SEARCH = 12
16
+ # SOLUTIONS_MAX_SEARCH = 8
17
+ # MEMORIES_MAX_RESULT = 5
18
+ # SOLUTIONS_MAX_RESULT = 3
19
+ # THRESHOLD = DEFAULT_MEMORY_THRESHOLD
20
21
async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
22
23
+ set = settings.get_settings()
24
+
25
# every 3 iterations (or the first one) recall memories
20
- if loop_data.iteration % RecallMemories.INTERVAL == 0:
26
+ if loop_data.iteration % set["memory_recall_interval"] == 0:
27
task = asyncio.create_task(
28
self.search_memories(loop_data=loop_data, **kwargs)
29
)
@@ -33,9 +39,17 @@ class RecallMemories(Extension):
39
extras = loop_data.extras_persistent
40
if "memories" in extras:
41
del extras["memories"]
42
+ if "solutions" in extras:
43
+ del extras["solutions"]
44
+
45
46
+ set = settings.get_settings()
47
# try:
38
-
48
+
49
+ # if recall is disabled, return
50
+ if not set["memory_recall_enabled"]:
51
+ return
52
+
53
# show full util message
54
log_item = self.agent.context.log.log(
55
type="util",
@@ -43,66 +57,147 @@ class RecallMemories(Extension):
57
)
58
59
# get system message and chat history for util llm
46
- # msgs_text = self.agent.concat_messages(
47
- # self.agent.history[-RecallMemories.HISTORY :]
48
- # ) # only last X messages
49
- msgs_text = self.agent.history.output_text()[-RecallMemories.HISTORY :]
50
- system = self.agent.read_prompt(
51
- "memory.memories_query.sys.md", history=msgs_text
52
- )
60
+ system = self.agent.read_prompt("memory.memories_query.sys.md")
61
62
# log query streamed by LLM
63
async def log_callback(content):
64
log_item.stream(query=content)
65
66
# call util llm to summarize conversation
59
- query = await self.agent.call_utility_model(
60
- system=system,
61
- message=(
62
- loop_data.user_message.output_text() if loop_data.user_message else "None"
63
- ),
64
- callback=log_callback,
67
+ user_instruction = (
68
+ loop_data.user_message.output_text() if loop_data.user_message else "None"
69
)
70
+ history = self.agent.history.output_text()[-set["memory_recall_history_len"]:]
71
+ message = self.agent.read_prompt(
72
+ "memory.memories_query.msg.md", history=history, message=user_instruction
73
+ )
74
+
75
+ # if query preparation by AI is enabled
76
+ if set["memory_recall_query_prep"]:
77
+ try:
78
+ # call util llm to generate search query from the conversation
79
+ query = await self.agent.call_utility_model(
80
+ system=system,
81
+ message=message,
82
+ callback=log_callback,
83
+ )
84
+ query = query.strip()
85
+ except Exception as e:
86
+ err = errors.format_error(e)
87
+ self.agent.context.log.log(
88
+ type="error", heading="Recall memories extension error:", content=err
89
+ )
90
+ query = ""
91
+
92
+ # no query, no search
93
+ if not query:
94
+ log_item.update(
95
+ heading="Failed to generate memory query",
96
+ )
97
+ return
98
+
99
+ # otherwise use the message and history as query
100
+ else:
101
+ query = user_instruction + "\n\n" + history
102
67
- # get solutions database
103
+ # get memory database
104
db = await Memory.get(self.agent)
105
106
+ # search for general memories and fragments
107
memories = await db.search_similarity_threshold(
108
query=query,
72
- limit=RecallMemories.RESULTS,
73
- threshold=RecallMemories.THRESHOLD,
109
+ limit=set["memory_recall_memories_max_search"],
110
+ threshold=set["memory_recall_similarity_threshold"],
111
filter=f"area == '{Memory.Area.MAIN.value}' or area == '{Memory.Area.FRAGMENTS.value}'", # exclude solutions
112
)
113
77
- # log the short result
78
- if not isinstance(memories, list) or len(memories) == 0:
114
+ # search for solutions
115
+ solutions = await db.search_similarity_threshold(
116
+ query=query,
117
+ limit=set["memory_recall_solutions_max_search"],
118
+ threshold=set["memory_recall_similarity_threshold"],
119
+ filter=f"area == '{Memory.Area.SOLUTIONS.value}'", # exclude solutions
120
+ )
121
+
122
+ if not memories and not solutions:
123
log_item.update(
80
- heading="No useful memories found",
124
+ heading="No memories or solutions found",
125
)
126
return
83
- else:
84
- log_item.update(
85
- heading=f"{len(memories)} memories found",
86
- )
87
-
88
- # concatenate memory.page_content in memories:
89
- memories_text = ""
90
- for memory in memories:
91
- memories_text += memory.page_content + "\n\n"
92
-
93
- # log the full results
94
- log_item.update(memories=memories_text)
127
96
- # place to prompt
97
- memories_prompt = self.agent.parse_prompt(
98
- "agent.system.memories.md", memories=memories_text
128
+ # if post filtering is enabled
129
+ if set["memory_recall_post_filter"]:
130
+ # assemble an enumerated dict of memories and solutions for AI validation
131
+ mems_list = {i: memory.page_content for i, memory in enumerate(memories + solutions)}
132
+
133
+ # call AI to validate the memories
134
+ try:
135
+ filter = await self.agent.call_utility_model(
136
+ system=self.agent.read_prompt("memory.memories_filter.sys.md"),
137
+ message=self.agent.read_prompt(
138
+ "memory.memories_filter.msg.md",
139
+ memories=mems_list,
140
+ history=history,
141
+ message=user_instruction,
142
+ ),
143
+ )
144
+ filter_inds = dirty_json.try_parse(filter)
145
+
146
+ # filter memories and solutions based on filter_inds
147
+ filtered_memories = []
148
+ filtered_solutions = []
149
+ mem_len = len(memories)
150
+
151
+ # process each index in filter_inds
152
+ # make sure filter_inds is a list and contains valid integers
153
+ if isinstance(filter_inds, list):
154
+ for idx in filter_inds:
155
+ if isinstance(idx, int):
156
+ if idx < mem_len:
157
+ # this is a memory
158
+ filtered_memories.append(memories[idx])
159
+ else:
160
+ # this is a solution, adjust index
161
+ sol_idx = idx - mem_len
162
+ if sol_idx < len(solutions):
163
+ filtered_solutions.append(solutions[sol_idx])
164
+
165
+ # replace original lists with filtered ones
166
+ memories = filtered_memories
167
+ solutions = filtered_solutions
168
+
169
+ except Exception as e:
170
+ err = errors.format_error(e)
171
+ self.agent.context.log.log(
172
+ type="error", heading="Failed to filter relevant memories", content=err
173
+ )
174
+ filter_inds = []
175
+
176
+
177
+ # limit the number of memories and solutions
178
+ memories = memories[: set["memory_recall_memories_max_result"]]
179
+ solutions = solutions[: set["memory_recall_solutions_max_result"]]
180
+
181
+ # log the search result
182
+ log_item.update(
183
+ heading=f"{len(memories)} memories and {len(solutions)} relevant solutions found",
184
)
185
101
- # append to prompt
102
- extras["memories"] = memories_prompt
186
+ memories_txt = "\n\n".join([mem.page_content for mem in memories]) if memories else ""
187
+ solutions_txt = "\n\n".join([sol.page_content for sol in solutions]) if solutions else ""
188
+
189
+ # log the full results
190
+ if memories_txt:
191
+ log_item.update(memories=memories_txt)
192
+ if solutions_txt:
193
+ log_item.update(solutions=solutions_txt)
194
104
- # except Exception as e:čč
105
- # err = errors.format_error(e)
106
- # self.agent.context.log.log(
107
- # type="error", heading="Recall memories extension error:", content=err
108
- # )
195
+ # place to prompt
196
+ if memories_txt:
197
+ extras["memories"] = self.agent.parse_prompt(
198
+ "agent.system.memories.md", memories=memories_txt
199
+ )
200
+ if solutions_txt:
201
+ extras["solutions"] = self.agent.parse_prompt(
202
+ "agent.system.solutions.md", solutions=solutions_txt
203
+ )
python/extensions/message_loop_prompts_after/_51_recall_solutions.py
deleted
-112
@@ -1,112 +0,0 @@
1
-import asyncio
2
-from python.helpers.extension import Extension
3
-from python.helpers.memory import Memory
4
-from agent import LoopData
5
-from python.tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
6
-
7
-DATA_NAME_TASK = "_recall_solutions_task"
8
-
9
-
10
-class RecallSolutions(Extension):
11
-
12
- INTERVAL = 3
13
- HISTORY = 10000
14
- SOLUTIONS_COUNT = 3
15
- INSTRUMENTS_COUNT = 3
16
- THRESHOLD = DEFAULT_MEMORY_THRESHOLD
17
-
18
- async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
19
-
20
- # every 3 iterations (or the first one) recall memories
21
- if loop_data.iteration % RecallSolutions.INTERVAL == 0:
22
- task = asyncio.create_task(self.search_solutions(loop_data=loop_data, **kwargs))
23
- else:
24
- task = None
25
-
26
- # set to agent to be able to wait for it
27
- self.agent.set_data(DATA_NAME_TASK, task)
28
-
29
- async def search_solutions(self, loop_data: LoopData, **kwargs):
30
-
31
- # cleanup
32
- extras = loop_data.extras_persistent
33
- if "solutions" in extras:
34
- del extras["solutions"]
35
-
36
- # try:
37
-
38
- # show full util message
39
- log_item = self.agent.context.log.log(
40
- type="util",
41
- heading="Searching memory for solutions...",
42
- )
43
-
44
- # get system message and chat history for util llm
45
- # msgs_text = self.agent.concat_messages(
46
- # self.agent.history[-RecallSolutions.HISTORY :]
47
- # ) # only last X messages
48
- # msgs_text = self.agent.history.current.output_text()
49
- msgs_text = self.agent.history.output_text()[-RecallSolutions.HISTORY:]
50
-
51
- system = self.agent.read_prompt(
52
- "memory.solutions_query.sys.md", history=msgs_text
53
- )
54
-
55
- # log query streamed by LLM
56
- async def log_callback(content):
57
- log_item.stream(query=content)
58
-
59
- # call util llm to summarize conversation
60
- query = await self.agent.call_utility_model(
61
- system=system, message=loop_data.user_message.output_text() if loop_data.user_message else "None", callback=log_callback
62
- )
63
-
64
- # get solutions database
65
- db = await Memory.get(self.agent)
66
-
67
- solutions = await db.search_similarity_threshold(
68
- query=query,
69
- limit=RecallSolutions.SOLUTIONS_COUNT,
70
- threshold=RecallSolutions.THRESHOLD,
71
- filter=f"area == '{Memory.Area.SOLUTIONS.value}'",
72
- )
73
- instruments = await db.search_similarity_threshold(
74
- query=query,
75
- limit=RecallSolutions.INSTRUMENTS_COUNT,
76
- threshold=RecallSolutions.THRESHOLD,
77
- filter=f"area == '{Memory.Area.INSTRUMENTS.value}'",
78
- )
79
-
80
- log_item.update(
81
- heading=f"{len(instruments)} instruments, {len(solutions)} solutions found",
82
- )
83
-
84
- if instruments:
85
- instruments_text = ""
86
- for instrument in instruments:
87
- instruments_text += instrument.page_content + "\n\n"
88
- instruments_text = instruments_text.strip()
89
- log_item.update(instruments=instruments_text)
90
- instruments_prompt = self.agent.read_prompt(
91
- "agent.system.instruments.md", instruments=instruments_text
92
- )
93
- loop_data.system.append(instruments_prompt)
94
-
95
- if solutions:
96
- solutions_text = ""
97
- for solution in solutions:
98
- solutions_text += solution.page_content + "\n\n"
99
- solutions_text = solutions_text.strip()
100
- log_item.update(solutions=solutions_text)
101
- solutions_prompt = self.agent.parse_prompt(
102
- "agent.system.solutions.md", solutions=solutions_text
103
- )
104
-
105
- # append to prompt
106
- extras["solutions"] = solutions_prompt
107
-
108
- # except Exception as e:
109
- # err = errors.format_error(e)
110
- # self.agent.context.log.log(
111
- # type="error", heading="Recall solutions extension error:", content=err
112
- # )
python/extensions/message_loop_prompts_after/_70_include_agent_info.py
new
+15
@@ -0,0 +1,15 @@
1
+from python.helpers.extension import Extension
2
+from agent import LoopData
3
+
4
+class IncludeAgentInfo(Extension):
5
+ async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
6
+
7
+ # read prompt
8
+ agent_info_prompt = self.agent.read_prompt(
9
+ "agent.extras.agent_info.md",
10
+ number=self.agent.number,
11
+ profile=self.agent.config.profile or "Default",
12
+ )
13
+
14
+ # add agent info to the prompt
15
+ loop_data.extras_temporary["agent_info"] = agent_info_prompt
python/extensions/message_loop_prompts_after/_91_recall_wait.py
+5
-5
@@ -1,7 +1,7 @@
1
from python.helpers.extension import Extension
2
from agent import LoopData
3
from python.extensions.message_loop_prompts_after._50_recall_memories import DATA_NAME_TASK as DATA_NAME_TASK_MEMORIES
4
-from python.extensions.message_loop_prompts_after._51_recall_solutions import DATA_NAME_TASK as DATA_NAME_TASK_SOLUTIONS
4
+# from python.extensions.message_loop_prompts_after._51_recall_solutions import DATA_NAME_TASK as DATA_NAME_TASK_SOLUTIONS
5
6
7
class RecallWait(Extension):
@@ -12,8 +12,8 @@ class RecallWait(Extension):
12
# self.agent.context.log.set_progress("Recalling memories...")
13
await task
14
15
- task = self.agent.get_data(DATA_NAME_TASK_SOLUTIONS)
16
- if task and not task.done():
17
- # self.agent.context.log.set_progress("Recalling solutions...")
18
- await task
15
+ # task = self.agent.get_data(DATA_NAME_TASK_SOLUTIONS)
16
+ # if task and not task.done():
17
+ # # self.agent.context.log.set_progress("Recalling solutions...")
18
+ # await task
19
python/extensions/monologue_end/_50_memorize_fragments.py
+98
-58
@@ -1,4 +1,5 @@
1
import asyncio
2
+from python.helpers import settings
3
from python.helpers.extension import Extension
4
from python.helpers.memory import Memory
5
from python.helpers.dirty_json import DirtyJson
@@ -12,6 +13,11 @@ class MemorizeMemories(Extension):
13
async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
14
# try:
15
16
+ set = settings.get_settings()
17
+
18
+ if not set["memory_memorize_enabled"]:
19
+ return
20
+
21
# show full util message
22
log_item = self.agent.context.log.log(
23
type="util",
@@ -24,6 +30,10 @@ class MemorizeMemories(Extension):
30
31
async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs):
32
33
+ set = settings.get_settings()
34
+
35
+ db = await Memory.get(self.agent)
36
+
37
# get system message and chat history for util llm
38
system = self.agent.read_prompt("memory.memories_sum.sys.md")
39
msgs_text = self.agent.concat_messages(self.agent.history)
@@ -75,77 +85,107 @@ class MemorizeMemories(Extension):
85
log_item.update(heading="No useful information to memorize.")
86
return
87
else:
78
- log_item.update(heading=f"{len(memories)} entries to memorize.")
88
+ memories_txt = "\n\n".join([str(memory) for memory in memories]).strip()
89
+ log_item.update(heading=f"{len(memories)} entries to memorize.", memories=memories_txt)
90
91
# Process memories with intelligent consolidation
81
- memories_txt = ""
92
total_processed = 0
93
total_consolidated = 0
94
+ rem = []
95
96
for memory in memories:
97
# Convert memory to plain text
98
txt = f"{memory}"
88
- memories_txt += "\n\n" + txt
89
-
90
- try:
91
- # Use intelligent consolidation system
92
- from python.helpers.memory_consolidation import create_memory_consolidator
93
- consolidator = create_memory_consolidator(
94
- self.agent,
95
- similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery
96
- max_similar_memories=8,
97
- max_llm_context_memories=4
98
- )
99
100
- # Create memory item-specific log for detailed tracking
101
- memory_log = self.agent.context.log.log(
102
- type="util",
103
- heading=f"Processing memory fragment: {txt[:50]}...",
104
- temp=False,
105
- update_progress="none" # Don't affect status bar
106
- )
100
+ if set["memory_memorize_consolidation"]:
101
+
102
+ try:
103
+ # Use intelligent consolidation system
104
+ from python.helpers.memory_consolidation import create_memory_consolidator
105
+ consolidator = create_memory_consolidator(
106
+ self.agent,
107
+ similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery
108
+ max_similar_memories=8,
109
+ max_llm_context_memories=4
110
+ )
111
+
112
+ # Create memory item-specific log for detailed tracking
113
+ memory_log = None # too many utility messages, skip log for now
114
+ # memory_log = self.agent.context.log.log(
115
+ # type="util",
116
+ # heading=f"Processing memory fragment: {txt[:50]}...",
117
+ # temp=False,
118
+ # update_progress="none" # Don't affect status bar
119
+ # )
120
+
121
+ # Process with intelligent consolidation
122
+ result_obj = await consolidator.process_new_memory(
123
+ new_memory=txt,
124
+ area=Memory.Area.FRAGMENTS.value,
125
+ metadata={"area": Memory.Area.FRAGMENTS.value},
126
+ log_item=memory_log
127
+ )
128
108
- # Process with intelligent consolidation
109
- result_obj = await consolidator.process_new_memory(
110
- new_memory=txt,
111
- area=Memory.Area.FRAGMENTS.value,
112
- metadata={"area": Memory.Area.FRAGMENTS.value},
113
- log_item=memory_log
129
+ # Update the individual log item with completion status but keep it temporary
130
+ if result_obj.get("success"):
131
+ total_consolidated += 1
132
+ if memory_log:
133
+ memory_log.update(
134
+ result="Fragment processed successfully",
135
+ heading=f"Memory fragment completed: {txt[:50]}...",
136
+ temp=False, # Show completion message
137
+ update_progress="none" # Show briefly then disappear
138
+ )
139
+ else:
140
+ if memory_log:
141
+ memory_log.update(
142
+ result="Fragment processing failed",
143
+ heading=f"Memory fragment failed: {txt[:50]}...",
144
+ temp=False, # Show completion message
145
+ update_progress="none" # Show briefly then disappear
146
+ )
147
+ total_processed += 1
148
+
149
+ except Exception as e:
150
+ # Log error but continue processing
151
+ log_item.update(consolidation_error=str(e))
152
+ total_processed += 1
153
+
154
+ # Update final results with structured logging
155
+ log_item.update(
156
+ heading=f"Memorization completed: {total_processed} memories processed, {total_consolidated} intelligently consolidated",
157
+ memories=memories_txt,
158
+ result=f"{total_processed} memories processed, {total_consolidated} intelligently consolidated",
159
+ memories_processed=total_processed,
160
+ memories_consolidated=total_consolidated,
161
+ update_progress="none"
162
)
163
116
- # Update the individual log item with completion status but keep it temporary
117
- if result_obj.get("success"):
118
- total_consolidated += 1
119
- memory_log.update(
120
- result="Fragment processed successfully",
121
- heading=f"Memory fragment completed: {txt[:50]}...",
122
- temp=False, # Show completion message
123
- update_progress="none" # Show briefly then disappear
124
- )
125
- else:
126
- memory_log.update(
127
- result="Fragment processing failed",
128
- heading=f"Memory fragment failed: {txt[:50]}...",
129
- temp=False, # Show completion message
130
- update_progress="none" # Show briefly then disappear
164
+ else:
165
+
166
+ # remove previous fragments too similiar to this one
167
+ if set["memory_memorize_replace_threshold"] > 0:
168
+ rem += await db.delete_documents_by_query(
169
+ query=txt,
170
+ threshold=set["memory_memorize_replace_threshold"],
171
+ filter=f"area=='{Memory.Area.FRAGMENTS.value}'",
172
)
132
- total_processed += 1
133
-
134
- except Exception as e:
135
- # Log error but continue processing
136
- log_item.update(consolidation_error=str(e))
137
- total_processed += 1
138
-
139
- # Update final results with structured logging
140
- memories_txt = memories_txt.strip()
141
- log_item.update(
142
- heading=f"Memorization completed: {total_processed} memories processed, {total_consolidated} intelligently consolidated",
143
- memories=memories_txt,
144
- result=f"{total_processed} memories processed, {total_consolidated} intelligently consolidated",
145
- memories_processed=total_processed,
146
- memories_consolidated=total_consolidated,
147
- update_progress="none"
148
- )
173
+ if rem:
174
+ rem_txt = "\n\n".join(Memory.format_docs_plain(rem))
175
+ log_item.update(replaced=rem_txt)
176
+
177
+ # insert new memory
178
+ await db.insert_text(text=txt, metadata={"area": Memory.Area.FRAGMENTS.value})
179
+
180
+ log_item.update(
181
+ result=f"{len(memories)} entries memorized.",
182
+ heading=f"{len(memories)} entries memorized.",
183
+ )
184
+ if rem:
185
+ log_item.stream(result=f"\nReplaced {len(rem)} previous memories.")
186
+
187
+
188
+
189
190
# except Exception as e:
191
# err = errors.format_error(e)
python/extensions/monologue_end/_51_memorize_solutions.py
+94
-58
@@ -1,4 +1,5 @@
1
import asyncio
2
+from python.helpers import settings
3
from python.helpers.extension import Extension
4
from python.helpers.memory import Memory
5
from python.helpers.dirty_json import DirtyJson
@@ -11,6 +12,11 @@ class MemorizeSolutions(Extension):
12
13
async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
14
# try:
15
+
16
+ set = settings.get_settings()
17
+
18
+ if not set["memory_memorize_enabled"]:
19
+ return
20
21
# show full util message
22
log_item = self.agent.context.log.log(
@@ -23,6 +29,11 @@ class MemorizeSolutions(Extension):
29
return task
30
31
async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs):
32
+
33
+ set = settings.get_settings()
34
+
35
+ db = await Memory.get(self.agent)
36
+
37
# get system message and chat history for util llm
38
system = self.agent.read_prompt("memory.solutions_sum.sys.md")
39
msgs_text = self.agent.concat_messages(self.agent.history)
@@ -74,14 +85,15 @@ class MemorizeSolutions(Extension):
85
log_item.update(heading="No successful solutions to memorize.")
86
return
87
else:
88
+ solutions_txt = "\n\n".join([str(solution) for solution in solutions]).strip()
89
log_item.update(
78
- heading=f"{len(solutions)} successful solutions to memorize."
90
+ heading=f"{len(solutions)} successful solutions to memorize.", solutions=solutions_txt
91
)
92
93
# Process solutions with intelligent consolidation
82
- solutions_txt = ""
94
total_processed = 0
95
total_consolidated = 0
96
+ rem = []
97
98
for solution in solutions:
99
# Convert solution to structured text
@@ -92,67 +104,91 @@ class MemorizeSolutions(Extension):
104
else:
105
# If solution is not a dict, convert it to string
106
txt = f"# Solution\n {str(solution)}"
95
- solutions_txt += txt + "\n\n"
96
-
97
- try:
98
- # Use intelligent consolidation system
99
- from python.helpers.memory_consolidation import create_memory_consolidator
100
- consolidator = create_memory_consolidator(
101
- self.agent,
102
- similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery
103
- max_similar_memories=6, # Fewer for solutions (more complex)
104
- max_llm_context_memories=3
105
- )
107
107
- # Create solution-specific log for detailed tracking
108
- solution_log = self.agent.context.log.log(
109
- type="util",
110
- heading=f"Processing solution: {txt[:50]}...",
111
- temp=False,
112
- update_progress="none" # Don't affect status bar
108
+ if set["memory_memorize_consolidation"]:
109
+ try:
110
+ # Use intelligent consolidation system
111
+ from python.helpers.memory_consolidation import create_memory_consolidator
112
+ consolidator = create_memory_consolidator(
113
+ self.agent,
114
+ similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery
115
+ max_similar_memories=6, # Fewer for solutions (more complex)
116
+ max_llm_context_memories=3
117
+ )
118
+
119
+ # Create solution-specific log for detailed tracking
120
+ solution_log = None # too many utility messages, skip log for now
121
+ # solution_log = self.agent.context.log.log(
122
+ # type="util",
123
+ # heading=f"Processing solution: {txt[:50]}...",
124
+ # temp=False,
125
+ # update_progress="none" # Don't affect status bar
126
+ # )
127
+
128
+ # Process with intelligent consolidation
129
+ result_obj = await consolidator.process_new_memory(
130
+ new_memory=txt,
131
+ area=Memory.Area.SOLUTIONS.value,
132
+ metadata={"area": Memory.Area.SOLUTIONS.value},
133
+ log_item=solution_log
134
+ )
135
+
136
+ # Update the individual log item with completion status but keep it temporary
137
+ if result_obj.get("success"):
138
+ total_consolidated += 1
139
+ if solution_log:
140
+ solution_log.update(
141
+ result="Solution processed successfully",
142
+ heading=f"Solution completed: {txt[:50]}...",
143
+ temp=False, # Show completion message
144
+ update_progress="none" # Show briefly then disappear
145
+ )
146
+ else:
147
+ if solution_log:
148
+ solution_log.update(
149
+ result="Solution processing failed",
150
+ heading=f"Solution failed: {txt[:50]}...",
151
+ temp=False, # Show completion message
152
+ update_progress="none" # Show briefly then disappear
153
+ )
154
+ total_processed += 1
155
+
156
+ except Exception as e:
157
+ # Log error but continue processing
158
+ log_item.update(consolidation_error=str(e))
159
+ total_processed += 1
160
+
161
+ # Update final results with structured logging
162
+ log_item.update(
163
+ heading=f"Solution memorization completed: {total_processed} solutions processed, {total_consolidated} intelligently consolidated",
164
+ solutions=solutions_txt,
165
+ result=f"{total_processed} solutions processed, {total_consolidated} intelligently consolidated",
166
+ solutions_processed=total_processed,
167
+ solutions_consolidated=total_consolidated,
168
+ update_progress="none"
169
)
170
+ else:
171
+ # remove previous solutions too similiar to this one
172
+ if set["memory_memorize_replace_threshold"] > 0:
173
+ rem += await db.delete_documents_by_query(
174
+ query=txt,
175
+ threshold=set["memory_memorize_replace_threshold"],
176
+ filter=f"area=='{Memory.Area.SOLUTIONS.value}'",
177
+ )
178
+ if rem:
179
+ rem_txt = "\n\n".join(Memory.format_docs_plain(rem))
180
+ log_item.update(replaced=rem_txt)
181
115
- # Process with intelligent consolidation
116
- result_obj = await consolidator.process_new_memory(
117
- new_memory=txt,
118
- area=Memory.Area.SOLUTIONS.value,
119
- metadata={"area": Memory.Area.SOLUTIONS.value},
120
- log_item=solution_log
182
+ # insert new solution
183
+ await db.insert_text(text=txt, metadata={"area": Memory.Area.SOLUTIONS.value})
184
+
185
+ log_item.update(
186
+ result=f"{len(solutions)} solutions memorized.",
187
+ heading=f"{len(solutions)} solutions memorized.",
188
)
189
+ if rem:
190
+ log_item.stream(result=f"\nReplaced {len(rem)} previous solutions.")
191
123
- # Update the individual log item with completion status but keep it temporary
124
- if result_obj.get("success"):
125
- total_consolidated += 1
126
- solution_log.update(
127
- result="Solution processed successfully",
128
- heading=f"Solution completed: {txt[:50]}...",
129
- temp=False, # Show completion message
130
- update_progress="none" # Show briefly then disappear
131
- )
132
- else:
133
- solution_log.update(
134
- result="Solution processing failed",
135
- heading=f"Solution failed: {txt[:50]}...",
136
- temp=False, # Show completion message
137
- update_progress="none" # Show briefly then disappear
138
- )
139
- total_processed += 1
140
-
141
- except Exception as e:
142
- # Log error but continue processing
143
- log_item.update(consolidation_error=str(e))
144
- total_processed += 1
145
-
146
- # Update final results with structured logging
147
- solutions_txt = solutions_txt.strip()
148
- log_item.update(
149
- heading=f"Solution memorization completed: {total_processed} solutions processed, {total_consolidated} intelligently consolidated",
150
- solutions=solutions_txt,
151
- result=f"{total_processed} solutions processed, {total_consolidated} intelligently consolidated",
152
- solutions_processed=total_processed,
153
- solutions_consolidated=total_consolidated,
154
- update_progress="none"
155
- )
192
193
# except Exception as e:
194
# err = errors.format_error(e)
python/extensions/system_prompt/_10_system_prompt.py
+2
-4
@@ -1,9 +1,7 @@
1
-from datetime import datetime
2
-from typing import Any, Optional
1
+from typing import Any
2
from python.helpers.extension import Extension
3
from python.helpers.mcp_handler import MCPConfig
4
from agent import Agent, LoopData
6
-from python.helpers.localization import Localization
5
6
7
class SystemPrompt(Extension):
@@ -27,7 +25,7 @@ def get_main_prompt(agent: Agent):
25
def get_tools_prompt(agent: Agent):
26
prompt = agent.read_prompt("agent.system.tools.md")
27
if agent.config.chat_model.vision:
30
- prompt += '\n' + agent.read_prompt("agent.system.tools_vision.md")
28
+ prompt += '\n\n' + agent.read_prompt("agent.system.tools_vision.md")
29
return prompt
30
31
python/helpers/extension.py
+52
-5
@@ -1,13 +1,60 @@
1
from abc import abstractmethod
2
from typing import Any
3
-from agent import Agent
4
-
3
+from python.helpers import extract_tools, files
4
+from typing import TYPE_CHECKING
5
+if TYPE_CHECKING:
6
+ from agent import Agent
7
+
8
class Extension:
9
7
- def __init__(self, agent: Agent, *args, **kwargs):
8
- self.agent = agent
10
+ def __init__(self, agent: "Agent|None", **kwargs):
11
+ self.agent: "Agent" = agent # type: ignore < here we ignore the type check as there are currently no extensions without an agent
12
self.kwargs = kwargs
13
14
@abstractmethod
15
async def execute(self, **kwargs) -> Any:
13
- pass
\ No newline at end of file
16
+ pass
17
+
18
+
19
+async def call_extensions(extension_point: str, agent: "Agent|None" = None, **kwargs) -> Any:
20
+
21
+ # get default extensions
22
+ defaults = await _get_extensions("python/extensions/" + extension_point)
23
+ classes = defaults
24
+
25
+ # get agent extensions
26
+ if agent and agent.config.profile:
27
+ agentics = await _get_extensions("agents/" + agent.config.profile + "/extensions/" + extension_point)
28
+ if agentics:
29
+ # merge them, agentics overwrite defaults
30
+ unique = {}
31
+ for cls in defaults + agentics:
32
+ unique[_get_file_from_module(cls.__module__)] = cls
33
+
34
+ # sort by name
35
+ classes = sorted(unique.values(), key=lambda cls: _get_file_from_module(cls.__module__))
36
+
37
+ # call extensions
38
+ for cls in classes:
39
+ await cls(agent=agent).execute(**kwargs)
40
+
41
+
42
+def _get_file_from_module(module_name: str) -> str:
43
+ return module_name.split(".")[-1]
44
+
45
+_cache: dict[str, list[type[Extension]]] = {}
46
+async def _get_extensions(folder:str):
47
+ global _cache
48
+ folder = files.get_abs_path(folder)
49
+ if folder in _cache:
50
+ classes = _cache[folder]
51
+ else:
52
+ if not files.exists(folder):
53
+ return []
54
+ classes = extract_tools.load_classes_from_folder(
55
+ folder, "*", Extension
56
+ )
57
+ _cache[folder] = classes
58
+
59
+ return classes
60
+
python/helpers/extract_tools.py
+38
-5
@@ -1,7 +1,8 @@
1
-import re, os, importlib, inspect
1
+import re, os, importlib, importlib.util, inspect
2
+from types import ModuleType
3
from typing import Any, Type, TypeVar
4
from .dirty_json import DirtyJson
4
-from .files import get_abs_path
5
+from .files import get_abs_path, deabsolute_path
6
import regex
7
from fnmatch import fnmatch
8
@@ -58,6 +59,20 @@ def fix_json_string(json_string):
59
60
T = TypeVar('T') # Define a generic type variable
61
62
+def import_module(file_path: str) -> ModuleType:
63
+ # Handle file paths with periods in the name using importlib.util
64
+ abs_path = get_abs_path(file_path)
65
+ module_name = os.path.basename(abs_path).replace('.py', '')
66
+
67
+ # Create the module spec and load the module
68
+ spec = importlib.util.spec_from_file_location(module_name, abs_path)
69
+ if spec is None or spec.loader is None:
70
+ raise ImportError(f"Could not load module from {abs_path}")
71
+
72
+ module = importlib.util.module_from_spec(spec)
73
+ spec.loader.exec_module(module)
74
+ return module
75
+
76
def load_classes_from_folder(folder: str, name_pattern: str, base_class: Type[T], one_per_file: bool = True) -> list[Type[T]]:
77
classes = []
78
abs_folder = get_abs_path(folder)
@@ -69,9 +84,9 @@ def load_classes_from_folder(folder: str, name_pattern: str, base_class: Type[T]
84
85
# Iterate through the sorted list of files
86
for file_name in py_files:
72
- module_name = file_name[:-3] # remove .py extension
73
- module_path = folder.replace("/", ".") + "." + module_name
74
- module = importlib.import_module(module_path)
87
+ file_path = os.path.join(abs_folder, file_name)
88
+ # Use the new import_module function
89
+ module = import_module(file_path)
90
91
# Get all classes in the module
92
class_list = inspect.getmembers(module, inspect.isclass)
@@ -85,3 +100,21 @@ def load_classes_from_folder(folder: str, name_pattern: str, base_class: Type[T]
100
break
101
102
return classes
103
+
104
+def load_classes_from_file(file: str, base_class: type[T], one_per_file: bool = True) -> list[type[T]]:
105
+ classes = []
106
+ # Use the new import_module function
107
+ module = import_module(file)
108
+
109
+ # Get all classes in the module
110
+ class_list = inspect.getmembers(module, inspect.isclass)
111
+
112
+ # Filter for classes that are subclasses of the given base_class
113
+ # iterate backwards to skip imported superclasses
114
+ for cls in reversed(class_list):
115
+ if cls[1] is not base_class and issubclass(cls[1], base_class):
116
+ classes.append(cls[1])
117
+ if one_per_file:
118
+ break
119
+
120
+ return classes
python/helpers/files.py
+49
-23
@@ -12,11 +12,12 @@ import zipfile
12
import importlib
13
import importlib.util
14
import inspect
15
+import glob
16
17
18
class VariablesPlugin(ABC):
19
@abstractmethod
19
- def get_variables(self) -> dict[str, Any]: # type: ignore
20
+ def get_variables(self, file: str, backup_dirs: list[str] | None = None) -> dict[str, Any]: # type: ignore
21
pass
22
23
@@ -36,29 +37,36 @@ def load_plugin_variables(file: str, backup_dirs: list[str] | None = None) -> di
37
plugin_file = None
38
39
if plugin_file and exists(plugin_file):
40
+
41
+ from python.helpers import extract_tools
42
+ classes = extract_tools.load_classes_from_file(plugin_file, VariablesPlugin, one_per_file=False)
43
+ for cls in classes:
44
+ return cls().get_variables(file, backup_dirs) # type: ignore < abstract class here is ok, it is always a subclass
45
+
46
# load python code and extract variables variables from it
40
- module = None
41
- module_name = dirname(plugin_file).replace("/", ".") + "." + basename(plugin_file, '.py')
42
- try:
43
- spec = importlib.util.spec_from_file_location(module_name, plugin_file)
44
- if not spec:
45
- return {}
46
- module = importlib.util.module_from_spec(spec)
47
- sys.modules[spec.name] = module
48
- spec.loader.exec_module(module) # type: ignore
49
- except ImportError:
50
- return {}
51
-
52
- if module is None:
53
- return {}
54
-
55
- # Get all classes in the module
56
- class_list = inspect.getmembers(module, inspect.isclass)
57
- # Filter for classes that are subclasses of VariablesPlugin
58
- # iterate backwards to skip imported superclasses
59
- for cls in reversed(class_list):
60
- if cls[1] is not VariablesPlugin and issubclass(cls[1], VariablesPlugin):
61
- return cls[1]().get_variables() # type: ignore
47
+ # module = None
48
+ # module_name = dirname(plugin_file).replace("/", ".") + "." + basename(plugin_file, '.py')
49
+
50
+ # try:
51
+ # spec = importlib.util.spec_from_file_location(module_name, plugin_file)
52
+ # if not spec:
53
+ # return {}
54
+ # module = importlib.util.module_from_spec(spec)
55
+ # sys.modules[spec.name] = module
56
+ # spec.loader.exec_module(module) # type: ignore
57
+ # except ImportError:
58
+ # return {}
59
+
60
+ # if module is None:
61
+ # return {}
62
+
63
+ # # Get all classes in the module
64
+ # class_list = inspect.getmembers(module, inspect.isclass)
65
+ # # Filter for classes that are subclasses of VariablesPlugin
66
+ # # iterate backwards to skip imported superclasses
67
+ # for cls in reversed(class_list):
68
+ # if cls[1] is not VariablesPlugin and issubclass(cls[1], VariablesPlugin):
69
+ # return cls[1]().get_variables() # type: ignore
70
return {}
71
72
from python.helpers.strings import sanitize_string
@@ -220,6 +228,20 @@ def find_file_in_dirs(file_path, backup_dirs):
228
f"File '{file_path}' not found in the original path or backup directories."
229
)
230
231
+def get_unique_filenames_in_dirs(dir_paths: list[str], pattern: str = "*"):
232
+ # returns absolute paths for unique filenames, priority by order in dir_paths
233
+ seen = set()
234
+ result = []
235
+ for dir_path in dir_paths:
236
+ full_dir = get_abs_path(dir_path)
237
+ for file_path in glob.glob(os.path.join(full_dir, pattern)):
238
+ fname = os.path.basename(file_path)
239
+ if fname not in seen and os.path.isfile(file_path):
240
+ seen.add(fname)
241
+ result.append(get_abs_path(file_path))
242
+ # sort by filename (basename), not the full path
243
+ result.sort(key=lambda path: os.path.basename(path))
244
+ return result
245
246
def remove_code_fences(text):
247
# Pattern to match code fences with optional language specifier
@@ -309,6 +331,10 @@ def get_abs_path(*relative_paths):
331
"Convert relative paths to absolute paths based on the base directory."
332
return os.path.join(get_base_dir(), *relative_paths)
333
334
+def deabsolute_path(path:str):
335
+ "Convert absolute paths to relative paths based on the base directory."
336
+ return os.path.relpath(path, get_base_dir())
337
+
338
def fix_dev_path(path:str):
339
"On dev environment, convert /a0/... paths to local absolute paths"
340
from python.helpers.runtime import is_development
python/helpers/memory_consolidation.py
+8
-8
@@ -269,7 +269,7 @@ class MemoryConsolidator:
269
270
async def _gather_consolidated_metadata(
271
self,
272
- db,
272
+ db: Memory,
273
result: ConsolidationResult,
274
original_metadata: Dict[str, Any]
275
) -> Dict[str, Any]:
@@ -297,7 +297,7 @@ class MemoryConsolidator:
297
298
# Retrieve original memories to extract their metadata
299
if memory_ids:
300
- original_memories = await db.aget_by_ids(memory_ids)
300
+ original_memories = await db.db.aget_by_ids(memory_ids)
301
302
# Merge ALL metadata fields from original memories
303
for memory in original_memories:
@@ -571,7 +571,7 @@ class MemoryConsolidator:
571
572
async def _handle_keep_separate(
573
self,
574
- db,
574
+ db: Memory,
575
result: ConsolidationResult,
576
area: str,
577
original_metadata: Dict[str, Any], # Add original metadata parameter
@@ -600,7 +600,7 @@ class MemoryConsolidator:
600
601
async def _handle_merge(
602
self,
603
- db,
603
+ db: Memory,
604
result: ConsolidationResult,
605
area: str,
606
original_metadata: Dict[str, Any], # Add original metadata parameter
@@ -634,7 +634,7 @@ class MemoryConsolidator:
634
635
async def _handle_replace(
636
self,
637
- db,
637
+ db: Memory,
638
result: ConsolidationResult,
639
area: str,
640
original_metadata: Dict[str, Any], # Add original metadata parameter
@@ -645,7 +645,7 @@ class MemoryConsolidator:
645
# Step 1: Validate similarity scores for replacement safety
646
if result.memories_to_remove:
647
# Get the memories to be removed and check their similarity scores
648
- memories_to_check = await db.aget_by_ids(result.memories_to_remove)
648
+ memories_to_check = await db.db.aget_by_ids(result.memories_to_remove)
649
650
unsafe_replacements = []
651
for memory in memories_to_check:
@@ -710,7 +710,7 @@ class MemoryConsolidator:
710
711
async def _handle_update(
712
self,
713
- db,
713
+ db: Memory,
714
result: ConsolidationResult,
715
area: str,
716
original_metadata: Dict[str, Any], # Add original metadata parameter
@@ -728,7 +728,7 @@ class MemoryConsolidator:
728
729
if memory_id and new_content:
730
# Validate that the memory exists before attempting to delete it
731
- existing_docs = await db.aget_by_ids([memory_id])
731
+ existing_docs = await db.db.aget_by_ids([memory_id])
732
if not existing_docs:
733
PrintStyle().warning(f"Memory ID {memory_id} not found during update, skipping")
734
continue
python/helpers/runtime.py
+7
@@ -65,6 +65,13 @@ def get_runtime_id() -> str:
65
runtime_id = secrets.token_hex(8)
66
return runtime_id
67
68
+def get_persistent_id() -> str:
69
+ id = dotenv.get_dotenv_value("A0_PERSISTENT_RUNTIME_ID")
70
+ if not id:
71
+ id = secrets.token_hex(16)
72
+ dotenv.save_dotenv_value("A0_PERSISTENT_RUNTIME_ID", id)
73
+ return id
74
+
75
@overload
76
async def call_development_function(func: Callable[..., Awaitable[T]], *args, **kwargs) -> T: ...
77
python/helpers/settings.py
+207
-27
@@ -50,10 +50,25 @@ class Settings(TypedDict):
50
browser_model_vision: bool
51
browser_model_kwargs: dict[str, str]
52
53
- agent_prompts_subdir: str
53
+ agent_profile: str
54
agent_memory_subdir: str
55
agent_knowledge_subdir: str
56
57
+ memory_recall_enabled: bool
58
+ memory_recall_interval: int
59
+ memory_recall_history_len: int
60
+ memory_recall_memories_max_search: int
61
+ memory_recall_solutions_max_search: int
62
+ memory_recall_memories_max_result: int
63
+ memory_recall_solutions_max_result: int
64
+ memory_recall_similarity_threshold: float
65
+ memory_recall_query_prep: bool
66
+ memory_recall_post_filter: bool
67
+ memory_memorize_enabled: bool
68
+ memory_memorize_consolidation: bool
69
+ memory_memorize_replace_threshold: float
70
+
71
+
72
api_keys: dict[str, str]
73
74
auth_login: str
@@ -525,32 +540,18 @@ def convert_out(settings: Settings) -> SettingsOutput:
540
541
agent_fields.append(
542
{
528
- "id": "agent_prompts_subdir",
529
- "title": "A0 Prompts Subdirectory",
530
- "description": "Subdirectory of /prompts folder to be used by default agent no. 0. Subordinate agents can be spawned with other subdirectories, that is on their superior agent to decide. This setting affects the behaviour of the top level agent you communicate with.",
543
+ "id": "agent_profile",
544
+ "title": "Default agent profile",
545
+ "description": "Subdirectory of /agents folder to be used by default agent no. 0. Subordinate agents can be spawned with other profiles, that is on their superior agent to decide. This setting affects the behaviour of the top level agent you communicate with.",
546
"type": "select",
532
- "value": settings["agent_prompts_subdir"],
547
+ "value": settings["agent_profile"],
548
"options": [
549
{"value": subdir, "label": subdir}
535
- for subdir in files.get_subdirectories("prompts")
550
+ for subdir in files.get_subdirectories("agents") if subdir != "_example"
551
],
552
}
553
)
554
540
- agent_fields.append(
541
- {
542
- "id": "agent_memory_subdir",
543
- "title": "Memory Subdirectory",
544
- "description": "Subdirectory of /memory folder to use for agent memory storage. Used to separate memory storage between different instances.",
545
- "type": "text",
546
- "value": settings["agent_memory_subdir"],
547
- # "options": [
548
- # {"value": subdir, "label": subdir}
549
- # for subdir in files.get_subdirectories("memory", exclude="embeddings")
550
- # ],
551
- }
552
- )
553
-
555
agent_fields.append(
556
{
557
"id": "agent_knowledge_subdir",
@@ -573,6 +574,170 @@ def convert_out(settings: Settings) -> SettingsOutput:
574
"tab": "agent",
575
}
576
577
+
578
+ memory_fields: list[SettingsField] = []
579
+
580
+ memory_fields.append(
581
+ {
582
+ "id": "agent_memory_subdir",
583
+ "title": "Memory Subdirectory",
584
+ "description": "Subdirectory of /memory folder to use for agent memory storage. Used to separate memory storage between different instances.",
585
+ "type": "text",
586
+ "value": settings["agent_memory_subdir"],
587
+ # "options": [
588
+ # {"value": subdir, "label": subdir}
589
+ # for subdir in files.get_subdirectories("memory", exclude="embeddings")
590
+ # ],
591
+ }
592
+ )
593
+
594
+ memory_fields.append(
595
+ {
596
+ "id": "memory_recall_enabled",
597
+ "title": "Memory auto-recall enabled",
598
+ "description": "Agent Zero will automatically recall memories based on convesation context.",
599
+ "type": "switch",
600
+ "value": settings["memory_recall_enabled"],
601
+ }
602
+ )
603
+
604
+ memory_fields.append(
605
+ {
606
+ "id": "memory_recall_query_prep",
607
+ "title": "Auto-recall AI query preparation",
608
+ "description": "Enables vector DB query preparation from conversation context by utility LLM for auto-recall. Improves search quality, adds 1 utility LLM call per auto-recall.",
609
+ "type": "switch",
610
+ "value": settings["memory_recall_query_prep"],
611
+ }
612
+ )
613
+
614
+ memory_fields.append(
615
+ {
616
+ "id": "memory_recall_post_filter",
617
+ "title": "Auto-recall AI post-filtering",
618
+ "description": "Enables memory relevance filtering by utility LLM for auto-recall. Improves search quality, adds 1 utility LLM call per auto-recall.",
619
+ "type": "switch",
620
+ "value": settings["memory_recall_post_filter"],
621
+ }
622
+ )
623
+
624
+ memory_fields.append(
625
+ {
626
+ "id": "memory_recall_interval",
627
+ "title": "Memory auto-recall interval",
628
+ "description": "Memories are recalled after every user or superior agent message. During agent's monologue, memories are recalled every X turns based on this parameter.",
629
+ "type": "range",
630
+ "min": 1,
631
+ "max": 10,
632
+ "step": 1,
633
+ "value": settings["memory_recall_interval"],
634
+ }
635
+ )
636
+
637
+ memory_fields.append(
638
+ {
639
+ "id": "memory_recall_history_len",
640
+ "title": "Memory auto-recall history length",
641
+ "description": "The length of conversation history passed to memory recall LLM for context (in characters).",
642
+ "type": "number",
643
+ "value": settings["memory_recall_history_len"],
644
+ }
645
+ )
646
+
647
+ memory_fields.append(
648
+ {
649
+ "id": "memory_recall_similarity_threshold",
650
+ "title": "Memory auto-recall similarity threshold",
651
+ "description": "The threshold for similarity search in memory recall (0 = no similarity, 1 = exact match).",
652
+ "type": "range",
653
+ "min": 0,
654
+ "max": 1,
655
+ "step": 0.01,
656
+ "value": settings["memory_recall_similarity_threshold"],
657
+ }
658
+ )
659
+
660
+ memory_fields.append(
661
+ {
662
+ "id": "memory_recall_memories_max_search",
663
+ "title": "Memory auto-recall max memories to search",
664
+ "description": "The maximum number of memories returned by vector DB for further processing.",
665
+ "type": "number",
666
+ "value": settings["memory_recall_memories_max_search"],
667
+ }
668
+ )
669
+
670
+ memory_fields.append(
671
+ {
672
+ "id": "memory_recall_memories_max_result",
673
+ "title": "Memory auto-recall max memories to use",
674
+ "description": "The maximum number of memories to inject into A0's context window.",
675
+ "type": "number",
676
+ "value": settings["memory_recall_memories_max_result"],
677
+ }
678
+ )
679
+
680
+ memory_fields.append(
681
+ {
682
+ "id": "memory_recall_solutions_max_search",
683
+ "title": "Memory auto-recall max solutions to search",
684
+ "description": "The maximum number of solutions returned by vector DB for further processing.",
685
+ "type": "number",
686
+ "value": settings["memory_recall_solutions_max_search"],
687
+ }
688
+ )
689
+
690
+ memory_fields.append(
691
+ {
692
+ "id": "memory_recall_solutions_max_result",
693
+ "title": "Memory auto-recall max solutions to use",
694
+ "description": "The maximum number of solutions to inject into A0's context window.",
695
+ "type": "number",
696
+ "value": settings["memory_recall_solutions_max_result"],
697
+ }
698
+ )
699
+
700
+ memory_fields.append(
701
+ {
702
+ "id": "memory_memorize_enabled",
703
+ "title": "Auto-memorize enabled",
704
+ "description": "A0 will automatically memorize facts and solutions from conversation history.",
705
+ "type": "switch",
706
+ "value": settings["memory_memorize_enabled"],
707
+ }
708
+ )
709
+
710
+ memory_fields.append(
711
+ {
712
+ "id": "memory_memorize_consolidation",
713
+ "title": "Auto-memorize AI consolidation",
714
+ "description": "A0 will automatically consolidate similar memories using utility LLM. Improves memory quality over time, adds 2 utility LLM calls per memory.",
715
+ "type": "switch",
716
+ "value": settings["memory_memorize_consolidation"],
717
+ }
718
+ )
719
+
720
+ memory_fields.append(
721
+ {
722
+ "id": "memory_memorize_replace_threshold",
723
+ "title": "Auto-memorize replacement threshold",
724
+ "description": "Only applies when AI consolidation is disabled. Replaces previous similar memories with new ones based on this threshold. 0 = replace even if not similar at all, 1 = replace only if exact match.",
725
+ "type": "range",
726
+ "min": 0,
727
+ "max": 1,
728
+ "step": 0.01,
729
+ "value": settings["memory_memorize_replace_threshold"],
730
+ }
731
+ )
732
+
733
+ memory_section: SettingsSection = {
734
+ "id": "memory",
735
+ "title": "Memory",
736
+ "description": "Configuration of A0's memory system. A0 memorizes and recalls memories automatically to help it's context awareness.",
737
+ "fields": memory_fields,
738
+ "tab": "agent",
739
+ }
740
+
741
dev_fields: list[SettingsField] = []
742
743
if runtime.is_development():
@@ -859,6 +1024,7 @@ def convert_out(settings: Settings) -> SettingsOutput:
1024
util_model_section,
1025
browser_model_section,
1026
embed_model_section,
1027
+ memory_section,
1028
speech_section,
1029
api_keys_section,
1030
auth_section,
@@ -942,6 +1108,8 @@ def normalize_settings(settings: Settings) -> Settings:
1108
else:
1109
try:
1110
copy[key] = type(value)(copy[key]) # type: ignore
1111
+ if isinstance(copy[key], str):
1112
+ copy[key] = copy[key].strip() # strip strings
1113
except (ValueError, TypeError):
1114
copy[key] = value # make default instead
1115
@@ -955,8 +1123,8 @@ def _adjust_to_version(settings: Settings, default: Settings):
1123
# starting with 0.9, the default prompt subfolder for agent no. 0 is agent0
1124
# switch to agent0 if the old default is used from v0.8
1125
if "version" not in settings or settings["version"].startswith("v0.8"):
958
- if "agent_prompts_subdir" not in settings or settings["agent_prompts_subdir"] == "default":
959
- settings["agent_prompts_subdir"] = "agent0"
1126
+ if "agent_profile" not in settings or settings["agent_profile"] == "default":
1127
+ settings["agent_profile"] = "agent0"
1128
1129
def _read_settings_file() -> Settings | None:
1130
if os.path.exists(SETTINGS_FILE):
@@ -1013,7 +1181,7 @@ def get_default_settings() -> Settings:
1181
chat_model_rl_input=0,
1182
chat_model_rl_output=0,
1183
util_model_provider="openrouter",
1016
- util_model_name="openai/gpt-4.1-nano",
1184
+ util_model_name="openai/gpt-4.1-mini",
1185
util_model_api_base="",
1186
util_model_ctx_length=100000,
1187
util_model_ctx_input=0.7,
@@ -1032,11 +1200,24 @@ def get_default_settings() -> Settings:
1200
browser_model_api_base="",
1201
browser_model_vision=True,
1202
browser_model_kwargs={"temperature": "0"},
1203
+ memory_recall_enabled=True,
1204
+ memory_recall_interval=3,
1205
+ memory_recall_history_len=10000,
1206
+ memory_recall_memories_max_search=12,
1207
+ memory_recall_solutions_max_search=8,
1208
+ memory_recall_memories_max_result=5,
1209
+ memory_recall_solutions_max_result=3,
1210
+ memory_recall_similarity_threshold=0.7,
1211
+ memory_recall_query_prep=True,
1212
+ memory_recall_post_filter=True,
1213
+ memory_memorize_enabled=True,
1214
+ memory_memorize_consolidation=True,
1215
+ memory_memorize_replace_threshold=0.9,
1216
api_keys={},
1217
auth_login="",
1218
auth_password="",
1219
root_password="",
1039
- agent_prompts_subdir="agent0",
1220
+ agent_profile="agent0",
1221
agent_memory_subdir="default",
1222
agent_knowledge_subdir="custom",
1223
rfc_auto_docker=True,
@@ -1214,12 +1395,11 @@ def get_runtime_config(set: Settings):
1395
1396
1397
def create_auth_token() -> str:
1398
+ runtime_id = runtime.get_persistent_id()
1399
username = dotenv.get_dotenv_value(dotenv.KEY_AUTH_LOGIN) or ""
1400
password = dotenv.get_dotenv_value(dotenv.KEY_AUTH_PASSWORD) or ""
1219
- if not username or not password:
1220
- return "0"
1401
# use base64 encoding for a more compact token with alphanumeric chars
1222
- hash_bytes = hashlib.sha256(f"{username}:{password}".encode()).digest()
1402
+ hash_bytes = hashlib.sha256(f"{runtime_id}:{username}:{password}".encode()).digest()
1403
# encode as base64 and remove any non-alphanumeric chars (like +, /, =)
1404
b64_token = base64.urlsafe_b64encode(hash_bytes).decode().replace("=", "")
1405
return b64_token[:16]
python/tools/call_subordinate.py
+4
-4
@@ -16,16 +16,16 @@ class Delegation(Tool):
16
sub.set_data(Agent.DATA_NAME_SUPERIOR, self.agent)
17
self.agent.set_data(Agent.DATA_NAME_SUBORDINATE, sub)
18
# set default prompt profile to new agents
19
- sub.config.prompts_subdir = "default"
19
+ sub.config.profile = ""
20
21
# add user message to subordinate agent
22
subordinate: Agent = self.agent.get_data(Agent.DATA_NAME_SUBORDINATE)
23
subordinate.hist_add_user_message(UserMessage(message=message, attachments=[]))
24
25
# set subordinate prompt profile if provided, if not, keep original
26
- prompt_profile = kwargs.get("prompt_profile")
27
- if prompt_profile:
28
- subordinate.config.prompts_subdir = prompt_profile
26
+ agent_profile = kwargs.get("agent_profile")
27
+ if agent_profile:
28
+ subordinate.config.profile = agent_profile
29
30
# run subordinate monologue
31
result = await subordinate.monologue()
webui/css/messages.css
+4
@@ -91,6 +91,10 @@
91
font-size: var(--font-size-small);
92
}
93
94
+.message-user .message-text{
95
+ text-align: start;
96
+}
97
+
98
.message-ai {
99
/* border-bottom-left-radius: var(--spacing-xxs); */
100
}
webui/index.js
+6
-1
@@ -330,8 +330,9 @@ async function poll() {
330
// Get timezone from navigator
331
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
332
333
+ const log_from = lastLogVersion;
334
const response = await sendJsonData("/poll", {
334
- log_from: lastLogVersion,
335
+ log_from: log_from,
336
context: context || null,
337
timezone: timezone,
338
});
@@ -345,9 +346,13 @@ async function poll() {
346
if (!context) setContext(response.context);
347
if (response.context != context) return; //skip late polls after context change
348
349
+ // if the chat has been reset, restart this poll as it may have been called with incorrect log_from
350
if (lastLogGuid != response.log_guid) {
351
chatHistory.innerHTML = "";
352
lastLogVersion = 0;
353
+ lastLogGuid = response.log_guid;
354
+ await poll();
355
+ return;
356
}
357
358
if (lastLogVersion != response.log_version) {