"""Mem0 bridge API — HTTP server wrapping the Qdrant+FastEmbed backend. Usage (CLI): python3 /opt/mem0-bridge.py add python3 /opt/mem0-bridge.py search Usage (HTTP): gunicorn -b 0.0.0.0:8000 mem0-api:app """ import json import os import sys import uuid import flask from urllib.parse import quote from qdrant_client import QdrantClient from qdrant_client.http import models from fastembed import TextEmbedding COLLECTION = "mem0" QDRANT_PATH = "/var/lib/nash/qdrant" MODEL_NAME = "BAAI/bge-small-en-v1.5" _model = None _client = None def _get_model(): global _model if _model is None: _model = TextEmbedding(model_name=MODEL_NAME) return _model def _get_client(): global _client if _client is None: _client = QdrantClient(path=QDRANT_PATH) return _client def _ensure_collection(client, dims=384): collections = client.get_collections().collections if not any(c.name == COLLECTION for c in collections): client.create_collection( collection_name=COLLECTION, vectors_config=models.VectorParams(size=dims, distance=models.Distance.COSINE), ) def memory_add(text, user_id="bridge-user"): model = _get_model() client = _get_client() _ensure_collection(client) emb = list(model.embed(text))[0] point_id = str(uuid.uuid4()) client.upsert( collection_name=COLLECTION, points=[ models.PointStruct( id=point_id, vector=emb.tolist(), payload={"text": text, "user_id": user_id}, ) ], ) return {"id": point_id, "text": text, "user_id": user_id} def memory_search(query, user_id="bridge-user", limit=5): model = _get_model() client = _get_client() _ensure_collection(client) emb = list(model.embed(query))[0] results = client.query_points( collection_name=COLLECTION, query=emb.tolist(), query_filter=models.Filter( must=[models.FieldCondition(key="user_id", match=models.MatchValue(value=user_id))] ), limit=limit, ) return [ {"id": p.id, "text": p.payload.get("text", ""), "user_id": p.payload.get("user_id", ""), "score": p.score} for p in results.points ] # ---- CLI ---- def main(): user_id = os.environ.get("MEM0_USER_ID", "bridge-user") if len(sys.argv) < 3: print(__doc__.strip(), file=sys.stderr) sys.exit(1) command = sys.argv[1] text = sys.argv[2] match command: case "add": result = memory_add(text, user_id) print(json.dumps(result, indent=2)) case "search": results = memory_search(text, user_id) print(json.dumps(results, indent=2)) case _: print(f"ERROR: unknown command {command!r}", file=sys.stderr) sys.exit(1) # ---- HTTP ---- app = flask.Flask(__name__) @app.route("/add", methods=["POST"]) def api_add(): data = flask.request.get_json(silent=True) or {} text = data.get("text", "") if not text: return {"error": "text is required"}, 400 user_id = data.get("user_id", os.environ.get("MEM0_USER_ID", "bridge-user")) result = memory_add(text, user_id) return flask.jsonify(result) @app.route("/search", methods=["POST"]) def api_search(): data = flask.request.get_json(silent=True) or {} query = data.get("query", "") if not query: return {"error": "query is required"}, 400 user_id = data.get("user_id", os.environ.get("MEM0_USER_ID", "bridge-user")) limit = data.get("limit", 5) results = memory_search(query, user_id, limit) return flask.jsonify(results) @app.route("/health", methods=["GET"]) def api_health(): return flask.jsonify({"status": "ok"}) if __name__ == "__main__": main() def memory_scroll(user_id="bridge-user", limit=100, offset=None): """Scroll through all points for a user (no embedding needed).""" client = _get_client() _ensure_collection(client) scroll_filter = models.Filter( must=[models.FieldCondition(key="user_id", match=models.MatchValue(value=user_id))] ) if user_id else None records, next_offset = client.scroll( collection_name=COLLECTION, scroll_filter=scroll_filter, limit=limit, offset=offset, with_payload=True, with_vectors=False, ) result = [ {"id": p.id, "text": p.payload.get("text", ""), "user_id": p.payload.get("user_id", "")} for p in records ] return {"entries": result, "next_offset": next_offset} def memory_delete(point_id: str): """Delete a point by ID.""" client = _get_client() client.delete( collection_name=COLLECTION, points_selector=models.PointIdsList( points=[point_id], ), ) return {"deleted": point_id} @app.route("/scroll", methods=["POST"]) def api_scroll(): data = flask.request.get_json(silent=True) or {} user_id = data.get("user_id", os.environ.get("MEM0_USER_ID", "bridge-user")) limit = data.get("limit", 100) offset = data.get("offset") result = memory_scroll(user_id, limit, offset) return flask.jsonify(result) @app.route("/delete", methods=["POST"]) def api_delete(): data = flask.request.get_json(silent=True) or {} point_id = data.get("id", "") if not point_id: return {"error": "id is required"}, 400 result = memory_delete(point_id) return flask.jsonify(result) # --------------------------------------------------------------------------- # Web UI # --------------------------------------------------------------------------- import html as _html import re as _re def _render_markdown(text: str) -> str: """Minimal markdown→HTML renderer (stdlib only).""" escaped = _html.escape(text) escaped = _re.sub(r"`([^`]+)`", r"\1", escaped) escaped = _re.sub(r"\*\*(\S[^*]*\S)\*\*", r"\1", escaped) escaped = _re.sub(r"\*(\S[^*]*\S)\*", r"\1", escaped) escaped = _re.sub(r"\[([^\]]+)\]\(([^)]+)\)", r'\1', escaped) return escaped UI_HTML = r""" nash memory

nash memory $COUNT entries

$ENTRIES """ @app.route("/", methods=["GET"]) def ui_index(): fmt = flask.request.args.get("format", "html") result = memory_scroll(user_id="bridge-user", limit=10000) entries = result["entries"] if fmt == "raw": lines = [f"nash memory — {len(entries)} entries\n"] for e in entries: lines.append(f"--- {e['id']} ---") lines.append(e["text"]) lines.append("") text = "\n".join(lines) return flask.Response(text, mimetype="text/plain") html_active = "active" if fmt != "raw" else "" raw_active = "active" if fmt == "raw" else "" cards = [] for e in entries: rendered = _render_markdown(e["text"]) short_id = e["id"][:8] card = ( '
' f'
{_html.escape(short_id)}' '
' '
' f'
{rendered}
' "
" ) cards.append(card) body = UI_HTML\ .replace("$COUNT", str(len(entries)))\ .replace("$HTML_ACTIVE", html_active)\ .replace("$RAW_ACTIVE", raw_active)\ .replace("$ENTRIES", "\n".join(cards)) return flask.Response(body, mimetype="text/html") @app.route("/-/delete/", methods=["POST"]) def ui_delete(point_id): memory_delete(point_id) return flask.redirect(flask.url_for("ui_index")) @app.route("/-/add", methods=["POST"]) def ui_add(): text = flask.request.form.get("text", "").strip() if text: memory_add(text, user_id="bridge-user") return flask.redirect(flask.url_for("ui_index"))