Spaces:
Running
Running
Download app.py from SMLBuilder/SML-Tree-Explorer: direct link, hf CLI and curl.
- Browser
- Download file 6.61 kB
-
https://huggingface.co/spaces/SMLBuilder/SML-Tree-Explorer/resolve/main/app.py
- Command line
-
hf download hf://spaces/SMLBuilder/SML-Tree-Explorer/app.py
-
curl -L -o app.py https://huggingface.co/spaces/SMLBuilder/SML-Tree-Explorer/resolve/main/app.py
6.61 kB
| #!/usr/bin/env python3 | |
| """Public, read-only conversation explorer. No API tokens or inference runtime.""" | |
| import gzip | |
| import json | |
| import os | |
| import threading | |
| import time | |
| import urllib.error | |
| from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer | |
| from urllib.parse import parse_qs, urlsplit | |
| from source_store import BASE, SourceStore | |
| from tree_decoder import DecodeError, VERSION | |
| STATIC = {"/": ("site/index.html", "text/html; charset=utf-8"), | |
| "/style.css": ("site/style.css", "text/css; charset=utf-8"), | |
| "/app.js": ("site/app.js", "text/javascript; charset=utf-8"), | |
| "/tree_decoder.py": ("tree_decoder.py", "text/x-python; charset=utf-8"), | |
| "/AAC_cli_original.py": ("public_decoder/AAC_cli_original.py", "text/x-python; charset=utf-8"), | |
| "/favicon.svg": ("site/favicon.svg", "image/svg+xml")} | |
| class Server(ThreadingHTTPServer): | |
| daemon_threads = True | |
| allow_reuse_address = True | |
| def __init__(self, address, store=None): | |
| self.store = store or SourceStore() | |
| self.slots = threading.BoundedSemaphore(12) | |
| super().__init__(address, Handler) | |
| def process_request(self, request, client_address): | |
| if not self.slots.acquire(blocking=False): | |
| try: | |
| request.sendall(b"HTTP/1.1 503 Service Unavailable\r\nRetry-After: 3\r\nContent-Length: 0\r\nConnection: close\r\n\r\n") | |
| finally: | |
| self.shutdown_request(request) | |
| return | |
| try: | |
| super().process_request(request, client_address) | |
| except Exception: | |
| self.slots.release() | |
| raise | |
| def process_request_thread(self, request, client_address): | |
| try: | |
| super().process_request_thread(request, client_address) | |
| finally: | |
| self.slots.release() | |
| class Handler(BaseHTTPRequestHandler): | |
| server_version = "SMLTree/" + VERSION | |
| def setup(self): | |
| super().setup() | |
| self.connection.settimeout(65) | |
| def log_message(self, fmt, *args): | |
| # Public tree paths only; no visitor conversation content is submitted or stored. | |
| print("http", self.command, urlsplit(self.path).path, args[1] if len(args) > 1 else "", flush=True) | |
| def send(self, status, raw, content_type="application/json; charset=utf-8", cache="no-store", download=None): | |
| self.send_response(status) | |
| self.send_header("Content-Type", content_type) | |
| self.send_header("Cache-Control", cache) | |
| self.send_header("X-Content-Type-Options", "nosniff") | |
| self.send_header("Referrer-Policy", "no-referrer") | |
| self.send_header("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self'; " | |
| "img-src 'self' data:; connect-src 'self'; object-src 'none'; base-uri 'none'") | |
| if download: | |
| self.send_header("Content-Disposition", 'attachment; filename="' + download + '"') | |
| if len(raw) > 2048 and "gzip" in self.headers.get("Accept-Encoding", ""): | |
| raw = gzip.compress(raw, compresslevel=4) | |
| self.send_header("Content-Encoding", "gzip") | |
| self.send_header("Vary", "Accept-Encoding") | |
| self.send_header("Content-Length", str(len(raw))) | |
| self.end_headers() | |
| if self.command != "HEAD": | |
| self.wfile.write(raw) | |
| def json(self, status, value, **kwargs): | |
| self.send(status, json.dumps(value, ensure_ascii=False).encode(), **kwargs) | |
| def do_HEAD(self): | |
| self.do_GET() | |
| def do_GET(self): | |
| started = time.perf_counter() | |
| try: | |
| if len(self.path) > 8192: | |
| raise DecodeError("Request is too long.") | |
| url = urlsplit(self.path) | |
| if url.path in STATIC: | |
| file, mime = STATIC[url.path] | |
| download = file.split("/")[-1] if file.endswith(".py") else None | |
| return self.send(200, (BASE / file).read_bytes(), mime, "public, max-age=300", download) | |
| if url.path == "/health": | |
| return self.json(200, {"ok": True, "decoder_version": VERSION, | |
| "catalog_files": len(self.server.store.rows)}) | |
| if url.path == "/api/catalog": | |
| return self.json(200, self.server.store.catalog(), cache="public, max-age=300") | |
| if url.path not in ("/api/document", "/api/trace", "/api/export"): | |
| return self.json(404, {"error": "Not found."}) | |
| query = parse_qs(url.query, keep_blank_values=True, max_num_fields=8) | |
| if any(len(v) != 1 for v in query.values()): | |
| raise DecodeError("Query parameters must appear once.") | |
| ident = query.get("id", [""])[0] | |
| document, source = self.server.store.get(ident) | |
| if url.path == "/api/document": | |
| summaries = document.summaries() | |
| return self.json(200, {"source": source, "conversations": summaries, | |
| "status": "supported" if summaries else "not-a-supported-conversation", | |
| "load_ms": round((time.perf_counter() - started) * 1000, 2)}) | |
| conversation = int(query.get("conversation", ["0"])[0]) | |
| raw_path = query.get("path", [""])[0] | |
| choices = [int(i) for i in raw_path.split(",")] if raw_path else [] | |
| if len(choices) > 128: | |
| raise DecodeError("Web paths are limited to 128 selections; the Python decoder has no path cap.") | |
| result = {**document.trace(conversation, choices), "source": source, | |
| "decode_ms": round((time.perf_counter() - started) * 1000, 2)} | |
| return self.json(200, result, download="sml-conversation-trace.json" if url.path == "/api/export" else None) | |
| except KeyError: | |
| self.json(404, {"error": "Unknown catalog source."}) | |
| except (DecodeError, ValueError, RecursionError) as error: | |
| self.json(422, {"error": str(error)}) | |
| except (urllib.error.URLError, TimeoutError, OSError): | |
| try: | |
| self.json(502, {"error": "The public source could not be loaded. Retry in a moment; your path is preserved."}) | |
| except OSError: | |
| pass | |
| if __name__ == "__main__": | |
| server = Server((os.environ.get("HOST", "0.0.0.0"), int(os.environ.get("PORT", "7860")))) | |
| print("SML Tree Explorer listening on port", server.server_port, flush=True) | |
| try: | |
| server.serve_forever() | |
| except KeyboardInterrupt: | |
| pass | |
| finally: | |
| server.server_close() | |