Update app.py
Browse files
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 |
|