Nymbo commited on
Commit
77fd5a2
·
verified ·
1 Parent(s): 1b92bc9

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +261 -0
app.py CHANGED
@@ -27,6 +27,9 @@ from PIL import Image
27
  from huggingface_hub import InferenceClient
28
  import time
29
  import tempfile
 
 
 
30
 
31
  # Optional imports for Kokoro TTS (loaded lazily)
32
  import numpy as np
@@ -743,6 +746,225 @@ def Generate_Speech( # <-- MCP tool #4 (Generate Speech)
743
  raise gr.Error(f"Error during speech generation: {str(e)}")
744
 
745
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
746
  # ======================
747
  # UI: four-tab interface
748
  # ======================
@@ -879,6 +1101,43 @@ kokoro_interface = gr.Interface(
879
  flagging_mode="never",
880
  )
881
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
882
  # ==========================
883
  # Image Generation (Serverless)
884
  # ==========================
@@ -1201,12 +1460,14 @@ _interfaces = [
1201
  fetch_interface,
1202
  concise_interface,
1203
  code_interface,
 
1204
  kokoro_interface,
1205
  ]
1206
  _tab_names = [
1207
  "Fetch Webpage",
1208
  "DuckDuckGo Search",
1209
  "Python Code Executor",
 
1210
  "Kokoro TTS",
1211
  ]
1212
 
 
27
  from huggingface_hub import InferenceClient
28
  import time
29
  import tempfile
30
+ import uuid
31
+ import threading
32
+ from datetime import datetime
33
 
34
  # Optional imports for Kokoro TTS (loaded lazily)
35
  import numpy as np
 
746
  raise gr.Error(f"Error during speech generation: {str(e)}")
747
 
748
 
749
+ # ==========================
750
+ # JSON Memory System (MCP tools #7–#10 if enabled)
751
+ # ==========================
752
+
753
+ # Implementation goals (aligned with Gradio MCP docs):
754
+ # * Each function has a rich docstring (used for tool description)
755
+ # * Type hints + Annotated param docs become the schema
756
+ # * Zero external dependencies (pure stdlib JSON file persistence)
757
+ # * Safe concurrent access via a process‑local lock
758
+ # * Human‑readable & recoverable even if file becomes corrupted
759
+
760
+ MEMORY_FILE = os.path.join(os.path.dirname(__file__), "memories.json")
761
+ _MEMORY_LOCK = threading.RLock()
762
+ _MAX_MEMORIES = 10_000 # soft cap to avoid unbounded growth
763
+
764
+
765
+ def _now_iso() -> str:
766
+ return datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
767
+
768
+
769
+ def _load_memories() -> List[Dict[str, str]]:
770
+ """Internal helper: load memory list from disk.
771
+
772
+ Returns an empty list if the file does not exist or is unreadable.
773
+ If the JSON is corrupted, a *.corrupt backup is written once and a
774
+ fresh empty list is returned (fail‑open philosophy for tool usage).
775
+ """
776
+ if not os.path.exists(MEMORY_FILE):
777
+ return []
778
+ try:
779
+ with open(MEMORY_FILE, "r", encoding="utf-8") as f:
780
+ data = json.load(f)
781
+ if isinstance(data, list):
782
+ # Filter only dict items containing required keys if present
783
+ cleaned: List[Dict[str, str]] = []
784
+ for item in data:
785
+ if isinstance(item, dict) and "id" in item and "text" in item:
786
+ cleaned.append(item)
787
+ return cleaned
788
+ return []
789
+ except Exception:
790
+ # Backup corrupted file once
791
+ try:
792
+ backup = MEMORY_FILE + ".corrupt"
793
+ if not os.path.exists(backup):
794
+ os.replace(MEMORY_FILE, backup)
795
+ except Exception:
796
+ pass
797
+ return []
798
+
799
+
800
+ def _save_memories(memories: List[Dict[str, str]]) -> None:
801
+ """Persist memory list atomically to disk (write temp then replace)."""
802
+ tmp_path = MEMORY_FILE + ".tmp"
803
+ with open(tmp_path, "w", encoding="utf-8") as f:
804
+ json.dump(memories, f, ensure_ascii=False, indent=2)
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:
836
+ return "Error: memory text is empty."
837
+
838
+ with _MEMORY_LOCK:
839
+ memories = _load_memories()
840
+ if memories and memories[-1].get("text") == text_clean:
841
+ return "Skipped: identical to last stored memory."
842
+
843
+ mem_id = str(uuid.uuid4())
844
+ entry = {
845
+ "id": mem_id,
846
+ "text": text_clean,
847
+ "timestamp": _now_iso(),
848
+ "tags": tags.strip(),
849
+ }
850
+ memories.append(entry)
851
+ if len(memories) > _MAX_MEMORIES:
852
+ # Drop oldest overflow
853
+ overflow = len(memories) - _MAX_MEMORIES
854
+ memories = memories[overflow:]
855
+ _save_memories(memories)
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 1–200).
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:
878
+ memories = _load_memories()
879
+ if not memories:
880
+ return "No memories stored yet."
881
+ # Already chronological (append order); display newest first
882
+ chosen = memories[-limit:][::-1]
883
+ lines: List[str] = []
884
+ for m in chosen:
885
+ base = f"{m['id'][:8]} [{m.get('timestamp','?')}] {m.get('text','')}"
886
+ if include_tags and m.get("tags"):
887
+ base += f" | tags: {m['tags']}"
888
+ lines.append(base)
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 1–200.
907
+
908
+ Returns:
909
+ Formatted lines (same style as List_Memories) or 'No matches'.
910
+ """
911
+ q = (query or "").strip()
912
+ if not q:
913
+ return "Error: empty query."
914
+ terms = [t.lower() for t in q.split() if t.strip()]
915
+ if not terms:
916
+ return "Error: no valid search terms."
917
+ limit = max(1, min(200, limit))
918
+ with _MEMORY_LOCK:
919
+ memories = _load_memories()
920
+ # Newest first iteration for early cutoff
921
+ matches: List[Dict[str, str]] = []
922
+ for m in reversed(memories): # newest backward
923
+ hay = (m.get("text", "") + " " + m.get("tags", "")).lower()
924
+ if all(t in hay for t in terms):
925
+ matches.append(m)
926
+ if len(matches) >= limit:
927
+ break
928
+ if not matches:
929
+ return f"No matches for: {query}"
930
+ lines = [f"{m['id'][:8]} [{m.get('timestamp','?')}] {m.get('text','')}" + (f" | tags: {m['tags']}" if m.get('tags') else "") for m in matches]
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:
950
+ return "Error: supply at least 4 characters of the id."
951
+ with _MEMORY_LOCK:
952
+ memories = _load_memories()
953
+ matched = [m for m in memories if m["id"].lower().startswith(key)]
954
+ if not matched:
955
+ return "Memory not found."
956
+ if len(matched) > 1 and key != matched[0]["id"].lower():
957
+ # ambiguous prefix
958
+ sample = ", ".join(m["id"][:8] for m in matched[:5])
959
+ more = "…" if len(matched) > 5 else ""
960
+ return f"Ambiguous prefix (matches {len(matched)} ids: {sample}{more}). Provide more characters."
961
+ # Unique match
962
+ target_id = matched[0]["id"]
963
+ memories = [m for m in memories if m["id"] != target_id]
964
+ _save_memories(memories)
965
+ return f"Deleted memory: {target_id}"
966
+
967
+
968
  # ======================
969
  # UI: four-tab interface
970
  # ======================
 
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)
1143
  # ==========================
 
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