Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions src/memos/dream/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Dream Plugin

Dream is an optional MemOS community feature, currently in beta. It explores how
an agent can reflect on recently added memories outside the foreground request
path, then consolidate them into higher-level context, insights, and diary
entries. The implementation is intentionally lightweight and extensible, and we
welcome the community to help improve the signal policy, prompts, persistence,
diary experience, and downstream integrations.

## Status and Enablement

Dream is disabled by default.

With no plugin environment variables configured, MemOS loads ordinary plugins as
before, but it does not load the built-in `dream` plugin. Enable it explicitly:

```bash
MEMOS_ENABLED_PLUGINS=dream
```

`MEMOS_DISABLED_PLUGINS` has the highest priority, so this keeps Dream disabled:

```bash
MEMOS_ENABLED_PLUGINS=dream
MEMOS_DISABLED_PLUGINS=dream
```

## Current Capabilities

When enabled, the built-in `CommunityDreamPlugin` provides:

- signal capture from successful memory add operations;
- deterministic Dream metadata enrichment during fine extraction;
- scheduler-driven Dream execution through the `dream.execute` hook;
- manual cube-level triggering through `POST /dream/trigger/cube`;
- Dream diary querying through `POST /dream/diary`;
- `Context` recall merged into normal search results.

The default pipeline is:

1. Build or update `Context` nodes from pending memories.
2. Form Dream motives from recently added source memories.
3. Recall related `UserMemory` and `LongTermMemory` nodes.
4. Use the configured LLM to produce at most one insight per motive.
5. Generate a human-readable Dream diary entry.
6. Persist valid insight actions and diary entries to the graph database.

If no LLM is available, Dream can still run the pipeline, but fallback reasoning
does not write new insight memories because zero-confidence actions are skipped.

## Usage

Enable Dream and start the API service:

```bash
export MEMOS_ENABLED_PLUGINS=dream
make serve
```

Check plugin health:

```bash
curl http://127.0.0.1:8000/dream/diary/health
```

Manually submit a cube-level Dream task:

```bash
curl -X POST "http://127.0.0.1:8000/dream/trigger/cube?cube_id=<cube_id>&user_id=<user_id>&user_name=<user_name>"
```

Query recent Dream diary entries:

```bash
curl -X POST http://127.0.0.1:8000/dream/diary \
-H "Content-Type: application/json" \
-d '{"cube_id": "<cube_id>", "filter": {"limit": 5}}'
```

Fetch one diary entry:

```bash
curl -X POST http://127.0.0.1:8000/dream/diary \
-H "Content-Type: application/json" \
-d '{"cube_id": "<cube_id>", "filter": {"task_id": "dream_diary_xxx"}}'
```

## Configuration

Plugin loading:

- `MEMOS_ENABLED_PLUGINS=dream`: enable Dream.
- `MEMOS_DISABLED_PLUGINS=dream`: disable Dream, even if it is also enabled.

Dream-specific options:

- `MEMOS_DREAM_HEURISTIC_ENRICHER`: defaults to `on`.
- `MEMOS_DREAM_ENRICH_OVERWRITE`: defaults to `off`.
- `MEMOS_DREAM_CONTEXT_ENABLED`: defaults to `on`.
- `MEMOS_DREAM_CONTEXT_SUMMARY_LLM`: defaults to `on`.
- `MEMOS_DREAM_CONTEXT_BINDING_LLM`: defaults to `on`.
- `MEMOS_DREAM_CONTEXT_BINDING_MIN_GROUP_SIZE`: defaults to `2`.
- `MEMOS_DREAM_CONTEXT_BINDING_MAX_GROUP_SIZE`: defaults to `30`.
- `MEMOS_DREAM_CONTEXT_BINDING_CONFIDENCE_THRESHOLD`: defaults to `0.65`.

## Beta Limitations

