| from __future__ import annotations |
|
|
| |
|
|
| 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: str, limit: int = 500) -> str: |
| 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) |
|
|
| |
| |
| sys.modules.setdefault("app", sys.modules[__name__]) |
|
|
| |
| from Modules.Web_Fetch import build_interface as build_fetch_interface |
| from Modules.Web_Search import build_interface as build_search_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.Shell_Command import build_interface as build_shell_interface |
|
|
| |
| 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")) |
|
|
| |
| CSS_STYLES = """ |
| /* App background: subtle top-left glow, light coming from one side */ |
| .gradio-container { |
| /* Keep existing theme background but add a faint glow overlay */ |
| background-image: |
| radial-gradient(1200px 800px at 0% 0%, rgba(99, 102, 241, 0.10), rgba(99, 102, 241, 0.00) 70%), |
| radial-gradient(700px 500px at 100% 0%, rgba(59, 130, 246, 0.05), rgba(59, 130, 246, 0.00) 70%); |
| background-attachment: fixed, fixed; /* gentle parallax feel on scroll */ |
| background-repeat: no-repeat; |
| background-blend-mode: screen; /* subtle light effect over dark themes */ |
| } |
| |
| @media (prefers-color-scheme: light) { |
| .gradio-container { |
| /* Slightly softer in light mode */ |
| background-image: |
| radial-gradient(1200px 800px at 0% 0%, rgba(99, 102, 241, 0.08), rgba(99, 102, 241, 0.00) 70%), |
| radial-gradient(700px 500px at 100% 0%, rgba(59, 130, 246, 0.04), rgba(59, 130, 246, 0.00) 70%); |
| background-blend-mode: multiply; /* keep gentle tint over light base */ |
| } |
| } |
| |
| /* Style only the top-level app title to avoid affecting headings elsewhere */ |
| .app-title { |
| text-align: center; |
| /* Ensure main title appears first, then our two subtitle lines */ |
| display: grid; |
| justify-items: center; |
| } |
| /* Place bold tools list on line 2, normal auth note on line 3 (below title) */ |
| .app-title::before { |
| grid-row: 2; |
| content: "Web Fetch | Web Search | Code Interpreter | Memory Manager | Generate Speech | Generate Image | Generate Video | Deep Research | File System | Shell Command"; |
| display: block; |
| font-size: 1rem; |
| font-weight: 700; |
| opacity: 0.9; |
| margin-top: 6px; |
| white-space: pre-wrap; |
| } |
| .app-title::after { |
| grid-row: 3; |
| content: "General purpose tools useful for any agent."; |
| display: block; |
| font-size: 1rem; |
| font-weight: 400; |
| opacity: 0.9; |
| margin-top: 2px; |
| white-space: pre-wrap; |
| } |
| |
| /* Historical safeguard: if any h1 appears inside tabs, don't attach pseudo content */ |
| .gradio-container [role=\"tabpanel\"] h1::before, |
| .gradio-container [role=\"tabpanel\"] h1::after { |
| content: none !important; |
| } |
| |
| /* Information accordion - modern info cards */ |
| .info-accordion { |
| margin: 8px 0 2px; |
| } |
| .info-grid { |
| display: grid; |
| gap: 12px; |
| /* Force a 2x2 layout on medium+ screens */ |
| grid-template-columns: repeat(2, minmax(0, 1fr)); |
| align-items: stretch; |
| } |
| /* On narrow screens, stack into a single column */ |
| @media (max-width: 800px) { |
| .info-grid { |
| grid-template-columns: 1fr; |
| } |
| } |
| .info-card { |
| display: flex; |
| gap: 14px; |
| padding: 14px 16px; |
| border: 1px solid rgba(255, 255, 255, 0.08); |
| background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.03)); |
| border-radius: 12px; |
| box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); |
| position: relative; |
| overflow: hidden; |
| backdrop-filter: blur(2px); |
| } |
| .info-card::before { |
| content: ""; |
| position: absolute; |
| inset: 0; |
| border-radius: 12px; |
| pointer-events: none; |
| background: linear-gradient(90deg, rgba(99,102,241,0.06), rgba(59,130,246,0.05)); |
| } |
| .info-card__icon { |
| font-size: 24px; |
| flex: 0 0 28px; |
| line-height: 1; |
| filter: saturate(1.1); |
| } |
| .info-card__body { |
| min-width: 0; |
| } |
| .info-card__body h3 { |
| margin: 0 0 6px; |
| font-size: 1.05rem; |
| } |
| .info-card__body p { |
| margin: 6px 0; |
| opacity: 0.95; |
| } |
| /* Readable code blocks inside info cards */ |
| .info-card pre { |
| margin: 8px 0; |
| padding: 10px 12px; |
| background: rgba(20, 20, 30, 0.55); |
| border: 1px solid rgba(255, 255, 255, 0.08); |
| border-radius: 10px; |
| overflow-x: auto; |
| white-space: pre; |
| } |
| .info-card code { |
| font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace; |
| font-size: 0.95em; |
| } |
| .info-card pre code { |
| display: block; |
| } |
| .info-list { |
| margin: 6px 0 0 18px; |
| padding: 0; |
| } |
| .info-hint { |
| margin-top: 8px; |
| font-size: 0.9em; |
| opacity: 0.9; |
| } |
| |
| /* Light theme adjustments */ |
| @media (prefers-color-scheme: light) { |
| .info-card { |
| border-color: rgba(0, 0, 0, 0.08); |
| background: linear-gradient(180deg, rgba(255,255,255,0.95), rgba(255,255,255,0.9)); |
| } |
| .info-card::before { |
| background: linear-gradient(90deg, rgba(99,102,241,0.08), rgba(59,130,246,0.06)); |
| } |
| .info-card pre { |
| background: rgba(245, 246, 250, 0.95); |
| border-color: rgba(0, 0, 0, 0.08); |
| } |
| } |
| |
| /* Tabs - modern, evenly distributed full-width buttons */ |
| .gradio-container [role="tablist"] { |
| display: flex; |
| gap: 8px; |
| flex-wrap: nowrap; |
| align-items: stretch; |
| width: 100%; |
| } |
| .gradio-container [role="tab"] { |
| flex: 1 1 0; |
| min-width: 0; /* allow shrinking to fit */ |
| display: inline-flex; |
| justify-content: center; |
| align-items: center; |
| padding: 10px 12px; |
| border-radius: 10px; |
| border: 1px solid rgba(255, 255, 255, 0.08); |
| background: linear-gradient(180deg, rgba(255,255,255,0.05), rgba(255,255,255,0.03)); |
| transition: background .2s ease, border-color .2s ease, box-shadow .2s ease, transform .06s ease; |
| overflow: hidden; |
| white-space: nowrap; |
| text-overflow: ellipsis; |
| } |
| .gradio-container [role="tab"]:hover { |
| border-color: rgba(99,102,241,0.28); |
| background: linear-gradient(180deg, rgba(99,102,241,0.10), rgba(59,130,246,0.08)); |
| } |
| .gradio-container [role="tab"][aria-selected="true"] { |
| border-color: rgba(99,102,241,0.35); |
| box-shadow: inset 0 0 0 1px rgba(99,102,241,0.25), 0 1px 2px rgba(0,0,0,0.25); |
| background: linear-gradient(180deg, rgba(99,102,241,0.18), rgba(59,130,246,0.14)); |
| color: rgba(255, 255, 255, 0.95) !important; |
| } |
| .gradio-container [role="tab"]:active { |
| transform: translateY(0.5px); |
| } |
| .gradio-container [role="tab"]:focus-visible { |
| outline: none; |
| box-shadow: 0 0 0 2px rgba(59,130,246,0.35); |
| } |
| @media (prefers-color-scheme: light) { |
| .gradio-container [role="tab"] { |
| border-color: rgba(0, 0, 0, 0.08); |
| background: linear-gradient(180deg, rgba(255,255,255,0.95), rgba(255,255,255,0.90)); |
| } |
| .gradio-container [role="tab"]:hover { |
| border-color: rgba(99,102,241,0.25); |
| background: linear-gradient(180deg, rgba(99,102,241,0.08), rgba(59,130,246,0.06)); |
| } |
| .gradio-container [role="tab"][aria-selected="true"] { |
| border-color: rgba(99,102,241,0.35); |
| background: linear-gradient(180deg, rgba(99,102,241,0.16), rgba(59,130,246,0.12)); |
| color: rgba(0, 0, 0, 0.85) !important; |
| } |
| } |
| """ |
|
|
| |
| fetch_interface = build_fetch_interface() |
| concise_interface = build_search_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() |
|
|
| _interfaces = [ |
| fetch_interface, |
| concise_interface, |
| code_interface, |
| shell_interface, |
| fs_interface, |
| memory_interface, |
| kokoro_interface, |
| image_generation_interface, |
| video_generation_interface, |
| deep_research_interface, |
| ] |
| _tab_names = [ |
| "Web Fetch", |
| "Web Search", |
| "Code Interpreter", |
| "Shell Command", |
| "File System", |
| "Memory Manager", |
| "Generate Speech", |
| "Generate Image", |
| "Generate Video", |
| "Deep Research", |
| ] |
|
|
| with gr.Blocks(title="Nymbo/Tools MCP", theme="Nymbo/Nymbo_Theme", css=CSS_STYLES) as demo: |
| |
| gr.HTML("<h1 class='app-title'>Nymbo/Tools MCP</h1>") |
|
|
| with gr.Accordion("Information", open=False): |
| gr.HTML( |
| """ |
| <div class="info-accordion"> |
| <div class="info-grid"> |
| <section class="info-card"> |
| <div class="info-card__icon">🔐</div> |
| <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"> |
| These tools are hidden as MCP tools without authentication to keep tool lists tidy, but remain visible in the UI. |
| </div> |
| </div> |
| </section> |
| |
| <section class="info-card"> |
| <div class="info-card__icon">🧠</div> |
| <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__icon">🔗</div> |
| <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://mcp.nymbo.net/gradio_api/mcp/</code> |
| </p> |
| <p>Example client configuration:</p> |
| <pre><code class="language-json">{ |
| "mcpServers": { |
| "nymbo-tools": { |
| "url": "https://mcp.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__icon">🛠️</div> |
| <div class="info-card__body"> |
| <h3>Tool Notes & Kokoro Voice Legend</h3> |
| <p> |
| No authentication required for: <code>Web_Fetch</code>, <code>Web_Search</code>, |
| <code>Code_Interpreter</code>, <code>Memory_Manager</code>, <code>Generate_Speech</code>, <code>File_System</code>, and <code>Shell_Command</code>. |
| </p> |
| <p><strong>Kokoro voice prefixes</strong></p> |
| <ul class="info-list" style="display:grid;grid-template-columns:repeat(2,minmax(160px,1fr));gap:6px 16px;"> |
| <li><code>af</code> — American female</li> |
| <li><code>am</code> — American male</li> |
| <li><code>bf</code> — British female</li> |
| <li><code>bm</code> — British male</li> |
| <li><code>ef</code> — European female</li> |
| <li><code>em</code> — European male</li> |
| <li><code>hf</code> — Hindi female</li> |
| <li><code>hm</code> — Hindi male</li> |
| <li><code>if</code> — Italian female</li> |
| <li><code>im</code> — Italian male</li> |
| <li><code>jf</code> — Japanese female</li> |
| <li><code>jm</code> — Japanese male</li> |
| <li><code>pf</code> — Portuguese female</li> |
| <li><code>pm</code> — Portuguese male</li> |
| <li><code>zf</code> — Chinese female</li> |
| <li><code>zm</code> — Chinese male</li> |
| <li><code>ff</code> — French female</li> |
| </ul> |
| </div> |
| </section> |
| </div> |
| </div> |
| """ |
| ) |
|
|
| gr.TabbedInterface(interface_list=_interfaces, tab_names=_tab_names) |
|
|
| if __name__ == "__main__": |
| demo.launch(mcp_server=True) |