Nymbo commited on
Commit
19a37f3
·
verified ·
1 Parent(s): a8b61f1

ADDING MEMORY MANAGER TOOL - CREATE, DELETE, LIST, SEARCH MEMORIES (intended for local use)

Browse files
Files changed (1) hide show
  1. app.py +150 -90
app.py CHANGED
@@ -1,10 +1,11 @@
1
- # Purpose: One Space that offers six 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) Image Generation - HF serverless inference providers (Default: FLUX.1-Krea-dev)
7
- # 6) Video Generation - HF serverless inference providers (Default: Wan2.2-T2V-A14B)
 
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 Save_Memory( # <-- MCP tool (Save memory)
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
- """Store a new memory entry in a local JSON file (no external DB required).
813
 
814
- Each memory is a JSON object with: id (UUID4), text, timestamp (UTC), tags.
815
- The file `memories.json` lives beside this script; it is created on demand.
816
 
817
- Behavior:
818
- * Trims surrounding whitespace in `text`; rejects empty result.
819
- * Deduplicates only if the newest existing memory has identical text
820
- (cheap heuristic to avoid accidental double submissions).
821
- * Soft cap (`_MAX_MEMORIES`); oldest entries are dropped if exceeded.
822
 
823
- Returns:
824
- Human‑readable confirmation containing the new memory UUID prefix.
 
 
 
825
 
826
- Example:
827
- Save_Memory(text="User prefers dark mode", tags="preference, ui")
828
 
829
- Limitations:
830
- * Not encrypted; do not store secrets.
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 List_Memories( # <-- MCP tool (List memories)
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
- """Return the N most recent memories in reverse chronological order.
864
 
865
- Args:
866
- limit: Upper bound of entries (clamped to 1200).
867
- include_tags: Whether to display tags in the formatted lines.
868
 
869
- Format:
870
- Each line: `<uuid_prefix> [YYYY-MM-DD HH:MM:SS] <text>` (+ ` | tags: ...` if present & enabled).
 
871
 
872
- Notes:
873
- * If no memories exist, returns a friendly message instead of empty string.
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 Search_Memories( # <-- MCP tool (Search memories)
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
- """Search memory text (and tags) for all provided terms.
897
 
898
- Matching logic:
899
- * Query is split on whitespace -> terms.
900
- * A memory matches if every term appears (case-insensitive) in either
901
- the text or the tag string.
902
- * Results are sorted by timestamp descending (newest first).
903
 
904
- Args:
905
- query: One or more words. Empty query is rejected.
906
- limit: Clamp 1200.
907
 
908
  Returns:
909
- Formatted lines (same style as List_Memories) or 'No matches'.
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 Delete_Memory( # <-- MCP tool (Delete memory)
935
  memory_id: Annotated[str, "Full UUID or a unique prefix (>=4 chars) of the memory id to delete."],
936
  ) -> str:
937
- """Delete a single memory by exact UUID or unique prefix.
938
 
939
- Args:
940
- memory_id: Full UUID (recommended) or prefix (minimum 4 chars). If the
941
- prefix matches multiple entries, no deletion occurs (safety).
942
 
943
  Returns:
944
- * Success message with full UUID
945
- * Disambiguation notice if prefix is not unique
946
- * Not-found message
 
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 | Code Interpreter | Kokoro TTS (54 voices) | Image Generation | Video Generation";
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
- # --- Memory Manager tab (JSON persistence) ---
1105
- with gr.Blocks(title="Memory Manager") as memory_interface:
1106
- gr.Markdown(
1107
- """
1108
- ### JSON Memory Manager
1109
- Lightweight, local persistence (no external DB). Four MCP tools are exposed:
1110
- - Save_Memory
1111
- - List_Memories
1112
- - Search_Memories
1113
- - Delete_Memory
1114
- Data stored in `memories.json` (UUID, text, timestamp, optional tags). Not encrypted—avoid secrets.
1115
- """
1116
- )
1117
- with gr.Tab("Save"):
1118
- in_text = gr.Textbox(label="Memory Text", lines=3)
1119
- in_tags = gr.Textbox(label="Tags (comma separated)")
1120
- out_save = gr.Textbox(label="Result", interactive=False)
1121
- save_btn = gr.Button("Save Memory")
1122
- save_btn.click(Save_Memory, inputs=[in_text, in_tags], outputs=out_save)
1123
- with gr.Tab("List"):
1124
- in_limit = gr.Slider(1, 200, value=20, step=1, label="Limit")
1125
- in_show_tags = gr.Checkbox(value=True, label="Include Tags")
1126
- out_list = gr.Textbox(label="Memories", lines=10)
1127
- list_btn = gr.Button("List Recent")
1128
- list_btn.click(List_Memories, inputs=[in_limit, in_show_tags], outputs=out_list)
1129
- with gr.Tab("Search"):
1130
- in_query = gr.Textbox(label="Query", placeholder="keywords…")
1131
- in_search_limit = gr.Slider(1, 200, value=20, step=1, label="Limit")
1132
- out_search = gr.Textbox(label="Matches", lines=10)
1133
- search_btn = gr.Button("Search")
1134
- search_btn.click(Search_Memories, inputs=[in_query, in_search_limit], outputs=out_search)
1135
- with gr.Tab("Delete"):
1136
- in_mem_id = gr.Textbox(label="Memory UUID or prefix")
1137
- out_delete = gr.Textbox(label="Result", interactive=False)
1138
- del_btn = gr.Button("Delete")
1139
- del_btn.click(Delete_Memory, inputs=[in_mem_id], outputs=out_delete)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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"])