From 6f9be2a66d346330cb53e2054e6f959a4b913a4d Mon Sep 17 00:00:00 2001 From: Brutus Date: Tue, 19 May 2026 20:45:00 -0400 Subject: [PATCH] Add Engram plugin (v0.0.2) First-party Dify plugin from Lumetra for the Engram durable-memory service. Six tools call api.lumetra.io directly: - store_memory, query_memory - list_memories, list_buckets - delete_memory, clear_memories Source: https://github.com/lumetra-io/engram-dify --- lumetra/engram/LICENSE | 21 +++++++++ lumetra/engram/PRIVACY.md | 7 +++ lumetra/engram/README.md | 57 +++++++++++++++++++++++ lumetra/engram/_assets/icon.svg | 4 ++ lumetra/engram/engram-0.0.2.difypkg | Bin 0 -> 13597 bytes lumetra/engram/main.py | 6 +++ lumetra/engram/manifest.yaml | 35 ++++++++++++++ lumetra/engram/provider/engram.py | 35 ++++++++++++++ lumetra/engram/provider/engram.yaml | 45 ++++++++++++++++++ lumetra/engram/pyproject.toml | 11 +++++ lumetra/engram/tools/clear_memories.py | 48 +++++++++++++++++++ lumetra/engram/tools/clear_memories.yaml | 22 +++++++++ lumetra/engram/tools/delete_memory.py | 45 ++++++++++++++++++ lumetra/engram/tools/delete_memory.yaml | 31 ++++++++++++ lumetra/engram/tools/list_buckets.py | 48 +++++++++++++++++++ lumetra/engram/tools/list_buckets.yaml | 33 +++++++++++++ lumetra/engram/tools/list_memories.py | 47 +++++++++++++++++++ lumetra/engram/tools/list_memories.yaml | 33 +++++++++++++ lumetra/engram/tools/query_memory.py | 45 ++++++++++++++++++ lumetra/engram/tools/query_memory.yaml | 32 +++++++++++++ lumetra/engram/tools/store_memory.py | 46 ++++++++++++++++++ lumetra/engram/tools/store_memory.yaml | 32 +++++++++++++ 22 files changed, 683 insertions(+) create mode 100644 lumetra/engram/LICENSE create mode 100644 lumetra/engram/PRIVACY.md create mode 100644 lumetra/engram/README.md create mode 100644 lumetra/engram/_assets/icon.svg create mode 100644 lumetra/engram/engram-0.0.2.difypkg create mode 100644 lumetra/engram/main.py create mode 100644 lumetra/engram/manifest.yaml create mode 100644 lumetra/engram/provider/engram.py create mode 100644 lumetra/engram/provider/engram.yaml create mode 100644 lumetra/engram/pyproject.toml create mode 100644 lumetra/engram/tools/clear_memories.py create mode 100644 lumetra/engram/tools/clear_memories.yaml create mode 100644 lumetra/engram/tools/delete_memory.py create mode 100644 lumetra/engram/tools/delete_memory.yaml create mode 100644 lumetra/engram/tools/list_buckets.py create mode 100644 lumetra/engram/tools/list_buckets.yaml create mode 100644 lumetra/engram/tools/list_memories.py create mode 100644 lumetra/engram/tools/list_memories.yaml create mode 100644 lumetra/engram/tools/query_memory.py create mode 100644 lumetra/engram/tools/query_memory.yaml create mode 100644 lumetra/engram/tools/store_memory.py create mode 100644 lumetra/engram/tools/store_memory.yaml diff --git a/lumetra/engram/LICENSE b/lumetra/engram/LICENSE new file mode 100644 index 000000000..6623d06b7 --- /dev/null +++ b/lumetra/engram/LICENSE @@ -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. diff --git a/lumetra/engram/PRIVACY.md b/lumetra/engram/PRIVACY.md new file mode 100644 index 000000000..97957ca2c --- /dev/null +++ b/lumetra/engram/PRIVACY.md @@ -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 . diff --git a/lumetra/engram/README.md b/lumetra/engram/README.md new file mode 100644 index 000000000..69e32a47e --- /dev/null +++ b/lumetra/engram/README.md @@ -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 — 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 . 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: +- Issues: +- Lumetra: · + +## License + +MIT — Lumetra diff --git a/lumetra/engram/_assets/icon.svg b/lumetra/engram/_assets/icon.svg new file mode 100644 index 000000000..546df08cc --- /dev/null +++ b/lumetra/engram/_assets/icon.svg @@ -0,0 +1,4 @@ + + + + diff --git a/lumetra/engram/engram-0.0.2.difypkg b/lumetra/engram/engram-0.0.2.difypkg new file mode 100644 index 0000000000000000000000000000000000000000..d2d6a7493a16a16bf503ec2feb710b58c0a71b18 GIT binary patch literal 13597 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 TLiek$ga`0}gzaF~U!VR5P7~U3 literal 0 HcmV?d00001 diff --git a/lumetra/engram/main.py b/lumetra/engram/main.py new file mode 100644 index 000000000..568839f52 --- /dev/null +++ b/lumetra/engram/main.py @@ -0,0 +1,6 @@ +from dify_plugin import Plugin, DifyPluginEnv + +plugin = Plugin(DifyPluginEnv(MAX_REQUEST_TIMEOUT=120)) + +if __name__ == "__main__": + plugin.run() diff --git a/lumetra/engram/manifest.yaml b/lumetra/engram/manifest.yaml new file mode 100644 index 000000000..849231635 --- /dev/null +++ b/lumetra/engram/manifest.yaml @@ -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 diff --git a/lumetra/engram/provider/engram.py b/lumetra/engram/provider/engram.py new file mode 100644 index 000000000..a3aac04a0 --- /dev/null +++ b/lumetra/engram/provider/engram.py @@ -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]}" + ) diff --git a/lumetra/engram/provider/engram.yaml b/lumetra/engram/provider/engram.yaml new file mode 100644 index 000000000..4ddfdb223 --- /dev/null +++ b/lumetra/engram/provider/engram.yaml @@ -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 diff --git a/lumetra/engram/pyproject.toml b/lumetra/engram/pyproject.toml new file mode 100644 index 000000000..be6e45e27 --- /dev/null +++ b/lumetra/engram/pyproject.toml @@ -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", +] diff --git a/lumetra/engram/tools/clear_memories.py b/lumetra/engram/tools/clear_memories.py new file mode 100644 index 000000000..b9d393b16 --- /dev/null +++ b/lumetra/engram/tools/clear_memories.py @@ -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 ".") + ) diff --git a/lumetra/engram/tools/clear_memories.yaml b/lumetra/engram/tools/clear_memories.yaml new file mode 100644 index 000000000..5b2c23549 --- /dev/null +++ b/lumetra/engram/tools/clear_memories.yaml @@ -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 diff --git a/lumetra/engram/tools/delete_memory.py b/lumetra/engram/tools/delete_memory.py new file mode 100644 index 000000000..b4b2e108b --- /dev/null +++ b/lumetra/engram/tools/delete_memory.py @@ -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}'.") diff --git a/lumetra/engram/tools/delete_memory.yaml b/lumetra/engram/tools/delete_memory.yaml new file mode 100644 index 000000000..ba2a79295 --- /dev/null +++ b/lumetra/engram/tools/delete_memory.yaml @@ -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 diff --git a/lumetra/engram/tools/list_buckets.py b/lumetra/engram/tools/list_buckets.py new file mode 100644 index 000000000..40379a902 --- /dev/null +++ b/lumetra/engram/tools/list_buckets.py @@ -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 ListBucketsTool(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("/") + + try: + limit = int(tool_parameters.get("limit")) if tool_parameters.get("limit") is not None else 50 + except (TypeError, ValueError): + limit = 50 + try: + offset = int(tool_parameters.get("offset")) if tool_parameters.get("offset") is not None else 0 + except (TypeError, ValueError): + offset = 0 + limit = max(1, min(limit, 500)) + offset = max(0, offset) + + try: + response = requests.get( + f"{base_url}/v1/buckets", + headers={"Authorization": f"Bearer {api_key}"}, + params={"limit": limit, "offset": offset}, + 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 + + payload = response.json() + yield self.create_json_message(payload) + buckets = payload.get("buckets") or [] + yield self.create_text_message(f"{len(buckets)} bucket(s) returned.") diff --git a/lumetra/engram/tools/list_buckets.yaml b/lumetra/engram/tools/list_buckets.yaml new file mode 100644 index 000000000..82abe3510 --- /dev/null +++ b/lumetra/engram/tools/list_buckets.yaml @@ -0,0 +1,33 @@ +identity: + name: list_buckets + author: lumetra + label: + en_US: List Buckets +description: + human: + en_US: List all memory buckets in your Engram tenant. + llm: "List all Engram memory buckets in the tenant. Use when the user asks what projects/buckets exist, or when you need to pick a bucket name before storing or querying." +parameters: + - name: limit + type: number + required: false + form: llm + default: 50 + label: + en_US: Limit + human_description: + en_US: Max number of buckets to return (default 50). + llm_description: 'Max number of buckets to return. Default 50.' + - name: offset + type: number + required: false + form: llm + default: 0 + label: + en_US: Offset + human_description: + en_US: Pagination offset (default 0). + llm_description: 'Pagination offset for the bucket list. Default 0.' +extra: + python: + source: tools/list_buckets.py diff --git a/lumetra/engram/tools/list_memories.py b/lumetra/engram/tools/list_memories.py new file mode 100644 index 000000000..4ea08d232 --- /dev/null +++ b/lumetra/engram/tools/list_memories.py @@ -0,0 +1,47 @@ +from collections.abc import Generator +from typing import Any + +import requests + +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage + + +class ListMemoriesTool(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 "default").strip() or "default" + limit_raw = tool_parameters.get("limit") + try: + limit = int(limit_raw) if limit_raw is not None else 20 + except (TypeError, ValueError): + limit = 20 + limit = max(1, min(limit, 200)) + + try: + response = requests.get( + f"{base_url}/v1/buckets/{bucket}/memories", + headers={"Authorization": f"Bearer {api_key}"}, + params={"limit": limit}, + 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 + + payload = response.json() + yield self.create_json_message(payload) + memories = payload.get("memories") or [] + yield self.create_text_message( + f"{len(memories)} memory(ies) in bucket '{bucket}' (total: {payload.get('total', len(memories))})." + ) diff --git a/lumetra/engram/tools/list_memories.yaml b/lumetra/engram/tools/list_memories.yaml new file mode 100644 index 000000000..ebe93202e --- /dev/null +++ b/lumetra/engram/tools/list_memories.yaml @@ -0,0 +1,33 @@ +identity: + name: list_memories + author: lumetra + label: + en_US: List Memories +description: + human: + en_US: List the newest memories in a bucket. + llm: "List the newest-first memories in an Engram bucket. Use when you need to enumerate what's stored (e.g. for review, audit, or to find an id to delete) rather than answer a question — use query_memory for question-answering." +parameters: + - name: bucket + type: string + required: false + form: llm + default: default + label: + en_US: Bucket + human_description: + en_US: 'Bucket to list. Use "default" unless scoping to a specific project.' + llm_description: 'The Engram bucket to list memories from.' + - name: limit + type: number + required: false + form: llm + default: 20 + label: + en_US: Limit + human_description: + en_US: Max number of memories to return (default 20). + llm_description: 'Max number of memories to return. Default 20. Increase only if the user explicitly asks for more.' +extra: + python: + source: tools/list_memories.py diff --git a/lumetra/engram/tools/query_memory.py b/lumetra/engram/tools/query_memory.py new file mode 100644 index 000000000..0d6d6362d --- /dev/null +++ b/lumetra/engram/tools/query_memory.py @@ -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 QueryMemoryTool(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("/") + + question = (tool_parameters.get("question") or "").strip() + if not question: + yield self.create_text_message("query_memory requires a non-empty question.") + return + + bucket = (tool_parameters.get("bucket") or "default").strip() or "default" + + try: + response = requests.post( + f"{base_url}/v1/query", + headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, + json={"query": question, "bucket": bucket}, + 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 + + payload = response.json() + yield self.create_json_message(payload) + answer = payload.get("answer") + if answer: + yield self.create_text_message(str(answer)) diff --git a/lumetra/engram/tools/query_memory.yaml b/lumetra/engram/tools/query_memory.yaml new file mode 100644 index 000000000..7c8ac1631 --- /dev/null +++ b/lumetra/engram/tools/query_memory.yaml @@ -0,0 +1,32 @@ +identity: + name: query_memory + author: lumetra + label: + en_US: Query Memory +description: + human: + en_US: Ask a natural-language question against your Engram memory. Returns a synthesized answer with citations. + llm: "Search Engram memory with a natural-language question. Use BEFORE answering questions where prior context, user preferences, or earlier decisions could be relevant. Returns a synthesized answer plus the source memories." +parameters: + - name: question + type: string + required: true + form: llm + label: + en_US: Question + human_description: + en_US: The natural-language question to ask of memory. + llm_description: "The natural-language question. Phrase it the way you would ask a colleague — Engram does hybrid retrieval (semantic + graph) and synthesizes an answer." + - name: bucket + type: string + required: false + form: llm + default: default + label: + en_US: Bucket + human_description: + en_US: 'Bucket to search. Use "default" unless you are scoping to a specific project.' + llm_description: 'The Engram bucket to search. Use "default" unless the user mentioned a project-specific bucket.' +extra: + python: + source: tools/query_memory.py diff --git a/lumetra/engram/tools/store_memory.py b/lumetra/engram/tools/store_memory.py new file mode 100644 index 000000000..e0c1084ec --- /dev/null +++ b/lumetra/engram/tools/store_memory.py @@ -0,0 +1,46 @@ +from collections.abc import Generator +from typing import Any + +import requests + +from dify_plugin import Tool +from dify_plugin.entities.tool import ToolInvokeMessage + + +class StoreMemoryTool(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("/") + + content = (tool_parameters.get("content") or "").strip() + if not content: + yield self.create_text_message("store_memory requires non-empty content.") + return + + bucket = (tool_parameters.get("bucket") or "default").strip() or "default" + + try: + response = requests.post( + f"{base_url}/v1/buckets/{bucket}/memories", + headers={"Authorization": f"Bearer {api_key}", "Content-Type": "application/json"}, + json={"content": content}, + 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 + + payload = response.json() + yield self.create_json_message(payload) + memory_id = payload.get("memory_id") or payload.get("id") or "(unknown)" + yield self.create_text_message( + f"Stored in bucket '{bucket}' (memory_id={memory_id})." + ) diff --git a/lumetra/engram/tools/store_memory.yaml b/lumetra/engram/tools/store_memory.yaml new file mode 100644 index 000000000..b68e949db --- /dev/null +++ b/lumetra/engram/tools/store_memory.yaml @@ -0,0 +1,32 @@ +identity: + name: store_memory + author: lumetra + label: + en_US: Store Memory +description: + human: + en_US: Save a fact, decision, or piece of context to long-term memory in Engram. + llm: "Persist a memory in the Engram knowledge graph. Use whenever the user states a durable fact, preference, or decision that should be available in future conversations. The content should be a single atomic fact phrased so it stands alone." +parameters: + - name: content + type: string + required: true + form: llm + label: + en_US: Memory content + human_description: + en_US: The fact to remember. One atomic concept per call works best. + llm_description: "The atomic fact, decision, or preference to memorize. Phrase it as a self-contained sentence so it stands alone outside of the current conversation." + - name: bucket + type: string + required: false + form: llm + default: default + label: + en_US: Bucket + human_description: + en_US: 'Bucket to store in. Use "default" unless you have a reason to segment.' + llm_description: 'The Engram bucket name. Use "default" unless the user or task implies a project-specific bucket. Buckets auto-create on first write.' +extra: + python: + source: tools/store_memory.py