- Signals are stored in memory and do not survive process restarts.
- The automatic trigger policy is currently a simple pending-memory threshold.
- The built-in signal source focuses on new-memory accumulation. Conflict,
feedback, frequency, and fragmentation signals are extension points.
- The diary and surfacing experience is still early.
- Write-back policies for update, merge, archive, and long-term maintenance are
available as extension directions, not complete product behavior.

## Contributing

Dream is designed as a community-building surface. Good places to contribute:

- better trigger policies and signal stores;
- stronger motive formation and recall strategies;
- safer and more useful reasoning prompts;
- richer diary generation and user-facing surfacing;
- memory lifecycle and maintenance policies;
- alternative `dream` plugin implementations with higher priority.

Projects can ship their own plugin with the same logical name (`dream`). When
multiple providers expose `dream`, the plugin manager keeps the implementation
with the highest priority.
1 change: 1 addition & 0 deletions src/memos/dream/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class CommunityDreamPlugin(MemOSPlugin):
version = "0.1.0"
description = "Built-in Dream plugin"
priority = 10
enabled_by_default = False

def on_load(self) -> None:
self.context: dict[str, Any] = {"shared": {}, "configs": {}}
Expand Down
173 changes: 173 additions & 0 deletions src/memos/plugins/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# MemOS Plugin System

This directory contains the Python plugin framework for MemOS. It is used by
the API service and scheduler runtime to load in-process extensions.

The framework currently supports:

- Python package discovery through the `memos.plugins` entry-point group.
- Plugin lifecycle hooks: `on_load`, `init_components`, `init_app`,
`on_shutdown`.
- FastAPI router and middleware registration.
- Hook callbacks for add/search/mem-reader/memory-version/Dream extension
points.
- Runtime component injection, such as graph DB, embedder, LLM, and configs.
- Enable/disable controls through environment variables.
- Priority-based selection when multiple packages provide the same logical
plugin.

It is not a remote sandbox, marketplace, or hot-reload system. Plugins run in
the same Python process as MemOS, so callbacks should be fast, defensive, and
careful with side effects.

## Files

| File | Purpose |
| ---- | ------- |
| `base.py` | `MemOSPlugin` base class and registration helpers. |
| `manager.py` | Entry-point discovery, enable/disable logic, lifecycle orchestration. |
| `hook_defs.py` | Core hook names and hook specs. |
| `hooks.py` | Hook registry, trigger helpers, and `@hookable`. |
| `component_bootstrap.py` | Builds the context passed to `init_components`. |

Useful references:

- Built-in plugin example: `src/memos/dream/plugin.py`
- API startup: `src/memos/api/server_api.py`
- Component bootstrap: `src/memos/api/handlers/component_init.py`
- Tests: `tests/plugins/`

## Lifecycle

Plugins are discovered from installed Python entry points:

```toml
[project.entry-points."memos.plugins"]
my_plugin = "my_plugin.plugin:MyPlugin"
```

During startup, `PluginManager`:

1. Loads each entry point and instantiates the plugin class.
2. Keeps only instances that inherit from `MemOSPlugin`.
3. Resolves duplicate logical names by `priority`.
4. Applies enable/disable environment variables.
5. Calls `on_load()`.
6. Calls `init_components(context)` when runtime components are ready.
7. Calls `init_app()` after binding the FastAPI app.

`on_shutdown()` is called when plugins are shut down.

Environment variables:

- `MEMOS_DISABLED_PLUGINS`: comma-separated plugin names to disable.
- `MEMOS_ENABLED_PLUGINS`: comma-separated plugin names to enable when
`enabled_by_default = False`.

Disable wins if a plugin appears in both lists.

## Minimal Plugin

```python
from fastapi import APIRouter

from memos.plugins import H, MemOSPlugin


class MyPlugin(MemOSPlugin):
name = "my_plugin"
version = "0.1.0"
description = "Example MemOS plugin"
priority = 0
enabled_by_default = True

def on_load(self) -> None:
self.register_hook(H.SEARCH_AFTER, self.on_search_after)

def init_components(self, context: dict) -> None:
self.context = context

def init_app(self) -> None:
router = APIRouter(prefix="/my-plugin", tags=["my-plugin"])

@router.get("/health")
def health() -> dict[str, object]:
return {"plugin": self.name, "version": self.version}

self.register_router(router)

def on_search_after(self, *, request, result, **kwargs):
return result

def on_shutdown(self) -> None:
self.context = {}
```

