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,31 @@
|
||||
# auto-summarizer
|
||||
|
||||
Maintains a running conversation summary to help agents handle long conversations without losing context. Uses extractive summarization (no ML or external dependencies) to identify the most important parts of a conversation.
|
||||
|
||||
## How it works
|
||||
|
||||
After each conversation turn, the plugin scans all messages and extracts:
|
||||
|
||||
- **Topic opener** -- the first user message that started the conversation
|
||||
- **Questions** -- any messages containing questions (detected via `?`)
|
||||
- **Decisions** -- messages with conclusion/decision language ("let's", "decided", "the plan is", etc.)
|
||||
- **Recent context** -- the last 2 exchanges to preserve immediate context
|
||||
|
||||
These are combined into a compact summary (max 500 characters) and persisted to disk. On the next ingest, the summary is returned as a memory fragment so the agent retains awareness of the full conversation.
|
||||
|
||||
Summarization only activates when the conversation exceeds 6 messages -- shorter conversations are passed through as-is.
|
||||
|
||||
## Hooks
|
||||
|
||||
| Hook | Script | Description |
|
||||
|------|--------|-------------|
|
||||
| ingest | `hooks/ingest.py` | Returns the stored conversation summary as a memory fragment |
|
||||
| after_turn | `hooks/after_turn.py` | Builds and persists an extractive summary of the conversation |
|
||||
|
||||
## Storage
|
||||
|
||||
Summaries are stored at `~/.librefang/plugins/auto-summarizer/{agent_id}.summary`.
|
||||
|
||||
## Usage
|
||||
|
||||
Installed automatically when enabled in agent configuration.
|
||||
@@ -0,0 +1,153 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Auto-summarizer after_turn hook.
|
||||
|
||||
Generates a compact extractive summary of the conversation after each turn.
|
||||
Keeps agents aware of conversation context even in long exchanges.
|
||||
|
||||
Receives via stdin:
|
||||
{"type": "after_turn", "agent_id": "...", "messages": [...]}
|
||||
|
||||
Prints to stdout:
|
||||
{"type": "ok"}
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
# Only summarize when conversation exceeds this many messages
|
||||
MIN_MESSAGES_FOR_SUMMARY = 6
|
||||
|
||||
# Maximum length of the generated summary
|
||||
MAX_SUMMARY_CHARS = 500
|
||||
|
||||
# Keywords that signal decisions or conclusions
|
||||
DECISION_KEYWORDS = (
|
||||
"let's", "i'll", "we should", "decided", "agreed",
|
||||
"the plan is", "we'll", "going to", "conclusion",
|
||||
"in summary", "to summarize", "final answer",
|
||||
)
|
||||
|
||||
|
||||
def get_storage_dir():
|
||||
"""Return the plugin storage directory, creating it if needed."""
|
||||
home = os.path.expanduser("~")
|
||||
path = os.path.join(home, ".librefang", "plugins", "auto-summarizer")
|
||||
os.makedirs(path, exist_ok=True)
|
||||
return path
|
||||
|
||||
|
||||
def get_content(msg):
|
||||
"""Extract text content from a message object."""
|
||||
if isinstance(msg, dict):
|
||||
return msg.get("content", "") or ""
|
||||
return str(msg)
|
||||
|
||||
|
||||
def get_role(msg):
|
||||
"""Extract the role from a message object."""
|
||||
if isinstance(msg, dict):
|
||||
return msg.get("role", "unknown")
|
||||
return "unknown"
|
||||
|
||||
|
||||
def contains_question(text):
|
||||
"""Check if text contains a question."""
|
||||
return "?" in text
|
||||
|
||||
|
||||
def contains_decision(text):
|
||||
"""Check if text contains decision/conclusion language."""
|
||||
lower = text.lower()
|
||||
return any(kw in lower for kw in DECISION_KEYWORDS)
|
||||
|
||||
|
||||
def truncate(text, max_len):
|
||||
"""Truncate text to max_len, adding ellipsis if needed."""
|
||||
if len(text) <= max_len:
|
||||
return text
|
||||
return text[:max_len - 3] + "..."
|
||||
|
||||
|
||||
def build_summary(messages):
|
||||
"""Build an extractive summary from the conversation messages.
|
||||
|
||||
Strategy:
|
||||
- First user message (topic opener)
|
||||
- Messages containing questions
|
||||
- Messages containing decisions/conclusions
|
||||
- Last 2 exchanges (most recent context)
|
||||
|
||||
Deduplicates and truncates to MAX_SUMMARY_CHARS.
|
||||
"""
|
||||
if len(messages) <= MIN_MESSAGES_FOR_SUMMARY:
|
||||
return ""
|
||||
|
||||
selected = []
|
||||
seen_indices = set()
|
||||
|
||||
# 1. First user message (topic opener)
|
||||
for i, msg in enumerate(messages):
|
||||
if get_role(msg) == "user":
|
||||
content = get_content(msg).strip()
|
||||
if content:
|
||||
selected.append(f"Topic: {truncate(content, 120)}")
|
||||
seen_indices.add(i)
|
||||
break
|
||||
|
||||
# 2. Messages containing questions
|
||||
for i, msg in enumerate(messages):
|
||||
if i in seen_indices:
|
||||
continue
|
||||
content = get_content(msg).strip()
|
||||
if content and contains_question(content):
|
||||
role = get_role(msg)
|
||||
prefix = "Q" if role == "user" else "Agent-Q"
|
||||
selected.append(f"{prefix}: {truncate(content, 100)}")
|
||||
seen_indices.add(i)
|
||||
|
||||
# 3. Messages containing decisions/conclusions
|
||||
for i, msg in enumerate(messages):
|
||||
if i in seen_indices:
|
||||
continue
|
||||
content = get_content(msg).strip()
|
||||
if content and contains_decision(content):
|
||||
selected.append(f"Decision: {truncate(content, 100)}")
|
||||
seen_indices.add(i)
|
||||
|
||||
# 4. Last 2 exchanges (up to 4 messages: user+assistant pairs)
|
||||
tail_start = max(0, len(messages) - 4)
|
||||
for i in range(tail_start, len(messages)):
|
||||
if i in seen_indices:
|
||||
continue
|
||||
content = get_content(messages[i]).strip()
|
||||
if content:
|
||||
role = get_role(messages[i])
|
||||
label = "User" if role == "user" else "Agent"
|
||||
selected.append(f"Recent({label}): {truncate(content, 100)}")
|
||||
seen_indices.add(i)
|
||||
|
||||
if not selected:
|
||||
return ""
|
||||
|
||||
summary = " | ".join(selected)
|
||||
return truncate(summary, MAX_SUMMARY_CHARS)
|
||||
|
||||
|
||||
def main():
|
||||
request = json.loads(sys.stdin.read())
|
||||
agent_id = request.get("agent_id", "unknown")
|
||||
messages = request.get("messages", [])
|
||||
|
||||
summary = build_summary(messages)
|
||||
|
||||
if summary:
|
||||
storage_dir = get_storage_dir()
|
||||
summary_path = os.path.join(storage_dir, f"{agent_id}.summary")
|
||||
with open(summary_path, "w", encoding="utf-8") as f:
|
||||
f.write(summary)
|
||||
|
||||
print(json.dumps({"type": "ok"}), flush=True)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Auto-summarizer ingest hook.
|
||||
|
||||
Returns the stored conversation summary as a memory fragment so agents
|
||||
maintain awareness of prior conversation context.
|
||||
|
||||
Receives via stdin:
|
||||
{"type": "ingest", "agent_id": "...", "message": "user message text"}
|
||||
|
||||
Prints to stdout:
|
||||
{"type": "ingest_result", "memories": [{"content": "..."}]}
|
||||
"""
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
|
||||
|
||||
def get_summary_path(agent_id):
|
||||
"""Return the path to the summary file for the given agent."""
|
||||
home = os.path.expanduser("~")
|
||||
return os.path.join(
|
||||
home, ".librefang", "plugins", "auto-summarizer", f"{agent_id}.summary"
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
request = json.loads(sys.stdin.read())
|
||||
agent_id = request.get("agent_id", "unknown")
|
||||
|
||||
memories = []
|
||||
summary_path = get_summary_path(agent_id)
|
||||
|
||||
if os.path.isfile(summary_path):
|
||||
try:
|
||||
with open(summary_path, "r", encoding="utf-8") as f:
|
||||
summary = f.read().strip()
|
||||
if summary:
|
||||
memories.append(
|
||||
{"content": f"[summary] Conversation so far: {summary}"}
|
||||
)
|
||||
except (OSError, IOError):
|
||||
# If we cannot read the file, return no memories silently.
|
||||
pass
|
||||
|
||||
print(json.dumps({"type": "ingest_result", "memories": memories}))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,41 @@
|
||||
name = "auto-summarizer"
|
||||
version = "0.1.0"
|
||||
description = "Maintains a running conversation summary to help agents handle long conversations without losing context"
|
||||
author = "librefang"
|
||||
|
||||
[hooks]
|
||||
ingest = "hooks/ingest.py"
|
||||
after_turn = "hooks/after_turn.py"
|
||||
|
||||
|
||||
[i18n.zh]
|
||||
name = "自动摘要"
|
||||
description = "持续维护会话摘要,帮助 Agent 在长对话中不丢失上下文。"
|
||||
|
||||
[i18n.zh-TW]
|
||||
name = "自動摘要"
|
||||
description = "持續維護會話摘要,幫助 Agent 在長對話中不遺失上下文。"
|
||||
|
||||
[i18n.ja]
|
||||
name = "自動要約"
|
||||
description = "会話の要約を逐次更新し、長い会話でもコンテキストを失わないよう支援。"
|
||||
|
||||
[i18n.ko]
|
||||
name = "자동 요약"
|
||||
description = "진행 중인 대화 요약을 유지하여 긴 대화에서 컨텍스트 손실을 방지."
|
||||
|
||||
[i18n.de]
|
||||
name = "Auto-Zusammenfassung"
|
||||
description = "Pflegt eine laufende Konversations-Zusammenfassung, damit Agenten bei langen Gesprächen den Kontext behalten."
|
||||
|
||||
[i18n.es]
|
||||
name = "Auto-resumen"
|
||||
description = "Mantiene un resumen continuo de la conversación para que los agentes no pierdan contexto en diálogos largos."
|
||||
|
||||
[i18n.fr]
|
||||
name = "Auto-résumé"
|
||||
description = "Maintient un résumé continu de la conversation pour que les agents ne perdent pas de contexte dans les longs échanges."
|
||||
|
||||
[integrity]
|
||||
"hooks/after_turn.py" = "fe24d5f8dd85348f855f7e2c27e607e57c4fad549d50d814101be14fa5ab34f1"
|
||||
"hooks/ingest.py" = "0581dd415457bffc3160580acf3036a2fe9e211e86b49bbc51cc30118dc67e0e"
|
||||
@@ -0,0 +1 @@
|
||||
# No external dependencies — uses only Python stdlib
|
||||
Reference in new issue
Block a user