Spaces:
Running
Running
YourUsername
Merge branch 'main' of https://huggingface.co/spaces/beyoru/clone-tools
c39527d
| from __future__ import annotations | |
| # Project by Nymbo | |
| import json | |
| import os | |
| import sys | |
| import threading | |
| import time | |
| from datetime import datetime, timedelta | |
| from typing import Any | |
| import gradio as gr | |
| class RateLimiter: | |
| """Best-effort in-process rate limiter for HTTP-heavy tools.""" | |
| def __init__(self, requests_per_minute: int = 30) -> None: | |
| self.requests_per_minute = requests_per_minute | |
| self._requests: list[datetime] = [] | |
| self._lock = threading.Lock() | |
| def acquire(self) -> None: | |
| now = datetime.now() | |
| with self._lock: | |
| self._requests = [req for req in self._requests if now - req < timedelta(minutes=1)] | |
| if len(self._requests) >= self.requests_per_minute: | |
| wait_time = 60 - (now - self._requests[0]).total_seconds() | |
| if wait_time > 0: | |
| time.sleep(max(1, wait_time)) | |
| self._requests.append(now) | |
| _search_rate_limiter = RateLimiter(requests_per_minute=20) | |
| _fetch_rate_limiter = RateLimiter(requests_per_minute=25) | |
| def _truncate_for_log(value: Any, limit: int = 500) -> str: | |
| if not isinstance(value, str): | |
| value = str(value) | |
| if len(value) <= limit: | |
| return value | |
| return value[: limit - 1] + "…" | |
| def _serialize_input(val: Any) -> Any: | |
| try: | |
| if isinstance(val, (str, int, float, bool)) or val is None: | |
| return val | |
| if isinstance(val, (list, tuple)): | |
| return [_serialize_input(v) for v in list(val)[:10]] + (["…"] if len(val) > 10 else []) | |
| if isinstance(val, dict): | |
| out: dict[str, Any] = {} | |
| for i, (k, v) in enumerate(val.items()): | |
| if i >= 12: | |
| out["…"] = "…" | |
| break | |
| out[str(k)] = _serialize_input(v) | |
| return out | |
| return repr(val)[:120] | |
| except Exception: | |
| return "<unserializable>" | |
| def _log_call_start(func_name: str, **kwargs: Any) -> None: | |
| try: | |
| compact = {k: _serialize_input(v) for k, v in kwargs.items()} | |
| print(f"[TOOL CALL] {func_name} inputs: {json.dumps(compact, ensure_ascii=False)[:800]}", flush=True) | |
| except Exception as exc: | |
| print(f"[TOOL CALL] {func_name} (failed to log inputs: {exc})", flush=True) | |
| def _log_call_end(func_name: str, output_desc: str) -> None: | |
| try: | |
| print(f"[TOOL RESULT] {func_name} output: {output_desc}", flush=True) | |
| except Exception as exc: | |
| print(f"[TOOL RESULT] {func_name} (failed to log output: {exc})", flush=True) | |
| # Ensure Tools modules can import 'app' when this file is executed as a script | |
| # (their code does `from app import ...`). | |
| sys.modules.setdefault("app", sys.modules[__name__]) | |
| # Import per-tool interface builders from the Tools package | |
| from Modules.Web_Fetch import build_interface as build_fetch_interface | |
| from Modules.Web_Search import build_interface as build_search_interface | |
| from Modules.Agent_Terminal import build_interface as build_agent_terminal_interface | |
| from Modules.Code_Interpreter import build_interface as build_code_interface | |
| from Modules.Memory_Manager import build_interface as build_memory_interface | |
| from Modules.Generate_Speech import build_interface as build_speech_interface | |
| from Modules.Generate_Image import build_interface as build_image_interface | |
| from Modules.Generate_Video import build_interface as build_video_interface | |
| from Modules.Deep_Research import build_interface as build_research_interface | |
| from Modules.File_System import build_interface as build_fs_interface | |
| from Modules.Obsidian_Vault import build_interface as build_obsidian_interface | |
| from Modules.Shell_Command import build_interface as build_shell_interface | |
| from Modules.MySQL import build_interface as build_mysql_interface | |
| # Optional environment flags used to conditionally show API schemas (unchanged behavior) | |
| HF_IMAGE_TOKEN = bool(os.getenv("HF_READ_TOKEN")) | |
| HF_VIDEO_TOKEN = bool(os.getenv("HF_READ_TOKEN") or os.getenv("HF_TOKEN")) | |
| HF_TEXTGEN_TOKEN = bool(os.getenv("HF_READ_TOKEN") or os.getenv("HF_TOKEN")) | |
| # Load CSS from external file | |
| _css_path = os.path.join(os.path.dirname(__file__), "styles.css") | |
| with open(_css_path, "r", encoding="utf-8") as _css_file: | |
| CSS_STYLES = _css_file.read() | |
| # Build each tab interface using modular builders | |
| fetch_interface = build_fetch_interface() | |
| web_search_interface = build_search_interface() | |
| agent_terminal_interface = build_agent_terminal_interface() | |
| code_interface = build_code_interface() | |
| memory_interface = build_memory_interface() | |
| kokoro_interface = build_speech_interface() | |
| image_generation_interface = build_image_interface() | |
| video_generation_interface = build_video_interface() | |
| deep_research_interface = build_research_interface() | |
| fs_interface = build_fs_interface() | |
| shell_interface = build_shell_interface() | |
| obsidian_interface = build_obsidian_interface() | |
| mysql_interface = build_mysql_interface() | |
| _interfaces = [ | |
| agent_terminal_interface, | |
| fetch_interface, | |
| web_search_interface, | |
| code_interface, | |
| shell_interface, | |
| fs_interface, | |
| obsidian_interface, | |
| memory_interface, | |
| kokoro_interface, | |
| image_generation_interface, | |
| video_generation_interface, | |
| deep_research_interface, | |
| mysql_interface, | |
| ] | |
| _tab_names = [ | |
| "Agent Terminal", | |
| "Web Fetch", | |
| "Web Search", | |
| "Code Interpreter", | |
| "Shell Command", | |
| "File System", | |
| "Obsidian Vault", | |
| "Memory Manager", | |
| "Generate Speech", | |
| "Generate Image", | |
| "Generate Video", | |
| "Deep Research", | |
| "MySQL Database", | |
| ] | |
| with gr.Blocks(title="Nymbo clone/Tools MCP") as demo: | |
| with gr.Sidebar(width=300, elem_classes="app-sidebar"): | |
| gr.Markdown("## Nymbo clone/Tools MCP\n<p style='font-size: 0.7rem; opacity: 0.85; margin-top: 2px;'>General purpose tools useful for any agent.</p>\n<code style='font-size: 0.7rem; word-break: break-all;'>https://nymbo.net/gradio_api/mcp/</code>") | |
| with gr.Accordion("Information", open=False): | |
| gr.HTML( | |
| """ | |
| <div class="info-accordion"> | |
| <div class="info-grid" style="grid-template-columns: 1fr;"> | |
| <section class="info-card"> | |
| <div class="info-card__body"> | |
| <h3>Connecting from an MCP Client</h3> | |
| <p> | |
| This Space also runs as a Model Context Protocol (MCP) server. Point your client to: | |
| <br/> | |
| <code>https://nymbo.net/gradio_api/mcp/</code> | |
| </p> | |
| <p>Example client configuration:</p> | |
| <pre><code class="language-json">{ | |
| "mcpServers": { | |
| "nymbo-tools": { | |
| "url": "https://nymbo.net/gradio_api/mcp/" | |
| } | |
| } | |
| }</code></pre> | |
| <p>Run the following commands in sequence to run the server locally:</p> | |
| <pre><code>git clone https://huggingface.co/spaces/Nymbo/Tools | |
| cd Tools | |
| python -m venv env | |
| source env/bin/activate | |
| pip install -r requirements.txt | |
| python app.py</code></pre> | |
| </div> | |
| </section> | |
| <section class="info-card"> | |
| <div class="info-card__body"> | |
| <h3>Enable Image Gen, Video Gen, and Deep Research</h3> | |
| <p> | |
| The <code>Generate_Image</code>, <code>Generate_Video</code>, and <code>Deep_Research</code> tools require a | |
| <code>HF_READ_TOKEN</code> set as a secret or environment variable. | |
| </p> | |
| <ul class="info-list"> | |
| <li>Duplicate this Space and add a HF token with model read access.</li> | |
| <li>Or run locally with <code>HF_READ_TOKEN</code> in your environment.</li> | |
| </ul> | |
| <div class="info-hint"> | |
| MCP clients can see these tools even without tokens, but calls will fail until a valid token is provided. | |
| </div> | |
| </div> | |
| </section> | |
| <section class="info-card"> | |
| <div class="info-card__body"> | |
| <h3>Persistent Memories and Files</h3> | |
| <p> | |
| In this public demo, memories and files created with the <code>Memory_Manager</code> and <code>File_System</code> are stored in the Space's running container and are cleared when the Space restarts. Content is visible to everyone—avoid personal data. | |
| </p> | |
| <p> | |
| When running locally, memories are saved to <code>memories.json</code> at the repo root for privacy, and files are saved to the <code>Tools/Filesystem</code> directory on disk. | |
| </p> | |
| </div> | |
| </section> | |
| <section class="info-card"> | |
| <div class="info-card__body"> | |
| <h3>Tool Notes & Kokoro Voice Legend</h3> | |
| <p><strong>No authentication required for:</strong></p> | |
| <ul class="info-list"> | |
| <li><code>Web_Fetch</code></li> | |
| <li><code>Web_Search</code></li> | |
| <li><code>Agent_Terminal</code></li> | |
| <li><code>Code_Interpreter</code></li> | |
| <li><code>Memory_Manager</code></li> | |
| <li><code>Generate_Speech</code></li> | |
| <li><code>File_System</code></li> | |
| <li><code>Shell_Command</code></li> | |
| </ul> | |
| <p><strong>Kokoro voice prefixes</strong></p> | |
| <table style="width:100%; border-collapse:collapse; font-size:0.9em; margin-top:8px;"> | |
| <thead> | |
| <tr style="border-bottom:1px solid rgba(255,255,255,0.15);"> | |
| <th style="padding:6px 8px; text-align:left;">Accent</th> | |
| <th style="padding:6px 8px; text-align:center;">Female</th> | |
| <th style="padding:6px 8px; text-align:center;">Male</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| <tr style="border-bottom:1px solid rgba(255,255,255,0.08);"> | |
| <td style="padding:6px 8px; font-weight:600;">American</td> | |
| <td style="padding:6px 8px; text-align:center;"><code>af</code></td> | |
| <td style="padding:6px 8px; text-align:center;"><code>am</code></td> | |
| </tr> | |
| <tr style="border-bottom:1px solid rgba(255,255,255,0.08);"> | |
| <td style="padding:6px 8px; font-weight:600;">British</td> | |
| <td style="padding:6px 8px; text-align:center;"><code>bf</code></td> | |
| <td style="padding:6px 8px; text-align:center;"><code>bm</code></td> | |
| </tr> | |
| <tr style="border-bottom:1px solid rgba(255,255,255,0.08);"> | |
| <td style="padding:6px 8px; font-weight:600;">European</td> | |
| <td style="padding:6px 8px; text-align:center;"><code>ef</code></td> | |
| <td style="padding:6px 8px; text-align:center;"><code>em</code></td> | |
| </tr> | |
| <tr style="border-bottom:1px solid rgba(255,255,255,0.08);"> | |
| <td style="padding:6px 8px; font-weight:600;">French</td> | |
| <td style="padding:6px 8px; text-align:center;"><code>ff</code></td> | |
| <td style="padding:6px 8px; text-align:center;">—</td> | |
| </tr> | |
| <tr style="border-bottom:1px solid rgba(255,255,255,0.08);"> | |
| <td style="padding:6px 8px; font-weight:600;">Hindi</td> | |
| <td style="padding:6px 8px; text-align:center;"><code>hf</code></td> | |
| <td style="padding:6px 8px; text-align:center;"><code>hm</code></td> | |
| </tr> | |
| <tr style="border-bottom:1px solid rgba(255,255,255,0.08);"> | |
| <td style="padding:6px 8px; font-weight:600;">Italian</td> | |
| <td style="padding:6px 8px; text-align:center;"><code>if</code></td> | |
| <td style="padding:6px 8px; text-align:center;"><code>im</code></td> | |
| </tr> | |
| <tr style="border-bottom:1px solid rgba(255,255,255,0.08);"> | |
| <td style="padding:6px 8px; font-weight:600;">Japanese</td> | |
| <td style="padding:6px 8px; text-align:center;"><code>jf</code></td> | |
| <td style="padding:6px 8px; text-align:center;"><code>jm</code></td> | |
| </tr> | |
| <tr style="border-bottom:1px solid rgba(255,255,255,0.08);"> | |
| <td style="padding:6px 8px; font-weight:600;">Portuguese</td> | |
| <td style="padding:6px 8px; text-align:center;"><code>pf</code></td> | |
| <td style="padding:6px 8px; text-align:center;"><code>pm</code></td> | |
| </tr> | |
| <tr> | |
| <td style="padding:6px 8px; font-weight:600;">Chinese</td> | |
| <td style="padding:6px 8px; text-align:center;"><code>zf</code></td> | |
| <td style="padding:6px 8px; text-align:center;"><code>zm</code></td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </section> | |
| </div> | |
| </div> | |
| """ | |
| ) | |
| gr.Markdown("### Tools") | |
| tool_selector = gr.Radio( | |
| choices=_tab_names, | |
| value=_tab_names[0], | |
| label="Select Tool", | |
| show_label=False, | |
| container=False, | |
| elem_classes="sidebar-nav" | |
| ) | |
| with gr.Tabs(elem_classes="hidden-tabs", selected=_tab_names[0]) as tool_tabs: | |
| for name, interface in zip(_tab_names, _interfaces): | |
| with gr.TabItem(label=name, id=name, elem_id=f"tab-{name}"): | |
| interface.render() | |
| # Use JavaScript to click the hidden tab button when the radio selection changes | |
| tool_selector.change( | |
| fn=None, | |
| inputs=tool_selector, | |
| outputs=None, | |
| js="(selected_tool) => { const buttons = document.querySelectorAll('.hidden-tabs button'); buttons.forEach(btn => { if (btn.innerText.trim() === selected_tool) { btn.click(); } }); }" | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch(mcp_server=True, theme="Nymbo/Nymbo_Theme", css=CSS_STYLES, ssr_mode=False) |