Files
librefang-registry/plugins/topic-memory/hooks/ingest.py
T
Evan d1cab3e33a feat: context engine plugins, scaffolding, and pricing fixes (#6)
* feat: add 4 context engine plugins

- topic-memory: keyword clustering for topic-aware memory recall
- episodic-memory: conversation segmentation and cross-session recall
- user-profile: persistent user profiling from conversation patterns
- context-decay: time-based memory decay with reinforcement dynamics

All plugins use the ingest/after_turn hook protocol with stdin/stdout JSON.

* chore: add plugin scaffolding, update docs and templates

- Add plugin.toml template with {{NAME}} placeholder
- Add new-plugin Makefile target with hooks/ scaffolding
- Update plugins/README.md with all 10 plugins
- Update README.md stats (10 plugins, 220+ models)
- Add Plugin checkbox and checklist to PR template
- Add Plugin to issue template content type dropdown
- Fix CONTRIBUTING.md: last_verified is recommended, not required

* fix: correct model pricing and remove deprecated entries

- openrouter/gemma-2-9b-it: fix pricing from 0.0 to 0.03/0.09 per M tokens
  (free variant correctly stays at 0.0)
- github-copilot: remove deprecated copilot/gpt-4 model entry
  (GPT-4 retired in favor of GPT-4o for Copilot)

* docs: annotate kimi-coding as membership-gated

Kimi Code CLI uses quota-based membership model (not per-token billing).
Free tier has limited weekly requests; underlying model is K2.5.
Pricing kept at 0.0 consistent with other subscription providers
(chatgpt, github-copilot) but with explanatory comments.

* style: fix trailing newline in github-copilot.toml

* fix: correct Moonshot/Kimi model pricing from official sources

All 5 models had incorrect pricing:
- moonshot-v1-8k: 0.10/0.10 → 0.20/2.00
- moonshot-v1-32k: 0.30/0.30 → 1.00/3.00
- moonshot-v1-128k: 0.80/0.80 → 2.00/5.00
- kimi-k2: 2.00/8.00 → 0.60/2.50
- kimi-k2.5: 2.00/8.00 → 0.45/2.20

Sources: platform.moonshot.ai/docs/pricing/chat, costgoat.com, getmaxim.ai

* feat: add MiniMax M2.7 and M2.7-highspeed models

Released 2026-03-18, MiniMax's latest flagship text model.
10B activated params, 200K context, 128K output, tool use, streaming.
Pricing: $0.30/$1.20 per M tokens (input/output).

Added to both international (minimax.io) and China (minimaxi.com) providers.
2026-03-21 03:36:32 +09:00

140 lines
4.3 KiB
Python

#!/usr/bin/env python3
"""Topic-memory ingest hook.
Reads the topic store for the given agent and returns the top matching
topic summaries based on Jaccard similarity between the incoming message
keywords and each stored topic cluster.
This hook is READ-ONLY -- it never modifies the topic store.
Receives via stdin:
{"type": "ingest", "agent_id": "...", "message": "user message text"}
Prints to stdout:
{"type": "ingest_result", "memories": [{"content": "..."}]}
"""
import json
import os
import re
import sys
# ---------------------------------------------------------------------------
# Stopwords & constants
# ---------------------------------------------------------------------------
STOPWORDS = frozenset({
"a", "an", "the", "and", "or", "but", "in", "on", "at", "to", "for",
"of", "with", "by", "from", "is", "are", "was", "were", "be", "been",
"being", "have", "has", "had", "do", "does", "did", "will", "would",
"could", "should", "may", "might", "shall", "can", "need", "must",
"it", "its", "i", "me", "my", "you", "your", "he", "she", "we",
"they", "them", "their", "this", "that", "these", "those", "what",
"which", "who", "how", "when", "where", "why", "if", "then", "so",
"not", "no", "just", "also", "very", "too", "about", "up", "out",
"all", "some", "any", "each", "every", "into", "over", "after",
})
MIN_WORD_LEN = 3
MAX_RESULTS = 3
MIN_SIMILARITY = 0.15
STORE_DIR = os.path.join(os.path.expanduser("~"), ".librefang", "plugins", "topic-memory")
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def extract_keywords(text):
"""Extract lowercase keywords from text, filtering stopwords and short tokens."""
words = re.findall(r"[a-zA-Z][a-zA-Z0-9\-]*[a-zA-Z0-9]|[a-zA-Z]", text)
seen = set()
keywords = set()
for w in words:
lower = w.lower()
if lower not in STOPWORDS and len(lower) >= MIN_WORD_LEN and lower not in seen:
seen.add(lower)
keywords.add(lower)
return keywords
def jaccard_similarity(set_a, set_b):
"""Compute Jaccard similarity between two sets."""
if not set_a or not set_b:
return 0.0
intersection = len(set_a & set_b)
union = len(set_a | set_b)
if union == 0:
return 0.0
return intersection / union
def load_store(agent_id):
"""Load the topic store JSON for an agent. Returns empty structure on any error."""
path = os.path.join(STORE_DIR, f"{agent_id}.json")
try:
with open(path, "r", encoding="utf-8") as f:
data = json.load(f)
if isinstance(data, dict) and isinstance(data.get("topics"), list):
return data
except (OSError, json.JSONDecodeError, ValueError):
pass
return {"topics": []}
def empty_result():
"""Return an empty ingest result."""
return json.dumps({"type": "ingest_result", "memories": []})
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
def main():
try:
request = json.loads(sys.stdin.read())
except (json.JSONDecodeError, ValueError):
print(empty_result())
return
message = request.get("message", "")
agent_id = request.get("agent_id", "")
if not message.strip() or not agent_id:
print(empty_result())
return
msg_keywords = extract_keywords(message)
if not msg_keywords:
print(empty_result())
return
store = load_store(agent_id)
topics = store.get("topics", [])
# Score each topic cluster against the current message keywords
scored = []
for topic in topics:
topic_kw = set(topic.get("keywords", []))
sim = jaccard_similarity(msg_keywords, topic_kw)
if sim >= MIN_SIMILARITY:
scored.append((sim, topic))
# Sort descending by similarity, take top N
scored.sort(key=lambda x: x[0], reverse=True)
top = scored[:MAX_RESULTS]
memories = []
for _sim, topic in top:
summary = topic.get("summary", "")
if summary:
memories.append({"content": f"[topic-memory] Related context: {summary}"})
print(json.dumps({"type": "ingest_result", "memories": memories}))
if __name__ == "__main__":
main()