hermes-plugin-qdrant-memory 0.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.
@@ -0,0 +1,153 @@
1
+ """Read-only memory inventory and portable export documents."""
2
+
3
+ import contextlib
4
+ import json
5
+ import os
6
+ import tempfile
7
+ from pathlib import Path
8
+
9
+ from .models import now
10
+
11
+ EXPORT_FORMAT = "qdrant-memory-export"
12
+ EXPORT_VERSION = 1
13
+
14
+
15
+ class ExportTargetExistsError(ValueError):
16
+ """Refuse to overwrite an existing export without explicit authorization."""
17
+
18
+
19
+ class ExportPublicationUnsupportedError(ValueError):
20
+ """Refuse a no-clobber export the target filesystem cannot publish atomically."""
21
+
22
+
23
+ def memory_view(point):
24
+ """Return one stored memory as a scope-annotated inventory record.
25
+
26
+ The reserved schema identity point never reaches this view because the
27
+ store's scroll excludes it from memory records.
28
+ """
29
+ value = point.payload or {}
30
+ return {
31
+ "point_id": str(point.id),
32
+ "user_id": value.get("user_id"),
33
+ "agent_id": value.get("agent_id"),
34
+ "text": value.get("text"),
35
+ "source": value.get("source"),
36
+ "session_id": value.get("session_id"),
37
+ "category": value.get("category") or [],
38
+ "importance": value.get("importance"),
39
+ "created_at": value.get("created_at"),
40
+ "updated_at": value.get("updated_at"),
41
+ "content_hash": value.get("content_hash"),
42
+ "cloud_origin": value.get("cloud_origin"),
43
+ }
44
+
45
+
46
+ def export_record(point):
47
+ """Return one stored memory in the shape the Mem0 JSON importer accepts.
48
+
49
+ Round trips preserve text, scope, categories and timestamps. Memories
50
+ migrated from Mem0 export their original Mem0 ID and re-import to the same
51
+ point; native memories export their point UUID for provenance, and
52
+ re-imports re-embed and derive fresh point IDs. Vectors are never exported.
53
+ """
54
+ value = point.payload or {}
55
+ origin = value.get("cloud_origin")
56
+ origin = origin if isinstance(origin, dict) else {}
57
+ record = {
58
+ "id": origin.get("id") or str(point.id),
59
+ "memory": value.get("text", ""),
60
+ "user_id": value.get("user_id"),
61
+ "created_at": value.get("created_at"),
62
+ "updated_at": value.get("updated_at"),
63
+ "categories": value.get("category") or [],
64
+ "hash": value.get("content_hash"),
65
+ }
66
+ # Always export the scope's agent explicitly: an omitted field would let an
67
+ # importer substitute its default agent (the profile scope) and silently
68
+ # move no-agent memories into that agent's scope.
69
+ record["agent_id"] = value.get("agent_id")
70
+ metadata = value.get("metadata")
71
+ if isinstance(metadata, dict) and origin.get("provider") == "mem0":
72
+ # Round-trip fidelity: a Mem0-origin payload stores its source metadata
73
+ # under "legacy_mem0"; export the original object so re-import does not
74
+ # nest the wrapper inside another wrapper.
75
+ metadata = metadata.get("legacy_mem0", metadata)
76
+ if metadata:
77
+ record["metadata"] = metadata
78
+ return record
79
+
80
+
81
+ def write_export(path, collection, points, force=False):
82
+ """Stream stored memories to a portable export atomically, owner-only.
83
+
84
+ Records are written incrementally so large scopes never materialize a full
85
+ document in memory. An existing target is refused unless overwrite is
86
+ explicitly authorized; the refusal is atomic (hard-link publication).
87
+ Filesystems without hard links cannot refuse an overwrite without
88
+ publishing a partial target first, so non-force exports are refused there
89
+ and ``--force`` must authorize the write instead. Returns the number of
90
+ exported records.
91
+ """
92
+ path = Path(path)
93
+ if path.exists() and not force:
94
+ raise ExportTargetExistsError("Export target already exists; pass --force to overwrite it")
95
+ descriptor, temporary_name = tempfile.mkstemp(
96
+ dir=path.parent, prefix=path.name + ".", suffix=".tmp"
97
+ )
98
+ temporary = Path(temporary_name)
99
+ count = 0
100
+ try:
101
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
102
+ handle.write(
103
+ "{\n"
104
+ ' "provider": "qdrant-memory",\n'
105
+ f' "format": {json.dumps(EXPORT_FORMAT)},\n'
106
+ f' "version": {EXPORT_VERSION},\n'
107
+ f' "exported_at": {json.dumps(now())},\n'
108
+ f' "collection": {json.dumps(collection)},\n'
109
+ ' "memories": ['
110
+ )
111
+ first = True
112
+ for point in points:
113
+ record = export_record(point)
114
+ if not first:
115
+ handle.write(",")
116
+ handle.write("\n " + json.dumps(record, ensure_ascii=False))
117
+ first = False
118
+ count += 1
119
+ handle.write('\n ],\n "count": ' + str(count) + "\n}\n")
120
+ except BaseException:
121
+ temporary.unlink(missing_ok=True)
122
+ raise
123
+ if force:
124
+ os.replace(temporary, path)
125
+ else:
126
+ try:
127
+ _publish_no_clobber(temporary, path)
128
+ finally:
129
+ # Best-effort cleanup: the published target is already correct.
130
+ with contextlib.suppress(OSError):
131
+ temporary.unlink(missing_ok=True)
132
+ path.chmod(0o600)
133
+ return count
134
+
135
+
136
+ def _publish_no_clobber(temporary, path):
137
+ """Publish a written temporary file without ever overwriting a target.
138
+
139
+ Hard-link publication is atomic and is the only supported no-clobber
140
+ mechanism: a filesystem that cannot hard-link also cannot refuse an
141
+ overwrite without publishing a partial target first, so the export is
142
+ refused there and the caller must authorize ``--force`` instead.
143
+ """
144
+ try:
145
+ os.link(temporary, path)
146
+ except FileExistsError:
147
+ raise ExportTargetExistsError(
148
+ "Export target already exists; pass --force to overwrite it"
149
+ ) from None
150
+ except OSError as error:
151
+ raise ExportPublicationUnsupportedError(
152
+ "target filesystem does not support atomic no-clobber publication"
153
+ ) from error