| | from __future__ import annotations |
| |
|
| | |
| |
|
| | import json |
| | import os |
| | import sys |
| | import threading |
| | import time |
| | import warnings |
| | from datetime import datetime, timedelta |
| | from typing import Any |
| |
|
| | |
| | |
| | def _patch_asyncio_event_loop_del(): |
| | """Patch BaseEventLoop.__del__ to suppress 'Invalid file descriptor: -1' errors.""" |
| | try: |
| | import asyncio.base_events as base_events |
| | original_del = getattr(base_events.BaseEventLoop, "__del__", None) |
| | if original_del is None: |
| | return |
| | def patched_del(self): |
| | try: |
| | original_del(self) |
| | except ValueError as e: |
| | if "Invalid file descriptor" not in str(e): |
| | raise |
| | base_events.BaseEventLoop.__del__ = patched_del |
| | except Exception: |
| | pass |
| |
|
| | _patch_asyncio_event_loop_del() |
| |
|
| | 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, file=sys.__stdout__) |
| | except Exception as exc: |
| | print(f"[TOOL CALL] {func_name} (failed to log inputs: {exc})", flush=True, file=sys.__stdout__) |
| |
|
| |
|
| | def _log_call_end(func_name: str, output_desc: str) -> None: |
| | try: |
| | |
| | print(f"[TOOL RESULT] {func_name} output: {output_desc}", flush=True, file=sys.__stdout__) |
| | except Exception as exc: |
| | print(f"[TOOL RESULT] {func_name} (failed to log output: {exc})", flush=True, file=sys.__stdout__) |
| |
|
| | |
| | |
| | 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.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.Agent_Skills import build_interface as build_skills_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_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() |
| |
|
| | |
| | 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() |
| | skills_interface = build_skills_interface() |
| |
|
| | _interfaces = [ |
| | agent_terminal_interface, |
| | skills_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, |
| | ] |
| | _tab_names = [ |
| | "Agent Terminal", |
| | "Agent Skills", |
| | "Web Fetch", |
| | "Web Search", |
| | "Code Interpreter", |
| | "Shell Command", |
| | "File System", |
| | "Obsidian Vault", |
| | "Memory Manager", |
| | "Generate Speech", |
| | "Generate Image", |
| | "Generate Video", |
| | "Deep Research", |
| | ] |
| |
|
| | with gr.Blocks(title="Nymbo/Tools MCP") as demo: |
| |
|
| | with gr.Sidebar(width=300, elem_classes="app-sidebar"): |
| | gr.Markdown( |
| | "## Nymbo/Tools MCP\n" |
| | "<p style='font-size: 0.7rem; opacity: 0.85; margin-top: 2px; margin-bottom: 6px;'>General purpose tools useful for any agent.</p>\n" |
| | "<a href='https://www.nymbo.net/nymbot' target='_blank' style='font-size: 0.7rem; display: block;'>Test with Nymbot</a>" |
| | ) |
| | |
| | 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-tools.hf.space/gradio_api/mcp/</code> |
| | </p> |
| | <p>Example client configuration:</p> |
| | <pre><code class="language-json">{ |
| | "mcpServers": { |
| | "nymbo-tools": { |
| | "url": "https://nymbo-tools.hf.space/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> |
| | <li><code>Agent_Skills</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() |
| |
|
| | |
| | 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) |