Nymbo commited on
Commit
3f76fc7
·
verified ·
1 Parent(s): 9f1e882

Update Modules/Memory_Manager.py

Browse files
Files changed (1) hide show
  1. Modules/Memory_Manager.py +252 -247
Modules/Memory_Manager.py CHANGED
@@ -1,247 +1,252 @@
1
- from __future__ import annotations
2
-
3
- import json
4
- import os
5
- import threading
6
- import uuid
7
- from datetime import datetime
8
- from typing import Annotated, Dict, List, Literal, Optional
9
-
10
- import gradio as gr
11
-
12
- MEMORY_FILE = os.path.join(os.path.dirname(__file__), "memories.json")
13
- _MEMORY_LOCK = threading.RLock()
14
- _MAX_MEMORIES = 10_000
15
-
16
-
17
- def _now_iso() -> str:
18
- return datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
19
-
20
-
21
- def _load_memories() -> List[Dict[str, str]]:
22
- if not os.path.exists(MEMORY_FILE):
23
- return []
24
- try:
25
- with open(MEMORY_FILE, "r", encoding="utf-8") as file:
26
- data = json.load(file)
27
- if isinstance(data, list):
28
- cleaned: List[Dict[str, str]] = []
29
- for item in data:
30
- if isinstance(item, dict) and "id" in item and "text" in item:
31
- cleaned.append(item)
32
- return cleaned
33
- return []
34
- except Exception:
35
- try:
36
- backup = MEMORY_FILE + ".corrupt"
37
- if not os.path.exists(backup):
38
- os.replace(MEMORY_FILE, backup)
39
- except Exception:
40
- pass
41
- return []
42
-
43
-
44
- def _save_memories(memories: List[Dict[str, str]]) -> None:
45
- tmp_path = MEMORY_FILE + ".tmp"
46
- with open(tmp_path, "w", encoding="utf-8") as file:
47
- json.dump(memories, file, ensure_ascii=False, indent=2)
48
- os.replace(tmp_path, MEMORY_FILE)
49
-
50
-
51
- def _mem_save(text: str, tags: str) -> str:
52
- text_clean = (text or "").strip()
53
- if not text_clean:
54
- return "Error: memory text is empty."
55
- with _MEMORY_LOCK:
56
- memories = _load_memories()
57
- if memories and memories[-1].get("text") == text_clean:
58
- return "Skipped: identical to last stored memory."
59
- mem_id = str(uuid.uuid4())
60
- entry = {
61
- "id": mem_id,
62
- "text": text_clean,
63
- "timestamp": _now_iso(),
64
- "tags": tags.strip(),
65
- }
66
- memories.append(entry)
67
- if len(memories) > _MAX_MEMORIES:
68
- overflow = len(memories) - _MAX_MEMORIES
69
- memories = memories[overflow:]
70
- _save_memories(memories)
71
- return f"Memory saved: {mem_id}"
72
-
73
-
74
- def _mem_list(limit: int, include_tags: bool) -> str:
75
- limit = max(1, min(200, limit))
76
- with _MEMORY_LOCK:
77
- memories = _load_memories()
78
- if not memories:
79
- return "No memories stored yet."
80
- chosen = memories[-limit:][::-1]
81
- lines: List[str] = []
82
- for memory in chosen:
83
- base = f"{memory['id'][:8]} [{memory.get('timestamp','?')}] {memory.get('text','')}"
84
- if include_tags and memory.get("tags"):
85
- base += f" | tags: {memory['tags']}"
86
- lines.append(base)
87
- omitted = len(memories) - len(chosen)
88
- if omitted > 0:
89
- lines.append(f"… ({omitted} older memorie{'s' if omitted!=1 else ''} omitted; total={len(memories)})")
90
- return "\n".join(lines)
91
-
92
-
93
- def _parse_search_query(query: str) -> Dict[str, List[str]]:
94
- import re
95
-
96
- result = {"tag_terms": [], "text_terms": [], "operator": "and"}
97
- if not query or not query.strip():
98
- return result
99
- query = re.sub(r"\s+", " ", query.strip())
100
- if re.search(r"\bOR\b", query, re.IGNORECASE):
101
- result["operator"] = "or"
102
- parts = re.split(r"\s+OR\s+", query, flags=re.IGNORECASE)
103
- else:
104
- parts = re.split(r"\s+(?:AND\s+)?", query, flags=re.IGNORECASE)
105
- parts = [p for p in parts if p.strip() and p.strip().upper() != "AND"]
106
- for part in parts:
107
- part = part.strip()
108
- if not part:
109
- continue
110
- tag_match = re.match(r"^tag:(.+)$", part, re.IGNORECASE)
111
- if tag_match:
112
- tag_name = tag_match.group(1).strip()
113
- if tag_name:
114
- result["tag_terms"].append(tag_name.lower())
115
- else:
116
- result["text_terms"].append(part.lower())
117
- return result
118
-
119
-
120
- def _match_memory_with_query(memory: Dict[str, str], parsed_query: Dict[str, List[str]]) -> bool:
121
- tag_terms = parsed_query["tag_terms"]
122
- text_terms = parsed_query["text_terms"]
123
- operator = parsed_query["operator"]
124
- if not tag_terms and not text_terms:
125
- return False
126
- memory_text = memory.get("text", "").lower()
127
- memory_tags = memory.get("tags", "").lower()
128
- memory_tag_list = [tag.strip() for tag in memory_tags.split(",") if tag.strip()]
129
- tag_matches = [any(tag_term in tag for tag in memory_tag_list) for tag_term in tag_terms]
130
- combined_text = memory_text + " " + memory_tags
131
- text_matches = [text_term in combined_text for text_term in text_terms]
132
- all_matches = tag_matches + text_matches
133
- if not all_matches:
134
- return False
135
- if operator == "or":
136
- return any(all_matches)
137
- return all(all_matches)
138
-
139
-
140
- def _mem_search(query: str, limit: int) -> str:
141
- q = (query or "").strip()
142
- if not q:
143
- return "Error: empty query."
144
- parsed_query = _parse_search_query(q)
145
- if not parsed_query["tag_terms"] and not parsed_query["text_terms"]:
146
- return "Error: no valid search terms found."
147
- limit = max(1, min(200, limit))
148
- with _MEMORY_LOCK:
149
- memories = _load_memories()
150
- matches: List[Dict[str, str]] = []
151
- total_matches = 0
152
- for memory in reversed(memories):
153
- if _match_memory_with_query(memory, parsed_query):
154
- total_matches += 1
155
- if len(matches) < limit:
156
- matches.append(memory)
157
- if not matches:
158
- return f"No matches for: {query}"
159
- lines = [
160
- f"{memory['id'][:8]} [{memory.get('timestamp','?')}] {memory.get('text','')}" + (f" | tags: {memory['tags']}" if memory.get('tags') else "")
161
- for memory in matches
162
- ]
163
- omitted = total_matches - len(matches)
164
- if omitted > 0:
165
- lines.append(f"… ({omitted} additional match{'es' if omitted!=1 else ''} omitted; total_matches={total_matches})")
166
- return "\n".join(lines)
167
-
168
-
169
- def _mem_delete(memory_id: str) -> str:
170
- key = (memory_id or "").strip().lower()
171
- if len(key) < 4:
172
- return "Error: supply at least 4 characters of the id."
173
- with _MEMORY_LOCK:
174
- memories = _load_memories()
175
- matched = [memory for memory in memories if memory["id"].lower().startswith(key)]
176
- if not matched:
177
- return "Memory not found."
178
- if len(matched) > 1 and key != matched[0]["id"].lower():
179
- sample = ", ".join(memory["id"][:8] for memory in matched[:5])
180
- more = "" if len(matched) > 5 else ""
181
- return f"Ambiguous prefix (matches {len(matched)} ids: {sample}{more}). Provide more characters."
182
- target_id = matched[0]["id"]
183
- memories = [memory for memory in memories if memory["id"] != target_id]
184
- _save_memories(memories)
185
- return f"Deleted memory: {target_id}"
186
-
187
-
188
- def Memory_Manager(
189
- action: Annotated[Literal["save", "list", "search", "delete"], "Action to perform: save | list | search | delete"],
190
- text: Annotated[Optional[str], "Text content (Save only)"] = None,
191
- tags: Annotated[Optional[str], "Comma-separated tags (Save only)"] = None,
192
- query: Annotated[Optional[str], "Enhanced search with tag:name syntax, AND/OR operators (Search only)"] = None,
193
- limit: Annotated[int, "Max results (List/Search only)"] = 20,
194
- memory_id: Annotated[Optional[str], "Full UUID or unique prefix (Delete only)"] = None,
195
- include_tags: Annotated[bool, "Include tags (List/Search only)"] = True,
196
- ) -> str:
197
- act = (action or "").lower().strip()
198
- text = text or ""
199
- tags = tags or ""
200
- query = query or ""
201
- memory_id = memory_id or ""
202
- if act == "save":
203
- if not text.strip():
204
- return "Error: 'text' is required when action=save."
205
- return _mem_save(text=text, tags=tags)
206
- if act == "list":
207
- return _mem_list(limit=limit, include_tags=include_tags)
208
- if act == "search":
209
- if not query.strip():
210
- return "Error: 'query' is required when action=search."
211
- return _mem_search(query=query, limit=limit)
212
- if act == "delete":
213
- if not memory_id.strip():
214
- return "Error: 'memory_id' is required when action=delete."
215
- return _mem_delete(memory_id=memory_id)
216
- return "Error: invalid action (use save|list|search|delete)."
217
-
218
-
219
- def build_interface() -> gr.Interface:
220
- return gr.Interface(
221
- fn=Memory_Manager,
222
- inputs=[
223
- gr.Dropdown(label="Action", choices=["save", "list", "search", "delete"], value="list"),
224
- gr.Textbox(label="Text", lines=3, placeholder="Memory text (save)"),
225
- gr.Textbox(label="Tags", placeholder="tag1, tag2", max_lines=1),
226
- gr.Textbox(label="Query", placeholder="tag:work AND tag:project OR meeting", max_lines=1),
227
- gr.Slider(1, 200, value=20, step=1, label="Limit"),
228
- gr.Textbox(label="Memory ID / Prefix", placeholder="UUID or prefix (delete)", max_lines=1),
229
- gr.Checkbox(value=True, label="Include Tags"),
230
- ],
231
- outputs=gr.Textbox(label="Result", lines=14),
232
- title="Memory Manager",
233
- description=(
234
- "<div style=\"text-align:center\">Lightweight local JSON memory store (no external DB). Choose an Action, fill only the relevant fields, and run.</div>"
235
- ),
236
- api_description=(
237
- "Manage short text memories with optional tags. Actions: save(text,tags), list(limit,include_tags), "
238
- "search(query,limit,include_tags), delete(memory_id). Enhanced search supports tag:name queries and AND/OR operators. "
239
- "Examples: 'tag:work', 'tag:work AND tag:project', 'meeting tag:work', 'tag:urgent OR important'. "
240
- "Action parameter is always required. Use Memory_Manager whenever you are given information worth remembering about the user, "
241
- "and search for memories when relevant."
242
- ),
243
- flagging_mode="never",
244
- )
245
-
246
-
247
- __all__ = ["Memory_Manager", "build_interface", "_load_memories", "_save_memories"]
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import threading
6
+ import uuid
7
+ from datetime import datetime
8
+ from typing import Annotated, Dict, List, Literal, Optional
9
+
10
+ import gradio as gr
11
+ from ._docstrings import autodoc
12
+
13
+ MEMORY_FILE = os.path.join(os.path.dirname(__file__), "memories.json")
14
+ _MEMORY_LOCK = threading.RLock()
15
+ _MAX_MEMORIES = 10_000
16
+
17
+
18
+ def _now_iso() -> str:
19
+ return datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
20
+
21
+
22
+ def _load_memories() -> List[Dict[str, str]]:
23
+ if not os.path.exists(MEMORY_FILE):
24
+ return []
25
+ try:
26
+ with open(MEMORY_FILE, "r", encoding="utf-8") as file:
27
+ data = json.load(file)
28
+ if isinstance(data, list):
29
+ cleaned: List[Dict[str, str]] = []
30
+ for item in data:
31
+ if isinstance(item, dict) and "id" in item and "text" in item:
32
+ cleaned.append(item)
33
+ return cleaned
34
+ return []
35
+ except Exception:
36
+ try:
37
+ backup = MEMORY_FILE + ".corrupt"
38
+ if not os.path.exists(backup):
39
+ os.replace(MEMORY_FILE, backup)
40
+ except Exception:
41
+ pass
42
+ return []
43
+
44
+
45
+ def _save_memories(memories: List[Dict[str, str]]) -> None:
46
+ tmp_path = MEMORY_FILE + ".tmp"
47
+ with open(tmp_path, "w", encoding="utf-8") as file:
48
+ json.dump(memories, file, ensure_ascii=False, indent=2)
49
+ os.replace(tmp_path, MEMORY_FILE)
50
+
51
+
52
+ def _mem_save(text: str, tags: str) -> str:
53
+ text_clean = (text or "").strip()
54
+ if not text_clean:
55
+ return "Error: memory text is empty."
56
+ with _MEMORY_LOCK:
57
+ memories = _load_memories()
58
+ if memories and memories[-1].get("text") == text_clean:
59
+ return "Skipped: identical to last stored memory."
60
+ mem_id = str(uuid.uuid4())
61
+ entry = {
62
+ "id": mem_id,
63
+ "text": text_clean,
64
+ "timestamp": _now_iso(),
65
+ "tags": tags.strip(),
66
+ }
67
+ memories.append(entry)
68
+ if len(memories) > _MAX_MEMORIES:
69
+ overflow = len(memories) - _MAX_MEMORIES
70
+ memories = memories[overflow:]
71
+ _save_memories(memories)
72
+ return f"Memory saved: {mem_id}"
73
+
74
+
75
+ def _mem_list(limit: int, include_tags: bool) -> str:
76
+ limit = max(1, min(200, limit))
77
+ with _MEMORY_LOCK:
78
+ memories = _load_memories()
79
+ if not memories:
80
+ return "No memories stored yet."
81
+ chosen = memories[-limit:][::-1]
82
+ lines: List[str] = []
83
+ for memory in chosen:
84
+ base = f"{memory['id'][:8]} [{memory.get('timestamp','?')}] {memory.get('text','')}"
85
+ if include_tags and memory.get("tags"):
86
+ base += f" | tags: {memory['tags']}"
87
+ lines.append(base)
88
+ omitted = len(memories) - len(chosen)
89
+ if omitted > 0:
90
+ lines.append(f"… ({omitted} older memorie{'s' if omitted!=1 else ''} omitted; total={len(memories)})")
91
+ return "\n".join(lines)
92
+
93
+
94
+ def _parse_search_query(query: str) -> Dict[str, List[str]]:
95
+ import re
96
+
97
+ result = {"tag_terms": [], "text_terms": [], "operator": "and"}
98
+ if not query or not query.strip():
99
+ return result
100
+ query = re.sub(r"\s+", " ", query.strip())
101
+ if re.search(r"\bOR\b", query, re.IGNORECASE):
102
+ result["operator"] = "or"
103
+ parts = re.split(r"\s+OR\s+", query, flags=re.IGNORECASE)
104
+ else:
105
+ parts = re.split(r"\s+(?:AND\s+)?", query, flags=re.IGNORECASE)
106
+ parts = [p for p in parts if p.strip() and p.strip().upper() != "AND"]
107
+ for part in parts:
108
+ part = part.strip()
109
+ if not part:
110
+ continue
111
+ tag_match = re.match(r"^tag:(.+)$", part, re.IGNORECASE)
112
+ if tag_match:
113
+ tag_name = tag_match.group(1).strip()
114
+ if tag_name:
115
+ result["tag_terms"].append(tag_name.lower())
116
+ else:
117
+ result["text_terms"].append(part.lower())
118
+ return result
119
+
120
+
121
+ def _match_memory_with_query(memory: Dict[str, str], parsed_query: Dict[str, List[str]]) -> bool:
122
+ tag_terms = parsed_query["tag_terms"]
123
+ text_terms = parsed_query["text_terms"]
124
+ operator = parsed_query["operator"]
125
+ if not tag_terms and not text_terms:
126
+ return False
127
+ memory_text = memory.get("text", "").lower()
128
+ memory_tags = memory.get("tags", "").lower()
129
+ memory_tag_list = [tag.strip() for tag in memory_tags.split(",") if tag.strip()]
130
+ tag_matches = [any(tag_term in tag for tag in memory_tag_list) for tag_term in tag_terms]
131
+ combined_text = memory_text + " " + memory_tags
132
+ text_matches = [text_term in combined_text for text_term in text_terms]
133
+ all_matches = tag_matches + text_matches
134
+ if not all_matches:
135
+ return False
136
+ if operator == "or":
137
+ return any(all_matches)
138
+ return all(all_matches)
139
+
140
+
141
+ def _mem_search(query: str, limit: int) -> str:
142
+ q = (query or "").strip()
143
+ if not q:
144
+ return "Error: empty query."
145
+ parsed_query = _parse_search_query(q)
146
+ if not parsed_query["tag_terms"] and not parsed_query["text_terms"]:
147
+ return "Error: no valid search terms found."
148
+ limit = max(1, min(200, limit))
149
+ with _MEMORY_LOCK:
150
+ memories = _load_memories()
151
+ matches: List[Dict[str, str]] = []
152
+ total_matches = 0
153
+ for memory in reversed(memories):
154
+ if _match_memory_with_query(memory, parsed_query):
155
+ total_matches += 1
156
+ if len(matches) < limit:
157
+ matches.append(memory)
158
+ if not matches:
159
+ return f"No matches for: {query}"
160
+ lines = [
161
+ f"{memory['id'][:8]} [{memory.get('timestamp','?')}] {memory.get('text','')}" + (f" | tags: {memory['tags']}" if memory.get('tags') else "")
162
+ for memory in matches
163
+ ]
164
+ omitted = total_matches - len(matches)
165
+ if omitted > 0:
166
+ lines.append(f"… ({omitted} additional match{'es' if omitted!=1 else ''} omitted; total_matches={total_matches})")
167
+ return "\n".join(lines)
168
+
169
+
170
+ def _mem_delete(memory_id: str) -> str:
171
+ key = (memory_id or "").strip().lower()
172
+ if len(key) < 4:
173
+ return "Error: supply at least 4 characters of the id."
174
+ with _MEMORY_LOCK:
175
+ memories = _load_memories()
176
+ matched = [memory for memory in memories if memory["id"].lower().startswith(key)]
177
+ if not matched:
178
+ return "Memory not found."
179
+ if len(matched) > 1 and key != matched[0]["id"].lower():
180
+ sample = ", ".join(memory["id"][:8] for memory in matched[:5])
181
+ more = "…" if len(matched) > 5 else ""
182
+ return f"Ambiguous prefix (matches {len(matched)} ids: {sample}{more}). Provide more characters."
183
+ target_id = matched[0]["id"]
184
+ memories = [memory for memory in memories if memory["id"] != target_id]
185
+ _save_memories(memories)
186
+ return f"Deleted memory: {target_id}"
187
+
188
+
189
+ # Single source of truth for the LLM-facing tool description
190
+ TOOL_SUMMARY = (
191
+ "Manage short text memories (save, list, search, delete) in a local JSON store with tags and simple query language; "
192
+ "returns a result string (confirmation, listing, matches, or error)."
193
+ )
194
+
195
+
196
+ @autodoc(
197
+ summary=TOOL_SUMMARY,
198
+ )
199
+ def Memory_Manager(
200
+ action: Annotated[Literal["save", "list", "search", "delete"], "Action to perform: save | list | search | delete"],
201
+ text: Annotated[Optional[str], "Text content (Save only)"] = None,
202
+ tags: Annotated[Optional[str], "Comma-separated tags (Save only)"] = None,
203
+ query: Annotated[Optional[str], "Enhanced search with tag:name syntax, AND/OR operators (Search only)"] = None,
204
+ limit: Annotated[int, "Max results (List/Search only)"] = 20,
205
+ memory_id: Annotated[Optional[str], "Full UUID or unique prefix (Delete only)"] = None,
206
+ include_tags: Annotated[bool, "Include tags (List/Search only)"] = True,
207
+ ) -> str:
208
+ act = (action or "").lower().strip()
209
+ text = text or ""
210
+ tags = tags or ""
211
+ query = query or ""
212
+ memory_id = memory_id or ""
213
+ if act == "save":
214
+ if not text.strip():
215
+ return "Error: 'text' is required when action=save."
216
+ return _mem_save(text=text, tags=tags)
217
+ if act == "list":
218
+ return _mem_list(limit=limit, include_tags=include_tags)
219
+ if act == "search":
220
+ if not query.strip():
221
+ return "Error: 'query' is required when action=search."
222
+ return _mem_search(query=query, limit=limit)
223
+ if act == "delete":
224
+ if not memory_id.strip():
225
+ return "Error: 'memory_id' is required when action=delete."
226
+ return _mem_delete(memory_id=memory_id)
227
+ return "Error: invalid action (use save|list|search|delete)."
228
+
229
+
230
+ def build_interface() -> gr.Interface:
231
+ return gr.Interface(
232
+ fn=Memory_Manager,
233
+ inputs=[
234
+ gr.Dropdown(label="Action", choices=["save", "list", "search", "delete"], value="list"),
235
+ gr.Textbox(label="Text", lines=3, placeholder="Memory text (save)"),
236
+ gr.Textbox(label="Tags", placeholder="tag1, tag2", max_lines=1),
237
+ gr.Textbox(label="Query", placeholder="tag:work AND tag:project OR meeting", max_lines=1),
238
+ gr.Slider(1, 200, value=20, step=1, label="Limit"),
239
+ gr.Textbox(label="Memory ID / Prefix", placeholder="UUID or prefix (delete)", max_lines=1),
240
+ gr.Checkbox(value=True, label="Include Tags"),
241
+ ],
242
+ outputs=gr.Textbox(label="Result", lines=14),
243
+ title="Memory Manager",
244
+ description=(
245
+ "<div style=\"text-align:center\">Lightweight local JSON memory store (no external DB). Choose an Action, fill only the relevant fields, and run.</div>"
246
+ ),
247
+ api_description=TOOL_SUMMARY,
248
+ flagging_mode="never",
249
+ )
250
+
251
+
252
+ __all__ = ["Memory_Manager", "build_interface", "_load_memories", "_save_memories"]