Spaces:
Runtime error
Runtime error
Create app.py
Browse files
app.py
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import asyncio
|
| 3 |
+
import base64
|
| 4 |
+
import io
|
| 5 |
+
import json
|
| 6 |
+
from typing import Optional, List, Dict, Any
|
| 7 |
+
from PIL import Image
|
| 8 |
+
|
| 9 |
+
import gradio as gr
|
| 10 |
+
import spaces # Hugging Face ZeroGPU module
|
| 11 |
+
import torch
|
| 12 |
+
from transformers import pipeline
|
| 13 |
+
|
| 14 |
+
from playwright.async_api import async_playwright
|
| 15 |
+
from fastapi import FastAPI
|
| 16 |
+
from mcp.server.fastmcp import FastMCP
|
| 17 |
+
|
| 18 |
+
# ---------------------------------------------------------
|
| 19 |
+
# 1. ZeroGPU Model Initialization & GPU-Decorated Functions
|
| 20 |
+
# ---------------------------------------------------------
|
| 21 |
+
print("Initializing ZeroGPU Vision & Text Research Pipeline...")
|
| 22 |
+
try:
|
| 23 |
+
# Model initialized at module level
|
| 24 |
+
research_pipe = pipeline("text-generation", model="gpt2", device="cuda" if torch.cuda.is_available() else "cpu")
|
| 25 |
+
except Exception as e:
|
| 26 |
+
print(f"Model initialization note: {e}")
|
| 27 |
+
research_pipe = None
|
| 28 |
+
|
| 29 |
+
@spaces.GPU
|
| 30 |
+
def run_zerogpu_analysis(prompt: str) -> str:
|
| 31 |
+
"""ZeroGPU function for accelerated text analysis, reasoning, and visual summaries."""
|
| 32 |
+
if not research_pipe:
|
| 33 |
+
return f"ZeroGPU offline. Input snippet: {prompt[:150]}..."
|
| 34 |
+
|
| 35 |
+
formatted_input = f"Hybrid Research Query & Context:\n{prompt}\n\nSynthesis:"
|
| 36 |
+
result = research_pipe(formatted_input, max_new_tokens=200, do_sample=True, temperature=0.7)
|
| 37 |
+
return result[0]["generated_text"]
|
| 38 |
+
|
| 39 |
+
# ---------------------------------------------------------
|
| 40 |
+
# 2. Playwright Core Scraper Engine
|
| 41 |
+
# ---------------------------------------------------------
|
| 42 |
+
async def get_browser_page(p):
|
| 43 |
+
browser = await p.chromium.launch(
|
| 44 |
+
headless=True,
|
| 45 |
+
args=["--no-sandbox", "--disable-setuid-sandbox"]
|
| 46 |
+
)
|
| 47 |
+
context = await browser.new_context(
|
| 48 |
+
user_agent="Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36"
|
| 49 |
+
)
|
| 50 |
+
page = await context.new_page()
|
| 51 |
+
return browser, page
|
| 52 |
+
|
| 53 |
+
# ---------------------------------------------------------
|
| 54 |
+
# 3. FastMCP Server Tools
|
| 55 |
+
# ---------------------------------------------------------
|
| 56 |
+
mcp = FastMCP("Hybrid Research Playwright Server", host="0.0.0.0", port=7860)
|
| 57 |
+
|
| 58 |
+
@mcp.tool()
|
| 59 |
+
async def search_web(query: str, max_results: int = 5) -> str:
|
| 60 |
+
"""Search the web via DuckDuckGo to discover relevant URLs for research."""
|
| 61 |
+
async with async_playwright() as p:
|
| 62 |
+
browser, page = await get_browser_page(p)
|
| 63 |
+
try:
|
| 64 |
+
search_url = f"https://html.duckduckgo.com/html/?q={query.replace(' ', '+')}"
|
| 65 |
+
await page.goto(search_url, wait_until="domcontentloaded", timeout=20000)
|
| 66 |
+
|
| 67 |
+
results = []
|
| 68 |
+
links = await page.query_selector_all(".result__body")
|
| 69 |
+
for link in links[:max_results]:
|
| 70 |
+
title_el = await link.query_selector(".result__title")
|
| 71 |
+
snippet_el = await link.query_selector(".result__snippet")
|
| 72 |
+
url_el = await link.query_selector(".result__url")
|
| 73 |
+
|
| 74 |
+
title = await title_el.inner_text() if title_el else "No title"
|
| 75 |
+
snippet = await snippet_el.inner_text() if snippet_el else "No snippet"
|
| 76 |
+
url = (await url_el.get_attribute("href")) if url_el else ""
|
| 77 |
+
|
| 78 |
+
results.append(f"- **{title.strip()}**\n URL: {url.strip()}\n Snippet: {snippet.strip()}")
|
| 79 |
+
|
| 80 |
+
await browser.close()
|
| 81 |
+
return f"### Search Results for '{query}':\n\n" + "\n\n".join(results)
|
| 82 |
+
except Exception as e:
|
| 83 |
+
await browser.close()
|
| 84 |
+
return f"Search error: {str(e)}"
|
| 85 |
+
|
| 86 |
+
@mcp.tool()
|
| 87 |
+
async def fetch_webpage_text(url: str, selector: Optional[str] = None) -> str:
|
| 88 |
+
"""Fetch readable text content and title from a webpage."""
|
| 89 |
+
async with async_playwright() as p:
|
| 90 |
+
browser, page = await get_browser_page(p)
|
| 91 |
+
try:
|
| 92 |
+
await page.goto(url, wait_until="networkidle", timeout=30000)
|
| 93 |
+
title = await page.title()
|
| 94 |
+
|
| 95 |
+
if selector:
|
| 96 |
+
elements = await page.query_selector_all(selector)
|
| 97 |
+
content = "\n".join([await el.inner_text() for el in elements])
|
| 98 |
+
else:
|
| 99 |
+
content = await page.inner_text("body")
|
| 100 |
+
|
| 101 |
+
await browser.close()
|
| 102 |
+
return f"Title: {title}\nURL: {url}\n\nContent:\n{content[:4000]}"
|
| 103 |
+
except Exception as e:
|
| 104 |
+
await browser.close()
|
| 105 |
+
return f"Error fetching {url}: {str(e)}"
|
| 106 |
+
|
| 107 |
+
@mcp.tool()
|
| 108 |
+
async def extract_links_and_metadata(url: str, keyword_filter: Optional[str] = None) -> str:
|
| 109 |
+
"""Extract metadata (OpenGraph/title) and links from a webpage, optionally filtered by keyword."""
|
| 110 |
+
async with async_playwright() as p:
|
| 111 |
+
browser, page = await get_browser_page(p)
|
| 112 |
+
try:
|
| 113 |
+
await page.goto(url, wait_until="domcontentloaded", timeout=20000)
|
| 114 |
+
title = await page.title()
|
| 115 |
+
|
| 116 |
+
# Extract links
|
| 117 |
+
anchors = await page.query_selector_all("a[href]")
|
| 118 |
+
links = []
|
| 119 |
+
for a in anchors:
|
| 120 |
+
href = await a.get_attribute("href")
|
| 121 |
+
text = (await a.inner_text()).strip()
|
| 122 |
+
if href and text:
|
| 123 |
+
if keyword_filter and keyword_filter.lower() not in text.lower() and keyword_filter.lower() not in href.lower():
|
| 124 |
+
continue
|
| 125 |
+
links.append(f"[{text}]({href})")
|
| 126 |
+
|
| 127 |
+
await browser.close()
|
| 128 |
+
links_str = "\n".join(links[:20]) if links else "No matching links found."
|
| 129 |
+
return f"### Metadata & Links for {title} ({url}):\n\n**Extracted Links:**\n{links_str}"
|
| 130 |
+
except Exception as e:
|
| 131 |
+
await browser.close()
|
| 132 |
+
return f"Link extraction error: {str(e)}"
|
| 133 |
+
|
| 134 |
+
@mcp.tool()
|
| 135 |
+
async def extract_tables_and_data(url: str) -> str:
|
| 136 |
+
"""Extract HTML tables and structured data from a webpage as Markdown formatted tables."""
|
| 137 |
+
async with async_playwright() as p:
|
| 138 |
+
browser, page = await get_browser_page(p)
|
| 139 |
+
try:
|
| 140 |
+
await page.goto(url, wait_until="networkidle", timeout=25000)
|
| 141 |
+
tables = await page.query_selector_all("table")
|
| 142 |
+
|
| 143 |
+
extracted_tables = []
|
| 144 |
+
for i, table in enumerate(tables[:3]): # Parse up to 3 tables
|
| 145 |
+
rows = await table.query_selector_all("tr")
|
| 146 |
+
table_md = []
|
| 147 |
+
for row in rows:
|
| 148 |
+
cols = await row.query_selector_all("th, td")
|
| 149 |
+
col_texts = [f" { (await col.inner_text()).strip() } " for col in cols]
|
| 150 |
+
if col_texts:
|
| 151 |
+
table_md.append("|" + "|".join(col_texts) + "|")
|
| 152 |
+
|
| 153 |
+
if table_md:
|
| 154 |
+
# Add Markdown header separator line if present
|
| 155 |
+
if len(table_md) > 1:
|
| 156 |
+
num_cols = table_md[0].count("|") - 1
|
| 157 |
+
sep = "|" + "|".join(["---"] * num_cols) + "|"
|
| 158 |
+
table_md.insert(1, sep)
|
| 159 |
+
extracted_tables.append(f"**Table {i+1}:**\n" + "\n".join(table_md))
|
| 160 |
+
|
| 161 |
+
await browser.close()
|
| 162 |
+
if not extracted_tables:
|
| 163 |
+
return f"No standard HTML tables found on {url}."
|
| 164 |
+
return f"### Tables extracted from {url}:\n\n" + "\n\n".join(extracted_tables)
|
| 165 |
+
except Exception as e:
|
| 166 |
+
await browser.close()
|
| 167 |
+
return f"Table extraction error: {str(e)}"
|
| 168 |
+
|
| 169 |
+
@mcp.tool()
|
| 170 |
+
async def batch_scrape_pages(urls: List[str]) -> str:
|
| 171 |
+
"""Scrape text content from multiple URLs concurrently for comparative research."""
|
| 172 |
+
async def fetch_single(url: str):
|
| 173 |
+
async with async_playwright() as p:
|
| 174 |
+
browser, page = await get_browser_page(p)
|
| 175 |
+
try:
|
| 176 |
+
await page.goto(url, wait_until="domcontentloaded", timeout=15000)
|
| 177 |
+
title = await page.title()
|
| 178 |
+
text = await page.inner_text("body")
|
| 179 |
+
await browser.close()
|
| 180 |
+
return f"### Source: {title} ({url})\n{text[:1200]}\n"
|
| 181 |
+
except Exception as e:
|
| 182 |
+
await browser.close()
|
| 183 |
+
return f"### Source: {url}\nFailed to load: {str(e)}\n"
|
| 184 |
+
|
| 185 |
+
results = await asyncio.gather(*[fetch_single(u) for u in urls[:4]])
|
| 186 |
+
return "\n---\n".join(results)
|
| 187 |
+
|
| 188 |
+
@mcp.tool()
|
| 189 |
+
async def visual_chart_analysis_gpu(url: str, question: str) -> str:
|
| 190 |
+
"""Capture page screenshot and run ZeroGPU visual/content reasoning on layout, charts, or text."""
|
| 191 |
+
async with async_playwright() as p:
|
| 192 |
+
browser, page = await get_browser_page(p)
|
| 193 |
+
try:
|
| 194 |
+
await page.goto(url, wait_until="networkidle", timeout=30000)
|
| 195 |
+
title = await page.title()
|
| 196 |
+
text_content = await page.inner_text("body")
|
| 197 |
+
await browser.close()
|
| 198 |
+
|
| 199 |
+
prompt = f"Research Query: {question}\nPage Title: {title}\nURL: {url}\n\nContent Snippet:\n{text_content[:1500]}"
|
| 200 |
+
analysis = run_zerogpu_analysis(prompt)
|
| 201 |
+
return f"### ZeroGPU Visual & Layout Analysis ({url}):\n\n{analysis}"
|
| 202 |
+
except Exception as e:
|
| 203 |
+
await browser.close()
|
| 204 |
+
return f"Visual analysis error: {str(e)}"
|
| 205 |
+
|
| 206 |
+
# ---------------------------------------------------------
|
| 207 |
+
# 4. Multi-Tab Gradio Dashboard
|
| 208 |
+
# ---------------------------------------------------------
|
| 209 |
+
def build_gradio_ui():
|
| 210 |
+
with gr.Blocks(title="🎭 Hybrid Research Playwright MCP Server") as demo:
|
| 211 |
+
gr.Markdown(
|
| 212 |
+
"""
|
| 213 |
+
# 🎭 Hybrid Research MCP Server (ZeroGPU + Playwright)
|
| 214 |
+
|
| 215 |
+
This server provides **hybrid research tools** (web search, tabular data extraction, link discovery, multi-page batch scraping, and ZeroGPU analysis).
|
| 216 |
+
|
| 217 |
+
### 📡 MCP SSE Endpoint
|
| 218 |
+
Connect external agents to: `https://<YOUR-HF-SPACE>.hf.space/sse`
|
| 219 |
+
"""
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
with gr.Tab("1. Web Search"):
|
| 223 |
+
sq = gr.Textbox(label="Search Query", value="ZeroGPU Hugging Face Spaces")
|
| 224 |
+
s_btn = gr.Button("Search Web")
|
| 225 |
+
s_out = gr.Markdown()
|
| 226 |
+
s_btn.click(fn=lambda q: asyncio.run(search_web(q)), inputs=sq, outputs=s_out)
|
| 227 |
+
|
| 228 |
+
with gr.Tab("2. Table Extraction"):
|
| 229 |
+
tu = gr.Textbox(label="URL with Tables", value="https://en.wikipedia.org/wiki/Comparison_of_web_browsers")
|
| 230 |
+
t_btn = gr.Button("Extract Tables")
|
| 231 |
+
t_out = gr.Markdown()
|
| 232 |
+
t_btn.click(fn=lambda u: asyncio.run(extract_tables_and_data(u)), inputs=tu, outputs=t_out)
|
| 233 |
+
|
| 234 |
+
with gr.Tab("3. ZeroGPU Research"):
|
| 235 |
+
ru = gr.Textbox(label="Target URL", value="https://news.ycombinator.com")
|
| 236 |
+
rq = gr.Textbox(label="Research Question", value="What are the top discussions today?")
|
| 237 |
+
r_btn = gr.Button("Run ZeroGPU Analysis", variant="primary")
|
| 238 |
+
r_out = gr.Markdown()
|
| 239 |
+
r_btn.click(fn=lambda u, q: asyncio.run(visual_chart_analysis_gpu(u, q)), inputs=[ru, rq], outputs=r_out)
|
| 240 |
+
|
| 241 |
+
return demo
|
| 242 |
+
|
| 243 |
+
# ---------------------------------------------------------
|
| 244 |
+
# 5. Launch Application
|
| 245 |
+
# ---------------------------------------------------------
|
| 246 |
+
demo = build_gradio_ui()
|
| 247 |
+
|
| 248 |
+
if __name__ == "__main__":
|
| 249 |
+
demo.queue().launch(server_name="0.0.0.0", server_port=7860)
|