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,486 @@
1
+ """FeedbackStore -- three-tier feedback persistence.
2
+
3
+ Tiers:
4
+ Lite -- in-memory dict + JSON file
5
+ SQLite -- SQLite database (connection lazily created)
6
+ Postgres -- asyncpg async PostgreSQL backend
7
+
8
+ Usage::
9
+
10
+ store = create_feedback_store("lite")
11
+ store.record(MemoryFeedback(...))
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import json
17
+ import logging
18
+ import tempfile
19
+ from datetime import datetime
20
+ from pathlib import Path
21
+ from typing import Any, Dict, List, Optional, Protocol, runtime_checkable
22
+
23
+ from memnex.models import (
24
+ FeedbackVerdict,
25
+ MemoryFeedback,
26
+ PendingReview,
27
+ FieldValue,
28
+ )
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ # ── Protocol ─────────────────────────────────────────────────────────
34
+
35
+
36
+ @runtime_checkable
37
+ class FeedbackStore(Protocol):
38
+ """Feedback persistence interface."""
39
+
40
+ def record(self, feedback: MemoryFeedback) -> None: ...
41
+
42
+ def get_pending(self) -> List[PendingReview]: ...
43
+
44
+ def resolve(self, memory_id: str, field_role: str, resolution: str) -> None: ...
45
+
46
+ def get_history(
47
+ self, memory_id: str, limit: int = 50
48
+ ) -> List[MemoryFeedback]: ...
49
+
50
+ def clear(self) -> None: ...
51
+
52
+
53
+ # ── Serialization helpers ───────────────────────────────────────────
54
+
55
+
56
+ def _serialize_feedback(fb: MemoryFeedback) -> dict:
57
+ ts = fb.timestamp
58
+ if isinstance(ts, datetime):
59
+ ts = ts.isoformat()
60
+ reviewed = fb.needs_review_until
61
+ if isinstance(reviewed, datetime):
62
+ reviewed = reviewed.isoformat()
63
+ resolved = fb.resolved_at
64
+ if isinstance(resolved, datetime):
65
+ resolved = resolved.isoformat()
66
+ return {
67
+ "memory_id": fb.memory_id,
68
+ "field_role": fb.field_role,
69
+ "value_index": fb.value_index,
70
+ "verdict": fb.verdict.value if isinstance(fb.verdict, FeedbackVerdict) else fb.verdict,
71
+ "reason": fb.reason,
72
+ "source": fb.source,
73
+ "timestamp": ts,
74
+ "owner": fb.owner,
75
+ "feedback_type": fb.feedback_type,
76
+ "old_value": fb.old_value,
77
+ "new_value": fb.new_value,
78
+ "needs_review": fb.needs_review,
79
+ "needs_review_until": reviewed,
80
+ "resolved_at": resolved,
81
+ "resolution": fb.resolution,
82
+ }
83
+
84
+
85
+ def _deserialize_feedback(d: dict) -> MemoryFeedback:
86
+ verdict = d.get("verdict", "correct")
87
+ if isinstance(verdict, str):
88
+ try:
89
+ verdict = FeedbackVerdict(verdict)
90
+ except ValueError:
91
+ verdict = FeedbackVerdict.CORRECT
92
+
93
+ ts = d.get("timestamp")
94
+ if isinstance(ts, str):
95
+ ts = datetime.fromisoformat(ts)
96
+ elif ts is None:
97
+ ts = datetime.now()
98
+
99
+ reviewed = d.get("needs_review_until")
100
+ if isinstance(reviewed, str):
101
+ reviewed = datetime.fromisoformat(reviewed)
102
+
103
+ resolved = d.get("resolved_at")
104
+ if isinstance(resolved, str):
105
+ resolved = datetime.fromisoformat(resolved)
106
+
107
+ return MemoryFeedback(
108
+ memory_id=d["memory_id"],
109
+ field_role=d.get("field_role", ""),
110
+ value_index=d.get("value_index", 0),
111
+ verdict=verdict,
112
+ reason=d.get("reason"),
113
+ source=d.get("source", "user"),
114
+ timestamp=ts,
115
+ owner=d.get("owner"),
116
+ feedback_type=d.get("feedback_type", "field_value"),
117
+ old_value=d.get("old_value"),
118
+ new_value=d.get("new_value"),
119
+ needs_review=d.get("needs_review", True),
120
+ needs_review_until=reviewed,
121
+ resolved_at=resolved,
122
+ resolution=d.get("resolution"),
123
+ )
124
+
125
+
126
+ # ── LiteFeedbackStore ────────────────────────────────────────────────
127
+
128
+
129
+ class LiteFeedbackStore:
130
+ """In-memory dict + JSON persistence."""
131
+
132
+ def __init__(self, path: Optional[Path] = None) -> None:
133
+ self._path = path or Path("~/.memnex/feedback.json").expanduser()
134
+ self._records: List[MemoryFeedback] = []
135
+ self._load()
136
+
137
+ def record(self, feedback: MemoryFeedback) -> None:
138
+ self._records.append(feedback)
139
+ self._save()
140
+
141
+ def get_pending(self) -> List[PendingReview]:
142
+ groups: Dict[str, List[MemoryFeedback]] = {}
143
+ for fb in self._records:
144
+ if not fb.needs_review or fb.resolved_at is not None:
145
+ continue
146
+ key = f"{fb.memory_id}:{fb.field_role}"
147
+ groups.setdefault(key, []).append(fb)
148
+
149
+ pending: List[PendingReview] = []
150
+ for key, fbs in groups.items():
151
+ mem_id, role = key.split(":", 1)
152
+ pending.append(PendingReview(
153
+ memory_id=mem_id,
154
+ field_role=role,
155
+ conflicting_values=[], # Populated by caller with actual FieldValues
156
+ detected_at=fbs[0].timestamp if fbs else None,
157
+ source=fbs[0].source if fbs else "",
158
+ ))
159
+ return pending
160
+
161
+ def resolve(self, memory_id: str, field_role: str, resolution: str) -> None:
162
+ for fb in self._records:
163
+ if (
164
+ fb.memory_id == memory_id
165
+ and fb.field_role == field_role
166
+ and fb.needs_review
167
+ and fb.resolved_at is None
168
+ ):
169
+ fb.needs_review = False
170
+ fb.resolved_at = datetime.now()
171
+ fb.resolution = resolution
172
+ self._save()
173
+
174
+ def get_history(self, memory_id: str, limit: int = 50) -> List[MemoryFeedback]:
175
+ matching = [fb for fb in self._records if fb.memory_id == memory_id]
176
+ matching.sort(
177
+ key=lambda fb: fb.timestamp if isinstance(fb.timestamp, datetime) else datetime.min,
178
+ reverse=True,
179
+ )
180
+ return matching[:limit]
181
+
182
+ def clear(self) -> None:
183
+ self._records.clear()
184
+ self._save()
185
+
186
+ # ── Persistence ──────────────────────────────────────────────
187
+
188
+ def _load(self) -> None:
189
+ if not self._path.exists():
190
+ return
191
+ try:
192
+ raw = json.loads(self._path.read_text(encoding="utf-8"))
193
+ self._records = [_deserialize_feedback(d) for d in raw]
194
+ except Exception:
195
+ logger.warning("Failed to load feedback from %s", self._path)
196
+
197
+ def _save(self) -> None:
198
+ self._path.parent.mkdir(parents=True, exist_ok=True)
199
+ data = [_serialize_feedback(fb) for fb in self._records]
200
+ tmp_fd, tmp_path = tempfile.mkstemp(
201
+ dir=str(self._path.parent), suffix=".tmp"
202
+ )
203
+ try:
204
+ with open(tmp_fd, "w", encoding="utf-8") as fh:
205
+ json.dump(data, fh, ensure_ascii=False, indent=2)
206
+ Path(tmp_path).replace(self._path)
207
+ except Exception:
208
+ Path(tmp_path).unlink(missing_ok=True)
209
+ raise
210
+
211
+
212
+ # ── SQLiteFeedbackStore ──────────────────────────────────────────────
213
+
214
+
215
+ class SQLiteFeedbackStore:
216
+ """SQLite-backed feedback store. Connection is created lazily."""
217
+
218
+ def __init__(self, db_path: Optional[str] = None) -> None:
219
+ self._db_path = db_path or str(
220
+ Path("~/.memnex/feedback.db").expanduser()
221
+ )
222
+ self._conn = None
223
+
224
+ def _ensure_conn(self):
225
+ if self._conn is not None:
226
+ return
227
+ import sqlite3
228
+
229
+ self._conn = sqlite3.connect(self._db_path)
230
+ self._conn.execute("""
231
+ CREATE TABLE IF NOT EXISTS feedback (
232
+ memory_id TEXT NOT NULL,
233
+ field_role TEXT NOT NULL,
234
+ value_index INTEGER DEFAULT 0,
235
+ verdict TEXT NOT NULL,
236
+ reason TEXT,
237
+ source TEXT DEFAULT 'user',
238
+ timestamp TEXT,
239
+ owner TEXT,
240
+ feedback_type TEXT DEFAULT 'field_value',
241
+ old_value TEXT,
242
+ new_value TEXT,
243
+ needs_review INTEGER DEFAULT 1,
244
+ needs_review_until TEXT,
245
+ resolved_at TEXT,
246
+ resolution TEXT
247
+ )
248
+ """)
249
+ self._conn.commit()
250
+
251
+ def record(self, feedback: MemoryFeedback) -> None:
252
+ self._ensure_conn()
253
+ self._conn.execute(
254
+ "INSERT INTO feedback VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
255
+ (
256
+ feedback.memory_id,
257
+ feedback.field_role,
258
+ feedback.value_index,
259
+ feedback.verdict.value if isinstance(feedback.verdict, FeedbackVerdict) else feedback.verdict,
260
+ feedback.reason,
261
+ feedback.source,
262
+ feedback.timestamp.isoformat() if isinstance(feedback.timestamp, datetime) else feedback.timestamp,
263
+ feedback.owner,
264
+ feedback.feedback_type,
265
+ feedback.old_value,
266
+ feedback.new_value,
267
+ 1 if feedback.needs_review else 0,
268
+ feedback.needs_review_until.isoformat() if isinstance(feedback.needs_review_until, datetime) else feedback.needs_review_until,
269
+ feedback.resolved_at.isoformat() if isinstance(feedback.resolved_at, datetime) else feedback.resolved_at,
270
+ feedback.resolution,
271
+ ),
272
+ )
273
+ self._conn.commit()
274
+
275
+ def get_pending(self) -> List[PendingReview]:
276
+ self._ensure_conn()
277
+ rows = self._conn.execute(
278
+ "SELECT DISTINCT memory_id, field_role, source, MIN(timestamp) "
279
+ "FROM feedback WHERE needs_review=1 AND resolved_at IS NULL "
280
+ "GROUP BY memory_id, field_role"
281
+ ).fetchall()
282
+ return [
283
+ PendingReview(
284
+ memory_id=r[0],
285
+ field_role=r[1],
286
+ detected_at=r[3],
287
+ source=r[2] or "",
288
+ )
289
+ for r in rows
290
+ ]
291
+
292
+ def resolve(self, memory_id: str, field_role: str, resolution: str) -> None:
293
+ self._ensure_conn()
294
+ now = datetime.now().isoformat()
295
+ self._conn.execute(
296
+ "UPDATE feedback SET needs_review=0, resolved_at=?, resolution=? "
297
+ "WHERE memory_id=? AND field_role=? AND needs_review=1 AND resolved_at IS NULL",
298
+ (now, resolution, memory_id, field_role),
299
+ )
300
+ self._conn.commit()
301
+
302
+ def get_history(self, memory_id: str, limit: int = 50) -> List[MemoryFeedback]:
303
+ self._ensure_conn()
304
+ rows = self._conn.execute(
305
+ "SELECT memory_id, field_role, value_index, verdict, reason, source, "
306
+ "timestamp, owner, feedback_type, old_value, new_value, "
307
+ "needs_review, needs_review_until, resolved_at, resolution "
308
+ "FROM feedback WHERE memory_id=? ORDER BY timestamp DESC LIMIT ?",
309
+ (memory_id, limit),
310
+ ).fetchall()
311
+ return [self._row_to_feedback(r) for r in rows]
312
+
313
+ def clear(self) -> None:
314
+ self._ensure_conn()
315
+ self._conn.execute("DELETE FROM feedback")
316
+ self._conn.commit()
317
+
318
+ @staticmethod
319
+ def _row_to_feedback(r: tuple) -> MemoryFeedback:
320
+ return MemoryFeedback(
321
+ memory_id=r[0],
322
+ field_role=r[1],
323
+ value_index=r[2],
324
+ verdict=FeedbackVerdict(r[3]),
325
+ reason=r[4],
326
+ source=r[5] or "user",
327
+ timestamp=datetime.fromisoformat(r[6]) if r[6] else datetime.now(),
328
+ owner=r[7],
329
+ feedback_type=r[8] or "field_value",
330
+ old_value=r[9],
331
+ new_value=r[10],
332
+ needs_review=bool(r[11]),
333
+ needs_review_until=datetime.fromisoformat(r[12]) if r[12] else None,
334
+ resolved_at=datetime.fromisoformat(r[13]) if r[13] else None,
335
+ resolution=r[14],
336
+ )
337
+
338
+
339
+ # ── PostgresFeedbackStore ────────────────────────────────────────────
340
+
341
+
342
+ class PostgresFeedbackStore:
343
+ """Async PostgreSQL-backed feedback store via asyncpg.
344
+
345
+ The pool is created lazily on first use.
346
+ """
347
+
348
+ def __init__(self, dsn: str = "", **pool_kwargs) -> None:
349
+ self._dsn = dsn
350
+ self._pool_kwargs = pool_kwargs
351
+ self._pool = None
352
+
353
+ async def _ensure_pool(self):
354
+ if self._pool is not None:
355
+ return
356
+ import asyncpg # type: ignore
357
+
358
+ self._pool = await asyncpg.create_pool(self._dsn, **self._pool_kwargs)
359
+ async with self._pool.acquire() as conn:
360
+ await conn.execute("""
361
+ CREATE TABLE IF NOT EXISTS feedback (
362
+ memory_id TEXT NOT NULL,
363
+ field_role TEXT NOT NULL,
364
+ value_index INTEGER DEFAULT 0,
365
+ verdict TEXT NOT NULL,
366
+ reason TEXT,
367
+ source TEXT DEFAULT 'user',
368
+ timestamp TIMESTAMPTZ,
369
+ owner TEXT,
370
+ feedback_type TEXT DEFAULT 'field_value',
371
+ old_value TEXT,
372
+ new_value TEXT,
373
+ needs_review BOOLEAN DEFAULT TRUE,
374
+ needs_review_until TIMESTAMPTZ,
375
+ resolved_at TIMESTAMPTZ,
376
+ resolution TEXT
377
+ )
378
+ """)
379
+
380
+ async def record(self, feedback: MemoryFeedback) -> None:
381
+ await self._ensure_pool()
382
+ async with self._pool.acquire() as conn:
383
+ await conn.execute(
384
+ "INSERT INTO feedback VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)",
385
+ feedback.memory_id,
386
+ feedback.field_role,
387
+ feedback.value_index,
388
+ feedback.verdict.value if isinstance(feedback.verdict, FeedbackVerdict) else feedback.verdict,
389
+ feedback.reason,
390
+ feedback.source,
391
+ feedback.timestamp,
392
+ feedback.owner,
393
+ feedback.feedback_type,
394
+ feedback.old_value,
395
+ feedback.new_value,
396
+ feedback.needs_review,
397
+ feedback.needs_review_until,
398
+ feedback.resolved_at,
399
+ feedback.resolution,
400
+ )
401
+
402
+ async def get_pending(self) -> List[PendingReview]:
403
+ await self._ensure_pool()
404
+ async with self._pool.acquire() as conn:
405
+ rows = await conn.fetch(
406
+ "SELECT DISTINCT memory_id, field_role, source, MIN(timestamp) as detected_at "
407
+ "FROM feedback WHERE needs_review=TRUE AND resolved_at IS NULL "
408
+ "GROUP BY memory_id, field_role, source"
409
+ )
410
+ return [
411
+ PendingReview(
412
+ memory_id=r["memory_id"],
413
+ field_role=r["field_role"],
414
+ detected_at=r["detected_at"],
415
+ source=r["source"] or "",
416
+ )
417
+ for r in rows
418
+ ]
419
+
420
+ async def resolve(self, memory_id: str, field_role: str, resolution: str) -> None:
421
+ await self._ensure_pool()
422
+ async with self._pool.acquire() as conn:
423
+ await conn.execute(
424
+ "UPDATE feedback SET needs_review=FALSE, resolved_at=now(), resolution=$1 "
425
+ "WHERE memory_id=$2 AND field_role=$3 AND needs_review=TRUE AND resolved_at IS NULL",
426
+ resolution, memory_id, field_role,
427
+ )
428
+
429
+ async def get_history(self, memory_id: str, limit: int = 50) -> List[MemoryFeedback]:
430
+ await self._ensure_pool()
431
+ async with self._pool.acquire() as conn:
432
+ rows = await conn.fetch(
433
+ "SELECT * FROM feedback WHERE memory_id=$1 ORDER BY timestamp DESC LIMIT $2",
434
+ memory_id, limit,
435
+ )
436
+ return [self._row_to_feedback(r) for r in rows]
437
+
438
+ async def clear(self) -> None:
439
+ await self._ensure_pool()
440
+ async with self._pool.acquire() as conn:
441
+ await conn.execute("DELETE FROM feedback")
442
+
443
+ @staticmethod
444
+ def _row_to_feedback(r) -> MemoryFeedback:
445
+ return MemoryFeedback(
446
+ memory_id=r["memory_id"],
447
+ field_role=r["field_role"],
448
+ value_index=r["value_index"],
449
+ verdict=FeedbackVerdict(r["verdict"]),
450
+ reason=r["reason"],
451
+ source=r["source"] or "user",
452
+ timestamp=r["timestamp"] or datetime.now(),
453
+ owner=r["owner"],
454
+ feedback_type=r["feedback_type"] or "field_value",
455
+ old_value=r["old_value"],
456
+ new_value=r["new_value"],
457
+ needs_review=r["needs_review"],
458
+ needs_review_until=r["needs_review_until"],
459
+ resolved_at=r["resolved_at"],
460
+ resolution=r["resolution"],
461
+ )
462
+
463
+
464
+ # ── Factory ──────────────────────────────────────────────────────────
465
+
466
+
467
+ def create_feedback_store(
468
+ backend: str = "lite",
469
+ **kwargs,
470
+ ):
471
+ """Create a feedback store by backend name.
472
+
473
+ Parameters
474
+ ----------
475
+ backend:
476
+ ``"lite"`` | ``"sqlite"`` | ``"postgres"``
477
+ """
478
+ if backend == "lite":
479
+ return LiteFeedbackStore(path=kwargs.get("path"))
480
+ if backend == "sqlite":
481
+ return SQLiteFeedbackStore(db_path=kwargs.get("db_path"))
482
+ if backend == "postgres":
483
+ return PostgresFeedbackStore(dsn=kwargs.get("dsn", ""), **{
484
+ k: v for k, v in kwargs.items() if k not in ("dsn", "path", "db_path")
485
+ })
486
+ raise ValueError(f"Unknown feedback store backend: {backend!r}")
@@ -0,0 +1,5 @@
1
+ """Lite storage backend -- in-memory + JSON persistence."""
2
+
3
+ from memnex.storage.lite.store import LiteMemoryStore
4
+
5
+ __all__ = ["LiteMemoryStore"]