initial plugins effort; memory system PoC

plugin manifest update add memory plugin PoC files offload memory prompts cleanup imports extract memory UI fix paths plugin docs

3clyp50 committed Feb 6, 2026 at 18:03 UTC 54fb4746a44003420eff39adf215c65bb0448c93
62 files changed +1737 -659
.gitignore
+3 -3
@@ -16,9 +16,9 @@
16 .venv/
17
18 # obsolete folders
19 -memory/
20 -knowledge/custom/
21 -instruments/
19 +/memory/
20 +/knowledge/custom/
21 +/instruments/
22
23 # Handle logs directory
24 logs/**
PLUGIN_SYSTEM.md new
+116
@@ -0,0 +1,116 @@
1 +# Unified Plugin System - Implementation Summary
2 +
3 +This document provides a technical overview of the Plugin System implementation in Agent Zero.
4 +
5 +## Overview
6 +
7 +The Plugin System allows users and the community to create full-stack plugins that integrate seamlessly with Agent Zero. It supports backend capabilities (API endpoints, tools, helpers) and frontend UI extensions. It uses a manifest-driven approach (`plugin.json`) to declare capabilities.
8 +
9 +## Architecture
10 +
11 +### Components
12 +
13 +1. Backend Plugin Discovery (`python/helpers/plugins.py`)
14 + - Resolves plugin directories across repo (`plugins/`) and user (`usr/plugins/`) locations.
15 + - Loads and validates `plugin.json` manifests.
16 + - Provides `import_plugin_module()` for dynamic dependency injection, replacing static imports.
17 + - Implements override logic (user plugins override repo plugins).
18 +
19 +2. API Endpoints
20 + - `POST /plugins_resolve` - Resolves a plugin manifest by ID (for frontend).
21 + - `GET/POST /plugins_list` - Lists all available plugins.
22 + - `GET /plugins/<id>/<path>` - Serves plugin static assets (UI, scripts).
23 +
24 +3. Frontend Plugin Loader (`webui/js/plugins.js`)
25 + - Discovers and loads `<x-extension>` tags.
26 + - Fetches plugin manifests via `/plugins_resolve`.
27 + - Path Traversal Strategy: Integrates with the standard component loader by adjusting paths (e.g., prepending `components/../` to plugin URLs) to bypass default path restrictions without modifying the core loader.
28 + - Handles module imports and props merging.
29 +
30 +4. Standard Component Loader (`webui/js/components.js`)
31 + - Vanilla Implementation: Remains unmodified to ensure stability and compatibility.
32 + - Enforces `components/` prefix for all loaded resources.
33 + - Used by `plugins.js` via relative path traversal to load plugin UI components.
34 +
35 +5. DOM Integration
36 + - Automatic loading on DOM ready.
37 + - MutationObserver for dynamic plugin injection.
38 + - Alpine.js integration via `globalThis.xAttrs()`.
39 +
40 +## File Structure
41 +
42 +```
43 +/plugins/ # Repo plugins (default)
44 + ├── memory/
45 + │ ├── plugin.json # Manifest
46 + │ ├── api/ # Backend endpoints
47 + │ ├── tools/ # Agent tools
48 + │ ├── helpers/ # Python helpers
49 + │ ├── ui/ # Frontend components
50 + │ │ ├── memory-dashboard.html
51 + │ │ └── memory-dashboard-store.js
52 + │ └── prompts/ # System prompts
53 + └── README.md
54 +
55 +/usr/plugins/ # User plugins (override)
56 + ├── my-plugin/
57 + │ ├── plugin.json
58 + │ └── ...
59 + └── README.md
60 +```
61 +
62 +## Plugin Manifest Schema
63 +
64 +```json
65 +{
66 + "id": "plugin-id", // Required: Must match directory name
67 + "name": "Display Name", // Optional: Human-readable name
68 + "provides": {
69 + "api": [
70 + { "module": "api/my_endpoint.py", "description": "..." }
71 + ],
72 + "tool": [
73 + { "module": "tools/my_tool.py", "description": "..." }
74 + ],
75 + "ui": {
76 + "component": "ui/index.html",
77 + "module": "ui/main.js"
78 + },
79 + "prompt": [
80 + { "module": "prompts/system.md", "description": "..." }
81 + ]
82 + },
83 + "props": { // Optional: Default UI properties
84 + "key": "value"
85 + }
86 +}
87 +```
88 +
89 +## Usage
90 +
91 +### Creating a Plugin
92 +
93 +1. Create plugin directory: `plugins/my-plugin`
94 +2. Create `plugin.json` declaring provided capabilities.
95 +3. Implement backend modules in `api/`, `tools/`, etc.
96 +4. Implement frontend components in `ui/`.
97 +
98 +### Using Plugin Capabilities
99 +
100 +Backend (Python):
101 +Instead of static imports, use the dynamic loader:
102 +```python
103 +from python.helpers.plugins import import_plugin_module
104 +
105 +# Dynamically load a helper from the 'memory' plugin
106 +memory = import_plugin_module("memory", "helpers/memory.py")
107 +memory.some_function()
108 +```
109 +
110 +Frontend (HTML/JS):
111 +Plugins can be embedded via `<x-extension>` or loaded dynamically via `openModal`.
112 +```javascript
113 +// Open a plugin component in a modal
114 +// Uses relative path to traverse out of 'components/' and into 'plugins/'
115 +openModal("../plugins/my-plugin/ui/dashboard.html");
116 +```
agent.py
+15 -1
@@ -18,7 +18,8 @@ from python.helpers import (
18 tokens,
19 context as context_helper,
20 dirty_json,
21 - subagents
21 + subagents,
22 + plugins
23 )
24 from python.helpers.print_style import PrintStyle
25
@@ -645,6 +646,10 @@ class Agent:
646
647 def parse_prompt(self, _prompt_file: str, **kwargs):
648 dirs = subagents.get_paths(self, "prompts")
649 +
650 + # Plugin prompt paths
651 + dirs.extend(plugins.get_plugin_paths("prompt"))
652 +
653 prompt = files.parse_file(
654 _prompt_file, _directories=dirs, _agent=self, **kwargs
655 )
@@ -652,6 +657,10 @@ class Agent:
657
658 def read_prompt(self, file: str, **kwargs) -> str:
659 dirs = subagents.get_paths(self, "prompts")
660 +
661 + # Plugin prompt paths
662 + dirs.extend(plugins.get_plugin_paths("prompt"))
663 +
664 prompt = files.read_prompt_file(file, _directories=dirs, _agent=self, **kwargs)
665 if files.is_full_json_template(prompt):
666 prompt = files.remove_code_fences(prompt)
@@ -987,6 +996,11 @@ class Agent:
996
997 # search for tools in agent's folder hierarchy
998 paths = subagents.get_paths(self, "tools", name + ".py", default_root="python")
999 +
1000 + # Add plugin tool paths
1001 + plugin_paths = plugins.get_plugin_paths("tool", name + ".py")
1002 + paths.extend(plugin_paths)
1003 +
1004 for path in paths:
1005 try:
1006 classes = extract_tools.load_classes_from_file(path, Tool) # type: ignore[arg-type]
plugins/README.md new
+84
@@ -0,0 +1,84 @@
1 +# Agent Zero Plugins
2 +
3 +This directory contains default plugins shipped with Agent Zero.
4 +
5 +## Plugin Architecture
6 +
7 +Agent Zero uses a unified capability-based plugin system where a single `plugin.json` manifest declares all capabilities via a `provides` dictionary. Each capability type maps to an integration point in the framework.
8 +
9 +## Plugin Structure
10 +
11 +Each plugin directory should contain:
12 +- `plugin.json` - Manifest file with capability declarations
13 +- Capability-specific files organized by type (helpers, tools, extensions, api, ui, etc.)
14 +- Other assets (CSS, images, documentation)
15 +
16 +## Manifest Schema
17 +
18 +The `plugin.json` uses a `provides` dict where keys are integration point types:
19 +
20 +```json
21 +{
22 + "id": "my-plugin",
23 + "name": "My Plugin",
24 + "version": "1.0.0",
25 + "author": "Author Name",
26 + "description": "Plugin description",
27 + "tags": ["tag1", "tag2"],
28 + "provides": {
29 + "helper": [
30 + { "module": "helpers/my_helper.py", "description": "Helper module description" }
31 + ],
32 + "tool": [
33 + { "module": "tools/my_tool.py", "description": "Tool description" }
34 + ],
35 + "extension": [
36 + { "module": "extensions/hook_name/my_extension.py", "description": "Extension description" }
37 + ],
38 + "api": [
39 + { "module": "api/my_api.py", "description": "API endpoint description" }
40 + ],
41 + "ui": {
42 + "component": "ui/component.html",
43 + "module": "ui/main.js",
44 + "description": "UI component description"
45 + }
46 + }
47 +}
48 +```
49 +
50 +## Capability Types
51 +
52 +- **helper**: Python modules providing reusable functionality (imported via proxy or direct)
53 +- **tool**: Agent tools extending agent capabilities
54 +- **extension**: Extension hooks that run at specific lifecycle points
55 +- **api**: Flask API endpoints (ApiHandler subclasses)
56 +- **ui**: Frontend components loaded via `<x-extension>` tags
57 +- **prompt**: Custom prompt templates
58 +- **knowledge**: Knowledge base files and directories
59 +- **instrument**: Custom instrumentation and monitoring
60 +
61 +## Using Plugins
62 +
63 +### UI Components
64 +```html
65 +<x-extension id="plugin-id"></x-extension>
66 +```
67 +
68 +### Backend Capabilities
69 +Backend capabilities (helpers, tools, extensions, APIs) are automatically discovered and integrated when the plugin is loaded.
70 +
71 +## User Plugins
72 +
73 +User-created plugins should be placed in one of the following directories:
74 +
75 +- `/usr/plugins/` (Global user plugins)
76 +- `/usr/projects/<project_name>/.a0proj/plugins/` (Project-specific plugins)
77 +
78 +Note: `/usr/` refers to the Agent Zero user data directory in the application root, not the system `/usr` directory.
79 +
80 +User plugins with the same ID as repo plugins will completely override the repo version.
81 +
82 +## Documentation
83 +
84 +See [docs/extensibility.md](../docs/extensibility.md) for complete documentation on creating plugins.
plugins/memory/api/memory_dashboard.py renamed
+8 -1
@@ -1,10 +1,17 @@
1 from python.helpers.api import ApiHandler, Request, Response
2 -from python.helpers.memory import Memory, get_existing_memory_subdirs, get_context_memory_subdir
2 from python.helpers import files
3 from models import ModelConfig, ModelType
4 from langchain_core.documents import Document
5 from agent import AgentContext
6
7 +# Import Memory functions from plugin
8 +import sys
9 +from pathlib import Path
10 +_plugin_root = Path(__file__).parent.parent
11 +if str(_plugin_root) not in sys.path:
12 + sys.path.insert(0, str(_plugin_root))
13 +from helpers.memory import Memory, get_existing_memory_subdirs, get_context_memory_subdir
14 +
15
16 class MemoryDashboard(ApiHandler):
17
plugins/memory/extensions/message_loop_prompts_after/_50_recall_memories.py new
+222
@@ -0,0 +1,222 @@
1 +import asyncio
2 +from python.helpers.extension import Extension
3 +from agent import LoopData
4 +from python.helpers import dirty_json, errors, settings, log
5 +
6 +# Import Memory and DEFAULT_THRESHOLD from plugin
7 +import sys
8 +from pathlib import Path
9 +_plugin_root = Path(__file__).parent.parent.parent
10 +if str(_plugin_root) not in sys.path:
11 + sys.path.insert(0, str(_plugin_root))
12 +from helpers.memory import Memory
13 +from tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
14 +
15 +
16 +DATA_NAME_TASK = "_recall_memories_task"
17 +DATA_NAME_ITER = "_recall_memories_iter"
18 +
19 +
20 +class RecallMemories(Extension):
21 +
22 + # INTERVAL = 3
23 + # HISTORY = 10000
24 + # MEMORIES_MAX_SEARCH = 12
25 + # SOLUTIONS_MAX_SEARCH = 8
26 + # MEMORIES_MAX_RESULT = 5
27 + # SOLUTIONS_MAX_RESULT = 3
28 + # THRESHOLD = DEFAULT_MEMORY_THRESHOLD
29 +
30 + async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
31 +
32 + set = settings.get_settings()
33 +
34 + # turned off in settings?
35 + if not set["memory_recall_enabled"]:
36 + return
37 +
38 + # every X iterations (or the first one) recall memories
39 + if loop_data.iteration % set["memory_recall_interval"] == 0:
40 +
41 + # show util message right away
42 + log_item = self.agent.context.log.log(
43 + type="util",
44 + heading="Searching memories...",
45 + )
46 +
47 + task = asyncio.create_task(
48 + self.search_memories(loop_data=loop_data, log_item=log_item, **kwargs)
49 + )
50 + else:
51 + task = None
52 +
53 + # set to agent to be able to wait for it
54 + self.agent.set_data(DATA_NAME_TASK, task)
55 + self.agent.set_data(DATA_NAME_ITER, loop_data.iteration)
56 +
57 + async def search_memories(self, log_item: log.LogItem, loop_data: LoopData, **kwargs):
58 +
59 + # cleanup
60 + extras = loop_data.extras_persistent
61 + if "memories" in extras:
62 + del extras["memories"]
63 + if "solutions" in extras:
64 + del extras["solutions"]
65 +
66 +
67 + set = settings.get_settings()
68 + # try:
69 +
70 + # get system message and chat history for util llm
71 + system = self.agent.read_prompt("memory.memories_query.sys.md")
72 +
73 + # # log query streamed by LLM
74 + # async def log_callback(content):
75 + # log_item.stream(query=content)
76 +
77 + # call util llm to summarize conversation
78 + user_instruction = (
79 + loop_data.user_message.output_text() if loop_data.user_message else "None"
80 + )
81 + history = self.agent.history.output_text()[-set["memory_recall_history_len"]:]
82 + message = self.agent.read_prompt(
83 + "memory.memories_query.msg.md", history=history, message=user_instruction
84 + )
85 +
86 + # if query preparation by AI is enabled
87 + if set["memory_recall_query_prep"]:
88 + try:
89 + # call util llm to generate search query from the conversation
90 + query = await self.agent.call_utility_model(
91 + system=system,
92 + message=message,
93 + # callback=log_callback,
94 + )
95 + query = query.strip()
96 + log_item.update(query=query) # no need for streaming here
97 + except Exception as e:
98 + err = errors.format_error(e)
99 + self.agent.context.log.log(
100 + type="warning", heading="Recall memories extension error:", content=err
101 + )
102 + query = ""
103 +
104 + # no query, no search
105 + if not query:
106 + log_item.update(
107 + heading="Failed to generate memory query",
108 + )
109 + return
110 +
111 + # otherwise use the message and history as query
112 + else:
113 + query = user_instruction + "\n\n" + history
114 +
115 + # if there is no query (or just dash by the LLM), do not continue
116 + if not query or len(query) <= 3:
117 + log_item.update(
118 + query="No relevant memory query generated, skipping search",
119 + )
120 + return
121 +
122 + # get memory database
123 + db = await Memory.get(self.agent)
124 +
125 + # search for general memories and fragments
126 + memories = await db.search_similarity_threshold(
127 + query=query,
128 + limit=set["memory_recall_memories_max_search"],
129 + threshold=set["memory_recall_similarity_threshold"],
130 + filter=f"area == '{Memory.Area.MAIN.value}' or area == '{Memory.Area.FRAGMENTS.value}'", # exclude solutions
131 + )
132 +
133 + # search for solutions
134 + solutions = await db.search_similarity_threshold(
135 + query=query,
136 + limit=set["memory_recall_solutions_max_search"],
137 + threshold=set["memory_recall_similarity_threshold"],
138 + filter=f"area == '{Memory.Area.SOLUTIONS.value}'", # exclude solutions
139 + )
140 +
141 + if not memories and not solutions:
142 + log_item.update(
143 + heading="No memories or solutions found",
144 + )
145 + return
146 +
147 + # if post filtering is enabled
148 + if set["memory_recall_post_filter"]:
149 + # assemble an enumerated dict of memories and solutions for AI validation
150 + mems_list = {i: memory.page_content for i, memory in enumerate(memories + solutions)}
151 +
152 + # call AI to validate the memories
153 + try:
154 + filter = await self.agent.call_utility_model(
155 + system=self.agent.read_prompt("memory.memories_filter.sys.md"),
156 + message=self.agent.read_prompt(
157 + "memory.memories_filter.msg.md",
158 + memories=mems_list,
159 + history=history,
160 + message=user_instruction,
161 + ),
162 + )
163 + filter_inds = dirty_json.try_parse(filter)
164 +
165 + # filter memories and solutions based on filter_inds
166 + filtered_memories = []
167 + filtered_solutions = []
168 + mem_len = len(memories)
169 +
170 + # process each index in filter_inds
171 + # make sure filter_inds is a list and contains valid integers
172 + if isinstance(filter_inds, list):
173 + for idx in filter_inds:
174 + if isinstance(idx, int):
175 + if idx < mem_len:
176 + # this is a memory
177 + filtered_memories.append(memories[idx])
178 + else:
179 + # this is a solution, adjust index
180 + sol_idx = idx - mem_len
181 + if sol_idx < len(solutions):
182 + filtered_solutions.append(solutions[sol_idx])
183 +
184 + # replace original lists with filtered ones
185 + memories = filtered_memories
186 + solutions = filtered_solutions
187 +
188 + except Exception as e:
189 + err = errors.format_error(e)
190 + self.agent.context.log.log(
191 + type="warning", heading="Failed to filter relevant memories", content=err
192 + )
193 + filter_inds = []
194 +
195 +
196 + # limit the number of memories and solutions
197 + memories = memories[: set["memory_recall_memories_max_result"]]
198 + solutions = solutions[: set["memory_recall_solutions_max_result"]]
199 +
200 + # log the search result
201 + log_item.update(
202 + heading=f"{len(memories)} memories and {len(solutions)} relevant solutions found",
203 + )
204 +
205 + memories_txt = "\n\n".join([mem.page_content for mem in memories]) if memories else ""
206 + solutions_txt = "\n\n".join([sol.page_content for sol in solutions]) if solutions else ""
207 +
208 + # log the full results
209 + if memories_txt:
210 + log_item.update(memories=memories_txt)
211 + if solutions_txt:
212 + log_item.update(solutions=solutions_txt)
213 +
214 + # place to prompt
215 + if memories_txt:
216 + extras["memories"] = self.agent.parse_prompt(
217 + "agent.system.memories.md", memories=memories_txt
218 + )
219 + if solutions_txt:
220 + extras["solutions"] = self.agent.parse_prompt(
221 + "agent.system.solutions.md", solutions=solutions_txt
222 + )
plugins/memory/extensions/monologue_end/_50_memorize_fragments.py new
+205
@@ -0,0 +1,205 @@
1 +import asyncio
2 +from python.helpers import settings, errors
3 +from python.helpers.extension import Extension
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 DeferredTask, THREAD_BACKGROUND
8 +
9 +# Import Memory and DEFAULT_THRESHOLD from plugin
10 +import sys
11 +from pathlib import Path
12 +_plugin_root = Path(__file__).parent.parent.parent
13 +if str(_plugin_root) not in sys.path:
14 + sys.path.insert(0, str(_plugin_root))
15 +from helpers.memory import Memory
16 +from tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
17 +
18 +
19 +class MemorizeMemories(Extension):
20 +
21 + async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
22 + # try:
23 +
24 + set = settings.get_settings()
25 +
26 + if not set["memory_memorize_enabled"]:
27 + return
28 +
29 + # show full util message
30 + log_item = self.agent.context.log.log(
31 + type="util",
32 + heading="Memorizing new information...",
33 + )
34 +
35 + # memorize in background
36 + task = DeferredTask(thread_name=THREAD_BACKGROUND)
37 + task.start_task(self.memorize, loop_data, log_item)
38 + # task = asyncio.create_task(self.memorize(loop_data, log_item))
39 + return task
40 +
41 + async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs):
42 +
43 + try:
44 + set = settings.get_settings()
45 +
46 + db = await Memory.get(self.agent)
47 +
48 + # get system message and chat history for util llm
49 + system = self.agent.read_prompt("memory.memories_sum.sys.md")
50 + msgs_text = self.agent.concat_messages(self.agent.history)
51 +
52 + # # log query streamed by LLM
53 + # async def log_callback(content):
54 + # log_item.stream(content=content)
55 +
56 + # call util llm to find info in history
57 + memories_json = await self.agent.call_utility_model(
58 + system=system,
59 + message=msgs_text,
60 + # callback=log_callback,
61 + background=True,
62 + )
63 +
64 + # log data < no need for streaming utility messages
65 + log_item.update(content=memories_json)
66 +
67 + # Add validation and error handling for memories_json
68 + if not memories_json or not isinstance(memories_json, str):
69 + log_item.update(heading="No response from utility model.")
70 + return
71 +
72 + # Strip any whitespace that might cause issues
73 + memories_json = memories_json.strip()
74 +
75 + if not memories_json:
76 + log_item.update(heading="Empty response from utility model.")
77 + return
78 +
79 + try:
80 + memories = DirtyJson.parse_string(memories_json)
81 + except Exception as e:
82 + log_item.update(heading=f"Failed to parse memories response: {str(e)}")
83 + return
84 +
85 + # Validate that memories is a list or convertible to one
86 + if memories is None:
87 + log_item.update(heading="No valid memories found in response.")
88 + return
89 +
90 + # If memories is not a list, try to make it one
91 + if not isinstance(memories, list):
92 + if isinstance(memories, (str, dict)):
93 + memories = [memories]
94 + else:
95 + log_item.update(heading="Invalid memories format received.")
96 + return
97 +
98 + if not isinstance(memories, list) or len(memories) == 0:
99 + log_item.update(heading="No useful information to memorize.")
100 + return
101 + else:
102 + memories_txt = "\n\n".join([str(memory) for memory in memories]).strip()
103 + log_item.update(heading=f"{len(memories)} entries to memorize.", memories=memories_txt)
104 +
105 + # Process memories with intelligent consolidation
106 + total_processed = 0
107 + total_consolidated = 0
108 + rem = []
109 +
110 + for memory in memories:
111 + # Convert memory to plain text
112 + txt = f"{memory}"
113 +
114 + if set["memory_memorize_consolidation"]:
115 +
116 + try:
117 + # Use intelligent consolidation system
118 + from helpers.memory_consolidation import create_memory_consolidator
119 + consolidator = create_memory_consolidator(
120 + self.agent,
121 + similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery
122 + max_similar_memories=8,
123 + max_llm_context_memories=4
124 + )
125 +
126 + # Create memory item-specific log for detailed tracking
127 + memory_log = None # too many utility messages, skip log for now
128 + # memory_log = self.agent.context.log.log(
129 + # type="util",
130 + # heading=f"Processing memory fragment: {txt[:50]}...",
131 + # update_progress="none" # Don't affect status bar
132 + # )
133 +
134 + # Process with intelligent consolidation
135 + result_obj = await consolidator.process_new_memory(
136 + new_memory=txt,
137 + area=Memory.Area.FRAGMENTS.value,
138 + metadata={"area": Memory.Area.FRAGMENTS.value},
139 + log_item=memory_log
140 + )
141 +
142 + # Update the individual log item with completion status but keep it temporary
143 + if result_obj.get("success"):
144 + total_consolidated += 1
145 + if memory_log:
146 + memory_log.update(
147 + result="Fragment processed successfully",
148 + heading=f"Memory fragment completed: {txt[:50]}...",
149 + update_progress="none" # Show briefly then disappear
150 + )
151 + else:
152 + if memory_log:
153 + memory_log.update(
154 + result="Fragment processing failed",
155 + heading=f"Memory fragment failed: {txt[:50]}...",
156 + update_progress="none" # Show briefly then disappear
157 + )
158 + total_processed += 1
159 +
160 + except Exception as e:
161 + # Log error but continue processing
162 + log_item.update(consolidation_error=str(e))
163 + total_processed += 1
164 +
165 + # Update final results with structured logging
166 + log_item.update(
167 + heading=f"Memorization completed: {total_processed} memories processed, {total_consolidated} intelligently consolidated",
168 + memories=memories_txt,
169 + result=f"{total_processed} memories processed, {total_consolidated} intelligently consolidated",
170 + memories_processed=total_processed,
171 + memories_consolidated=total_consolidated,
172 + update_progress="none"
173 + )
174 +
175 + else:
176 +
177 + # remove previous fragments too similiar to this one
178 + if set["memory_memorize_replace_threshold"] > 0:
179 + rem += await db.delete_documents_by_query(
180 + query=txt,
181 + threshold=set["memory_memorize_replace_threshold"],
182 + filter=f"area=='{Memory.Area.FRAGMENTS.value}'",
183 + )
184 + if rem:
185 + rem_txt = "\n\n".join(Memory.format_docs_plain(rem))
186 + log_item.update(replaced=rem_txt)
187 +
188 + # insert new memory
189 + await db.insert_text(text=txt, metadata={"area": Memory.Area.FRAGMENTS.value})
190 +
191 + log_item.update(
192 + result=f"{len(memories)} entries memorized.",
193 + heading=f"{len(memories)} entries memorized.",
194 + )
195 + if rem:
196 + log_item.stream(result=f"\nReplaced {len(rem)} previous memories.")
197 +
198 +
199 +
200 +
201 + except Exception as e:
202 + err = errors.format_error(e)
203 + self.agent.context.log.log(
204 + type="warning", heading="Memorize memories extension error", content=err
205 + )
plugins/memory/extensions/monologue_end/_51_memorize_solutions.py new
+208
@@ -0,0 +1,208 @@
1 +import asyncio
2 +from python.helpers import settings, errors
3 +from python.helpers.extension import Extension
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 DeferredTask, THREAD_BACKGROUND
8 +
9 +# Import Memory and DEFAULT_THRESHOLD from plugin
10 +import sys
11 +from pathlib import Path
12 +_plugin_root = Path(__file__).parent.parent.parent
13 +if str(_plugin_root) not in sys.path:
14 + sys.path.insert(0, str(_plugin_root))
15 +from helpers.memory import Memory
16 +from tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
17 +
18 +class MemorizeSolutions(Extension):
19 +
20 + async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
21 + # try:
22 +
23 + set = settings.get_settings()
24 +
25 + if not set["memory_memorize_enabled"]:
26 + return
27 +
28 + # show full util message
29 + log_item = self.agent.context.log.log(
30 + type="util",
31 + heading="Memorizing succesful solutions...",
32 + )
33 +
34 + # memorize in background
35 + task = DeferredTask(thread_name=THREAD_BACKGROUND)
36 + task.start_task(self.memorize, loop_data, log_item)
37 + # task = asyncio.create_task(self.memorize(loop_data, log_item))
38 + return task
39 +
40 + async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs):
41 + try:
42 + set = settings.get_settings()
43 +
44 + db = await Memory.get(self.agent)
45 +
46 + # get system message and chat history for util llm
47 + system = self.agent.read_prompt("memory.solutions_sum.sys.md")
48 + msgs_text = self.agent.concat_messages(self.agent.history)
49 +
50 + # log query streamed by LLM
51 + # async def log_callback(content):
52 + # log_item.stream(content=content)
53 +
54 + # call util llm to find solutions in history
55 + solutions_json = await self.agent.call_utility_model(
56 + system=system,
57 + message=msgs_text,
58 + # callback=log_callback,
59 + background=True,
60 + )
61 +
62 + # log query < no need for streaming utility messages
63 + log_item.update(content=solutions_json)
64 +
65 +
66 +
67 + # Add validation and error handling for solutions_json
68 + if not solutions_json or not isinstance(solutions_json, str):
69 + log_item.update(heading="No response from utility model.")
70 + return
71 +
72 + # Strip any whitespace that might cause issues
73 + solutions_json = solutions_json.strip()
74 +
75 + if not solutions_json:
76 + log_item.update(heading="Empty response from utility model.")
77 + return
78 +
79 + try:
80 + solutions = DirtyJson.parse_string(solutions_json)
81 + except Exception as e:
82 + log_item.update(heading=f"Failed to parse solutions response: {str(e)}")
83 + return
84 +
85 + # Validate that solutions is a list or convertible to one
86 + if solutions is None:
87 + log_item.update(heading="No valid solutions found in response.")
88 + return
89 +
90 + # If solutions is not a list, try to make it one
91 + if not isinstance(solutions, list):
92 + if isinstance(solutions, (str, dict)):
93 + solutions = [solutions]
94 + else:
95 + log_item.update(heading="Invalid solutions format received.")
96 + return
97 +
98 + if not isinstance(solutions, list) or len(solutions) == 0:
99 + log_item.update(heading="No successful solutions to memorize.")
100 + return
101 + else:
102 + solutions_txt = "\n\n".join([str(solution) for solution in solutions]).strip()
103 + log_item.update(
104 + heading=f"{len(solutions)} successful solutions to memorize.", solutions=solutions_txt
105 + )
106 +
107 + # Process solutions with intelligent consolidation
108 + total_processed = 0
109 + total_consolidated = 0
110 + rem = []
111 +
112 + for solution in solutions:
113 + # Convert solution to structured text
114 + if isinstance(solution, dict):
115 + problem = solution.get('problem', 'Unknown problem')
116 + solution_text = solution.get('solution', 'Unknown solution')
117 + txt = f"# Problem\n {problem}\n# Solution\n {solution_text}"
118 + else:
119 + # If solution is not a dict, convert it to string
120 + txt = f"# Solution\n {str(solution)}"
121 +
122 + if set["memory_memorize_consolidation"]:
123 + try:
124 + # Use intelligent consolidation system
125 + from helpers.memory_consolidation import create_memory_consolidator
126 + consolidator = create_memory_consolidator(
127 + self.agent,
128 + similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery
129 + max_similar_memories=6, # Fewer for solutions (more complex)
130 + max_llm_context_memories=3
131 + )
132 +
133 + # Create solution-specific log for detailed tracking
134 + solution_log = None # too many utility messages, skip log for now
135 + # solution_log = self.agent.context.log.log(
136 + # type="util",
137 + # heading=f"Processing solution: {txt[:50]}...",
138 + # update_progress="none" # Don't affect status bar
139 + # )
140 +
141 + # Process with intelligent consolidation
142 + result_obj = await consolidator.process_new_memory(
143 + new_memory=txt,
144 + area=Memory.Area.SOLUTIONS.value,
145 + metadata={"area": Memory.Area.SOLUTIONS.value},
146 + log_item=solution_log
147 + )
148 +
149 + # Update the individual log item with completion status but keep it temporary
150 + if result_obj.get("success"):
151 + total_consolidated += 1
152 + if solution_log:
153 + solution_log.update(
154 + result="Solution processed successfully",
155 + heading=f"Solution completed: {txt[:50]}...",
156 + update_progress="none" # Show briefly then disappear
157 + )
158 + else:
159 + if solution_log:
160 + solution_log.update(
161 + result="Solution processing failed",
162 + heading=f"Solution failed: {txt[:50]}...",
163 + update_progress="none" # Show briefly then disappear
164 + )
165 + total_processed += 1
166 +
167 + except Exception as e:
168 + # Log error but continue processing
169 + log_item.update(consolidation_error=str(e))
170 + total_processed += 1
171 +
172 + # Update final results with structured logging
173 + log_item.update(
174 + heading=f"Solution memorization completed: {total_processed} solutions processed, {total_consolidated} intelligently consolidated",
175 + solutions=solutions_txt,
176 + result=f"{total_processed} solutions processed, {total_consolidated} intelligently consolidated",
177 + solutions_processed=total_processed,
178 + solutions_consolidated=total_consolidated,
179 + update_progress="none"
180 + )
181 + else:
182 + # remove previous solutions too similiar to this one
183 + if set["memory_memorize_replace_threshold"] > 0:
184 + rem += await db.delete_documents_by_query(
185 + query=txt,
186 + threshold=set["memory_memorize_replace_threshold"],
187 + filter=f"area=='{Memory.Area.SOLUTIONS.value}'",
188 + )
189 + if rem:
190 + rem_txt = "\n\n".join(Memory.format_docs_plain(rem))
191 + log_item.update(replaced=rem_txt)
192 +
193 + # insert new solution
194 + await db.insert_text(text=txt, metadata={"area": Memory.Area.SOLUTIONS.value})
195 +
196 + log_item.update(
197 + result=f"{len(solutions)} solutions memorized.",
198 + heading=f"{len(solutions)} solutions memorized.",
199 + )
200 + if rem:
201 + log_item.stream(result=f"\nReplaced {len(rem)} previous solutions.")
202 +
203 +
204 + except Exception as e:
205 + err = errors.format_error(e)
206 + self.agent.context.log.log(
207 + type="warning", heading="Memorize solutions extension error", content=err
208 + )
plugins/memory/extensions/monologue_start/_10_memory_init.py new
+20
@@ -0,0 +1,20 @@
1 +from python.helpers.extension import Extension
2 +from agent import LoopData
3 +import asyncio
4 +
5 +# Import memory from plugin
6 +import sys
7 +from pathlib import Path
8 +_plugin_root = Path(__file__).parent.parent.parent
9 +if str(_plugin_root) not in sys.path:
10 + sys.path.insert(0, str(_plugin_root))
11 +from helpers import memory
12 +
13 +
14 +class MemoryInit(Extension):
15 +
16 + async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
17 + db = await memory.Memory.get(self.agent)
18 +
19 +
20 +
\ No newline at end of file
plugins/memory/helpers/knowledge_import.py renamed
plugins/memory/helpers/memory.py renamed
+2 -2
@@ -23,9 +23,9 @@ import os, json
23 import numpy as np
24
25 from python.helpers.print_style import PrintStyle
26 -from . import files
26 +from python.helpers import files
27 from langchain_core.documents import Document
28 -from python.helpers import knowledge_import
28 +from . import knowledge_import
29 from python.helpers.log import Log, LogItem
30 from enum import Enum
31 from agent import Agent, AgentContext
plugins/memory/helpers/memory_consolidation.py renamed
+9 -2
@@ -7,13 +7,20 @@ from enum import Enum
7
8 from langchain_core.documents import Document
9
10 -from python.helpers.memory import Memory
10 +from .memory import Memory
11 from python.helpers.dirty_json import DirtyJson
12 from python.helpers.log import LogItem
13 from python.helpers.print_style import PrintStyle
14 -from python.tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
14 from agent import Agent
15
16 +# Import from tools within plugin
17 +import sys
18 +from pathlib import Path
19 +_plugin_root = Path(__file__).parent.parent
20 +if str(_plugin_root) not in sys.path:
21 + sys.path.insert(0, str(_plugin_root))
22 +from tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
23 +
24
25 class ConsolidationAction(Enum):
26 """Actions that can be taken during memory consolidation."""
plugins/memory/plugin.json new
+151
@@ -0,0 +1,151 @@
1 +{
2 + "id": "memory",
3 + "name": "Memory System",
4 + "version": "1.0.0",
5 + "author": "Agent Zero Team",
6 + "description": "FAISS-based vector memory with embeddings caching and knowledge preloading",
7 + "tags": ["memory", "vector", "faiss", "core"],
8 + "provides": {
9 + "helper": [
10 + {
11 + "module": "helpers/memory.py",
12 + "description": "Core Memory class with FAISS vector store"
13 + },
14 + {
15 + "module": "helpers/memory_consolidation.py",
16 + "description": "Intelligent memory deduplication"
17 + },
18 + {
19 + "module": "helpers/knowledge_import.py",
20 + "description": "Knowledge file ingestion pipeline"
21 + }
22 + ],
23 + "tool": [
24 + {
25 + "module": "tools/memory_save.py",
26 + "description": "Save memories"
27 + },
28 + {
29 + "module": "tools/memory_load.py",
30 + "description": "Load/search memories"
31 + },
32 + {
33 + "module": "tools/memory_delete.py",
34 + "description": "Delete specific memories"
35 + },
36 + {
37 + "module": "tools/memory_forget.py",
38 + "description": "Forget memories by query"
39 + }
40 + ],
41 + "extension": [
42 + {
43 + "module": "extensions/monologue_start/_10_memory_init.py",
44 + "description": "Initialize memory on monologue start"
45 + },
46 + {
47 + "module": "extensions/monologue_end/_50_memorize_fragments.py",
48 + "description": "Auto-memorize conversation fragments"
49 + },
50 + {
51 + "module": "extensions/monologue_end/_51_memorize_solutions.py",
52 + "description": "Auto-memorize solutions"
53 + },
54 + {
55 + "module": "extensions/message_loop_prompts_after/_50_recall_memories.py",
56 + "description": "Recall relevant memories into prompt"
57 + }
58 + ],
59 + "api": [
60 + {
61 + "module": "api/memory_dashboard.py",
62 + "description": "Memory browser and management dashboard"
63 + }
64 + ],
65 + "ui": [
66 + {
67 + "component": "ui/memory-dashboard.html",
68 + "module": "ui/memory-dashboard-store.js",
69 + "description": "Memory Dashboard UI"
70 + },
71 + {
72 + "component": "ui/memory-detail-modal.html",
73 + "description": "Memory Detail Modal"
74 + }
75 + ],
76 + "prompt": [
77 + {
78 + "module": "prompts/agent.system.memories.md",
79 + "description": "Agent system prompt for memories"
80 + },
81 + {
82 + "module": "prompts/agent.system.solutions.md",
83 + "description": "Agent system prompt for solutions"
84 + },
85 + {
86 + "module": "prompts/agent.system.tool.memory.md",
87 + "description": "Memory tool system prompt"
88 + },
89 + {
90 + "module": "prompts/fw.memory.hist_suc.sys.md",
91 + "description": "Framework memory history success prompt"
92 + },
93 + {
94 + "module": "prompts/fw.memory.hist_sum.sys.md",
95 + "description": "Framework memory history summary prompt"
96 + },
97 + {
98 + "module": "prompts/fw.memory_saved.md",
99 + "description": "Framework memory saved prompt"
100 + },
101 + {
102 + "module": "prompts/memory.consolidation.msg.md",
103 + "description": "Memory consolidation message"
104 + },
105 + {
106 + "module": "prompts/memory.consolidation.sys.md",
107 + "description": "Memory consolidation system prompt"
108 + },
109 + {
110 + "module": "prompts/memory.keyword_extraction.msg.md",
111 + "description": "Keyword extraction message"
112 + },
113 + {
114 + "module": "prompts/memory.keyword_extraction.sys.md",
115 + "description": "Keyword extraction system prompt"
116 + },
117 + {
118 + "module": "prompts/memory.memories_filter.msg.md",
119 + "description": "Memories filter message"
120 + },
121 + {
122 + "module": "prompts/memory.memories_filter.sys.md",
123 + "description": "Memories filter system prompt"
124 + },
125 + {
126 + "module": "prompts/memory.memories_query.msg.md",
127 + "description": "Memories query message"
128 + },
129 + {
130 + "module": "prompts/memory.memories_query.sys.md",
131 + "description": "Memory query system prompt"
132 + },
133 + {
134 + "module": "prompts/memory.memories_sum.sys.md",
135 + "description": "Memories summary system prompt"
136 + },
137 + {
138 + "module": "prompts/memory.recall_delay_msg.md",
139 + "description": "Recall delay message"
140 + },
141 + {
142 + "module": "prompts/memory.solutions_query.sys.md",
143 + "description": "Solutions query system prompt"
144 + },
145 + {
146 + "module": "prompts/memory.solutions_sum.sys.md",
147 + "description": "Solutions summary system prompt"
148 + }
149 + ]
150 + }
151 +}
plugins/memory/prompts/agent.system.memories.md renamed
plugins/memory/prompts/agent.system.solutions.md renamed
plugins/memory/prompts/agent.system.tool.memory.md renamed
plugins/memory/prompts/fw.memory.hist_suc.sys.md renamed
plugins/memory/prompts/fw.memory.hist_sum.sys.md renamed
plugins/memory/prompts/fw.memory_saved.md renamed
plugins/memory/prompts/memory.consolidation.msg.md renamed
plugins/memory/prompts/memory.consolidation.sys.md renamed
plugins/memory/prompts/memory.keyword_extraction.msg.md renamed
plugins/memory/prompts/memory.keyword_extraction.sys.md renamed
plugins/memory/prompts/memory.memories_filter.msg.md renamed
plugins/memory/prompts/memory.memories_filter.sys.md renamed
plugins/memory/prompts/memory.memories_query.msg.md renamed
plugins/memory/prompts/memory.memories_query.sys.md renamed
plugins/memory/prompts/memory.memories_sum.sys.md renamed
plugins/memory/prompts/memory.recall_delay_msg.md renamed
plugins/memory/prompts/memory.solutions_query.sys.md renamed
plugins/memory/prompts/memory.solutions_sum.sys.md renamed
plugins/memory/tools/memory_delete.py renamed
+8 -1
@@ -1,6 +1,13 @@
1 -from python.helpers.memory import Memory
1 from python.helpers.tool import Tool, Response
2
3 +# Import Memory from plugin
4 +import sys
5 +from pathlib import Path
6 +_plugin_root = Path(__file__).parent.parent
7 +if str(_plugin_root) not in sys.path:
8 + sys.path.insert(0, str(_plugin_root))
9 +from helpers.memory import Memory
10 +
11
12 class MemoryDelete(Tool):
13
plugins/memory/tools/memory_forget.py renamed
+9 -2
@@ -1,6 +1,13 @@
1 -from python.helpers.memory import Memory
1 from python.helpers.tool import Tool, Response
3 -from python.tools.memory_load import DEFAULT_THRESHOLD
2 +
3 +# Import Memory and DEFAULT_THRESHOLD from plugin
4 +import sys
5 +from pathlib import Path
6 +_plugin_root = Path(__file__).parent.parent
7 +if str(_plugin_root) not in sys.path:
8 + sys.path.insert(0, str(_plugin_root))
9 +from helpers.memory import Memory
10 +from tools.memory_load import DEFAULT_THRESHOLD
11
12
13 class MemoryForget(Tool):
plugins/memory/tools/memory_load.py renamed
+8 -1
@@ -1,6 +1,13 @@
1 -from python.helpers.memory import Memory
1 from python.helpers.tool import Tool, Response
2
3 +# Import Memory from plugin
4 +import sys
5 +from pathlib import Path
6 +_plugin_root = Path(__file__).parent.parent
7 +if str(_plugin_root) not in sys.path:
8 + sys.path.insert(0, str(_plugin_root))
9 +from helpers.memory import Memory
10 +
11 DEFAULT_THRESHOLD = 0.7
12 DEFAULT_LIMIT = 10
13
plugins/memory/tools/memory_save.py renamed
+8 -1
@@ -1,6 +1,13 @@
1 -from python.helpers.memory import Memory
1 from python.helpers.tool import Tool, Response
2
3 +# Import Memory from plugin
4 +import sys
5 +from pathlib import Path
6 +_plugin_root = Path(__file__).parent.parent
7 +if str(_plugin_root) not in sys.path:
8 + sys.path.insert(0, str(_plugin_root))
9 +from helpers.memory import Memory
10 +
11
12 class MemorySave(Tool):
13
plugins/memory/ui/memory-dashboard-store.js renamed
+2 -2
@@ -53,7 +53,7 @@ const memoryDashboardStore = {
53 pollingEnabled: false,
54
55 async openModal() {
56 - await openModal("modals/memory/memory-dashboard.html");
56 + await openModal("../plugins/memory/ui/memory-dashboard.html");
57 },
58
59 init() {
@@ -448,7 +448,7 @@ ${memory.content_full}
448 this.editMode = false;
449 this.editMemoryBackup = null;
450 // Use global modal system
451 - openModal("modals/memory/memory-detail-modal.html");
451 + openModal("../plugins/memory/ui/memory-detail-modal.html");
452 },
453
454 closeMemoryDetails() {
plugins/memory/ui/memory-dashboard.html renamed
+1 -1
@@ -3,7 +3,7 @@
3 <head>
4 <title>Memory Dashboard</title>
5 <script type="module">
6 - import { store } from "/components/modals/memory/memory-dashboard-store.js";
6 + import { store } from "/plugins/memory/ui/memory-dashboard-store.js";
7 </script>
8 </head>
9
plugins/memory/ui/memory-detail-modal.html renamed
python/api/import_knowledge.py
+3 -1
@@ -1,5 +1,7 @@
1 from python.helpers.api import ApiHandler, Request, Response
2 -from python.helpers import files, memory
2 +from python.helpers import files
3 +from python.helpers.plugins import import_plugin_module
4 +memory = import_plugin_module("memory", "helpers/memory.py")
5 import os
6 from python.helpers.security import safe_filename
7
python/api/knowledge_path_get.py
+3 -1
@@ -1,5 +1,7 @@
1 from python.helpers.api import ApiHandler, Request, Response
2 -from python.helpers import files, memory, notification, projects, notification
2 +from python.helpers import files, notification, projects, notification
3 +from python.helpers.plugins import import_plugin_module
4 +memory = import_plugin_module("memory", "helpers/memory.py")
5 import os
6 from werkzeug.utils import secure_filename
7
python/api/knowledge_reindex.py
+3 -1
@@ -1,5 +1,7 @@
1 from python.helpers.api import ApiHandler, Request, Response
2 -from python.helpers import files, memory, notification, projects, notification
2 +from python.helpers import files, notification, projects, notification
3 +from python.helpers.plugins import import_plugin_module
4 +memory = import_plugin_module("memory", "helpers/memory.py")
5 import os
6
7
python/api/plugins_list.py new
+22
@@ -0,0 +1,22 @@
1 +from python.helpers.api import ApiHandler, Request, Response
2 +from python.helpers import plugins
3 +
4 +
5 +class PluginsList(ApiHandler):
6 + """
7 + API handler for listing all available plugins.
8 + Returns a list of plugin manifests.
9 + """
10 +
11 + @classmethod
12 + def get_methods(cls):
13 + return ["GET", "POST"]
14 +
15 + async def process(self, input: dict, request: Request) -> dict | Response:
16 + # Get all available plugins
17 + plugin_list = plugins.list_plugins()
18 +
19 + # Serialize plugin objects using helper
20 + data = [plugins.build_plugin_response_data(p) for p in plugin_list]
21 +
22 + return {"ok": True, "data": data}
python/api/plugins_resolve.py new
+50
@@ -0,0 +1,50 @@
1 +from python.helpers.api import ApiHandler, Request, Response
2 +from python.helpers import plugins
3 +
4 +
5 +class PluginsResolve(ApiHandler):
6 + """
7 + API handler for resolving plugin manifests.
8 + Accepts a plugin ID or list of IDs and returns normalized manifest(s) with URLs and props.
9 +
10 + Single ID: {"id": "example"} -> {"ok": True, "data": {...}}
11 + Multiple IDs: {"ids": ["example", "another"]} -> {"ok": True, "data": [{...}, {...}]}
12 + """
13 +
14 + @classmethod
15 + def get_methods(cls):
16 + return ["POST"]
17 +
18 + async def process(self, input: dict, request: Request) -> dict | Response:
19 + # Support both single ID and array of IDs
20 + plugin_id = input.get("id")
21 + plugin_ids = input.get("ids")
22 +
23 + # Batch mode: array of IDs
24 + if plugin_ids:
25 + if not isinstance(plugin_ids, list):
26 + return {"ok": False, "error": "ids must be an array"}
27 +
28 + results = []
29 + for pid in plugin_ids:
30 + plugin = plugins.find_plugin(pid)
31 + if plugin:
32 + results.append(plugins.build_plugin_response_data(plugin))
33 + else:
34 + results.append({
35 + "id": pid,
36 + "error": f"Plugin '{pid}' not found or invalid manifest"
37 + })
38 +
39 + return {"ok": True, "data": results}
40 +
41 + # Single mode: one ID
42 + elif plugin_id:
43 + plugin = plugins.find_plugin(plugin_id)
44 + if not plugin:
45 + return {"ok": False, "error": f"Plugin '{plugin_id}' not found or invalid manifest"}
46 +
47 + return {"ok": True, "data": plugins.build_plugin_response_data(plugin)}
48 +
49 + else:
50 + return {"ok": False, "error": "Missing plugin ID or IDs"}
python/extensions/message_loop_prompts_after/_50_recall_memories.py
+6 -217
@@ -1,219 +1,8 @@
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 -from python.helpers import dirty_json, errors, settings, log
1 +"""Recall memories extension - implementation provided by the memory plugin."""
2 +from python.helpers.plugins import import_plugin_module
3
4 +# Import the actual implementation from the plugin
5 +_mod = import_plugin_module("memory", "extensions/message_loop_prompts_after/_50_recall_memories.py")
6
9 -DATA_NAME_TASK = "_recall_memories_task"
10 -DATA_NAME_ITER = "_recall_memories_iter"
11 -SEARCH_TIMEOUT = 30
12 -
13 -
14 -class RecallMemories(Extension):
15 -
16 - # INTERVAL = 3
17 - # HISTORY = 10000
18 - # MEMORIES_MAX_SEARCH = 12
19 - # SOLUTIONS_MAX_SEARCH = 8
20 - # MEMORIES_MAX_RESULT = 5
21 - # SOLUTIONS_MAX_RESULT = 3
22 - # THRESHOLD = DEFAULT_MEMORY_THRESHOLD
23 -
24 - async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
25 -
26 - set = settings.get_settings()
27 -
28 - # turned off in settings?
29 - if not set["memory_recall_enabled"]:
30 - return
31 -
32 - # every X iterations (or the first one) recall memories
33 - if loop_data.iteration % set["memory_recall_interval"] == 0:
34 -
35 - # show util message right away
36 - log_item = self.agent.context.log.log(
37 - type="util",
38 - heading="Searching memories...",
39 - )
40 -
41 - task = asyncio.create_task(
42 - asyncio.wait_for(
43 - self.search_memories(loop_data=loop_data, log_item=log_item, **kwargs),
44 - timeout=SEARCH_TIMEOUT,
45 - )
46 - )
47 - else:
48 - task = None
49 -
50 - # set to agent to be able to wait for it
51 - self.agent.set_data(DATA_NAME_TASK, task)
52 - self.agent.set_data(DATA_NAME_ITER, loop_data.iteration)
53 -
54 - async def search_memories(self, log_item: log.LogItem, loop_data: LoopData, **kwargs):
55 -
56 - # cleanup
57 - extras = loop_data.extras_persistent
58 - if "memories" in extras:
59 - del extras["memories"]
60 - if "solutions" in extras:
61 - del extras["solutions"]
62 -
63 -
64 - set = settings.get_settings()
65 - # try:
66 -
67 - # get system message and chat history for util llm
68 - system = self.agent.read_prompt("memory.memories_query.sys.md")
69 -
70 - # # log query streamed by LLM
71 - # async def log_callback(content):
72 - # log_item.stream(query=content)
73 -
74 - # call util llm to summarize conversation
75 - user_instruction = (
76 - loop_data.user_message.output_text() if loop_data.user_message else "None"
77 - )
78 - history = self.agent.history.output_text()[-set["memory_recall_history_len"]:]
79 - message = self.agent.read_prompt(
80 - "memory.memories_query.msg.md", history=history, message=user_instruction
81 - )
82 -
83 - # if query preparation by AI is enabled
84 - if set["memory_recall_query_prep"]:
85 - try:
86 - # call util llm to generate search query from the conversation
87 - query = await self.agent.call_utility_model(
88 - system=system,
89 - message=message,
90 - # callback=log_callback,
91 - )
92 - query = query.strip()
93 - log_item.update(query=query) # no need for streaming here
94 - except Exception as e:
95 - err = errors.format_error(e)
96 - self.agent.context.log.log(
97 - type="warning", heading="Recall memories extension error:", content=err
98 - )
99 - query = ""
100 -
101 - # no query, no search
102 - if not query:
103 - log_item.update(
104 - heading="Failed to generate memory query",
105 - )
106 - return
107 -
108 - # otherwise use the message and history as query
109 - else:
110 - query = user_instruction + "\n\n" + history
111 -
112 - # if there is no query (or just dash by the LLM), do not continue
113 - if not query or len(query) <= 3:
114 - log_item.update(
115 - query="No relevant memory query generated, skipping search",
116 - )
117 - return
118 -
119 - # get memory database
120 - db = await Memory.get(self.agent)
121 -
122 - # search for general memories and fragments
123 - memories = await db.search_similarity_threshold(
124 - query=query,
125 - limit=set["memory_recall_memories_max_search"],
126 - threshold=set["memory_recall_similarity_threshold"],
127 - filter=f"area == '{Memory.Area.MAIN.value}' or area == '{Memory.Area.FRAGMENTS.value}'", # exclude solutions
128 - )
129 -
130 - # search for solutions
131 - solutions = await db.search_similarity_threshold(
132 - query=query,
133 - limit=set["memory_recall_solutions_max_search"],
134 - threshold=set["memory_recall_similarity_threshold"],
135 - filter=f"area == '{Memory.Area.SOLUTIONS.value}'", # exclude solutions
136 - )
137 -
138 - if not memories and not solutions:
139 - log_item.update(
140 - heading="No memories or solutions found",
141 - )
142 - return
143 -
144 - # if post filtering is enabled
145 - if set["memory_recall_post_filter"]:
146 - # assemble an enumerated dict of memories and solutions for AI validation
147 - mems_list = {i: memory.page_content for i, memory in enumerate(memories + solutions)}
148 -
149 - # call AI to validate the memories
150 - try:
151 - filter = await self.agent.call_utility_model(
152 - system=self.agent.read_prompt("memory.memories_filter.sys.md"),
153 - message=self.agent.read_prompt(
154 - "memory.memories_filter.msg.md",
155 - memories=mems_list,
156 - history=history,
157 - message=user_instruction,
158 - ),
159 - )
160 - filter_inds = dirty_json.try_parse(filter)
161 -
162 - # filter memories and solutions based on filter_inds
163 - filtered_memories = []
164 - filtered_solutions = []
165 - mem_len = len(memories)
166 -
167 - # process each index in filter_inds
168 - # make sure filter_inds is a list and contains valid integers
169 - if isinstance(filter_inds, list):
170 - for idx in filter_inds:
171 - if isinstance(idx, int):
172 - if idx < mem_len:
173 - # this is a memory
174 - filtered_memories.append(memories[idx])
175 - else:
176 - # this is a solution, adjust index
177 - sol_idx = idx - mem_len
178 - if sol_idx < len(solutions):
179 - filtered_solutions.append(solutions[sol_idx])
180 -
181 - # replace original lists with filtered ones
182 - memories = filtered_memories
183 - solutions = filtered_solutions
184 -
185 - except Exception as e:
186 - err = errors.format_error(e)
187 - self.agent.context.log.log(
188 - type="warning", heading="Failed to filter relevant memories", content=err
189 - )
190 - filter_inds = []
191 -
192 -
193 - # limit the number of memories and solutions
194 - memories = memories[: set["memory_recall_memories_max_result"]]
195 - solutions = solutions[: set["memory_recall_solutions_max_result"]]
196 -
197 - # log the search result
198 - log_item.update(
199 - heading=f"{len(memories)} memories and {len(solutions)} relevant solutions found",
200 - )
201 -
202 - memories_txt = "\n\n".join([mem.page_content for mem in memories]) if memories else ""
203 - solutions_txt = "\n\n".join([sol.page_content for sol in solutions]) if solutions else ""
204 -
205 - # log the full results
206 - if memories_txt:
207 - log_item.update(memories=memories_txt)
208 - if solutions_txt:
209 - log_item.update(solutions=solutions_txt)
210 -
211 - # place to prompt
212 - if memories_txt:
213 - extras["memories"] = self.agent.parse_prompt(
214 - "agent.system.memories.md", memories=memories_txt
215 - )
216 - if solutions_txt:
217 - extras["solutions"] = self.agent.parse_prompt(
218 - "agent.system.solutions.md", solutions=solutions_txt
219 - )
7 +# Re-export all public names
8 +globals().update({k: v for k, v in vars(_mod).items() if not k.startswith('_')})
python/extensions/monologue_end/_50_memorize_fragments.py
+6 -196
@@ -1,198 +1,8 @@
1 -import asyncio
2 -from python.helpers import settings, errors
3 -from python.helpers.extension import Extension
4 -from python.helpers.memory import Memory
5 -from python.helpers.dirty_json import DirtyJson
6 -from agent import LoopData
7 -from python.helpers.log import LogItem
8 -from python.tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
9 -from python.helpers.defer import DeferredTask, THREAD_BACKGROUND
1 +"""Memorize fragments extension - implementation provided by the memory plugin."""
2 +from python.helpers.plugins import import_plugin_module
3
4 +# Import the actual implementation from the plugin
5 +_mod = import_plugin_module("memory", "extensions/monologue_end/_50_memorize_fragments.py")
6
12 -class MemorizeMemories(Extension):
13 -
14 - async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
15 - # try:
16 -
17 - set = settings.get_settings()
18 -
19 - if not set["memory_memorize_enabled"]:
20 - return
21 -
22 - # show full util message
23 - log_item = self.agent.context.log.log(
24 - type="util",
25 - heading="Memorizing new information...",
26 - )
27 -
28 - # memorize in background
29 - task = DeferredTask(thread_name=THREAD_BACKGROUND)
30 - task.start_task(self.memorize, loop_data, log_item)
31 - # task = asyncio.create_task(self.memorize(loop_data, log_item))
32 - return task
33 -
34 - async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs):
35 -
36 - try:
37 - set = settings.get_settings()
38 -
39 - db = await Memory.get(self.agent)
40 -
41 - # get system message and chat history for util llm
42 - system = self.agent.read_prompt("memory.memories_sum.sys.md")
43 - msgs_text = self.agent.concat_messages(self.agent.history)
44 -
45 - # # log query streamed by LLM
46 - # async def log_callback(content):
47 - # log_item.stream(content=content)
48 -
49 - # call util llm to find info in history
50 - memories_json = await self.agent.call_utility_model(
51 - system=system,
52 - message=msgs_text,
53 - # callback=log_callback,
54 - background=True,
55 - )
56 -
57 - # log data < no need for streaming utility messages
58 - log_item.update(content=memories_json)
59 -
60 - # Add validation and error handling for memories_json
61 - if not memories_json or not isinstance(memories_json, str):
62 - log_item.update(heading="No response from utility model.")
63 - return
64 -
65 - # Strip any whitespace that might cause issues
66 - memories_json = memories_json.strip()
67 -
68 - if not memories_json:
69 - log_item.update(heading="Empty response from utility model.")
70 - return
71 -
72 - try:
73 - memories = DirtyJson.parse_string(memories_json)
74 - except Exception as e:
75 - log_item.update(heading=f"Failed to parse memories response: {str(e)}")
76 - return
77 -
78 - # Validate that memories is a list or convertible to one
79 - if memories is None:
80 - log_item.update(heading="No valid memories found in response.")
81 - return
82 -
83 - # If memories is not a list, try to make it one
84 - if not isinstance(memories, list):
85 - if isinstance(memories, (str, dict)):
86 - memories = [memories]
87 - else:
88 - log_item.update(heading="Invalid memories format received.")
89 - return
90 -
91 - if not isinstance(memories, list) or len(memories) == 0:
92 - log_item.update(heading="No useful information to memorize.")
93 - return
94 - else:
95 - memories_txt = "\n\n".join([str(memory) for memory in memories]).strip()
96 - log_item.update(heading=f"{len(memories)} entries to memorize.", memories=memories_txt)
97 -
98 - # Process memories with intelligent consolidation
99 - total_processed = 0
100 - total_consolidated = 0
101 - rem = []
102 -
103 - for memory in memories:
104 - # Convert memory to plain text
105 - txt = f"{memory}"
106 -
107 - if set["memory_memorize_consolidation"]:
108 -
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=8,
116 - max_llm_context_memories=4
117 - )
118 -
119 - # Create memory item-specific log for detailed tracking
120 - memory_log = None # too many utility messages, skip log for now
121 - # memory_log = self.agent.context.log.log(
122 - # type="util",
123 - # heading=f"Processing memory fragment: {txt[:50]}...",
124 - # update_progress="none" # Don't affect status bar
125 - # )
126 -
127 - # Process with intelligent consolidation
128 - result_obj = await consolidator.process_new_memory(
129 - new_memory=txt,
130 - area=Memory.Area.FRAGMENTS.value,
131 - metadata={"area": Memory.Area.FRAGMENTS.value},
132 - log_item=memory_log
133 - )
134 -
135 - # Update the individual log item with completion status but keep it temporary
136 - if result_obj.get("success"):
137 - total_consolidated += 1
138 - if memory_log:
139 - memory_log.update(
140 - result="Fragment processed successfully",
141 - heading=f"Memory fragment completed: {txt[:50]}...",
142 - update_progress="none" # Show briefly then disappear
143 - )
144 - else:
145 - if memory_log:
146 - memory_log.update(
147 - result="Fragment processing failed",
148 - heading=f"Memory fragment failed: {txt[:50]}...",
149 - update_progress="none" # Show briefly then disappear
150 - )
151 - total_processed += 1
152 -
153 - except Exception as e:
154 - # Log error but continue processing
155 - log_item.update(consolidation_error=str(e))
156 - total_processed += 1
157 -
158 - # Update final results with structured logging
159 - log_item.update(
160 - heading=f"Memorization completed: {total_processed} memories processed, {total_consolidated} intelligently consolidated",
161 - memories=memories_txt,
162 - result=f"{total_processed} memories processed, {total_consolidated} intelligently consolidated",
163 - memories_processed=total_processed,
164 - memories_consolidated=total_consolidated,
165 - update_progress="none"
166 - )
167 -
168 - else:
169 -
170 - # remove previous fragments too similiar to this one
171 - if set["memory_memorize_replace_threshold"] > 0:
172 - rem += await db.delete_documents_by_query(
173 - query=txt,
174 - threshold=set["memory_memorize_replace_threshold"],
175 - filter=f"area=='{Memory.Area.FRAGMENTS.value}'",
176 - )
177 - if rem:
178 - rem_txt = "\n\n".join(Memory.format_docs_plain(rem))
179 - log_item.update(replaced=rem_txt)
180 -
181 - # insert new memory
182 - await db.insert_text(text=txt, metadata={"area": Memory.Area.FRAGMENTS.value})
183 -
184 - log_item.update(
185 - result=f"{len(memories)} entries memorized.",
186 - heading=f"{len(memories)} entries memorized.",
187 - )
188 - if rem:
189 - log_item.stream(result=f"\nReplaced {len(rem)} previous memories.")
190 -
191 -
192 -
193 -
194 - except Exception as e:
195 - err = errors.format_error(e)
196 - self.agent.context.log.log(
197 - type="warning", heading="Memorize memories extension error", content=err
198 - )
7 +# Re-export all public names
8 +globals().update({k: v for k, v in vars(_mod).items() if not k.startswith('_')})
python/extensions/monologue_end/_51_memorize_solutions.py
+6 -199
@@ -1,201 +1,8 @@
1 -import asyncio
2 -from python.helpers import settings, errors
3 -from python.helpers.extension import Extension
4 -from python.helpers.memory import Memory
5 -from python.helpers.dirty_json import DirtyJson
6 -from agent import LoopData
7 -from python.helpers.log import LogItem
8 -from python.tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
9 -from python.helpers.defer import DeferredTask, THREAD_BACKGROUND
1 +"""Memorize solutions extension - implementation provided by the memory plugin."""
2 +from python.helpers.plugins import import_plugin_module
3
11 -class MemorizeSolutions(Extension):
4 +# Import the actual implementation from the plugin
5 +_mod = import_plugin_module("memory", "extensions/monologue_end/_51_memorize_solutions.py")
6
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 - heading="Memorizing succesful solutions...",
25 - )
26 -
27 - # memorize in background
28 - task = DeferredTask(thread_name=THREAD_BACKGROUND)
29 - task.start_task(self.memorize, loop_data, log_item)
30 - # task = asyncio.create_task(self.memorize(loop_data, log_item))
31 - return task
32 -
33 - async def memorize(self, loop_data: LoopData, log_item: LogItem, **kwargs):
34 - try:
35 - set = settings.get_settings()
36 -
37 - db = await Memory.get(self.agent)
38 -
39 - # get system message and chat history for util llm
40 - system = self.agent.read_prompt("memory.solutions_sum.sys.md")
41 - msgs_text = self.agent.concat_messages(self.agent.history)
42 -
43 - # log query streamed by LLM
44 - # async def log_callback(content):
45 - # log_item.stream(content=content)
46 -
47 - # call util llm to find solutions in history
48 - solutions_json = await self.agent.call_utility_model(
49 - system=system,
50 - message=msgs_text,
51 - # callback=log_callback,
52 - background=True,
53 - )
54 -
55 - # log query < no need for streaming utility messages
56 - log_item.update(content=solutions_json)
57 -
58 -
59 -
60 - # Add validation and error handling for solutions_json
61 - if not solutions_json or not isinstance(solutions_json, str):
62 - log_item.update(heading="No response from utility model.")
63 - return
64 -
65 - # Strip any whitespace that might cause issues
66 - solutions_json = solutions_json.strip()
67 -
68 - if not solutions_json:
69 - log_item.update(heading="Empty response from utility model.")
70 - return
71 -
72 - try:
73 - solutions = DirtyJson.parse_string(solutions_json)
74 - except Exception as e:
75 - log_item.update(heading=f"Failed to parse solutions response: {str(e)}")
76 - return
77 -
78 - # Validate that solutions is a list or convertible to one
79 - if solutions is None:
80 - log_item.update(heading="No valid solutions found in response.")
81 - return
82 -
83 - # If solutions is not a list, try to make it one
84 - if not isinstance(solutions, list):
85 - if isinstance(solutions, (str, dict)):
86 - solutions = [solutions]
87 - else:
88 - log_item.update(heading="Invalid solutions format received.")
89 - return
90 -
91 - if not isinstance(solutions, list) or len(solutions) == 0:
92 - log_item.update(heading="No successful solutions to memorize.")
93 - return
94 - else:
95 - solutions_txt = "\n\n".join([str(solution) for solution in solutions]).strip()
96 - log_item.update(
97 - heading=f"{len(solutions)} successful solutions to memorize.", solutions=solutions_txt
98 - )
99 -
100 - # Process solutions with intelligent consolidation
101 - total_processed = 0
102 - total_consolidated = 0
103 - rem = []
104 -
105 - for solution in solutions:
106 - # Convert solution to structured text
107 - if isinstance(solution, dict):
108 - problem = solution.get('problem', 'Unknown problem')
109 - solution_text = solution.get('solution', 'Unknown solution')
110 - txt = f"# Problem\n {problem}\n# Solution\n {solution_text}"
111 - else:
112 - # If solution is not a dict, convert it to string
113 - txt = f"# Solution\n {str(solution)}"
114 -
115 - if set["memory_memorize_consolidation"]:
116 - try:
117 - # Use intelligent consolidation system
118 - from python.helpers.memory_consolidation import create_memory_consolidator
119 - consolidator = create_memory_consolidator(
120 - self.agent,
121 - similarity_threshold=DEFAULT_MEMORY_THRESHOLD, # More permissive for discovery
122 - max_similar_memories=6, # Fewer for solutions (more complex)
123 - max_llm_context_memories=3
124 - )
125 -
126 - # Create solution-specific log for detailed tracking
127 - solution_log = None # too many utility messages, skip log for now
128 - # solution_log = self.agent.context.log.log(
129 - # type="util",
130 - # heading=f"Processing solution: {txt[:50]}...",
131 - # update_progress="none" # Don't affect status bar
132 - # )
133 -
134 - # Process with intelligent consolidation
135 - result_obj = await consolidator.process_new_memory(
136 - new_memory=txt,
137 - area=Memory.Area.SOLUTIONS.value,
138 - metadata={"area": Memory.Area.SOLUTIONS.value},
139 - log_item=solution_log
140 - )
141 -
142 - # Update the individual log item with completion status but keep it temporary
143 - if result_obj.get("success"):
144 - total_consolidated += 1
145 - if solution_log:
146 - solution_log.update(
147 - result="Solution processed successfully",
148 - heading=f"Solution completed: {txt[:50]}...",
149 - update_progress="none" # Show briefly then disappear
150 - )
151 - else:
152 - if solution_log:
153 - solution_log.update(
154 - result="Solution processing failed",
155 - heading=f"Solution failed: {txt[:50]}...",
156 - update_progress="none" # Show briefly then disappear
157 - )
158 - total_processed += 1
159 -
160 - except Exception as e:
161 - # Log error but continue processing
162 - log_item.update(consolidation_error=str(e))
163 - total_processed += 1
164 -
165 - # Update final results with structured logging
166 - log_item.update(
167 - heading=f"Solution memorization completed: {total_processed} solutions processed, {total_consolidated} intelligently consolidated",
168 - solutions=solutions_txt,
169 - result=f"{total_processed} solutions processed, {total_consolidated} intelligently consolidated",
170 - solutions_processed=total_processed,
171 - solutions_consolidated=total_consolidated,
172 - update_progress="none"
173 - )
174 - else:
175 - # remove previous solutions too similiar to this one
176 - if set["memory_memorize_replace_threshold"] > 0:
177 - rem += await db.delete_documents_by_query(
178 - query=txt,
179 - threshold=set["memory_memorize_replace_threshold"],
180 - filter=f"area=='{Memory.Area.SOLUTIONS.value}'",
181 - )
182 - if rem:
183 - rem_txt = "\n\n".join(Memory.format_docs_plain(rem))
184 - log_item.update(replaced=rem_txt)
185 -
186 - # insert new solution
187 - await db.insert_text(text=txt, metadata={"area": Memory.Area.SOLUTIONS.value})
188 -
189 - log_item.update(
190 - result=f"{len(solutions)} solutions memorized.",
191 - heading=f"{len(solutions)} solutions memorized.",
192 - )
193 - if rem:
194 - log_item.stream(result=f"\nReplaced {len(rem)} previous solutions.")
195 -
196 -
197 - except Exception as e:
198 - err = errors.format_error(e)
199 - self.agent.context.log.log(
200 - type="warning", heading="Memorize solutions extension error", content=err
201 - )
7 +# Re-export all public names
8 +globals().update({k: v for k, v in vars(_mod).items() if not k.startswith('_')})
python/extensions/monologue_start/_10_memory_init.py
+6 -11
@@ -1,13 +1,8 @@
1 -from python.helpers.extension import Extension
2 -from agent import LoopData
3 -from python.helpers import memory
4 -import asyncio
1 +"""Memory initialization extension - implementation provided by the memory plugin."""
2 +from python.helpers.plugins import import_plugin_module
3
4 +# Import the actual implementation from the plugin
5 +_mod = import_plugin_module("memory", "extensions/monologue_start/_10_memory_init.py")
6
7 -class MemoryInit(Extension):
8 -
9 - async def execute(self, loop_data: LoopData = LoopData(), **kwargs):
10 - db = await memory.Memory.get(self.agent)
11 -
12 -
13 -
\ No newline at end of file
7 +# Re-export all public names
8 +globals().update({k: v for k, v in vars(_mod).items() if not k.startswith('_')})
python/extensions/system_prompt/_20_behaviour_prompt.py
+4 -1
@@ -1,7 +1,10 @@
1 from datetime import datetime
2 from python.helpers.extension import Extension
3 from agent import Agent, LoopData
4 -from python.helpers import files, memory
4 +from python.helpers import files
5 +from python.helpers.plugins import import_plugin_module
6 +
7 +memory = import_plugin_module("memory", "helpers/memory.py")
8
9
10 class BehaviourPrompt(Extension):
python/helpers/extension.py
+6 -1
@@ -27,10 +27,15 @@ class Extension:
27 async def call_extensions(
28 extension_point: str, agent: "Agent|None" = None, **kwargs
29 ) -> Any:
30 - from python.helpers import projects, subagents
30 + from python.helpers import projects, subagents, plugins
31
32 # search for extension folders in all agent's paths
33 paths = subagents.get_paths(agent, "extensions", extension_point, default_root="python")
34 +
35 + # Add plugin extension paths
36 + plugin_paths = plugins.get_plugin_paths("extension", extension_point)
37 + paths.extend(plugin_paths)
38 +
39 all_exts = [cls for path in paths for cls in _get_extensions(path)]
40
41 # merge: first ocurrence of file name is the override
python/helpers/plugins.py new
+262
@@ -0,0 +1,262 @@
1 +from __future__ import annotations
2 +
3 +import json
4 +from dataclasses import dataclass, field
5 +from pathlib import Path
6 +from typing import Any, Dict, List, Optional
7 +
8 +from python.helpers import files
9 +
10 +
11 +# ============================================================================
12 +# Core Data Structures
13 +# ============================================================================
14 +
15 +@dataclass(slots=True)
16 +class Plugin:
17 + id: str
18 + name: str
19 + path: Path
20 + manifest_path: Path
21 + provides: Dict[str, Any] = field(default_factory=dict) # capability map
22 + version: str = ""
23 + author: str = ""
24 + description: str = ""
25 + tags: List[str] = field(default_factory=list)
26 +
27 + # Optional heavy fields
28 + raw_manifest: Dict[str, Any] = field(default_factory=dict)
29 +
30 +
31 +# ============================================================================
32 +# Discovery & Loading
33 +# ============================================================================
34 +
35 +def get_plugin_roots() -> List[str]:
36 + """
37 + Get plugin root directories.
38 + Priority: project plugins > usr plugins > core plugins
39 + """
40 + # Project-specific plugins
41 + projects = files.find_existing_paths_by_pattern("usr/projects/*/.a0proj/plugins")
42 +
43 + return [
44 + *projects,
45 + files.get_abs_path("usr/plugins"),
46 + files.get_abs_path("plugins"),
47 + ]
48 +
49 +
50 +def discover_plugin_manifests(root: str) -> List[Path]:
51 + """Recursively discover plugin.json files under a root directory."""
52 + root_path = Path(root)
53 + if not root_path.exists():
54 + return []
55 +
56 + results: List[Path] = []
57 + for p in root_path.rglob("plugin.json"):
58 + if p.is_file() and ".git" not in str(p):
59 + results.append(p)
60 +
61 + return sorted(results, key=lambda x: str(x))
62 +
63 +
64 +def plugin_from_manifest(manifest_path: Path) -> Optional[Plugin]:
65 + """Load plugin from manifest file."""
66 + try:
67 + text = manifest_path.read_text(encoding="utf-8", errors="replace")
68 + manifest = json.loads(text)
69 + except Exception:
70 + return None
71 +
72 + if not isinstance(manifest, dict):
73 + return None
74 +
75 + plugin_dir = manifest_path.parent
76 + plugin_id = manifest.get("id", "").strip()
77 +
78 + if not plugin_id:
79 + return None
80 +
81 + plugin = Plugin(
82 + id=plugin_id,
83 + name=manifest.get("name", plugin_id),
84 + path=plugin_dir,
85 + manifest_path=manifest_path,
86 + provides=manifest.get("provides", {}),
87 + version=manifest.get("version", ""),
88 + author=manifest.get("author", ""),
89 + description=manifest.get("description", ""),
90 + tags=manifest.get("tags", []),
91 + raw_manifest=manifest,
92 + )
93 +
94 + return plugin
95 +
96 +
97 +# ============================================================================
98 +# Registry Access (List/Find)
99 +# ============================================================================
100 +
101 +def list_plugins() -> List[Plugin]:
102 + """List all discovered plugins."""
103 + plugins: List[Plugin] = []
104 +
105 + for root in get_plugin_roots():
106 + for manifest_path in discover_plugin_manifests(root):
107 + p = plugin_from_manifest(manifest_path)
108 + if p:
109 + plugins.append(p)
110 +
111 + # Dedupe by ID (earlier roots win)
112 + by_id: Dict[str, Plugin] = {}
113 + for p in plugins:
114 + if p.id not in by_id:
115 + by_id[p.id] = p
116 +
117 + return list(by_id.values())
118 +
119 +
120 +def find_plugin(plugin_id: str) -> Optional[Plugin]:
121 + """Find a specific plugin by ID."""
122 + if not plugin_id:
123 + return None
124 +
125 + # Search roots in priority order (first one found wins)
126 + for root in get_plugin_roots():
127 + for manifest_path in discover_plugin_manifests(root):
128 + p = plugin_from_manifest(manifest_path)
129 + if p and p.id == plugin_id:
130 + return p
131 +
132 + return None
133 +
134 +
135 +# ============================================================================
136 +# Path Resolution & Import
137 +# ============================================================================
138 +
139 +def _extract_module_dir(module_path: str) -> str:
140 + """Extract directory from module path (e.g., 'tools/my_tool.py' -> 'tools')."""
141 + return str(Path(module_path).parent)
142 +
143 +
144 +def get_plugin_paths(cap_type: str, *subpaths: str) -> List[str]:
145 + """
146 + Get all paths from loaded plugins for a given capability type.
147 +
148 + Args:
149 + cap_type: Capability type (e.g., "tool", "extension", "api")
150 + subpaths: Additional path components to append
151 +
152 + Returns:
153 + List of absolute paths matching the capability type
154 + """
155 + paths: List[str] = []
156 +
157 + for plugin in list_plugins():
158 + cap_config = plugin.provides.get(cap_type)
159 + if not cap_config:
160 + continue
161 +
162 + # Normalize to list for uniform processing
163 + modules = []
164 + if isinstance(cap_config, list):
165 + modules = [cap.get("module") for cap in cap_config if isinstance(cap, dict) and cap.get("module")]
166 + elif isinstance(cap_config, dict) and cap_config.get("module"):
167 + modules = [cap_config["module"]]
168 +
169 + # Build paths from modules
170 + for module_path in modules:
171 + module_dir = _extract_module_dir(module_path)
172 + full_path = files.get_abs_path(str(plugin.path), module_dir, *subpaths)
173 + if files.exists(full_path) and full_path not in paths:
174 + paths.append(full_path)
175 +
176 + return paths
177 +
178 +
179 +def import_plugin_module(plugin_id: str, module_path: str) -> Any:
180 + """
181 + Import a Python module from a plugin using importlib.
182 +
183 + Args:
184 + plugin_id: Plugin ID
185 + module_path: Relative path to module within plugin (e.g., "helpers/memory.py")
186 +
187 + Returns:
188 + The imported module object
189 +
190 + Raises:
191 + ImportError: If plugin or module not found
192 + """
193 + import importlib.util
194 + import sys
195 +
196 + plugin = find_plugin(plugin_id)
197 + if not plugin:
198 + raise ImportError(f"Plugin '{plugin_id}' not found")
199 +
200 + full_path = plugin.path / module_path
201 + if not full_path.exists():
202 + raise ImportError(f"Module '{module_path}' not found in plugin '{plugin_id}'")
203 +
204 + # Create a unique module name to avoid conflicts
205 + module_name = f"plugins.{plugin_id}.{module_path.replace('/', '.').replace('.py', '')}"
206 +
207 + # Check if already loaded
208 + if module_name in sys.modules:
209 + return sys.modules[module_name]
210 +
211 + # Load the module
212 + spec = importlib.util.spec_from_file_location(module_name, full_path)
213 + if spec is None or spec.loader is None:
214 + raise ImportError(f"Could not load module spec from {full_path}")
215 +
216 + module = importlib.util.module_from_spec(spec)
217 + sys.modules[module_name] = module
218 + spec.loader.exec_module(module)
219 +
220 + return module
221 +
222 +
223 +# ============================================================================
224 +# API Helpers
225 +# ============================================================================
226 +
227 +def build_plugin_response_data(plugin: Plugin) -> dict:
228 + """
229 + Build normalized API response data for a plugin.
230 + Resolves URLs for UI capabilities.
231 +
232 + Args:
233 + plugin: Plugin object to serialize
234 +
235 + Returns:
236 + Dictionary with plugin data, component_url, module_url, and provides
237 + """
238 + base_url = f"/plugins/{plugin.id}/"
239 +
240 + response_data = {
241 + "id": plugin.id,
242 + "name": plugin.name,
243 + "base_url": base_url,
244 + "version": plugin.version,
245 + "author": plugin.author,
246 + "description": plugin.description,
247 + "tags": plugin.tags,
248 + "provides": plugin.provides,
249 + }
250 +
251 + # Resolve UI capability URLs from provides.ui
252 + if "ui" in plugin.provides:
253 + ui_config = plugin.provides["ui"]
254 + if isinstance(ui_config, dict):
255 + if ui_config.get("component"):
256 + response_data["component_url"] = f"{base_url}{ui_config['component']}"
257 + if ui_config.get("module"):
258 + response_data["module_url"] = f"{base_url}{ui_config['module']}"
259 + if ui_config.get("props"):
260 + response_data["props"] = ui_config["props"]
261 +
262 + return response_data
\ No newline at end of file
python/helpers/projects.py
+2 -1
@@ -479,7 +479,8 @@ def create_project_meta_folders(name: str):
479
480 # create knowledge folders
481 files.create_dir(get_project_meta_folder(name, PROJECT_KNOWLEDGE_DIR))
482 - from python.helpers import memory
482 + from python.helpers.plugins import import_plugin_module
483 + memory = import_plugin_module("memory", "helpers/memory.py")
484
485 for memory_type in memory.Memory.Area:
486 files.create_dir(
python/helpers/settings.py
+3 -1
@@ -629,7 +629,9 @@ def _apply_settings(previous: Settings | None):
629 or _settings["embed_model_provider"] != previous["embed_model_provider"]
630 or _settings["embed_model_kwargs"] != previous["embed_model_kwargs"]
631 ):
632 - from python.helpers.memory import reload as memory_reload
632 + from python.helpers.plugins import import_plugin_module
633 + memory = import_plugin_module("memory", "helpers/memory.py")
634 + memory_reload = memory.reload
635
636 memory_reload()
637
python/tools/behaviour_adjustment.py
+3 -1
@@ -1,4 +1,6 @@
1 -from python.helpers import files, memory
1 +from python.helpers import files
2 +from python.helpers.plugins import import_plugin_module
3 +memory = import_plugin_module("memory", "helpers/memory.py")
4 from python.helpers.tool import Tool, Response
5 from agent import Agent
6 from python.helpers.log import LogItem
python/tools/knowledge_tool._py
+6 -5
@@ -1,10 +1,11 @@
1 import asyncio
2 -from python.helpers import dotenv, memory, perplexity_search, duckduckgo_search
2 +from python.helpers import dotenv, perplexity_search, duckduckgo_search
3 +from python.helpers.plugins import import_plugin_module
4 +memory = import_plugin_module("memory", "helpers/memory.py")
5 +memory_load_tool = import_plugin_module("memory", "tools/memory_load.py")
6 +DEFAULT_MEMORY_THRESHOLD = memory_load_tool.DEFAULT_THRESHOLD
7 +
8 from python.helpers.tool import Tool, Response
4 -from python.helpers.print_style import PrintStyle
5 -from python.helpers.errors import handle_error
6 -from python.helpers.searxng import search as searxng
7 -from python.tools.memory_load import DEFAULT_THRESHOLD as DEFAULT_MEMORY_THRESHOLD
9 from python.helpers.document_query import DocumentQueryHelper
10
11 SEARCH_ENGINE_RESULTS = 10
python/tools/search_engine.py
+3 -1
@@ -1,6 +1,8 @@
1 import os
2 import asyncio
3 -from python.helpers import dotenv, memory, perplexity_search, duckduckgo_search
3 +from python.helpers import dotenv, perplexity_search, duckduckgo_search
4 +from python.helpers.plugins import import_plugin_module
5 +memory = import_plugin_module("memory", "helpers/memory.py")
6 from python.helpers.tool import Tool, Response
7 from python.helpers.print_style import PrintStyle
8 from python.helpers.errors import handle_error
run_ui.py
+43
@@ -237,6 +237,41 @@ async def serve_index():
237 return index
238
239
240 +# Serve plugin assets
241 +@webapp.route("/plugins/<plugin_id>/<path:asset_path>", methods=["GET"])
242 +@requires_auth
243 +async def serve_plugin_asset(plugin_id, asset_path):
244 + """
245 + Serve static assets from plugin directories.
246 + Resolves using the plugin system (with overrides).
247 + """
248 + from python.helpers import plugins
249 + from flask import send_file
250 +
251 + # Use the new find_plugin helper
252 + plugin = plugins.find_plugin(plugin_id)
253 + if not plugin:
254 + return Response("Plugin not found", 404)
255 +
256 + # Resolve the plugin asset path with security checks
257 + try:
258 + # Construct path using plugin root
259 + asset_file = (plugin.path / asset_path).resolve()
260 + plugin_root = plugin.path.resolve()
261 +
262 + # Security: ensure the resolved path is within the plugin directory
263 + if not str(asset_file).startswith(str(plugin_root) + os.sep) and str(asset_file) != str(plugin_root):
264 + return Response("Access denied", 403)
265 +
266 + if not asset_file.is_file():
267 + return Response("Asset not found", 404)
268 +
269 + return send_file(str(asset_file))
270 + except Exception as e:
271 + PrintStyle.error(f"Error serving plugin asset: {e}")
272 + return Response("Error serving asset", 500)
273 +
274 +
275 def _build_websocket_handlers_by_namespace(
276 socketio_server: socketio.AsyncServer,
277 lock: threading.RLock,
@@ -459,6 +494,14 @@ def run():
494 handlers = load_classes_from_folder("python/api", "*.py", ApiHandler)
495 for handler in handlers:
496 register_api_handler(webapp, handler)
497 +
498 + # Load API handlers from plugins
499 + from python.helpers import plugins
500 + plugin_api_paths = plugins.get_plugin_paths("api")
501 + for api_path in plugin_api_paths:
502 + plugin_handlers = load_classes_from_folder(api_path, "*.py", ApiHandler)
503 + for handler in plugin_handlers:
504 + register_api_handler(webapp, handler)
505
506 handlers_by_namespace = _build_websocket_handlers_by_namespace(socketio_server, lock)
507 configure_websocket_namespaces(
webui/components/settings/agent/memory.html
+1 -1
@@ -34,7 +34,7 @@
34 <div class="field-control">
35 <button
36 class="btn btn-field"
37 - @click="openModal('modals/memory/memory-dashboard.html');"
37 + @click="openModal('../plugins/memory/ui/memory-dashboard.html');"
38 >
39 Open Dashboard
40 </button>
webui/components/sidebar/top-section/quick-actions.html
+2 -2
@@ -17,7 +17,7 @@
17 </button>
18
19 <!-- Memory -->
20 - <button class="config-button" id="memory-dash" @click="openModal('modals/memory/memory-dashboard.html')" title="Memory">
20 + <button class="config-button" id="memory-dash" @click="openModal('../plugins/memory/ui/memory-dashboard.html')" title="Memory">
21 <span class="material-symbols-outlined">psychology</span>
22 </button>
23
@@ -54,7 +54,7 @@
54 <span>Projects</span>
55 </button>
56
57 - <button class="dropdown-item" @click="openModal('modals/memory/memory-dashboard.html'); dropdownOpen = false">
57 + <button class="dropdown-item" @click="openModal('../plugins/memory/ui/memory-dashboard.html'); dropdownOpen = false">
58 <span class="material-symbols-outlined">psychology</span>
59 <span>Memories</span>
60 </button>
webui/components/welcome/welcome-store.js
+1 -1
@@ -1,7 +1,7 @@
1 import { createStore } from "/js/AlpineStore.js";
2 import { getContext } from "/index.js";
3 import { store as chatsStore } from "/components/sidebar/chats/chats-store.js";
4 -import { store as memoryStore } from "/components/modals/memory/memory-dashboard-store.js";
4 +import { store as memoryStore } from "/plugins/memory/ui/memory-dashboard-store.js";
5 import { store as projectsStore } from "/components/projects/projects-store.js";
6 import { store as chatInputStore } from "/components/chat/input/input-store.js";
7 import * as API from "/js/api.js";
webui/js/components.js
+1 -1
@@ -261,4 +261,4 @@ const observer = new MutationObserver((mutations) => {
261 }
262 }
263 });
264 -observer.observe(document.body, { childList: true, subtree: true });
264 +observer.observe(document.body, { childList: true, subtree: true });
\ No newline at end of file
webui/js/initFw.js
+1
@@ -1,6 +1,7 @@
1 import * as initializer from "./initializer.js";
2 import * as _modals from "./modals.js";
3 import * as _components from "./components.js";
4 +import * as _plugins from "./plugins.js";
5 import { registerAlpineMagic } from "./confirmClick.js";
6
7 // initialize required elements
webui/js/plugins.js new
+215
@@ -0,0 +1,215 @@
1 +// Plugin system for Agent Zero
2 +// Loads x-extension tags by resolving plugin manifests and reusing component infrastructure
3 +
4 +import { importComponent, getParentAttributes } from './components.js';
5 +import { getCsrfToken } from './api.js';
6 +
7 +// Cache for plugin manifests
8 +const extensionCache = {};
9 +
10 +// Batch fetch plugin manifests from API
11 +async function fetchPluginManifests(pluginIds) {
12 + try {
13 + const response = await fetch("/plugins_resolve", {
14 + method: "POST",
15 + headers: {
16 + "Content-Type": "application/json",
17 + "X-CSRF-Token": await getCsrfToken(),
18 + },
19 + body: JSON.stringify({ ids: pluginIds }),
20 + });
21 +
22 + if (!response.ok) {
23 + throw new Error(`Failed to resolve plugins: ${response.statusText}`);
24 + }
25 +
26 + const result = await response.json();
27 + if (!result.ok || !result.data) {
28 + throw new Error("Invalid plugins response");
29 + }
30 +
31 + return result.data;
32 + } catch (error) {
33 + console.error("Error batch-fetching plugin manifests:", error);
34 + return [];
35 + }
36 +}
37 +
38 +// Merge manifest props with element attributes (element attributes override manifest)
39 +function mergePropsWithAttributes(manifestProps, element) {
40 + const elementAttrs = {};
41 + for (let attr of element.attributes) {
42 + if (attr.name !== "id") {
43 + try {
44 + elementAttrs[attr.name] = JSON.parse(attr.value);
45 + } catch (_e) {
46 + elementAttrs[attr.name] = attr.value;
47 + }
48 + }
49 + }
50 + return { ...manifestProps, ...elementAttrs };
51 +}
52 +
53 +// Set merged props as data attributes on element
54 +function setAttributesOnElement(props, element) {
55 + for (const [key, value] of Object.entries(props)) {
56 + if (typeof value === "object") {
57 + element.setAttribute(key, JSON.stringify(value));
58 + } else {
59 + element.setAttribute(key, value);
60 + }
61 + }
62 +}
63 +
64 +// Load a single plugin by calling importComponent with manifest URLs
65 +async function loadPlugin(pluginId, targetElement) {
66 + // Get manifest from cache
67 + let manifest = extensionCache[pluginId];
68 +
69 + if (!manifest || manifest.error) {
70 + throw new Error(manifest?.error || `Plugin '${pluginId}' not found`);
71 + }
72 +
73 + // Extract UI configuration from provides.ui
74 + // The API already resolves component_url and module_url from provides.ui
75 + const componentUrl = manifest.component_url;
76 + const moduleUrl = manifest.module_url;
77 + const props = manifest.props || {};
78 +
79 + // Merge props and set as attributes
80 + const mergedProps = mergePropsWithAttributes(props, targetElement);
81 + setAttributesOnElement(mergedProps, targetElement);
82 +
83 + // Call importComponent with plugin URLs
84 + // We prepend "components/../" to bypass the check in importComponent
85 + if (componentUrl) {
86 + const adjustedUrl = "components/.." + componentUrl;
87 + await importComponent(adjustedUrl, targetElement);
88 + }
89 +
90 + // Load module if specified
91 + // Browser's native module cache handles deduplication
92 + if (moduleUrl) {
93 + await import(moduleUrl);
94 + }
95 +}
96 +
97 +// Find all x-extension tags in root elements
98 +function findAllExtensionTags(roots) {
99 + const rootElements = Array.isArray(roots) ? roots : [roots];
100 + return rootElements.flatMap((root) =>
101 + Array.from(root.querySelectorAll("x-extension"))
102 + );
103 +}
104 +
105 +// Collect unique plugin IDs that need to be fetched (not in cache)
106 +function collectUniqueUncachedPluginIds(extensions) {
107 + const pluginIds = [];
108 + const seen = new Set();
109 +
110 + for (const extension of extensions) {
111 + const pluginId = extension.getAttribute("id");
112 + if (!pluginId) {
113 + console.error("x-extension missing id attribute:", extension);
114 + continue;
115 + }
116 +
117 + // Only add if not seen and not cached
118 + if (!seen.has(pluginId) && !extensionCache[pluginId]) {
119 + pluginIds.push(pluginId);
120 + seen.add(pluginId);
121 + }
122 + }
123 +
124 + return pluginIds;
125 +}
126 +
127 +// Main loader: scan DOM, batch fetch manifests, load all plugins
128 +export async function loadExtensions(roots = [document.documentElement]) {
129 + try {
130 + // Find all x-extension tags
131 + const extensions = findAllExtensionTags(roots);
132 +
133 + if (extensions.length === 0) return;
134 +
135 + // Collect plugin IDs that need fetching
136 + const pluginIds = collectUniqueUncachedPluginIds(extensions);
137 +
138 + // Batch fetch all uncached manifests in one API call
139 + if (pluginIds.length > 0) {
140 + const manifests = await fetchPluginManifests(pluginIds);
141 +
142 + // Update cache with fetched manifests
143 + for (const manifest of manifests) {
144 + if (!manifest.error) {
145 + extensionCache[manifest.id] = manifest;
146 + } else {
147 + console.error(`Plugin '${manifest.id}' failed to load:`, manifest.error);
148 + }
149 + }
150 + }
151 +
152 + // Map plugin IDs to extension elements for parallel loading
153 + const extensionMap = new Map();
154 + for (const extension of extensions) {
155 + const pluginId = extension.getAttribute("id");
156 + if (!pluginId) continue;
157 +
158 + if (!extensionMap.has(pluginId)) {
159 + extensionMap.set(pluginId, []);
160 + }
161 + extensionMap.get(pluginId).push(extension);
162 + }
163 +
164 + // Load all plugins in parallel using cached manifests
165 + await Promise.all(
166 + Array.from(extensionMap.entries()).flatMap(([pluginId, extensionElements]) =>
167 + extensionElements.map(async (extension) => {
168 + try {
169 + await loadPlugin(pluginId, extension);
170 + } catch (error) {
171 + console.error(`Error loading extension '${pluginId}':`, error);
172 + extension.innerHTML = `<div class="error">Failed to load plugin: ${pluginId}</div>`;
173 + }
174 + })
175 + )
176 + );
177 + } catch (error) {
178 + console.error("Error loading extensions:", error);
179 + }
180 +}
181 +
182 +// Extend global xAttrs to check both x-component and x-extension tags
183 +// This allows plugins to use globalThis.xAttrs() and get both component and extension attrs
184 +globalThis.xAttrs = function(el) {
185 + return getParentAttributes(el, ['x-component', 'x-extension']);
186 +};
187 +
188 +// Initialize when DOM is ready
189 +if (document.readyState === 'loading') {
190 + document.addEventListener('DOMContentLoaded', () => loadExtensions());
191 +} else {
192 + loadExtensions();
193 +}
194 +
195 +// Watch for DOM changes to dynamically load x-extension tags
196 +const observer = new MutationObserver((mutations) => {
197 + for (const mutation of mutations) {
198 + for (const node of mutation.addedNodes) {
199 + if (node.nodeType === 1) {
200 + // ELEMENT_NODE
201 + // Check if this node is an x-extension tag
202 + if (node.matches?.("x-extension")) {
203 + loadExtensions([node.parentElement || document.documentElement]);
204 + } else if (node.querySelectorAll) {
205 + // Check if descendants contain x-extension tags
206 + const extensions = node.querySelectorAll("x-extension");
207 + if (extensions.length > 0) {
208 + loadExtensions([node]);
209 + }
210 + }
211 + }
212 + }
213 + }
214 +});
215 +observer.observe(document.body, { childList: true, subtree: true });