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.
- memnex/__init__.py +31 -0
- memnex/__main__.py +6 -0
- memnex/_plugin/.claude-plugin/plugin.json +24 -0
- memnex/_plugin/.mcp.json +9 -0
- memnex/_plugin/__init__.py +0 -0
- memnex/_plugin/hooks/hooks.json +43 -0
- memnex/_plugin/scripts/hook-runner.py +166 -0
- memnex/_plugin/skills/mem-explore/SKILL.md +83 -0
- memnex/_plugin/skills/mem-manage/SKILL.md +92 -0
- memnex/_plugin/skills/mem-search/SKILL.md +85 -0
- memnex/_plugin/skills/mem-write/SKILL.md +78 -0
- memnex/adapters/__init__.py +14 -0
- memnex/adapters/claude_skill.py +169 -0
- memnex/adapters/cli.py +525 -0
- memnex/adapters/http_api.py +314 -0
- memnex/adapters/mcp_server.py +448 -0
- memnex/compaction.py +563 -0
- memnex/config.py +366 -0
- memnex/core/__init__.py +13 -0
- memnex/core/associator/__init__.py +8 -0
- memnex/core/associator/domain_classifier.py +75 -0
- memnex/core/associator/entity_aligner.py +127 -0
- memnex/core/associator/ref_linker.py +197 -0
- memnex/core/associator/term_mapper.py +77 -0
- memnex/core/dictionaries/__init__.py +50 -0
- memnex/core/engine.py +667 -0
- memnex/core/extractors/__init__.py +15 -0
- memnex/core/extractors/docx.py +97 -0
- memnex/core/extractors/image.py +233 -0
- memnex/core/extractors/markdown.py +139 -0
- memnex/core/extractors/pdf.py +133 -0
- memnex/core/extractors/vision_mapper.py +131 -0
- memnex/core/handlers/__init__.py +7 -0
- memnex/core/handlers/clipboard.py +40 -0
- memnex/core/handlers/file_handler.py +62 -0
- memnex/core/handlers/url_handler.py +132 -0
- memnex/llm/__init__.py +25 -0
- memnex/llm/enhancer.py +226 -0
- memnex/llm/fallback_chain.py +87 -0
- memnex/llm/injection_guard.py +178 -0
- memnex/llm/provider.py +130 -0
- memnex/llm/providers/__init__.py +22 -0
- memnex/llm/providers/anthropic.py +135 -0
- memnex/llm/providers/local.py +135 -0
- memnex/llm/providers/rule_based.py +68 -0
- memnex/llm/sanitizer.py +67 -0
- memnex/models/__init__.py +68 -0
- memnex/models/feedback.py +42 -0
- memnex/models/graph.py +33 -0
- memnex/models/memory.py +102 -0
- memnex/models/misc.py +185 -0
- memnex/models/paragraph.py +45 -0
- memnex/models/search.py +51 -0
- memnex/models/source.py +23 -0
- memnex/models/task.py +62 -0
- memnex/processing/__init__.py +1 -0
- memnex/processing/graph_builder.py +278 -0
- memnex/processing/merger/__init__.py +6 -0
- memnex/processing/merger/confidence_calculator.py +127 -0
- memnex/processing/merger/conflict_resolver.py +116 -0
- memnex/retrieval/__init__.py +1 -0
- memnex/retrieval/dedup.py +386 -0
- memnex/retrieval/embedding.py +289 -0
- memnex/retrieval/reranker.py +299 -0
- memnex/service.py +902 -0
- memnex/storage/__init__.py +65 -0
- memnex/storage/base.py +132 -0
- memnex/storage/changelog.py +106 -0
- memnex/storage/feedback.py +486 -0
- memnex/storage/lite/__init__.py +5 -0
- memnex/storage/lite/store.py +606 -0
- memnex/storage/vector.py +265 -0
- memnex/wiki/__init__.py +11 -0
- memnex/wiki/community.py +221 -0
- memnex/wiki/compiler.py +545 -0
- memnex/wiki/generator.py +270 -0
- memnex/wiki/search.py +282 -0
- memnex/worker.py +412 -0
- memplex-3.2.0.dist-info/METADATA +37 -0
- memplex-3.2.0.dist-info/RECORD +83 -0
- memplex-3.2.0.dist-info/WHEEL +5 -0
- memplex-3.2.0.dist-info/entry_points.txt +2 -0
- 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
|