ADDING MEMORY MANAGER TOOL - CREATE, DELETE, LIST, SEARCH MEMORIES (intended for local use)
Browse files
app.py
CHANGED
|
@@ -1,10 +1,11 @@
|
|
| 1 |
-
# Purpose: One Space that offers
|
| 2 |
# 1) Fetch — convert webpages to clean Markdown format
|
| 3 |
# 2) DuckDuckGo Search — compact JSONL search output (short keys to minimize tokens)
|
| 4 |
# 3) Python Code Executor — run Python code and capture stdout/errors
|
| 5 |
# 4) Kokoro TTS — synthesize speech from text using Kokoro-82M with 54 voice options
|
| 6 |
-
# 5)
|
| 7 |
-
# 6)
|
|
|
|
| 8 |
|
| 9 |
from __future__ import annotations
|
| 10 |
|
|
@@ -14,7 +15,7 @@ import sys
|
|
| 14 |
import os
|
| 15 |
import random
|
| 16 |
from io import StringIO
|
| 17 |
-
from typing import List, Dict, Tuple, Annotated
|
| 18 |
|
| 19 |
import gradio as gr
|
| 20 |
import requests
|
|
@@ -805,31 +806,32 @@ def _save_memories(memories: List[Dict[str, str]]) -> None:
|
|
| 805 |
os.replace(tmp_path, MEMORY_FILE)
|
| 806 |
|
| 807 |
|
| 808 |
-
def
|
| 809 |
text: Annotated[str, "Raw textual content to remember (will be stored verbatim)."],
|
| 810 |
tags: Annotated[str, "Optional comma-separated tags for lightweight categorization (e.g. 'user, preference')."] = "",
|
| 811 |
) -> str:
|
| 812 |
-
"""
|
| 813 |
|
| 814 |
-
|
| 815 |
-
|
| 816 |
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 821 |
-
|
| 822 |
|
| 823 |
-
|
| 824 |
-
|
|
|
|
|
|
|
|
|
|
| 825 |
|
| 826 |
-
|
| 827 |
-
|
| 828 |
|
| 829 |
-
|
| 830 |
-
|
| 831 |
-
* Concurrency protection is process‑local (thread lock). For multi‑process
|
| 832 |
-
usage you would need an OS file lock (out of scope here).
|
| 833 |
"""
|
| 834 |
text_clean = (text or "").strip()
|
| 835 |
if not text_clean:
|
|
@@ -856,22 +858,22 @@ def Save_Memory( # <-- MCP tool (Save memory)
|
|
| 856 |
return f"Memory saved: {mem_id}"
|
| 857 |
|
| 858 |
|
| 859 |
-
def
|
| 860 |
limit: Annotated[int, "Maximum number of most recent memories to return (1–200)."] = 20,
|
| 861 |
include_tags: Annotated[bool, "If true, include tags column in output."] = True,
|
| 862 |
) -> str:
|
| 863 |
-
"""
|
| 864 |
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
|
| 869 |
-
Format:
|
| 870 |
-
|
|
|
|
| 871 |
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
* UUID prefix is first 8 chars to keep responses compact (full id retained on disk).
|
| 875 |
"""
|
| 876 |
limit = max(1, min(200, limit))
|
| 877 |
with _MEMORY_LOCK:
|
|
@@ -889,24 +891,23 @@ def List_Memories( # <-- MCP tool (List memories)
|
|
| 889 |
return "\n".join(lines)
|
| 890 |
|
| 891 |
|
| 892 |
-
def
|
| 893 |
query: Annotated[str, "Case-insensitive substring search; space-separated terms are ANDed."],
|
| 894 |
limit: Annotated[int, "Maximum number of matches (1–200)."] = 20,
|
| 895 |
) -> str:
|
| 896 |
-
"""
|
| 897 |
|
| 898 |
-
|
| 899 |
-
|
| 900 |
-
|
| 901 |
-
|
| 902 |
-
* Results are sorted by timestamp descending (newest first).
|
| 903 |
|
| 904 |
-
|
| 905 |
-
|
| 906 |
-
|
| 907 |
|
| 908 |
Returns:
|
| 909 |
-
|
| 910 |
"""
|
| 911 |
q = (query or "").strip()
|
| 912 |
if not q:
|
|
@@ -931,19 +932,19 @@ def Search_Memories( # <-- MCP tool (Search memories)
|
|
| 931 |
return "\n".join(lines)
|
| 932 |
|
| 933 |
|
| 934 |
-
def
|
| 935 |
memory_id: Annotated[str, "Full UUID or a unique prefix (>=4 chars) of the memory id to delete."],
|
| 936 |
) -> str:
|
| 937 |
-
"""Delete
|
| 938 |
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
prefix matches multiple entries, no deletion occurs (safety).
|
| 942 |
|
| 943 |
Returns:
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
|
|
|
| 947 |
"""
|
| 948 |
key = (memory_id or "").strip().lower()
|
| 949 |
if len(key) < 4:
|
|
@@ -1046,7 +1047,7 @@ CSS_STYLES = """
|
|
| 1046 |
/* Place bold tools list on line 2, normal auth note on line 3 (below title) */
|
| 1047 |
.gradio-container h1::before {
|
| 1048 |
grid-row: 2;
|
| 1049 |
-
content: "Fetch Webpage | Search DuckDuckGo |
|
| 1050 |
display: block;
|
| 1051 |
font-size: 1rem;
|
| 1052 |
font-weight: 700;
|
|
@@ -1056,7 +1057,7 @@ CSS_STYLES = """
|
|
| 1056 |
}
|
| 1057 |
.gradio-container h1::after {
|
| 1058 |
grid-row: 3;
|
| 1059 |
-
content: "Authentication is optional but Image/Video Generation require a `HF_READ_TOKEN` in env secrets. They are hidden otherwise.";
|
| 1060 |
display: block;
|
| 1061 |
font-size: 1rem;
|
| 1062 |
font-weight: 400;
|
|
@@ -1101,42 +1102,95 @@ kokoro_interface = gr.Interface(
|
|
| 1101 |
flagging_mode="never",
|
| 1102 |
)
|
| 1103 |
|
| 1104 |
-
|
| 1105 |
-
|
| 1106 |
-
|
| 1107 |
-
|
| 1108 |
-
|
| 1109 |
-
|
| 1110 |
-
|
| 1111 |
-
|
| 1112 |
-
|
| 1113 |
-
|
| 1114 |
-
|
| 1115 |
-
|
| 1116 |
-
|
| 1117 |
-
|
| 1118 |
-
|
| 1119 |
-
|
| 1120 |
-
|
| 1121 |
-
|
| 1122 |
-
|
| 1123 |
-
|
| 1124 |
-
|
| 1125 |
-
|
| 1126 |
-
|
| 1127 |
-
|
| 1128 |
-
|
| 1129 |
-
|
| 1130 |
-
|
| 1131 |
-
|
| 1132 |
-
|
| 1133 |
-
|
| 1134 |
-
|
| 1135 |
-
|
| 1136 |
-
|
| 1137 |
-
|
| 1138 |
-
|
| 1139 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1140 |
|
| 1141 |
# ==========================
|
| 1142 |
# Image Generation (Serverless)
|
|
@@ -1460,17 +1514,23 @@ _interfaces = [
|
|
| 1460 |
fetch_interface,
|
| 1461 |
concise_interface,
|
| 1462 |
code_interface,
|
| 1463 |
-
memory_interface,
|
| 1464 |
kokoro_interface,
|
| 1465 |
]
|
| 1466 |
_tab_names = [
|
| 1467 |
"Fetch Webpage",
|
| 1468 |
"DuckDuckGo Search",
|
| 1469 |
"Python Code Executor",
|
| 1470 |
-
"Memory Manager",
|
| 1471 |
"Kokoro TTS",
|
| 1472 |
]
|
| 1473 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1474 |
if HAS_HF_TOKEN:
|
| 1475 |
_interfaces.extend([image_generation_interface, video_generation_interface])
|
| 1476 |
_tab_names.extend(["Image Generation", "Video Generation"])
|
|
|
|
| 1 |
+
# Purpose: One Space that offers up to seven tools/tabs (all exposed as MCP tools):
|
| 2 |
# 1) Fetch — convert webpages to clean Markdown format
|
| 3 |
# 2) DuckDuckGo Search — compact JSONL search output (short keys to minimize tokens)
|
| 4 |
# 3) Python Code Executor — run Python code and capture stdout/errors
|
| 5 |
# 4) Kokoro TTS — synthesize speech from text using Kokoro-82M with 54 voice options
|
| 6 |
+
# 5) Memory Manager — lightweight JSON-based local memory store (requires HF_READ_TOKEN)
|
| 7 |
+
# 6) Image Generation - HF serverless inference providers (requires HF_READ_TOKEN)
|
| 8 |
+
# 7) Video Generation - HF serverless inference providers (requires HF_READ_TOKEN)
|
| 9 |
|
| 10 |
from __future__ import annotations
|
| 11 |
|
|
|
|
| 15 |
import os
|
| 16 |
import random
|
| 17 |
from io import StringIO
|
| 18 |
+
from typing import List, Dict, Tuple, Annotated, Literal
|
| 19 |
|
| 20 |
import gradio as gr
|
| 21 |
import requests
|
|
|
|
| 806 |
os.replace(tmp_path, MEMORY_FILE)
|
| 807 |
|
| 808 |
|
| 809 |
+
def _mem_save(
|
| 810 |
text: Annotated[str, "Raw textual content to remember (will be stored verbatim)."],
|
| 811 |
tags: Annotated[str, "Optional comma-separated tags for lightweight categorization (e.g. 'user, preference')."] = "",
|
| 812 |
) -> str:
|
| 813 |
+
"""(Internal) Persist a new memory record.
|
| 814 |
|
| 815 |
+
Summary:
|
| 816 |
+
Adds a memory object to the local JSON store (no external database).
|
| 817 |
|
| 818 |
+
Stored Fields:
|
| 819 |
+
- id (str, UUID4)
|
| 820 |
+
- text (str, verbatim user content)
|
| 821 |
+
- timestamp (UTC "YYYY-MM-DD HH:MM:SS")
|
| 822 |
+
- tags (str, original comma-separated tag string)
|
| 823 |
|
| 824 |
+
Behavior / Rules:
|
| 825 |
+
1. Whitespace is trimmed; empty text is rejected.
|
| 826 |
+
2. If the most recent existing memory has identical text, the new one is skipped (light dedupe heuristic).
|
| 827 |
+
3. When total entries exceed _MAX_MEMORIES, oldest entries are pruned (soft cap).
|
| 828 |
+
4. Operation is protected by an in‑process reentrant lock only (no cross‑process locking).
|
| 829 |
|
| 830 |
+
Returns:
|
| 831 |
+
str: Human readable confirmation containing the new memory UUID (full or prefix).
|
| 832 |
|
| 833 |
+
Security / Privacy:
|
| 834 |
+
Data is plaintext JSON on local disk; do NOT store secrets or regulated data.
|
|
|
|
|
|
|
| 835 |
"""
|
| 836 |
text_clean = (text or "").strip()
|
| 837 |
if not text_clean:
|
|
|
|
| 858 |
return f"Memory saved: {mem_id}"
|
| 859 |
|
| 860 |
|
| 861 |
+
def _mem_list(
|
| 862 |
limit: Annotated[int, "Maximum number of most recent memories to return (1–200)."] = 20,
|
| 863 |
include_tags: Annotated[bool, "If true, include tags column in output."] = True,
|
| 864 |
) -> str:
|
| 865 |
+
"""(Internal) List most recent memories.
|
| 866 |
|
| 867 |
+
Parameters:
|
| 868 |
+
limit (int): Max rows to return; clamped to [1, 200].
|
| 869 |
+
include_tags (bool): Include tags section when True.
|
| 870 |
|
| 871 |
+
Output Format (one per line):
|
| 872 |
+
<uuid_prefix> [YYYY-MM-DD HH:MM:SS] <text> | tags: <tag list>
|
| 873 |
+
(Tag column omitted if empty or include_tags=False.)
|
| 874 |
|
| 875 |
+
Returns:
|
| 876 |
+
str: Joined newline string or a friendly "No memories stored." message.
|
|
|
|
| 877 |
"""
|
| 878 |
limit = max(1, min(200, limit))
|
| 879 |
with _MEMORY_LOCK:
|
|
|
|
| 891 |
return "\n".join(lines)
|
| 892 |
|
| 893 |
|
| 894 |
+
def _mem_search(
|
| 895 |
query: Annotated[str, "Case-insensitive substring search; space-separated terms are ANDed."],
|
| 896 |
limit: Annotated[int, "Maximum number of matches (1–200)."] = 20,
|
| 897 |
) -> str:
|
| 898 |
+
"""(Internal) Full-text style AND search across text and tags.
|
| 899 |
|
| 900 |
+
Search Semantics:
|
| 901 |
+
- Split query on whitespace into individual terms.
|
| 902 |
+
- A memory matches only if EVERY term appears (case-insensitive) in the text OR tags field.
|
| 903 |
+
- Results are ordered newest-first (descending timestamp).
|
|
|
|
| 904 |
|
| 905 |
+
Parameters:
|
| 906 |
+
query (str): Raw user query string; must contain at least one non-space character.
|
| 907 |
+
limit (int): Max rows to return; clamped to [1, 200].
|
| 908 |
|
| 909 |
Returns:
|
| 910 |
+
str: Formatted lines identical to _mem_list output or "No matches".
|
| 911 |
"""
|
| 912 |
q = (query or "").strip()
|
| 913 |
if not q:
|
|
|
|
| 932 |
return "\n".join(lines)
|
| 933 |
|
| 934 |
|
| 935 |
+
def _mem_delete(
|
| 936 |
memory_id: Annotated[str, "Full UUID or a unique prefix (>=4 chars) of the memory id to delete."],
|
| 937 |
) -> str:
|
| 938 |
+
"""(Internal) Delete one memory by UUID or unique prefix.
|
| 939 |
|
| 940 |
+
Parameters:
|
| 941 |
+
memory_id (str): Full UUID4 (preferred) OR a unique prefix (>=4 chars). If prefix is ambiguous, no deletion occurs.
|
|
|
|
| 942 |
|
| 943 |
Returns:
|
| 944 |
+
str: One of: success message, ambiguity notice, or not-found message.
|
| 945 |
+
|
| 946 |
+
Safety:
|
| 947 |
+
Ambiguous prefixes are rejected to prevent accidental mass deletion.
|
| 948 |
"""
|
| 949 |
key = (memory_id or "").strip().lower()
|
| 950 |
if len(key) < 4:
|
|
|
|
| 1047 |
/* Place bold tools list on line 2, normal auth note on line 3 (below title) */
|
| 1048 |
.gradio-container h1::before {
|
| 1049 |
grid-row: 2;
|
| 1050 |
+
content: "Fetch Webpage | Search DuckDuckGo | Python Interpreter | Memory Manager | Kokoro TTS | Image Generation | Video Generation";
|
| 1051 |
display: block;
|
| 1052 |
font-size: 1rem;
|
| 1053 |
font-weight: 700;
|
|
|
|
| 1057 |
}
|
| 1058 |
.gradio-container h1::after {
|
| 1059 |
grid-row: 3;
|
| 1060 |
+
content: "Authentication is optional but Image/Video Generation require a `HF_READ_TOKEN` in env secrets. They are hidden otherwise. Same with Memory (intended for local)";
|
| 1061 |
display: block;
|
| 1062 |
font-size: 1rem;
|
| 1063 |
font-weight: 400;
|
|
|
|
| 1102 |
flagging_mode="never",
|
| 1103 |
)
|
| 1104 |
|
| 1105 |
+
def Memory_Tool(
|
| 1106 |
+
action: Annotated[Literal["save","list","search","delete"], "Action to perform: save | list | search | delete"],
|
| 1107 |
+
text: Annotated[str, "Text content (used when action=save)"] = "",
|
| 1108 |
+
tags: Annotated[str, "Comma-separated tags (used when action=save)"] = "",
|
| 1109 |
+
query: Annotated[str, "Search query terms (used when action=search)"] = "",
|
| 1110 |
+
limit: Annotated[int, "Max results for list/search (1–200)"] = 20,
|
| 1111 |
+
memory_id: Annotated[str, "Full UUID or unique prefix (used when action=delete)"] = "",
|
| 1112 |
+
include_tags: Annotated[bool, "Include tags in list/search output"] = True,
|
| 1113 |
+
) -> str:
|
| 1114 |
+
"""Manage lightweight local JSON “memories” (save | list | search | delete) in one MCP tool.
|
| 1115 |
+
|
| 1116 |
+
Overview:
|
| 1117 |
+
This tool provides simple, local, append‑only style persistence for short text memories
|
| 1118 |
+
with optional tags. Data is stored in a plaintext JSON file ("memories.json") beside the
|
| 1119 |
+
application; no external database or network access is required.
|
| 1120 |
+
|
| 1121 |
+
Supported Actions:
|
| 1122 |
+
- save : Store a new memory (requires 'text'; optional 'tags').
|
| 1123 |
+
- list : Return the most recent memories (respects 'limit' + 'include_tags').
|
| 1124 |
+
- search : AND match space‑separated terms across text and tags (uses 'query', 'limit').
|
| 1125 |
+
- delete : Remove one memory by full UUID or unique prefix (uses 'memory_id').
|
| 1126 |
+
|
| 1127 |
+
Parameter Usage by Action:
|
| 1128 |
+
action=save -> text (required), tags (optional)
|
| 1129 |
+
action=list -> limit, include_tags
|
| 1130 |
+
action=search -> query (required), limit, include_tags
|
| 1131 |
+
action=delete -> memory_id (required)
|
| 1132 |
+
|
| 1133 |
+
Parameters:
|
| 1134 |
+
action (Literal[save|list|search|delete]): Operation selector (case-insensitive).
|
| 1135 |
+
text (str): Raw memory content; leading/trailing whitespace trimmed (save only).
|
| 1136 |
+
tags (str): Optional comma-separated tags; stored verbatim (save only).
|
| 1137 |
+
query (str): Space-separated terms (AND logic, case-insensitive) across text+tags (search only).
|
| 1138 |
+
limit (int): Maximum rows for list/search (clamped internally to 1–200).
|
| 1139 |
+
memory_id (str): Full UUID or unique prefix (>=4 chars) (delete only).
|
| 1140 |
+
include_tags (bool): When True, show tag column in list/search output.
|
| 1141 |
+
|
| 1142 |
+
Storage Format (per entry):
|
| 1143 |
+
{"id": "<uuid4>", "text": "<original text>", "timestamp": "YYYY-MM-DD HH:MM:SS", "tags": "tag1, tag2"}
|
| 1144 |
+
|
| 1145 |
+
Lifecycle & Constraints:
|
| 1146 |
+
- A soft cap of {_MAX_MEMORIES} entries is enforced by pruning oldest records on save.
|
| 1147 |
+
- A light duplicate guard skips saving if the newest existing entry has identical text.
|
| 1148 |
+
- All operations are protected by a thread‑local reentrant lock (NOT multi‑process safe).
|
| 1149 |
+
|
| 1150 |
+
Returns:
|
| 1151 |
+
str: Human‑readable status / result lines (never raw JSON) suitable for direct model consumption.
|
| 1152 |
+
|
| 1153 |
+
Error Modes:
|
| 1154 |
+
- Invalid action -> error string.
|
| 1155 |
+
- Missing required field for the chosen action -> explanatory message.
|
| 1156 |
+
- Ambiguous or unknown memory_id on delete -> clarification message.
|
| 1157 |
+
|
| 1158 |
+
Security & Privacy:
|
| 1159 |
+
Plaintext JSON; do not store secrets, credentials, or regulated personal data.
|
| 1160 |
+
"""
|
| 1161 |
+
act = (action or "").lower().strip()
|
| 1162 |
+
if act == "save":
|
| 1163 |
+
return _mem_save(text=text, tags=tags)
|
| 1164 |
+
if act == "list":
|
| 1165 |
+
return _mem_list(limit=limit, include_tags=include_tags)
|
| 1166 |
+
if act == "search":
|
| 1167 |
+
return _mem_search(query=query, limit=limit)
|
| 1168 |
+
if act == "delete":
|
| 1169 |
+
return _mem_delete(memory_id=memory_id)
|
| 1170 |
+
return "Error: invalid action (use save|list|search|delete)."
|
| 1171 |
+
|
| 1172 |
+
memory_interface = gr.Interface(
|
| 1173 |
+
fn=Memory_Tool,
|
| 1174 |
+
inputs=[
|
| 1175 |
+
gr.Dropdown(label="Action", choices=["save","list","search","delete"], value="list"),
|
| 1176 |
+
gr.Textbox(label="Text", lines=3, placeholder="Memory text (save)"),
|
| 1177 |
+
gr.Textbox(label="Tags", placeholder="tag1, tag2"),
|
| 1178 |
+
gr.Textbox(label="Query", placeholder="Search terms (search)"),
|
| 1179 |
+
gr.Slider(1, 200, value=20, step=1, label="Limit"),
|
| 1180 |
+
gr.Textbox(label="Memory ID / Prefix", placeholder="UUID or prefix (delete)"),
|
| 1181 |
+
gr.Checkbox(value=True, label="Include Tags"),
|
| 1182 |
+
],
|
| 1183 |
+
outputs=gr.Textbox(label="Result", lines=14),
|
| 1184 |
+
title="Memory Manager",
|
| 1185 |
+
description=(
|
| 1186 |
+
"Lightweight local JSON memory store (no external DB). Choose an Action, fill only the relevant fields, and run."
|
| 1187 |
+
),
|
| 1188 |
+
api_description=(
|
| 1189 |
+
"Manage short text memories with optional tags. Actions: save(text,tags), list(limit,include_tags), "
|
| 1190 |
+
"search(query,limit,include_tags), delete(memory_id). Plaintext JSON; avoid secrets. Returns human-readable lines."
|
| 1191 |
+
),
|
| 1192 |
+
flagging_mode="never",
|
| 1193 |
+
)
|
| 1194 |
|
| 1195 |
# ==========================
|
| 1196 |
# Image Generation (Serverless)
|
|
|
|
| 1514 |
fetch_interface,
|
| 1515 |
concise_interface,
|
| 1516 |
code_interface,
|
|
|
|
| 1517 |
kokoro_interface,
|
| 1518 |
]
|
| 1519 |
_tab_names = [
|
| 1520 |
"Fetch Webpage",
|
| 1521 |
"DuckDuckGo Search",
|
| 1522 |
"Python Code Executor",
|
|
|
|
| 1523 |
"Kokoro TTS",
|
| 1524 |
]
|
| 1525 |
|
| 1526 |
+
# Only expose memory manager if an HF_READ_TOKEN is present (parity with Image/Video gating)
|
| 1527 |
+
HAS_HF_READ = bool(HF_API_TOKEN)
|
| 1528 |
+
if HAS_HF_READ:
|
| 1529 |
+
# Insert before Kokoro TTS for previous ordering
|
| 1530 |
+
insert_index = 3 if len(_interfaces) >= 3 else len(_interfaces)
|
| 1531 |
+
_interfaces.insert(insert_index, memory_interface)
|
| 1532 |
+
_tab_names.insert(insert_index, "Memory Manager")
|
| 1533 |
+
|
| 1534 |
if HAS_HF_TOKEN:
|
| 1535 |
_interfaces.extend([image_generation_interface, video_generation_interface])
|
| 1536 |
_tab_names.extend(["Image Generation", "Video Generation"])
|