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,314 @@
1
+ """MemNex HTTP/REST API -- FastAPI application factory.
2
+
3
+ Usage::
4
+
5
+ from memnex.adapters.http_api import create_app
6
+
7
+ app = create_app() # uses default config
8
+ # or
9
+ from memnex.config import load_config
10
+ app = create_app(load_config(path="custom.yaml"))
11
+
12
+ Run with uvicorn::
13
+
14
+ uvicorn memnex.adapters.http_api:app --host 127.0.0.1 --port 8900
15
+
16
+ Requires optional dependencies: ``fastapi``, ``uvicorn``.
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import logging
22
+ import os
23
+ from dataclasses import asdict
24
+ from typing import Any, Dict, List, Optional
25
+
26
+ logger = logging.getLogger(__name__)
27
+
28
+ # FastAPI is an optional dependency -- import lazily so the rest of
29
+ # the adapter package remains importable without it.
30
+ try:
31
+ from fastapi import FastAPI, HTTPException, Query
32
+ from fastapi.responses import JSONResponse
33
+
34
+ _FASTAPI_AVAILABLE = True
35
+ except ImportError: # pragma: no cover
36
+ _FASTAPI_AVAILABLE = False
37
+
38
+
39
+ # ── Pydantic-like request models (plain dicts for now) ──────────────
40
+ # We avoid a hard pydantic dependency by using simple dicts validated
41
+ # inside the route handlers. When pydantic is present (FastAPI pulls
42
+ # it in), FastAPI still gets the benefit of automatic doc generation.
43
+
44
+
45
+ # ── Helpers ─────────────────────────────────────────────────────────
46
+
47
+
48
+ def _dataclass_to_dict(obj) -> Any:
49
+ """Recursively convert dataclasses to plain dicts."""
50
+ if hasattr(obj, "__dataclass_fields__"):
51
+ return asdict(obj)
52
+ if isinstance(obj, list):
53
+ return [_dataclass_to_dict(item) for item in obj]
54
+ if isinstance(obj, dict):
55
+ return {k: _dataclass_to_dict(v) for k, v in obj.items()}
56
+ return obj
57
+
58
+
59
+ def _get_service(request) -> "MemNexService":
60
+ """Retrieve the shared MemNexService from app state."""
61
+ return request.app.state.memnex_service
62
+
63
+
64
+ # ── Security helpers ────────────────────────────────────────────────
65
+
66
+
67
+ def _check_bind_security(app: FastAPI) -> None:
68
+ """Warn or refuse if binding to non-local address without auth."""
69
+ host = os.environ.get("MEMNEX_HOST", "127.0.0.1")
70
+ non_local = host not in ("127.0.0.1", "localhost", "::1")
71
+
72
+ api_key = os.environ.get("MEMNEX_API_KEY")
73
+ bearer_token = os.environ.get("MEMNEX_BEARER_TOKEN")
74
+
75
+ if non_local and not api_key and not bearer_token:
76
+ logger.warning(
77
+ "SECURITY: Binding to %s without authentication. "
78
+ "Set MEMNEX_API_KEY or MEMNEX_BEARER_TOKEN to enable auth.",
79
+ host,
80
+ )
81
+
82
+
83
+ # ── App factory ─────────────────────────────────────────────────────
84
+
85
+
86
+ def create_app(config=None) -> "FastAPI":
87
+ """Build and return a FastAPI application.
88
+
89
+ Parameters
90
+ ----------
91
+ config:
92
+ A :class:`MemNexConfig` instance. When ``None``, defaults are
93
+ loaded via :func:`load_config`.
94
+
95
+ Returns
96
+ -------
97
+ FastAPI
98
+ Configured application with lifecycle hooks.
99
+ """
100
+ if not _FASTAPI_AVAILABLE:
101
+ raise ImportError(
102
+ "FastAPI is required for the HTTP adapter. "
103
+ "Install it with: pip install fastapi uvicorn"
104
+ )
105
+
106
+ from memnex.config import load_config
107
+ from memnex.service import MemNexService
108
+
109
+ if config is None:
110
+ config = load_config()
111
+
112
+ app = FastAPI(
113
+ title="MemNex API",
114
+ version="0.1.0",
115
+ description="Multi-agent memory system REST API",
116
+ )
117
+
118
+ # ── Lifecycle ────────────────────────────────────────────────
119
+
120
+ @app.on_event("startup")
121
+ async def _on_startup() -> None:
122
+ svc = MemNexService(config=config)
123
+ svc.start()
124
+ app.state.memnex_service = svc
125
+ logger.info("MemNex HTTP API started (backend=%s)", config.storage.backend)
126
+
127
+ @app.on_event("shutdown")
128
+ async def _on_shutdown() -> None:
129
+ svc: Optional[MemNexService] = getattr(app.state, "memnex_service", None)
130
+ if svc is not None:
131
+ svc.stop()
132
+ logger.info("MemNex HTTP API stopped")
133
+
134
+ _check_bind_security(app)
135
+
136
+ # ── CORS (opt-in via env) ────────────────────────────────────
137
+ cors_origins = os.environ.get("MEMNEX_CORS_ORIGINS", "")
138
+ if cors_origins:
139
+ try:
140
+ from fastapi.middleware.cors import CORSMiddleware
141
+
142
+ origins = [o.strip() for o in cors_origins.split(",") if o.strip()]
143
+ app.add_middleware(
144
+ CORSMiddleware,
145
+ allow_origins=origins,
146
+ allow_credentials=True,
147
+ allow_methods=["*"],
148
+ allow_headers=["*"],
149
+ )
150
+ except Exception:
151
+ logger.warning("Failed to configure CORS middleware")
152
+
153
+ # ══════════════════════════════════════════════════════════════
154
+ # Routes
155
+ # ══════════════════════════════════════════════════════════════
156
+
157
+ @app.post("/memories", summary="Write a new memory")
158
+ async def write_memory(body: dict) -> JSONResponse:
159
+ """Write new content into memory.
160
+
161
+ Request body::
162
+
163
+ {
164
+ "type": "text" | "file" | "url",
165
+ "content": "...",
166
+ "source_type": "wiki" // optional
167
+ }
168
+ """
169
+ svc = _get_service(write_memory)
170
+ content = body.get("content", "")
171
+ source_type = body.get("type", "text")
172
+
173
+ from memnex.models import SourceType as ST
174
+
175
+ st_map = {
176
+ "requirement": ST.REQUIREMENT,
177
+ "meeting": ST.MEETING,
178
+ "code": ST.CODE,
179
+ "wiki": ST.WIKI,
180
+ }
181
+ source_type_enum = st_map.get(body.get("source_type", "wiki"), ST.WIKI)
182
+
183
+ from memnex.models import SourceDocument
184
+
185
+ source = SourceDocument(
186
+ type=source_type,
187
+ content=content,
188
+ source_type=source_type_enum,
189
+ )
190
+ result = svc.write(source)
191
+ return JSONResponse(_dataclass_to_dict(result))
192
+
193
+ @app.get("/memories", summary="Query memories")
194
+ async def query_memories(
195
+ q: str = Query(..., description="Query text"),
196
+ top_k: int = Query(10, ge=1, le=100),
197
+ owner: Optional[str] = Query(None),
198
+ max_tokens: int = Query(4000, ge=0),
199
+ ) -> JSONResponse:
200
+ """Search memories with natural language."""
201
+ svc = _get_service(query_memories)
202
+ result = await svc.query_async(
203
+ text=q, top_k=top_k, owner=owner, max_tokens=max_tokens,
204
+ )
205
+ return JSONResponse(_dataclass_to_dict(result))
206
+
207
+ @app.get("/memories/{memory_id}", summary="Get memory detail")
208
+ async def get_memory(memory_id: str) -> JSONResponse:
209
+ """Retrieve a single memory by ID."""
210
+ svc = _get_service(get_memory)
211
+ func = svc.get(memory_id)
212
+ if func is None:
213
+ raise HTTPException(status_code=404, detail="Memory not found")
214
+ return JSONResponse(_dataclass_to_dict(func))
215
+
216
+ @app.get("/memories/{memory_id}/timeline", summary="Get memory timeline")
217
+ async def get_timeline(memory_id: str) -> JSONResponse:
218
+ """Get the changelog timeline for a memory."""
219
+ svc = _get_service(get_timeline)
220
+ events = svc.store.get_timeline(memory_id)
221
+ return JSONResponse(_dataclass_to_dict(events))
222
+
223
+ @app.delete("/memories/{memory_id}", summary="Delete memory")
224
+ async def delete_memory(memory_id: str) -> JSONResponse:
225
+ """Soft-delete a memory."""
226
+ svc = _get_service(delete_memory)
227
+ svc.delete(memory_id)
228
+ return JSONResponse({"status": "deleted", "id": memory_id})
229
+
230
+ @app.post("/memories/{memory_id}/feedback", summary="Submit feedback")
231
+ async def submit_feedback(memory_id: str, body: dict) -> JSONResponse:
232
+ """Submit feedback for a memory field value.
233
+
234
+ Request body::
235
+
236
+ {
237
+ "role": "trigger" | "action" | "condition" | "benefit",
238
+ "index": 0,
239
+ "verdict": "correct" | "wrong",
240
+ "reason": "optional explanation"
241
+ }
242
+ """
243
+ svc = _get_service(submit_feedback)
244
+ svc.submit_feedback(
245
+ memory_id=memory_id,
246
+ field_role=body["role"],
247
+ value_index=body["index"],
248
+ verdict=body["verdict"],
249
+ reason=body.get("reason"),
250
+ )
251
+ return JSONResponse({"status": "recorded"})
252
+
253
+ @app.get("/memories/pending_reviews", summary="List pending reviews")
254
+ async def pending_reviews(
255
+ owner: Optional[str] = Query(None),
256
+ limit: int = Query(100, ge=1, le=1000),
257
+ ) -> JSONResponse:
258
+ """Retrieve pending feedback reviews."""
259
+ svc = _get_service(pending_reviews)
260
+ reviews = svc.get_pending_reviews(owner=owner, limit=limit)
261
+ return JSONResponse({
262
+ "total": len(reviews),
263
+ "reviews": _dataclass_to_dict(reviews),
264
+ })
265
+
266
+ @app.post("/memories/{memory_id}/resolve", summary="Resolve a review")
267
+ async def resolve_review(memory_id: str, body: dict) -> JSONResponse:
268
+ """Apply a resolution to a pending review.
269
+
270
+ Request body::
271
+
272
+ {
273
+ "field_role": "trigger",
274
+ "action": "accept" | "reject" | "merge",
275
+ "new_value": "optional replacement when action=merge"
276
+ }
277
+ """
278
+ svc = _get_service(resolve_review)
279
+ result = svc.apply_resolution(
280
+ memory_id=memory_id,
281
+ field_role=body["field_role"],
282
+ action=body["action"],
283
+ new_value=body.get("new_value"),
284
+ )
285
+ return JSONResponse(result)
286
+
287
+ @app.get("/health", summary="Health check")
288
+ async def health() -> JSONResponse:
289
+ """Return service health status."""
290
+ svc = _get_service(health)
291
+ return JSONResponse(svc.health())
292
+
293
+ @app.get("/stats", summary="Statistics")
294
+ async def stats() -> JSONResponse:
295
+ """Return storage and usage statistics."""
296
+ svc = _get_service(stats)
297
+ return JSONResponse(svc.stats())
298
+
299
+ @app.post("/compact", summary="Trigger compaction")
300
+ async def compact(body: Optional[dict] = None) -> JSONResponse:
301
+ """Run the compaction pipeline.
302
+
303
+ Request body (optional)::
304
+
305
+ {"scope": "project" | "session" | "global"}
306
+ """
307
+ svc = _get_service(compact)
308
+ scope = "project"
309
+ if body:
310
+ scope = body.get("scope", "project")
311
+ result = svc.compact(scope=scope)
312
+ return JSONResponse(_dataclass_to_dict(result))
313
+
314
+ return app