memplex 3.2.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.
Files changed (83) hide show
  1. memnex/__init__.py +31 -0
  2. memnex/__main__.py +6 -0
  3. memnex/_plugin/.claude-plugin/plugin.json +24 -0
  4. memnex/_plugin/.mcp.json +9 -0
  5. memnex/_plugin/__init__.py +0 -0
  6. memnex/_plugin/hooks/hooks.json +43 -0
  7. memnex/_plugin/scripts/hook-runner.py +166 -0
  8. memnex/_plugin/skills/mem-explore/SKILL.md +83 -0
  9. memnex/_plugin/skills/mem-manage/SKILL.md +92 -0
  10. memnex/_plugin/skills/mem-search/SKILL.md +85 -0
  11. memnex/_plugin/skills/mem-write/SKILL.md +78 -0
  12. memnex/adapters/__init__.py +14 -0
  13. memnex/adapters/claude_skill.py +169 -0
  14. memnex/adapters/cli.py +525 -0
  15. memnex/adapters/http_api.py +314 -0
  16. memnex/adapters/mcp_server.py +448 -0
  17. memnex/compaction.py +563 -0
  18. memnex/config.py +366 -0
  19. memnex/core/__init__.py +13 -0
  20. memnex/core/associator/__init__.py +8 -0
  21. memnex/core/associator/domain_classifier.py +75 -0
  22. memnex/core/associator/entity_aligner.py +127 -0
  23. memnex/core/associator/ref_linker.py +197 -0
  24. memnex/core/associator/term_mapper.py +77 -0
  25. memnex/core/dictionaries/__init__.py +50 -0
  26. memnex/core/engine.py +667 -0
  27. memnex/core/extractors/__init__.py +15 -0
  28. memnex/core/extractors/docx.py +97 -0
  29. memnex/core/extractors/image.py +233 -0
  30. memnex/core/extractors/markdown.py +139 -0
  31. memnex/core/extractors/pdf.py +133 -0
  32. memnex/core/extractors/vision_mapper.py +131 -0
  33. memnex/core/handlers/__init__.py +7 -0
  34. memnex/core/handlers/clipboard.py +40 -0
  35. memnex/core/handlers/file_handler.py +62 -0
  36. memnex/core/handlers/url_handler.py +132 -0
  37. memnex/llm/__init__.py +25 -0
  38. memnex/llm/enhancer.py +226 -0
  39. memnex/llm/fallback_chain.py +87 -0
  40. memnex/llm/injection_guard.py +178 -0
  41. memnex/llm/provider.py +130 -0
  42. memnex/llm/providers/__init__.py +22 -0
  43. memnex/llm/providers/anthropic.py +135 -0
  44. memnex/llm/providers/local.py +135 -0
  45. memnex/llm/providers/rule_based.py +68 -0
  46. memnex/llm/sanitizer.py +67 -0
  47. memnex/models/__init__.py +68 -0
  48. memnex/models/feedback.py +42 -0
  49. memnex/models/graph.py +33 -0
  50. memnex/models/memory.py +102 -0
  51. memnex/models/misc.py +185 -0
  52. memnex/models/paragraph.py +45 -0
  53. memnex/models/search.py +51 -0
  54. memnex/models/source.py +23 -0
  55. memnex/models/task.py +62 -0
  56. memnex/processing/__init__.py +1 -0
  57. memnex/processing/graph_builder.py +278 -0
  58. memnex/processing/merger/__init__.py +6 -0
  59. memnex/processing/merger/confidence_calculator.py +127 -0
  60. memnex/processing/merger/conflict_resolver.py +116 -0
  61. memnex/retrieval/__init__.py +1 -0
  62. memnex/retrieval/dedup.py +386 -0
  63. memnex/retrieval/embedding.py +289 -0
  64. memnex/retrieval/reranker.py +299 -0
  65. memnex/service.py +902 -0
  66. memnex/storage/__init__.py +65 -0
  67. memnex/storage/base.py +132 -0
  68. memnex/storage/changelog.py +106 -0
  69. memnex/storage/feedback.py +486 -0
  70. memnex/storage/lite/__init__.py +5 -0
  71. memnex/storage/lite/store.py +606 -0
  72. memnex/storage/vector.py +265 -0
  73. memnex/wiki/__init__.py +11 -0
  74. memnex/wiki/community.py +221 -0
  75. memnex/wiki/compiler.py +545 -0
  76. memnex/wiki/generator.py +270 -0
  77. memnex/wiki/search.py +282 -0
  78. memnex/worker.py +412 -0
  79. memplex-3.2.0.dist-info/METADATA +37 -0
  80. memplex-3.2.0.dist-info/RECORD +83 -0
  81. memplex-3.2.0.dist-info/WHEEL +5 -0
  82. memplex-3.2.0.dist-info/entry_points.txt +2 -0
  83. memplex-3.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,448 @@
