Initial Arka plugin registry: Official plugins mirror + Arka-signed index
11 plugins from github.com/librefang/librefang-registry plugins/. index.json / index.json.sig are signed with Arka's Ed25519 key (not upstream stats.librefang.ai). Private key is not in this repo.
This commit is contained in:
commit
8dec8f6038
62 files changed
+5189
No files matched your search
@@ -0,0 +1,129 @@
|
||||
#!/usr/bin/env python3
|
||||
"""MemPalace ingest hook for LibreFang.
|
||||
|
||||
Searches MemPalace for memories relevant to the incoming user message
|
||||
and injects them into the agent's context as MemoryFragments.
|
||||
|
||||
Input (stdin): {"type": "ingest", "agent_id": "...", "message": "user text"}
|
||||
Output (stdout): {"memories": [{"content": "..."}]}
|
||||
|
||||
Install: librefang plugin install mempalace-indexer && librefang plugin requirements mempalace-indexer
|
||||
"""
|
||||
import sys
|
||||
import json
|
||||
import os
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration: read from LIBREFANG_PLUGIN_CONFIG (written by the runtime),
|
||||
# fall back to individual environment variables for direct invocation.
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _load_config():
|
||||
cfg_path = os.environ.get("LIBREFANG_PLUGIN_CONFIG")
|
||||
if cfg_path:
|
||||
try:
|
||||
with open(cfg_path) as f:
|
||||
return json.load(f)
|
||||
except (OSError, json.JSONDecodeError):
|
||||
pass
|
||||
return {}
|
||||
|
||||
_cfg = _load_config()
|
||||
|
||||
|
||||
def _cfg_str(key, env_key, default):
|
||||
if key in _cfg:
|
||||
return str(_cfg[key])
|
||||
return os.environ.get(env_key, default)
|
||||
|
||||
|
||||
def _cfg_int(key, env_key, default):
|
||||
if key in _cfg:
|
||||
try:
|
||||
return int(_cfg[key])
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return int(os.environ.get(env_key, default))
|
||||
|
||||
|
||||
def _cfg_float(key, env_key, default):
|
||||
if key in _cfg:
|
||||
try:
|
||||
return float(_cfg[key])
|
||||
except (TypeError, ValueError):
|
||||
pass
|
||||
return float(os.environ.get(env_key, default))
|
||||
|
||||
|
||||
PALACE_PATH = _cfg_str("palace_path", "MEMPALACE_PALACE_PATH",
|
||||
os.path.expanduser("~/.mempalace/palace"))
|
||||
MAX_MEMORY_CHARS = _cfg_int("max_chars", "MEMPALACE_MAX_CHARS", "300")
|
||||
|
||||
# MemPalace returns similarity in [0, 1] — higher means more relevant.
|
||||
# Results below MIN_SIMILARITY are too dissimilar to be useful.
|
||||
# Set to 0 (or MEMPALACE_MIN_SIMILARITY=0) to disable filtering.
|
||||
MIN_SIMILARITY = _cfg_float("min_similarity", "MEMPALACE_MIN_SIMILARITY", "0.3")
|
||||
N_RESULTS = _cfg_int("n_results", "MEMPALACE_N_RESULTS", "5")
|
||||
|
||||
|
||||
def emit(obj):
|
||||
"""Write JSON response to stdout with trailing newline."""
|
||||
json.dump(obj, sys.stdout)
|
||||
sys.stdout.write("\n")
|
||||
|
||||
|
||||
def truncate_at_word(text, max_len):
|
||||
"""Truncate text at nearest word boundary."""
|
||||
if len(text) <= max_len:
|
||||
return text
|
||||
truncated = text[:max_len]
|
||||
last_space = truncated.rfind(" ")
|
||||
if last_space > max_len // 2:
|
||||
return truncated[:last_space] + "..."
|
||||
return truncated + "..."
|
||||
|
||||
|
||||
def main():
|
||||
try:
|
||||
data = json.load(sys.stdin)
|
||||
except (json.JSONDecodeError, EOFError):
|
||||
emit({"memories": []})
|
||||
return
|
||||
|
||||
message = data.get("message", "")
|
||||
if not message or len(message) < 5:
|
||||
emit({"memories": []})
|
||||
return
|
||||
|
||||
try:
|
||||
from mempalace.searcher import search_memories
|
||||
|
||||
results = search_memories(message, PALACE_PATH, n_results=N_RESULTS)
|
||||
|
||||
memories = []
|
||||
for r in results.get("results", []):
|
||||
text = r.get("text", "")
|
||||
wing = r.get("wing", "") or "memory"
|
||||
room = r.get("room", "")
|
||||
similarity = r.get("similarity")
|
||||
|
||||
if not text:
|
||||
continue
|
||||
|
||||
# Filter out low-relevance results when the backend provides a score.
|
||||
# similarity=None means the backend didn't return one — allow through.
|
||||
if similarity is not None and MIN_SIMILARITY > 0 and similarity < MIN_SIMILARITY:
|
||||
continue
|
||||
|
||||
snippet = truncate_at_word(text, MAX_MEMORY_CHARS)
|
||||
# Use wing/room (semantically meaningful) instead of raw source filename.
|
||||
label = f"{wing}/{room}" if room else wing
|
||||
memories.append({"content": f"[{label}] {snippet}"})
|
||||
|
||||
emit({"memories": memories})
|
||||
except Exception as e:
|
||||
emit({"memories": [], "error": str(e)})
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in new issue
Block a user