mesh-context-layer 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mesh/__init__.py +10 -0
- mesh/audit.py +189 -0
- mesh/cli.py +321 -0
- mesh/context_builder.py +96 -0
- mesh/dashboard.py +1869 -0
- mesh/digest.py +194 -0
- mesh/http_server.py +226 -0
- mesh/io.py +215 -0
- mesh/mcp_server.py +387 -0
- mesh/memory.py +343 -0
- mesh/namespaces.py +218 -0
- mesh/passive.py +690 -0
- mesh/store.py +393 -0
- mesh_context_layer-0.4.0.dist-info/METADATA +215 -0
- mesh_context_layer-0.4.0.dist-info/RECORD +18 -0
- mesh_context_layer-0.4.0.dist-info/WHEEL +4 -0
- mesh_context_layer-0.4.0.dist-info/entry_points.txt +14 -0
- mesh_context_layer-0.4.0.dist-info/licenses/LICENSE +21 -0
mesh/__init__.py
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from .memory import Mesh
|
|
2
|
+
from .io import export_namespace, import_namespace
|
|
3
|
+
from .namespaces import list_namespaces, delete_namespace, rename_namespace, namespace_stats
|
|
4
|
+
from .audit import log_recall, log_learn, get_audit_log, get_audit_stats
|
|
5
|
+
from .context_builder import build_context
|
|
6
|
+
from .digest import generate_digest
|
|
7
|
+
from .passive import extract_memories_from_log
|
|
8
|
+
|
|
9
|
+
__all__ = ["Mesh", "export_namespace", "import_namespace", "list_namespaces", "delete_namespace", "rename_namespace", "namespace_stats", "log_recall", "log_learn", "get_audit_log", "get_audit_stats", "build_context", "generate_digest", "extract_memories_from_log"]
|
|
10
|
+
__version__ = "0.4.0"
|
mesh/audit.py
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mesh/audit.py — Audit trail for all memory reads and writes.
|
|
3
|
+
Logs every recall() and learn() call with timestamp, agent, query/content, and result.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import sqlite3
|
|
7
|
+
import json
|
|
8
|
+
from datetime import datetime, timezone
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import Optional
|
|
11
|
+
|
|
12
|
+
MESH_DIR = Path.home() / ".mesh"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _get_audit_db_path(namespace: str) -> Path:
|
|
16
|
+
return MESH_DIR / f"audit_{namespace}.db"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _init_audit_db(namespace: str):
|
|
20
|
+
db_path = _get_audit_db_path(namespace)
|
|
21
|
+
MESH_DIR.mkdir(exist_ok=True)
|
|
22
|
+
conn = sqlite3.connect(db_path)
|
|
23
|
+
cursor = conn.cursor()
|
|
24
|
+
cursor.execute("""
|
|
25
|
+
CREATE TABLE IF NOT EXISTS audit_log (
|
|
26
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
27
|
+
timestamp TEXT NOT NULL,
|
|
28
|
+
action TEXT NOT NULL, -- 'recall' or 'learn'
|
|
29
|
+
agent_id TEXT,
|
|
30
|
+
namespace TEXT,
|
|
31
|
+
query TEXT, -- for recalls: the query string
|
|
32
|
+
content TEXT, -- for learns: the content stored
|
|
33
|
+
top_result TEXT, -- for recalls: the top result content
|
|
34
|
+
top_similarity REAL, -- for recalls: top result similarity
|
|
35
|
+
result_count INTEGER, -- for recalls: number of results
|
|
36
|
+
memory_id TEXT, -- for learns: the stored memory ID
|
|
37
|
+
had_conflict INTEGER DEFAULT 0 -- for learns: 1 if contradiction detected
|
|
38
|
+
)
|
|
39
|
+
""")
|
|
40
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_timestamp ON audit_log(timestamp)")
|
|
41
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_action ON audit_log(action)")
|
|
42
|
+
cursor.execute("CREATE INDEX IF NOT EXISTS idx_agent ON audit_log(agent_id)")
|
|
43
|
+
conn.commit()
|
|
44
|
+
conn.close()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def log_recall(
|
|
48
|
+
query: str,
|
|
49
|
+
namespace: str,
|
|
50
|
+
agent_id: str,
|
|
51
|
+
results: list
|
|
52
|
+
):
|
|
53
|
+
"""Log a recall event to the audit trail."""
|
|
54
|
+
try:
|
|
55
|
+
_init_audit_db(namespace)
|
|
56
|
+
db_path = _get_audit_db_path(namespace)
|
|
57
|
+
conn = sqlite3.connect(db_path)
|
|
58
|
+
cursor = conn.cursor()
|
|
59
|
+
|
|
60
|
+
top_result = results[0]["content"] if results else None
|
|
61
|
+
top_similarity = results[0].get("similarity", 0.0) if results else None
|
|
62
|
+
|
|
63
|
+
cursor.execute("""
|
|
64
|
+
INSERT INTO audit_log
|
|
65
|
+
(timestamp, action, agent_id, namespace, query, top_result, top_similarity, result_count)
|
|
66
|
+
VALUES (?, 'recall', ?, ?, ?, ?, ?, ?)
|
|
67
|
+
""", (
|
|
68
|
+
datetime.now(timezone.utc).isoformat(),
|
|
69
|
+
agent_id,
|
|
70
|
+
namespace,
|
|
71
|
+
query,
|
|
72
|
+
top_result,
|
|
73
|
+
top_similarity,
|
|
74
|
+
len(results)
|
|
75
|
+
))
|
|
76
|
+
conn.commit()
|
|
77
|
+
conn.close()
|
|
78
|
+
except Exception:
|
|
79
|
+
pass # Audit logging must never crash the main flow
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def log_learn(
|
|
83
|
+
content: str,
|
|
84
|
+
namespace: str,
|
|
85
|
+
agent_id: str,
|
|
86
|
+
result: dict
|
|
87
|
+
):
|
|
88
|
+
"""Log a learn event to the audit trail."""
|
|
89
|
+
try:
|
|
90
|
+
_init_audit_db(namespace)
|
|
91
|
+
db_path = _get_audit_db_path(namespace)
|
|
92
|
+
conn = sqlite3.connect(db_path)
|
|
93
|
+
cursor = conn.cursor()
|
|
94
|
+
|
|
95
|
+
cursor.execute("""
|
|
96
|
+
INSERT INTO audit_log
|
|
97
|
+
(timestamp, action, agent_id, namespace, content, memory_id, had_conflict)
|
|
98
|
+
VALUES (?, 'learn', ?, ?, ?, ?, ?)
|
|
99
|
+
""", (
|
|
100
|
+
datetime.now(timezone.utc).isoformat(),
|
|
101
|
+
agent_id,
|
|
102
|
+
namespace,
|
|
103
|
+
content,
|
|
104
|
+
result.get("memory_id"),
|
|
105
|
+
1 if result.get("conflict") else 0
|
|
106
|
+
))
|
|
107
|
+
conn.commit()
|
|
108
|
+
conn.close()
|
|
109
|
+
except Exception:
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def get_audit_log(
|
|
114
|
+
namespace: str,
|
|
115
|
+
limit: int = 50,
|
|
116
|
+
action_filter: str = "all"
|
|
117
|
+
) -> list[dict]:
|
|
118
|
+
"""
|
|
119
|
+
Return recent audit entries for a namespace, most recent first.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
namespace: The namespace to query.
|
|
123
|
+
limit: Max entries to return.
|
|
124
|
+
action_filter: "all", "read" (maps to recall), or "write" (maps to learn).
|
|
125
|
+
"""
|
|
126
|
+
db_path = _get_audit_db_path(namespace)
|
|
127
|
+
if not db_path.exists():
|
|
128
|
+
return []
|
|
129
|
+
|
|
130
|
+
conn = sqlite3.connect(db_path)
|
|
131
|
+
conn.row_factory = sqlite3.Row
|
|
132
|
+
cursor = conn.cursor()
|
|
133
|
+
|
|
134
|
+
action_map = {"read": "recall", "write": "learn"}
|
|
135
|
+
action_sql = action_map.get(action_filter)
|
|
136
|
+
|
|
137
|
+
if action_sql:
|
|
138
|
+
cursor.execute("""
|
|
139
|
+
SELECT * FROM audit_log
|
|
140
|
+
WHERE action = ?
|
|
141
|
+
ORDER BY timestamp DESC
|
|
142
|
+
LIMIT ?
|
|
143
|
+
""", (action_sql, limit))
|
|
144
|
+
else:
|
|
145
|
+
cursor.execute("""
|
|
146
|
+
SELECT * FROM audit_log
|
|
147
|
+
ORDER BY timestamp DESC
|
|
148
|
+
LIMIT ?
|
|
149
|
+
""", (limit,))
|
|
150
|
+
|
|
151
|
+
rows = cursor.fetchall()
|
|
152
|
+
conn.close()
|
|
153
|
+
return [dict(r) for r in rows]
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def get_audit_stats(namespace: str) -> dict:
|
|
157
|
+
"""Return aggregate stats from the audit log."""
|
|
158
|
+
db_path = _get_audit_db_path(namespace)
|
|
159
|
+
if not db_path.exists():
|
|
160
|
+
return {"total_recalls": 0, "total_learns": 0, "agents": [], "conflicts_detected": 0}
|
|
161
|
+
|
|
162
|
+
conn = sqlite3.connect(db_path)
|
|
163
|
+
conn.row_factory = sqlite3.Row
|
|
164
|
+
cursor = conn.cursor()
|
|
165
|
+
|
|
166
|
+
cursor.execute("SELECT COUNT(*) as c FROM audit_log WHERE action = 'recall'")
|
|
167
|
+
recalls = cursor.fetchone()["c"]
|
|
168
|
+
|
|
169
|
+
cursor.execute("SELECT COUNT(*) as c FROM audit_log WHERE action = 'learn'")
|
|
170
|
+
learns = cursor.fetchone()["c"]
|
|
171
|
+
|
|
172
|
+
cursor.execute("SELECT DISTINCT agent_id FROM audit_log WHERE agent_id IS NOT NULL")
|
|
173
|
+
agents = [r["agent_id"] for r in cursor.fetchall()]
|
|
174
|
+
|
|
175
|
+
cursor.execute("SELECT COUNT(*) as c FROM audit_log WHERE had_conflict = 1")
|
|
176
|
+
conflicts = cursor.fetchone()["c"]
|
|
177
|
+
|
|
178
|
+
cursor.execute("SELECT MAX(timestamp) as last FROM audit_log")
|
|
179
|
+
last = cursor.fetchone()["last"]
|
|
180
|
+
|
|
181
|
+
conn.close()
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
"total_recalls": recalls,
|
|
185
|
+
"total_learns": learns,
|
|
186
|
+
"agents": agents,
|
|
187
|
+
"conflicts_detected": conflicts,
|
|
188
|
+
"last_activity": last
|
|
189
|
+
}
|
mesh/cli.py
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mesh/cli.py — Command-line interface for Mesh.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import sys
|
|
6
|
+
import json
|
|
7
|
+
import argparse
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def export_cmd():
|
|
11
|
+
"""Entry point for: mesh-export"""
|
|
12
|
+
parser = argparse.ArgumentParser(
|
|
13
|
+
description="Export Mesh memories to a JSON file.",
|
|
14
|
+
epilog="Example: mesh-export backup.json --namespace work"
|
|
15
|
+
)
|
|
16
|
+
parser.add_argument("output", help="Output file path (e.g. backup.json)")
|
|
17
|
+
parser.add_argument("--namespace", default="shared", help="Namespace to export (default: shared)")
|
|
18
|
+
parser.add_argument("--no-embeddings", action="store_true",
|
|
19
|
+
help="Exclude embedding vectors (smaller file, human-readable)")
|
|
20
|
+
args = parser.parse_args()
|
|
21
|
+
|
|
22
|
+
from .io import export_namespace
|
|
23
|
+
export_namespace(
|
|
24
|
+
namespace=args.namespace,
|
|
25
|
+
output_path=args.output,
|
|
26
|
+
include_embeddings=not args.no_embeddings
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def import_cmd():
|
|
31
|
+
"""Entry point for: mesh-import"""
|
|
32
|
+
parser = argparse.ArgumentParser(
|
|
33
|
+
description="Import Mesh memories from a JSON export file.",
|
|
34
|
+
epilog="Example: mesh-import backup.json --namespace work"
|
|
35
|
+
)
|
|
36
|
+
parser.add_argument("input", help="Input export file path")
|
|
37
|
+
parser.add_argument("--namespace", default=None,
|
|
38
|
+
help="Override target namespace (default: use namespace from export file)")
|
|
39
|
+
parser.add_argument("--overwrite", action="store_true",
|
|
40
|
+
help="Overwrite existing memories with same ID (default: skip)")
|
|
41
|
+
parser.add_argument("--dry-run", action="store_true",
|
|
42
|
+
help="Preview what would be imported without writing anything")
|
|
43
|
+
args = parser.parse_args()
|
|
44
|
+
|
|
45
|
+
from .io import import_namespace
|
|
46
|
+
import_namespace(
|
|
47
|
+
input_path=args.input,
|
|
48
|
+
target_namespace=args.namespace,
|
|
49
|
+
merge_strategy="overwrite" if args.overwrite else "skip_existing",
|
|
50
|
+
dry_run=args.dry_run
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def namespaces_cmd():
|
|
55
|
+
"""Entry point for: mesh-namespaces"""
|
|
56
|
+
parser = argparse.ArgumentParser(
|
|
57
|
+
description="Manage Mesh namespaces.",
|
|
58
|
+
epilog="""Commands:
|
|
59
|
+
mesh-namespaces list List all namespaces
|
|
60
|
+
mesh-namespaces delete work Delete namespace 'work' and all its memories
|
|
61
|
+
mesh-namespaces rename work work-old Rename namespace 'work' to 'work-old'
|
|
62
|
+
mesh-namespaces stats work Show stats for namespace 'work'
|
|
63
|
+
"""
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument("command", choices=["list", "delete", "rename", "stats"])
|
|
66
|
+
parser.add_argument("args", nargs="*", help="Arguments for the command")
|
|
67
|
+
parsed = parser.parse_args()
|
|
68
|
+
|
|
69
|
+
from .namespaces import list_namespaces, delete_namespace, rename_namespace, namespace_stats
|
|
70
|
+
|
|
71
|
+
if parsed.command == "list":
|
|
72
|
+
namespaces = list_namespaces()
|
|
73
|
+
if not namespaces:
|
|
74
|
+
print("No namespaces found.")
|
|
75
|
+
return
|
|
76
|
+
print(f"{'Namespace':<30} {'Memories':>10} {'Local-only':>12} {'Last updated':<25}")
|
|
77
|
+
print("-" * 80)
|
|
78
|
+
for ns in namespaces:
|
|
79
|
+
print(f"{ns['namespace']:<30} {ns['memory_count']:>10} {ns['local_only_count']:>12} {ns['last_updated']:<25}")
|
|
80
|
+
|
|
81
|
+
elif parsed.command == "delete":
|
|
82
|
+
if not parsed.args:
|
|
83
|
+
print("Error: provide a namespace name. Example: mesh-namespaces delete work")
|
|
84
|
+
sys.exit(1)
|
|
85
|
+
name = parsed.args[0]
|
|
86
|
+
confirm = input(f"Delete ALL memories in namespace '{name}'? This cannot be undone. [y/N] ")
|
|
87
|
+
if confirm.lower() != "y":
|
|
88
|
+
print("Cancelled.")
|
|
89
|
+
return
|
|
90
|
+
count = delete_namespace(name)
|
|
91
|
+
print(f"✓ Deleted namespace '{name}' ({count} memories removed)")
|
|
92
|
+
|
|
93
|
+
elif parsed.command == "rename":
|
|
94
|
+
if len(parsed.args) < 2:
|
|
95
|
+
print("Error: provide old and new names. Example: mesh-namespaces rename work work-archived")
|
|
96
|
+
sys.exit(1)
|
|
97
|
+
old, new = parsed.args[0], parsed.args[1]
|
|
98
|
+
count = rename_namespace(old, new)
|
|
99
|
+
print(f"✓ Renamed '{old}' → '{new}' ({count} memories updated)")
|
|
100
|
+
|
|
101
|
+
elif parsed.command == "stats":
|
|
102
|
+
if not parsed.args:
|
|
103
|
+
print("Error: provide a namespace name. Example: mesh-namespaces stats work")
|
|
104
|
+
sys.exit(1)
|
|
105
|
+
name = parsed.args[0]
|
|
106
|
+
stats = namespace_stats(name)
|
|
107
|
+
print(f"\nNamespace: {name}")
|
|
108
|
+
print(f" Total memories: {stats['memory_count']}")
|
|
109
|
+
print(f" Local-only: {stats['local_only_count']}")
|
|
110
|
+
print(f" Exportable: {stats['exportable_count']}")
|
|
111
|
+
print(f" Memory types: {json.dumps(stats['type_breakdown'], indent=4)}")
|
|
112
|
+
print(f" Avg confidence: {stats['avg_confidence']:.2f}")
|
|
113
|
+
print(f" Stale memories: {stats['stale_count']}")
|
|
114
|
+
print(f" Total recalls: {stats['total_recalls']}")
|
|
115
|
+
print(f" Created: {stats['oldest_memory']}")
|
|
116
|
+
print(f" Last updated: {stats['newest_memory']}")
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def context_cmd():
|
|
120
|
+
"""Entry point for: mesh-context"""
|
|
121
|
+
import argparse
|
|
122
|
+
parser = argparse.ArgumentParser(
|
|
123
|
+
description="Output Mesh memories as a formatted system prompt block.",
|
|
124
|
+
epilog="""Examples:
|
|
125
|
+
mesh-context # Top 10 memories, markdown format
|
|
126
|
+
mesh-context --query "deploy process" # Focused on deploy memories
|
|
127
|
+
mesh-context --format plain --count 5 # Plain text, 5 memories
|
|
128
|
+
CONTEXT=$(mesh-context) && echo $CONTEXT # Capture for use in scripts
|
|
129
|
+
|
|
130
|
+
Pipe directly into agent invocations:
|
|
131
|
+
aider --system-prompt "$(mesh-context)" ...
|
|
132
|
+
sgpt --system "$(mesh-context)" "help me with the deploy"
|
|
133
|
+
"""
|
|
134
|
+
)
|
|
135
|
+
parser.add_argument("--query", default=None, help="Focus context on a specific topic")
|
|
136
|
+
parser.add_argument("--namespace", default=None, help="Namespace to query (default: shared)")
|
|
137
|
+
parser.add_argument("--count", type=int, default=10, help="Number of memories to include")
|
|
138
|
+
parser.add_argument("--format", choices=["markdown", "plain", "xml", "json"], default="markdown")
|
|
139
|
+
parser.add_argument("--min-confidence", type=float, default=0.3)
|
|
140
|
+
args = parser.parse_args()
|
|
141
|
+
|
|
142
|
+
from .context_builder import build_context
|
|
143
|
+
result = build_context(
|
|
144
|
+
query=args.query,
|
|
145
|
+
namespace=args.namespace,
|
|
146
|
+
count=args.count,
|
|
147
|
+
format=args.format,
|
|
148
|
+
min_confidence=args.min_confidence
|
|
149
|
+
)
|
|
150
|
+
if result:
|
|
151
|
+
print(result)
|
|
152
|
+
else:
|
|
153
|
+
import sys
|
|
154
|
+
print("No memories found.", file=sys.stderr)
|
|
155
|
+
sys.exit(1)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def add_cmd():
|
|
159
|
+
"""Entry point for: mesh-add"""
|
|
160
|
+
import argparse
|
|
161
|
+
parser = argparse.ArgumentParser(
|
|
162
|
+
description="Store a memory directly into Mesh.",
|
|
163
|
+
epilog="Example: mesh-add 'prod DB password rotated on May 1' --type fact --tags db,security"
|
|
164
|
+
)
|
|
165
|
+
parser.add_argument("content", help="The memory to store")
|
|
166
|
+
parser.add_argument("--type", dest="memory_type", default="general",
|
|
167
|
+
choices=["fact", "process", "decision", "error", "general"],
|
|
168
|
+
help="Memory type (default: general)")
|
|
169
|
+
parser.add_argument("--tags", default="", help="Comma-separated tags")
|
|
170
|
+
parser.add_argument("--private", action="store_true",
|
|
171
|
+
help="Mark as local-only (never synced or exported)")
|
|
172
|
+
parser.add_argument("--namespace", default=None)
|
|
173
|
+
parser.add_argument("--confidence", type=float, default=1.0)
|
|
174
|
+
parser.add_argument("--ttl", type=int, default=None,
|
|
175
|
+
help="Days before this memory is considered stale")
|
|
176
|
+
args = parser.parse_args()
|
|
177
|
+
|
|
178
|
+
from .memory import Mesh
|
|
179
|
+
import os
|
|
180
|
+
ns = args.namespace or os.environ.get("MESH_NAMESPACE", "shared")
|
|
181
|
+
mesh = Mesh(namespace=ns, agent_id="human-cli")
|
|
182
|
+
|
|
183
|
+
tags = [t.strip() for t in args.tags.split(",") if t.strip()] if args.tags else []
|
|
184
|
+
|
|
185
|
+
result = mesh.learn(
|
|
186
|
+
content=args.content,
|
|
187
|
+
memory_type=args.memory_type,
|
|
188
|
+
confidence=args.confidence,
|
|
189
|
+
tags=tags,
|
|
190
|
+
local_only=args.private,
|
|
191
|
+
ttl_days=args.ttl
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
private_note = " (private — will not be exported or synced)" if args.private else ""
|
|
195
|
+
conflict_note = ""
|
|
196
|
+
if result.get("conflict"):
|
|
197
|
+
conflict_note = f"\n⚠️ Possible conflict with existing memory:\n \"{result['conflict']['existing_memory']}\""
|
|
198
|
+
|
|
199
|
+
print(f"✓ Stored [{args.memory_type}] in namespace '{ns}'{private_note}{conflict_note}")
|
|
200
|
+
if result.get("memory_id"):
|
|
201
|
+
print(f" ID: {result['memory_id']}")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def ask_cmd():
|
|
205
|
+
"""Entry point for: mesh-ask"""
|
|
206
|
+
import argparse
|
|
207
|
+
parser = argparse.ArgumentParser(
|
|
208
|
+
description="Search Mesh for memories relevant to a question.",
|
|
209
|
+
epilog="Example: mesh-ask 'what is the deploy process?'"
|
|
210
|
+
)
|
|
211
|
+
parser.add_argument("query", help="What to search for")
|
|
212
|
+
parser.add_argument("--count", type=int, default=5)
|
|
213
|
+
parser.add_argument("--namespace", default=None)
|
|
214
|
+
parser.add_argument("--type", dest="memory_type", default=None)
|
|
215
|
+
parser.add_argument("--min-confidence", type=float, default=0.0)
|
|
216
|
+
args = parser.parse_args()
|
|
217
|
+
|
|
218
|
+
from .memory import Mesh
|
|
219
|
+
import os
|
|
220
|
+
ns = args.namespace or os.environ.get("MESH_NAMESPACE", "shared")
|
|
221
|
+
mesh = Mesh(namespace=ns, agent_id="human-cli")
|
|
222
|
+
|
|
223
|
+
results = mesh.recall(
|
|
224
|
+
query=args.query,
|
|
225
|
+
count=args.count,
|
|
226
|
+
memory_type=args.memory_type,
|
|
227
|
+
min_confidence=args.min_confidence
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
if not results:
|
|
231
|
+
print(f"No memories found for: {args.query}")
|
|
232
|
+
return
|
|
233
|
+
|
|
234
|
+
print(f"\nFound {len(results)} memories in namespace '{ns}':\n")
|
|
235
|
+
for i, mem in enumerate(results, 1):
|
|
236
|
+
sim = mem.get("similarity", 0)
|
|
237
|
+
conf = mem.get("effective_confidence", mem.get("confidence", 1.0))
|
|
238
|
+
stale = " [STALE]" if mem.get("is_stale") else ""
|
|
239
|
+
private = " [PRIVATE]" if mem.get("local_only") else ""
|
|
240
|
+
tags = f" #{' #'.join(mem['tags'])}" if mem.get("tags") else ""
|
|
241
|
+
|
|
242
|
+
print(f" {i}. {mem['content']}{stale}{private}")
|
|
243
|
+
print(f" type={mem.get('memory_type','general')} | "
|
|
244
|
+
f"similarity={sim:.2f} | confidence={conf:.2f} | "
|
|
245
|
+
f"from={mem.get('source_agent','unknown')}{tags}")
|
|
246
|
+
print()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def why_cmd():
|
|
250
|
+
"""Entry point for: mesh-why"""
|
|
251
|
+
import argparse
|
|
252
|
+
parser = argparse.ArgumentParser(
|
|
253
|
+
description="Show recent memory reads and writes across all agents (audit trail).",
|
|
254
|
+
epilog="Example: mesh-why --limit 30 --namespace work"
|
|
255
|
+
)
|
|
256
|
+
parser.add_argument("--limit", type=int, default=20)
|
|
257
|
+
parser.add_argument("--namespace", default=None)
|
|
258
|
+
parser.add_argument("--action", choices=["read", "write", "all"], default="all")
|
|
259
|
+
args = parser.parse_args()
|
|
260
|
+
|
|
261
|
+
from .audit import get_audit_log
|
|
262
|
+
import os
|
|
263
|
+
ns = args.namespace or os.environ.get("MESH_NAMESPACE", "shared")
|
|
264
|
+
entries = get_audit_log(namespace=ns, limit=args.limit, action_filter=args.action)
|
|
265
|
+
|
|
266
|
+
if not entries:
|
|
267
|
+
print(f"No audit entries found for namespace '{ns}'.")
|
|
268
|
+
return
|
|
269
|
+
|
|
270
|
+
print(f"\nAudit trail for namespace '{ns}' (most recent first):\n")
|
|
271
|
+
for entry in entries:
|
|
272
|
+
action_icon = "📖" if entry["action"] == "recall" else "✏️"
|
|
273
|
+
print(f" {action_icon} [{entry['timestamp'][:19]}] {entry['agent_id']}")
|
|
274
|
+
if entry["action"] == "recall":
|
|
275
|
+
print(f" asked: \"{entry['query']}\"")
|
|
276
|
+
if entry.get("top_result"):
|
|
277
|
+
print(f" got: \"{entry['top_result'][:80]}...\""
|
|
278
|
+
if len(entry.get("top_result","")) > 80
|
|
279
|
+
else f" got: \"{entry['top_result']}\"")
|
|
280
|
+
else:
|
|
281
|
+
print(f" stored: \"{entry['content'][:80]}\"")
|
|
282
|
+
print()
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def status_cmd():
|
|
286
|
+
"""Entry point for: mesh-status"""
|
|
287
|
+
import argparse
|
|
288
|
+
parser = argparse.ArgumentParser(description="Show Mesh status overview.")
|
|
289
|
+
parser.add_argument("--namespace", default=None)
|
|
290
|
+
args = parser.parse_args()
|
|
291
|
+
|
|
292
|
+
from .namespaces import list_namespaces, namespace_stats
|
|
293
|
+
from .audit import get_audit_log
|
|
294
|
+
import os
|
|
295
|
+
|
|
296
|
+
ns = args.namespace or os.environ.get("MESH_NAMESPACE", "shared")
|
|
297
|
+
|
|
298
|
+
try:
|
|
299
|
+
stats = namespace_stats(ns)
|
|
300
|
+
except ValueError:
|
|
301
|
+
print(f"Namespace '{ns}' is empty or does not exist yet.")
|
|
302
|
+
print(f"Run 'mesh-add \"something\"' to create it.")
|
|
303
|
+
return
|
|
304
|
+
|
|
305
|
+
recent = get_audit_log(namespace=ns, limit=5)
|
|
306
|
+
last_activity = recent[0]["timestamp"][:19] if recent else "never"
|
|
307
|
+
|
|
308
|
+
print(f"\nMesh Status — namespace: {ns}\n")
|
|
309
|
+
print(f" Memories: {stats['memory_count']} total, "
|
|
310
|
+
f"{stats['local_only_count']} private, "
|
|
311
|
+
f"{stats['stale_count']} stale")
|
|
312
|
+
print(f" Exportable: {stats['exportable_count']}")
|
|
313
|
+
print(f" Total recalls: {stats['total_recalls']}")
|
|
314
|
+
print(f" Last activity: {last_activity}")
|
|
315
|
+
print(f" Types: {stats['type_breakdown']}")
|
|
316
|
+
|
|
317
|
+
all_ns = list_namespaces()
|
|
318
|
+
if len(all_ns) > 1:
|
|
319
|
+
others = [n["namespace"] for n in all_ns if n["namespace"] != ns]
|
|
320
|
+
print(f" Other namespaces: {', '.join(others)}")
|
|
321
|
+
print()
|
mesh/context_builder.py
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
mesh/context_builder.py — Builds formatted system prompt context blocks from Mesh memories.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
from typing import Optional
|
|
7
|
+
from .memory import Mesh
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def build_context(
|
|
11
|
+
query: Optional[str] = None,
|
|
12
|
+
namespace: Optional[str] = None,
|
|
13
|
+
count: int = 10,
|
|
14
|
+
format: str = "markdown",
|
|
15
|
+
min_confidence: float = 0.3,
|
|
16
|
+
agent_id: str = "context-builder"
|
|
17
|
+
) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Build a formatted context block from the most relevant memories.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
query: Optional query to focus the context. If None, pulls top memories by access frequency.
|
|
23
|
+
namespace: Namespace to query. Defaults to MESH_NAMESPACE env var or "shared".
|
|
24
|
+
count: Number of memories to include. Default 10.
|
|
25
|
+
format: Output format — "markdown", "plain", "xml", or "json".
|
|
26
|
+
min_confidence: Minimum confidence score to include. Default 0.3.
|
|
27
|
+
agent_id: Agent ID to log this context pull under.
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
Formatted string ready to paste into a system prompt.
|
|
31
|
+
"""
|
|
32
|
+
ns = namespace or os.environ.get("MESH_NAMESPACE", "shared")
|
|
33
|
+
mesh = Mesh(namespace=ns, agent_id=agent_id)
|
|
34
|
+
|
|
35
|
+
if query:
|
|
36
|
+
memories = mesh.recall(query=query, n=count, min_confidence=min_confidence)
|
|
37
|
+
else:
|
|
38
|
+
# No query: pull most-accessed memories as general context
|
|
39
|
+
memories = mesh.inspect(limit=count * 3) # get more, then sort
|
|
40
|
+
memories = sorted(memories, key=lambda m: m.get("access_count", 0), reverse=True)
|
|
41
|
+
memories = [m for m in memories if m.get("confidence", 1.0) >= min_confidence][:count]
|
|
42
|
+
|
|
43
|
+
if not memories:
|
|
44
|
+
return ""
|
|
45
|
+
|
|
46
|
+
return _format(memories, format, ns)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _format(memories: list, format: str, namespace: str) -> str:
|
|
50
|
+
if format == "json":
|
|
51
|
+
import json
|
|
52
|
+
return json.dumps({
|
|
53
|
+
"mesh_context": True,
|
|
54
|
+
"namespace": namespace,
|
|
55
|
+
"memories": memories
|
|
56
|
+
}, indent=2)
|
|
57
|
+
|
|
58
|
+
if format == "xml":
|
|
59
|
+
lines = ["<mesh_context>"]
|
|
60
|
+
for mem in memories:
|
|
61
|
+
stale = ' stale="true"' if mem.get("is_stale") else ""
|
|
62
|
+
lines.append(f' <memory type="{mem.get("memory_type", "general")}"{stale}>')
|
|
63
|
+
lines.append(f' {mem["content"]}')
|
|
64
|
+
lines.append(f' </memory>')
|
|
65
|
+
lines.append("</mesh_context>")
|
|
66
|
+
return "\n".join(lines)
|
|
67
|
+
|
|
68
|
+
if format == "plain":
|
|
69
|
+
lines = ["[MESH CONTEXT]"]
|
|
70
|
+
for mem in memories:
|
|
71
|
+
stale = " (possibly stale)" if mem.get("is_stale") else ""
|
|
72
|
+
lines.append(f"- {mem['content']}{stale}")
|
|
73
|
+
return "\n".join(lines)
|
|
74
|
+
|
|
75
|
+
# Default: markdown (best for LLM system prompts)
|
|
76
|
+
lines = [
|
|
77
|
+
"<mesh_context>",
|
|
78
|
+
"The following facts were stored by AI agents working on this project.",
|
|
79
|
+
"Trust these as ground truth unless you have newer information.",
|
|
80
|
+
"",
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
by_type: dict = {}
|
|
84
|
+
for mem in memories:
|
|
85
|
+
t = mem.get("memory_type", "general")
|
|
86
|
+
by_type.setdefault(t, []).append(mem)
|
|
87
|
+
|
|
88
|
+
for mem_type, mems in sorted(by_type.items()):
|
|
89
|
+
lines.append(f"**{mem_type.title()}**")
|
|
90
|
+
for mem in mems:
|
|
91
|
+
stale = " *(possibly stale)*" if mem.get("is_stale") else ""
|
|
92
|
+
lines.append(f"- {mem['content']}{stale}")
|
|
93
|
+
lines.append("")
|
|
94
|
+
|
|
95
|
+
lines.append("</mesh_context>")
|
|
96
|
+
return "\n".join(lines)
|