nimo007's picture
Update app.py
33da02e verified
Raw
History Blame Contribute Delete
6.33 kB
# app.py
import json
import logging
from datetime import datetime, timezone
from typing import Dict
import gradio as gr
# --------------------------
# Logging setup (quiet + precise)
# --------------------------
class JsonFormatter(logging.Formatter):
def format(self, record: logging.LogRecord) -> str:
payload = {
"ts": datetime.now(timezone.utc).isoformat(timespec="milliseconds"),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
# Attach any extra dict passed via logger.extra
for k, v in getattr(record, "__dict__", {}).items():
if k not in payload and k not in (
"args", "asctime", "created", "exc_info", "exc_text", "filename",
"funcName", "levelno", "lineno", "module", "msecs", "msg",
"name", "pathname", "process", "processName", "relativeCreated",
"stack_info", "thread", "threadName"
):
payload[k] = v
return json.dumps(payload, ensure_ascii=False)
def setup_logging() -> logging.Logger:
root = logging.getLogger()
root.setLevel(logging.WARNING) # default higher; opt-in with our logger
# Silence noisy libs
logging.getLogger("httpx").setLevel(logging.ERROR) # suppress asset GETs
logging.getLogger("uvicorn").setLevel(logging.WARNING)
logging.getLogger("uvicorn.access").setLevel(logging.ERROR) # access logs off/noisy
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
app_logger = logging.getLogger("app")
app_logger.setLevel(logging.INFO)
# Avoid duplicate handlers if reloaded
if not any(isinstance(h, logging.StreamHandler) for h in app_logger.handlers):
app_logger.addHandler(handler)
app_logger.propagate = False
return app_logger
log = setup_logging()
# --------------------------
# Time function (LLM-friendly JSON)
# --------------------------
def _time_payload_utc() -> Dict[str, object]:
"""
Return multiple canonical time representations for LLMs/tools.
All times are UTC.
"""
now = datetime.now(timezone.utc)
iso8601 = now.isoformat(timespec="milliseconds").replace("+00:00", "Z")
# ISO week/date parts
iso_year, iso_week, iso_weekday = now.isocalendar()
yday = int(now.strftime("%j"))
return {
# Canonical machine formats
"utc_iso8601": iso8601, # e.g., 2025-11-11T04:15:30.123Z
"unix_seconds": int(now.timestamp()), # e.g., 1762824930
"unix_milliseconds": int(now.timestamp() * 1000),
# Human-friendly hints (still machine-parseable)
"date_utc": now.strftime("%Y-%m-%d"), # 2025-11-11
"time_utc": now.strftime("%H:%M:%S.%f")[:-3], # 04:15:30.123
"weekday_utc": now.strftime("%A"), # Tuesday
"rfc_1123_utc": now.strftime("%a, %d %b %Y %H:%M:%S GMT"),
# Components
"year": now.year,
"month": now.month,
"day": now.day,
"hour": now.hour,
"minute": now.minute,
"second": now.second,
"millisecond": int(now.microsecond / 1000),
# ISO calendar context
"iso_year": iso_year,
"iso_week": iso_week,
"iso_weekday": iso_weekday, # 1=Mon ... 7=Sun
"yearday": yday,
# Provenance
"timezone": "UTC",
"source": "Datetime MCP Server",
"note": "All values are UTC. Prefer 'utc_iso8601' for portability."
}
def now_utc():
"""
Return current time (UTC) as a rich JSON object.
Designed to be easily consumed by LLMs/tools.
"""
payload = _time_payload_utc()
# Application-level audit log (one line per query)
log.info("time_requested", extra={"route": "predict", "out_keys": list(payload.keys())})
return payload
# --------------------------
# Gradio Interface
# --------------------------
demo = gr.Interface(
fn=now_utc,
inputs=None,
outputs=gr.JSON(label="UTC time (LLM-friendly JSON)"),
title="Datetime MCP Server",
description="Returns the current UTC datetime in multiple canonical formats."
)
# --------------------------
# Middleware to log only meaningful hits
# --------------------------
def attach_selective_logging_middleware(app):
"""
Add a lightweight middleware that logs only when an API/predict-like
endpoint is hit; skips UI asset routes (/_app, /assets, /favicon, etc).
"""
from fastapi import Request
from starlette.responses import Response
ASSET_PREFIXES = ("/_app", "/assets", "/static", "/favicon", "/logo", "/file=")
INTERESTING_PREFIXES = ("/run", "/queue", "/predict", "/api", "/chat", "/submit")
@app.middleware("http")
async def selective_access_log(request: Request, call_next):
path = request.url.path or "/"
# Skip common asset paths
if path.startswith(ASSET_PREFIXES):
return await call_next(request)
# Log only interesting interactive endpoints
if path.startswith(INTERESTING_PREFIXES) or path == "/":
ua = request.headers.get("user-agent", "")
client_ip = getattr(request.client, "host", None)
log.info(
"incoming_request",
extra={
"method": request.method,
"path": path,
"client_ip": client_ip,
"user_agent": ua[:300],
},
)
response: Response = await call_next(request)
return response
# Attach middleware to the FastAPI app Gradio runs under
# Gradio 4.x exposes the FastAPI app at demo.server.app
try:
attach_selective_logging_middleware(demo.server.app) # type: ignore[attr-defined]
except Exception:
# Fallback for older/newer Gradio internals; safe no-op if unavailable.
pass
if __name__ == "__main__":
# quiet=True reduces banner/noise; mcp_server=True exposes the MCP SSE endpoint.
demo.launch(
mcp_server=True,
quiet=True, # suppresses gradio banner/extra logs
show_error=True, # still useful while developing
server_name="0.0.0.0",
server_port=None,
)
# https://nimo007-mcp-datetime-server.hf.space/gradio_api/mcp/