Tools / app.py
Nymbo's picture
Update app.py
d3ed4f4 verified
raw
history blame
16.9 kB
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()}
# Use sys.__stdout__ to avoid capturing logs in redirected output
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:
# Use sys.__stdout__ to avoid capturing logs in redirected output
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__)
# 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
# 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()
_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,
]
_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",
]
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"
"<code style='font-size: 0.7rem; word-break: break-all; display: block; margin-bottom: 6px;'>https://nymbo.net/gradio_api/mcp/</code>\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.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 &amp; 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)