1
+ """MemNex MCP Server -- Model Context Protocol over stdio JSON-RPC.
2
+
3
+ Implements a lightweight MCP server that communicates via stdin/stdout
4
+ using JSON-RPC 2.0. No external MCP SDK dependency -- pure Python.
5
+
6
+ Protocol::
7
+
8
+ Request: {"jsonrpc": "2.0", "method": "tools/call", "params": {"name": "...", "arguments": {...}}, "id": 1}
9
+ Response: {"jsonrpc": "2.0", "result": {...}, "id": 1}
10
+
11
+ Usage::
12
+
13
+ from memnex.adapters.mcp_server import MCPServer
14
+
15
+ server = MCPServer()
16
+ server.run() # reads from stdin, writes to stdout
17
+
18
+ Or as a module::
19
+
20
+ python -m memnex.adapters.mcp_server
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import json
26
+ import logging
27
+ import sys
28
+ import traceback
29
+ from dataclasses import asdict
30
+ from typing import Any, Dict, List, Optional
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ # ── Helpers ─────────────────────────────────────────────────────────
36
+
37
+
38
+ def _dataclass_to_dict(obj) -> Any:
39
+ """Recursively convert dataclasses to plain dicts."""
40
+ if hasattr(obj, "__dataclass_fields__"):
41
+ return asdict(obj)
42
+ if isinstance(obj, list):
43
+ return [_dataclass_to_dict(item) for item in obj]
44
+ if isinstance(obj, dict):
45
+ return {k: _dataclass_to_dict(v) for k, v in obj.items()}
46
+ return obj
47
+
48
+
49
+ # ── Tool definitions ────────────────────────────────────────────────
50
+
51
+ _TOOL_DEFINITIONS = [
52
+ {
53
+ "name": "memory_search",
54
+ "description": "Search MemNex knowledge graph. Returns index with IDs, names, relevance scores. ALWAYS use this before memory_get to filter results (10x token savings).",
55
+ "inputSchema": {
56
+ "type": "object",
57
+ "properties": {
58
+ "query": {"type": "string", "description": "Natural language search query"},
59
+ "top_k": {"type": "integer", "description": "Max results (default 10, max 100)", "default": 10},
60
+ },
61
+ "required": ["query"],
62
+ },
63
+ },
64
+ {
65
+ "name": "memory_add",
66
+ "description": "Add a new memory from text content.",
67
+ "inputSchema": {
68
+ "type": "object",
69
+ "properties": {
70
+ "content": {"type": "string", "description": "Text content to store"},
71
+ "source_type": {
72
+ "type": "string",
73
+ "description": "Source type: text | file | url (default: text)",
74
+ "default": "text",
75
+ },
76
+ },
77
+ "required": ["content"],
78
+ },
79
+ },
80
+ {
81
+ "name": "memory_get",
82
+ "description": "Retrieve full details for a specific memory. Use AFTER memory_search to get details only for filtered IDs (~500-1000 tokens each).",
83
+ "inputSchema": {
84
+ "type": "object",
85
+ "properties": {
86
+ "memory_id": {"type": "string", "description": "Memory ID from search results"},
87
+ },
88
+ "required": ["memory_id"],
89
+ },
90
+ },
91
+ {
92
+ "name": "memory_update",
93
+ "description": "Update a field value of an existing memory (self-editing).",
94
+ "inputSchema": {
95
+ "type": "object",
96
+ "properties": {
97
+ "memory_id": {"type": "string", "description": "Memory ID"},
98
+ "role": {
99
+ "type": "string",
100
+ "description": "Field role: trigger | condition | action | benefit",
101
+ },
102
+ "new_value": {"type": "string", "description": "New field value text"},
103
+ },
104
+ "required": ["memory_id", "role", "new_value"],
105
+ },
106
+ },
107
+ {
108
+ "name": "memory_delete",
109
+ "description": "Delete a memory by ID.",
110
+ "inputSchema": {
111
+ "type": "object",
112
+ "properties": {
113
+ "memory_id": {"type": "string", "description": "Memory ID"},
114
+ },
115
+ "required": ["memory_id"],
116
+ },
117
+ },
118
+ {
119
+ "name": "memory_feedback",
120
+ "description": "Submit feedback on a memory field value.",
121
+ "inputSchema": {
122
+ "type": "object",
123
+ "properties": {
124
+ "memory_id": {"type": "string", "description": "Memory ID"},
125
+ "role": {
126
+ "type": "string",
127
+ "description": "Field role: trigger | action | condition | benefit",
128
+ },
129
+ "index": {"type": "integer", "description": "Value index within the field"},
130
+ "verdict": {
131
+ "type": "string",
132
+ "description": "Verdict: correct | wrong",
133
+ },
134
+ "reason": {"type": "string", "description": "Optional explanation"},
135
+ },
136
+ "required": ["memory_id", "role", "index", "verdict"],
137
+ },
138
+ },
139
+ {
140
+ "name": "memory_pending_reviews",
141
+ "description": "List pending feedback reviews that need resolution.",
142
+ "inputSchema": {
143
+ "type": "object",
144
+ "properties": {
145
+ "owner": {"type": "string", "description": "Filter by owner (optional)"},
146
+ "limit": {"type": "integer", "description": "Max results (default 100)", "default": 100},
147
+ },
148
+ },
149
+ },
150
+ {
151
+ "name": "memory_resolve",
152
+ "description": "Apply a resolution to a pending review.",
153
+ "inputSchema": {
154
+ "type": "object",
155
+ "properties": {
156
+ "memory_id": {"type": "string", "description": "Memory ID"},
157
+ "field_role": {"type": "string", "description": "Field role under review"},
158
+ "action": {
159
+ "type": "string",
160
+ "description": "Resolution action: accept | reject | merge",
161
+ },
162
+ "new_value": {
163
+ "type": "string",
164
+ "description": "Replacement value (required when action=merge)",
165
+ },
166
+ },
167
+ "required": ["memory_id", "field_role", "action"],
168
+ },
169
+ },
170
+ {
171
+ "name": "memory_health",
172
+ "description": "Check MemNex service health status.",
173
+ "inputSchema": {
174
+ "type": "object",
175
+ "properties": {},
176
+ },
177
+ },
178
+ ]
179
+
180
+
181
+ # ── MCPServer ───────────────────────────────────────────────────────
182
+
183
+
184
+ class MCPServer:
185
+ """MCP Server for MemNex, communicating over stdio JSON-RPC.
186
+
187
+ Parameters
188
+ ----------
189
+ config:
190
+ Optional :class:`MemNexConfig`. When ``None``, loaded via
191
+ :func:`load_config`.
192
+ """
193
+
194
+ def __init__(self, config=None) -> None:
195
+ self._config = config
196
+ self._service = None
197
+
198
+ # ── Lifecycle ────────────────────────────────────────────────
199
+
200
+ def _ensure_service(self):
201
+ """Lazy-initialize the MemNexService."""
202
+ if self._service is not None:
203
+ return
204
+
205
+ from memnex.config import load_config
206
+ from memnex.service import MemNexService
207
+
208
+ cfg = self._config or load_config()
209
+ self._service = MemNexService(config=cfg)
210
+ self._service.start()
211
+
212
+ # ── JSON-RPC I/O ─────────────────────────────────────────────
213
+
214
+ def _read_request(self) -> Optional[dict]:
215
+ """Read a single JSON-RPC request from stdin."""
216
+ line = sys.stdin.readline()
217
+ if not line:
218
+ return None
219
+ line = line.strip()
220
+ if not line:
221
+ return None
222
+ try:
223
+ return json.loads(line)
224
+ except json.JSONDecodeError as exc:
225
+ logger.warning("Invalid JSON from stdin: %s", exc)
226
+ return None
227
+
228
+ def _write_response(self, response: dict) -> None:
229
+ """Write a JSON-RPC response to stdout."""
230
+ sys.stdout.write(json.dumps(response, default=str, ensure_ascii=False) + "\n")
231
+ sys.stdout.flush()
232
+
233
+ def _make_result(self, result: Any, req_id: Any) -> dict:
234
+ """Build a JSON-RPC success response."""
235
+ return {"jsonrpc": "2.0", "result": result, "id": req_id}
236
+
237
+ def _make_error(self, code: int, message: str, req_id: Any = None) -> dict:
238
+ """Build a JSON-RPC error response."""
239
+ return {
240
+ "jsonrpc": "2.0",
241
+ "error": {"code": code, "message": message},
242
+ "id": req_id,
243
+ }
244
+
245
+ # ── Method dispatch ──────────────────────────────────────────
246
+
247
+ def _handle_initialize(self, params: dict) -> dict:
248
+ """Handle ``initialize`` request."""
249
+ return {
250
+ "protocolVersion": "2024-11-05",
251
+ "capabilities": {
252
+ "tools": {},
253
+ },
254
+ "serverInfo": {
255
+ "name": "memnex",
256
+ "version": "3.2.0",
257
+ },
258
+ }
259
+
260
+ def _handle_tools_list(self, params: dict) -> dict:
261
+ """Handle ``tools/list`` request."""
262
+ return {"tools": _TOOL_DEFINITIONS}
263
+
264
+ def _handle_tools_call(self, params: dict) -> dict:
265
+ """Handle ``tools/call`` request."""
266
+ tool_name = params.get("name", "")
267
+ arguments = params.get("arguments", {})
268
+
269
+ handler = self._tool_handlers.get(tool_name)
270
+ if handler is None:
271
+ raise ValueError(f"Unknown tool: {tool_name!r}")
272
+
273
+ result = handler(self, arguments)
274
+ return {
275
+ "content": [
276
+ {
277
+ "type": "text",
278
+ "text": json.dumps(result, default=str, ensure_ascii=False, indent=2),
279
+ }
280
+ ],
281
+ }
282
+
283
+ def _handle_request(self, request: dict) -> Optional[dict]:
284
+ """Route a JSON-RPC request to the correct handler."""
285
+ method = request.get("method", "")
286
+ params = request.get("params", {})
287
+ req_id = request.get("id")
288
+
289
+ # Notifications (no id) do not expect a response
290
+ if req_id is None and method.endswith("/notification"):
291
+ return None
292
+
293
+ try:
294
+ if method == "initialize":
295
+ result = self._handle_initialize(params)
296
+ elif method == "tools/list":
297
+ result = self._handle_tools_list(params)
298
+ elif method == "tools/call":
299
+ self._ensure_service()
300
+ result = self._handle_tools_call(params)
301
+ elif method == "ping":
302
+ result = {}
303
+ else:
304
+ return self._make_error(-32601, f"Method not found: {method}", req_id)
305
+
306
+ return self._make_result(result, req_id)
307
+
308
+ except Exception as exc:
309
+ logger.error("Error handling %s: %s", method, exc)
310
+ traceback.print_exc(file=sys.stderr)
311
+ return self._make_error(-32603, str(exc), req_id)
312
+
313
+ # ── Tool implementations ─────────────────────────────────────
314
+
315
+ def _tool_memory_search(self, args: dict) -> dict:
316
+ """Search memories."""
317
+ result = self._service.query(
318
+ text=args["query"],
319
+ top_k=args.get("top_k", 10),
320
+ )
321
+ return {
322
+ "total": len(result.results),
323
+ "scope": result.scope.value if hasattr(result.scope, "value") else str(result.scope),
324
+ "latency_ms": result.latency_ms,
325
+ "results": [
326
+ {
327
+ "id": r.func_id,
328
+ "name": r.name,
329
+ "relevance": round(r.relevance_score, 4),
330
+ "summary": r.summary,
331
+ "domain": r.domain,
332
+ }
333
+ for r in result.results
334
+ ],
335
+ }
336
+
337
+ def _tool_memory_add(self, args: dict) -> dict:
338
+ """Add a new memory."""
339
+ content = args["content"]
340
+ source_type = args.get("source_type", "text")
341
+ result = self._service.write_text(text=content, source_type=source_type)
342
+ return {
343
+ "functions_extracted": len(result.functions),
344
+ "edges": len(result.graph.edges),
345
+ "function_ids": [f.id for f in result.functions],
346
+ }
347
+
348
+ def _tool_memory_get(self, args: dict) -> dict:
349
+ """Get a memory by ID."""
350
+ func = self._service.get(args["memory_id"])
351
+ if func is None:
352
+ return {"error": "Memory not found", "memory_id": args["memory_id"]}
353
+ return _dataclass_to_dict(func)
354
+
355
+ def _tool_memory_update(self, args: dict) -> dict:
356
+ """Update a memory field."""
357
+ result = self._service.update_memory(
358
+ memory_id=args["memory_id"],
359
+ role=args["role"],
360
+ new_value=args["new_value"],
361
+ )
362
+ return _dataclass_to_dict(result)
363
+
364
+ def _tool_memory_delete(self, args: dict) -> dict:
365
+ """Delete a memory."""
366
+ self._service.delete(args["memory_id"])
367
+ return {"status": "deleted", "id": args["memory_id"]}
368
+
369
+ def _tool_memory_feedback(self, args: dict) -> dict:
370
+ """Submit feedback."""
371
+ self._service.submit_feedback(
372
+ memory_id=args["memory_id"],
373
+ field_role=args["role"],
374
+ value_index=args["index"],
375
+ verdict=args["verdict"],
376
+ reason=args.get("reason"),
377
+ )
378
+ return {"status": "recorded"}
379
+
380
+ def _tool_memory_pending_reviews(self, args: dict) -> dict:
381
+ """List pending reviews."""
382
+ reviews = self._service.get_pending_reviews(
383
+ owner=args.get("owner"),
384
+ limit=args.get("limit", 100),
385
+ )
386
+ return {
387
+ "total": len(reviews),
388
+ "reviews": _dataclass_to_dict(reviews),
389
+ }
390
+
391
+ def _tool_memory_resolve(self, args: dict) -> dict:
392
+ """Resolve a pending review."""
393
+ return self._service.apply_resolution(
394
+ memory_id=args["memory_id"],
395
+ field_role=args["field_role"],
396
+ action=args["action"],
397
+ new_value=args.get("new_value"),
398
+ )
399
+
400
+ def _tool_memory_health(self, args: dict) -> dict:
401
+ """Health check."""
402
+ self._ensure_service()
403
+ return self._service.health()
404
+
405
+ # Map tool names to handler methods
406
+ _tool_handlers: Dict[str, Any] = {
407
+ "memory_search": _tool_memory_search,
408
+ "memory_add": _tool_memory_add,
409
+ "memory_get": _tool_memory_get,
410
+ "memory_update": _tool_memory_update,
411
+ "memory_delete": _tool_memory_delete,
412
+ "memory_feedback": _tool_memory_feedback,
413
+ "memory_pending_reviews": _tool_memory_pending_reviews,
414
+ "memory_resolve": _tool_memory_resolve,
415
+ "memory_health": _tool_memory_health,
416
+ }
417
+
418
+ # ── Main loop ────────────────────────────────────────────────
419
+
420
+ def run(self) -> None:
421
+ """Run the MCP server, reading requests from stdin.
422
+
423
+ Blocks until stdin is closed (EOF) or a fatal error occurs.
424
+ """
425
+ logger.info("MemNex MCP Server starting (stdio JSON-RPC)")
426
+
427
+ try:
428
+ while True:
429
+ request = self._read_request()
430
+ if request is None:
431
+ break # EOF
432
+
433
+ response = self._handle_request(request)
434
+ if response is not None:
435
+ self._write_response(response)
436
+ except KeyboardInterrupt:
437
+ pass
438
+ finally:
439
+ if self._service is not None:
440
+ self._service.stop()
441
+ logger.info("MemNex MCP Server stopped")
442
+
443
+
444
+ # ── CLI entry point ─────────────────────────────────────────────────
445
+
446
+ if __name__ == "__main__":
447
+ logging.basicConfig(level=logging.INFO, stream=sys.stderr)
448
+ MCPServer().run()