Install the package into the same Python environment as MemOS:

```bash
pip install -e /path/to/my_plugin
```

Then restart the MemOS service.

## Hooks

Core hook names are exposed through `memos.plugins.H`.

Common hooks include:

| Hook | Purpose |
| ---- | ------- |
| `add.before` / `add.after` | Modify add requests or results. |
| `search.before` / `search.after` | Modify search requests or results. |
| `search.memory_results` | Add result buckets before thresholding, dedup, and reranking. |
| `mem_reader.pre_extract` | Customize memory-reader extraction prompts. |
| `memory_items.after_fine_extract` | Post-process extracted memory items. |
| `memory_version.prepare_updates` | Prepare versioned-memory candidates. |
| `memory_version.apply_updates` | Apply versioned-memory updates. |
| `memory_version.apply_feedback_update` | Apply version semantics during feedback updates. |
| `dream.execute` | Execute the active Dream pipeline. |

Hooks may define a `pipe_key`. If a callback returns a non-`None` value, that
value replaces the piped argument for the next callback. Returning `None` means
"leave the current value unchanged".

Plugin-owned hooks should be declared inside the plugin package with
`define_hook`, not added to core `hook_defs.py`.

## Runtime Context

`init_components(context)` receives a mutable context with:

- `context["shared"]`: runtime objects such as `graph_db`, `embedder`, `llm`,
`mem_scheduler`, and scheduler submit handles.
- `context["configs"]`: default cube, NLI, mem-reader, reranker, feedback
reranker, and internet retriever configs.

Keep the context reference if your plugin needs values that may be attached
later during bootstrap.

## Development Notes

- Namespace plugin routes, for example `/my-plugin/...`.
- Keep hook callbacks narrow and resilient; do not let optional features break
core add/search paths.
- Guard optional third-party imports with clear installation messages.
- Use `memos.log.get_logger(__name__)`; do not print secrets, vectors, or raw
user data.
- Make `on_shutdown()` idempotent.
- Add tests for hook registration, piped returns, component context handling,
router registration, and enable/disable behavior.

For core framework changes, run:

```bash
poetry run pytest tests/plugins/ -q
```

## Related Plugin-Like Projects

The TypeScript/OpenClaw/Hermes projects under `apps/` are separate host
integrations. They are not loaded by this Python `PluginManager` unless they
also publish a Python entry point under `memos.plugins`.
1 change: 1 addition & 0 deletions src/memos/plugins/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class MemOSPlugin:
version: str = "0.0.0"
description: str = ""
priority: int = 0
enabled_by_default: bool = True

_app: FastAPI | None = None

Expand Down
11 changes: 9 additions & 2 deletions src/memos/plugins/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,14 @@ def _parse_plugin_names(value: str | None) -> set[str]:
@classmethod
def _is_plugin_enabled(cls, plugin: MemOSPlugin) -> bool:
disabled = cls._parse_plugin_names(os.getenv("MEMOS_DISABLED_PLUGINS"))
return plugin.name not in disabled
if plugin.name in disabled:
return False

if plugin.enabled_by_default:
return True

enabled = cls._parse_plugin_names(os.getenv("MEMOS_ENABLED_PLUGINS"))
return plugin.name in enabled

@staticmethod
def _select_plugin_winners(
Expand Down Expand Up @@ -121,7 +128,7 @@ def discover(self) -> None:
for plugin_name, plugin in winners.items():
if not self._is_plugin_enabled(plugin):
logger.info(
"Plugin discovered but disabled: %s v%s (MEMOS_DISABLED_PLUGINS)",
"Plugin discovered but disabled: %s v%s",
plugin.name,
plugin.version,
)
Expand Down
Loading
Loading