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
21 changes: 21 additions & 0 deletions lumetra/engram/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
MIT License

Copyright (c) 2026 Lumetra

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
7 changes: 7 additions & 0 deletions lumetra/engram/PRIVACY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Privacy

This plugin sends the parameters you (or your agent) pass to its tools — `content`, `question`, `bucket`, `memory_id` — to the Engram REST API at `https://api.lumetra.io` (or the self-hosted base URL you configured). Memories are stored under your Engram tenant, scoped by the API key you provided in the Authorize dialog.

The plugin does not collect, log, or transmit data to any third party other than the Engram service you've explicitly authorized. The plugin does not read other Dify resources (datasets, conversations, files) — only the parameters supplied to each tool call.

For Engram's own data-handling and retention policy, see <https://lumetra.io/privacy>.
57 changes: 57 additions & 0 deletions lumetra/engram/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
# engram-dify

[Engram](https://lumetra.io) tools for [Dify](https://dify.ai) — durable, explainable memory for AI agents and chatflows.

This is a first-party Dify plugin. Six tools (`store_memory`, `query_memory`, `list_memories`, `list_buckets`, `delete_memory`, `clear_memories`) call the hosted Engram REST API directly. No MCP bridge, no servers_config JSON, no community-plugin dependency — install from the Dify Marketplace and the tools appear in the catalog.

## Setup

### 1. Get an Engram API key

Sign up at <https://lumetra.io> — free tier, no card. You'll see an `eng_live_…` token in your dashboard.

### 2. Configure a BYOK provider key

Engram is bring-your-own-key for the LLM that handles extraction and synthesis. Configure one provider at <https://lumetra.io/models>. DeepSeek is what we recommend — cheap and fast. Without a provider key, `store_memory` and `query_memory` return HTTP 412.

### 3. Install the plugin

In your Dify console: **Plugins → Marketplace** → search **"Engram"** → install. Then **Plugins → Installed → Engram** → **Authorize** and paste your `eng_live_...` API key.

The six tools are now available in the tool catalog for Agents, Chatflows, and Workflows.

## Tools

| Tool | What it does |
|---|---|
| `store_memory(content, bucket?)` | Save an atomic fact to a bucket. Defaults to `"default"`. Buckets auto-create on first write. |
| `query_memory(question, bucket?)` | Natural-language question against memory. Returns a synthesized answer with citations. |
| `list_memories(bucket?, limit?)` | Newest-first list of memories in a bucket. |
| `list_buckets(limit?, offset?)` | Paginated list of buckets in your tenant. |
| `delete_memory(memory_id, bucket)` | Remove a single memory by UUID. |
| `clear_memories(bucket)` | Empty a bucket. **Destructive.** |

## Self-hosted Engram

If you're running Engram on your own infrastructure instead of `api.lumetra.io`, set the **Engram API Base URL** field in the Authorize dialog to your endpoint (e.g. `https://engram.internal.example.com`).

## Manual verification

Outside Dify, confirm Engram itself is reachable with your key:

```bash
curl -s https://api.lumetra.io/v1/buckets \
-H "Authorization: Bearer eng_live_..." | head -c 300
```

A JSON bucket list confirms the key is valid. If Dify shows the plugin as installed but tools fail, double-check the key in the Authorize dialog and that the Dify worker process can reach `api.lumetra.io` from inside its container.

## Source & contact

- Source: <https://github.com/lumetra-io/engram-dify>
- Issues: <https://github.com/lumetra-io/engram-dify/issues>
- Lumetra: <https://lumetra.io> · <support@lumetra.io>

## License

MIT — Lumetra
4 changes: 4 additions & 0 deletions lumetra/engram/_assets/icon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added lumetra/engram/engram-0.0.2.difypkg
Binary file not shown.
6 changes: 6 additions & 0 deletions lumetra/engram/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
from dify_plugin import Plugin, DifyPluginEnv

plugin = Plugin(DifyPluginEnv(MAX_REQUEST_TIMEOUT=120))

if __name__ == "__main__":
plugin.run()
35 changes: 35 additions & 0 deletions lumetra/engram/manifest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
author: lumetra
created_at: "2026-05-19T00:00:00Z"
description:
en_US: Engram — durable, explainable memory for AI agents. Six tools for storing and querying long-term memory backed by Lumetra's hosted Engram service.
icon: icon.svg
label:
en_US: Engram
meta:
arch:
- amd64
- arm64
runner:
entrypoint: main
language: python
version: "3.12"
version: 0.0.2
name: engram
plugins:
tools:
- provider/engram.yaml
resource:
memory: 1048576
permission:
model:
enabled: false
tool:
enabled: true
tags:
- productivity
- utilities
type: plugin
version: 0.0.2
privacy: PRIVACY.md
repo: https://github.com/lumetra-io/engram-dify
verified: false
35 changes: 35 additions & 0 deletions lumetra/engram/provider/engram.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
from typing import Any

import requests

from dify_plugin import ToolProvider
from dify_plugin.errors.tool import ToolProviderCredentialValidationError


class EngramProvider(ToolProvider):
def _validate_credentials(self, credentials: dict[str, Any]) -> None:
api_key = credentials.get("engram_api_key")
if not api_key:
raise ToolProviderCredentialValidationError("Engram API key is required.")

base_url = (credentials.get("engram_api_base") or "https://api.lumetra.io").rstrip("/")
try:
response = requests.get(
f"{base_url}/v1/buckets",
headers={"Authorization": f"Bearer {api_key}"},
params={"limit": 1},
timeout=10,
)
except requests.RequestException as exc:
raise ToolProviderCredentialValidationError(
f"Could not reach Engram at {base_url}: {exc}"
) from exc

if response.status_code == 401:
raise ToolProviderCredentialValidationError(
"Engram rejected the API key (HTTP 401). Double-check the eng_live_... value."
)
if response.status_code >= 400:
raise ToolProviderCredentialValidationError(
f"Engram returned HTTP {response.status_code} during validation: {response.text[:200]}"
)
45 changes: 45 additions & 0 deletions lumetra/engram/provider/engram.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
identity:
author: lumetra
name: engram
label:
en_US: Engram
description:
en_US: "Durable, explainable memory for AI agents. Six tools — store_memory, query_memory, list_memories, list_buckets, delete_memory, clear_memories — backed by the hosted Engram service at api.lumetra.io."
icon: icon.svg
tags:
- productivity
- utilities

credentials_for_provider:
engram_api_key:
type: secret-input
required: true
label:
en_US: Engram API Key
placeholder:
en_US: eng_live_...
help:
en_US: "Bearer token for api.lumetra.io. Sign up at https://lumetra.io to get one — free tier, no card."
url: https://lumetra.io
engram_api_base:
type: text-input
required: false
default: https://api.lumetra.io
label:
en_US: Engram API Base URL
placeholder:
en_US: https://api.lumetra.io
help:
en_US: "Override only if you're running a self-hosted Engram. Leave as default for the hosted service."

tools:
- tools/store_memory.yaml
- tools/query_memory.yaml
- tools/list_memories.yaml
- tools/list_buckets.yaml
- tools/delete_memory.yaml
- tools/clear_memories.yaml

extra:
python:
source: provider/engram.py
11 changes: 11 additions & 0 deletions lumetra/engram/pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
[project]
name = "engram"
version = "0.0.2"
description = "Durable, explainable memory for AI agents — Engram tools for Dify."
readme = "README.md"
requires-python = ">=3.12"

dependencies = [
"dify_plugin>=0.5.0",
"requests>=2.31.0",
]
48 changes: 48 additions & 0 deletions lumetra/engram/tools/clear_memories.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from collections.abc import Generator
from typing import Any

import requests

from dify_plugin import Tool
from dify_plugin.entities.tool import ToolInvokeMessage


class ClearMemoriesTool(Tool):
def _invoke(
self, tool_parameters: dict[str, Any]
) -> Generator[ToolInvokeMessage, None, None]:
api_key = self.runtime.credentials.get("engram_api_key")
base_url = (self.runtime.credentials.get("engram_api_base") or "https://api.lumetra.io").rstrip("/")

bucket = (tool_parameters.get("bucket") or "").strip()
if not bucket:
yield self.create_text_message("clear_memories requires a bucket name.")
return

try:
response = requests.delete(
f"{base_url}/v1/buckets/{bucket}/memories",
headers={"Authorization": f"Bearer {api_key}"},
timeout=60,
)
except requests.RequestException as exc:
yield self.create_text_message(f"Engram request failed: {exc}")
return

if response.status_code >= 400:
yield self.create_text_message(
f"Engram returned HTTP {response.status_code}: {response.text[:300]}"
)
return

try:
payload = response.json()
except ValueError:
payload = {"status": "cleared", "bucket": bucket}

yield self.create_json_message(payload)
cleared = payload.get("cleared_count")
yield self.create_text_message(
f"Cleared bucket '{bucket}'"
+ (f" ({cleared} memories removed)." if cleared is not None else ".")
)
22 changes: 22 additions & 0 deletions lumetra/engram/tools/clear_memories.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
identity:
name: clear_memories
author: lumetra
label:
en_US: Clear Memories
description:
human:
en_US: Empty a bucket. Destructive — all memories in the bucket are deleted.
llm: "Delete ALL memories in a bucket. Destructive and irreversible. Only call when the user explicitly asks to wipe / clear / reset / empty a bucket. Always confirm with the user before invoking."
parameters:
- name: bucket
type: string
required: true
form: llm
label:
en_US: Bucket
human_description:
en_US: Bucket to empty.
llm_description: 'The bucket to empty. Destructive — every memory in this bucket will be deleted. Confirm with the user first.'
extra:
python:
source: tools/clear_memories.py
45 changes: 45 additions & 0 deletions lumetra/engram/tools/delete_memory.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from collections.abc import Generator
from typing import Any

import requests

from dify_plugin import Tool
from dify_plugin.entities.tool import ToolInvokeMessage


class DeleteMemoryTool(Tool):
def _invoke(
self, tool_parameters: dict[str, Any]
) -> Generator[ToolInvokeMessage, None, None]:
api_key = self.runtime.credentials.get("engram_api_key")
base_url = (self.runtime.credentials.get("engram_api_base") or "https://api.lumetra.io").rstrip("/")

memory_id = (tool_parameters.get("memory_id") or "").strip()
bucket = (tool_parameters.get("bucket") or "").strip()
if not memory_id or not bucket:
yield self.create_text_message("delete_memory requires both memory_id and bucket.")
return

try:
response = requests.delete(
f"{base_url}/v1/buckets/{bucket}/memories/{memory_id}",
headers={"Authorization": f"Bearer {api_key}"},
timeout=30,
)
except requests.RequestException as exc:
yield self.create_text_message(f"Engram request failed: {exc}")
return

if response.status_code >= 400:
yield self.create_text_message(
f"Engram returned HTTP {response.status_code}: {response.text[:300]}"
)
return

try:
payload = response.json()
except ValueError:
payload = {"status": "deleted", "memory_id": memory_id, "bucket": bucket}

yield self.create_json_message(payload)
yield self.create_text_message(f"Deleted memory {memory_id} from bucket '{bucket}'.")
31 changes: 31 additions & 0 deletions lumetra/engram/tools/delete_memory.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
identity:
name: delete_memory
author: lumetra
label:
en_US: Delete Memory
description:
human:
en_US: Remove a single memory by id.
llm: "Delete one memory from a bucket by its UUID. Use only when the user explicitly asks to remove a specific memory. Get the memory_id from list_memories first."
parameters:
- name: memory_id
type: string
required: true
form: llm
label:
en_US: Memory ID
human_description:
en_US: UUID of the memory to delete.
llm_description: 'The UUID of the memory to delete. Get this from list_memories before calling.'
- name: bucket
type: string
required: true
form: llm
label:
en_US: Bucket
human_description:
en_US: Bucket the memory lives in.
llm_description: 'The bucket the memory belongs to. Required.'
extra:
python:
source: tools/delete_memory.py
Loading