Tools / app.py
Nymbo's picture
Update app.py
88f05c4 verified
raw
history blame
16.2 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: 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)
# 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.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
# 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"))
# CSS copied from prior app.py to preserve exact look-and-feel
CSS_STYLES = """
/* 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";
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;
}
}
"""
# Build each tab interface using modular builders
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()
_interfaces = [
fetch_interface,
concise_interface,
code_interface,
memory_interface,
kokoro_interface,
image_generation_interface,
video_generation_interface,
deep_research_interface,
]
_tab_names = [
"Web Fetch",
"Web Search",
"Code Interpreter",
"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:
# Title and information panel unchanged to preserve UI
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 &amp; Video Generation</h3>
<p>
The <code>Generate_Image</code> and <code>Generate_Video</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</h3>
<p>
In this public demo, memories 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.
</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>
</div>
</section>
<section class="info-card">
<div class="info-card__icon">🛠️</div>
<div class="info-card__body">
<h3>Tool Notes &amp; Kokoro Voice Legend</h3>
<p>
No authentication required for: <code>Web_Fetch</code>, <code>Web_Search</code>,
<code>Code_Interpreter</code>, and <code>Generate_Speech</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)