nimo007 commited on
Commit
30c74cf
·
verified ·
1 Parent(s): 2e00e7a

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +142 -79
app.py CHANGED
@@ -1,112 +1,175 @@
1
  # app.py
2
  import json
3
  import logging
4
- import time
5
- import uuid
6
  from datetime import datetime, timezone
 
 
7
  import gradio as gr
8
 
9
- # ---------- Logging (JSON, UTC) ----------
 
 
10
  class JsonFormatter(logging.Formatter):
11
- # Ensure all timestamps are UTC and RFC3339-like
12
- converter = time.gmtime
13
-
14
  def format(self, record: logging.LogRecord) -> str:
15
- base = {
16
- "ts": self.formatTime(record, "%Y-%m-%dT%H:%M:%S.%fZ"),
17
  "level": record.levelname,
18
  "logger": record.name,
19
  "message": record.getMessage(),
20
  }
21
- # Include extras if provided
22
- for k in ("request_id", "duration_ms", "event", "path", "client"):
23
- if hasattr(record, k):
24
- base[k] = getattr(record, k)
25
- return json.dumps(base, ensure_ascii=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
 
27
- def _setup_logging():
28
  handler = logging.StreamHandler()
29
  handler.setFormatter(JsonFormatter())
30
- root = logging.getLogger()
31
- root.handlers.clear()
32
- root.addHandler(handler)
33
- root.setLevel(logging.INFO)
34
 
35
- _setup_logging()
36
- log = logging.getLogger("datetime_mcp")
 
 
 
 
 
37
 
38
- # ---------- Time payload builder ----------
39
- def build_time_payload(dt: datetime) -> dict:
 
 
 
 
40
  """
41
- Return a compact, LLM-friendly bundle with multiple canonical formats
42
- and useful calendar context. Always UTC.
43
  """
44
- # Basic forms
45
- iso_utc = dt.replace(tzinfo=timezone.utc).isoformat().replace("+00:00", "Z")
46
- unix_ms = int(dt.timestamp() * 1000)
47
- unix_s = unix_ms // 1000
48
-
49
- # Calendar context
50
- iso_week_year, iso_week, iso_weekday = dt.isocalendar()
51
- day_of_year = int(dt.strftime("%j"))
52
-
53
- payload = {
54
- # Canonical machine-readable
55
- "iso_utc": iso_utc, # RFC3339/ISO-8601 with Z
56
- "unix_s": unix_s, # seconds since epoch
57
- "unix_ms": unix_ms, # milliseconds since epoch
58
-
59
- # Human-readable disambiguated strings (still UTC)
60
- "date_utc": dt.strftime("%Y-%m-%d"),
61
- "time_utc": dt.strftime("%H:%M:%S"),
62
- "weekday_utc": dt.strftime("%A"),
63
-
64
- # Calendar context (helps reasoning about “week/day”)
65
- "iso_week": f"{iso_week_year}-W{iso_week:02d}",
66
- "iso_weekday": iso_weekday, # 1=Mon .. 7=Sun
67
- "day_of_year": day_of_year,
68
- "quarter": (dt.month - 1) // 3 + 1,
69
-
70
- # Explicit timezone grounding
 
 
 
 
 
 
 
 
71
  "timezone": "UTC",
72
- "offset": "+00:00",
73
-
74
- # Friendly summary (for quick reading / prompts)
75
- "summary": dt.strftime("%A, %d %B %Y %H:%M:%S UTC"),
76
  }
77
- return payload
78
 
79
- # ---------- Tool function ----------
80
  def now_utc():
81
  """
82
- Return the current time bundle in JSON (UTC).
83
- Generator -> streamable over HTTP/SSE.
84
  """
85
- req_id = str(uuid.uuid4())
86
- t0 = time.perf_counter()
87
-
88
- log.info("request_start", extra={"request_id": req_id, "event": "now_utc_start"})
89
- dt = datetime.now(timezone.utc)
90
- payload = build_time_payload(dt)
91
-
92
- # Stream one JSON event (LLM-friendly)
93
- yield json.dumps(payload, ensure_ascii=False)
94
-
95
- dur_ms = int((time.perf_counter() - t0) * 1000)
96
- log.info(
97
- "request_end",
98
- extra={"request_id": req_id, "event": "now_utc_end", "duration_ms": dur_ms},
99
- )
100
 
101
- # ---------- Gradio app ----------
 
 
102
  demo = gr.Interface(
103
  fn=now_utc,
104
  inputs=None,
105
- outputs=gr.JSON(label="UTC time bundle (LLM-friendly)"),
106
  title="Datetime MCP Server",
107
- description="Returns the current UTC datetime as a structured JSON bundle for LLMs.",
108
  )
109
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
110
  if __name__ == "__main__":
111
- # Starts the web UI and an MCP SSE endpoint; no authentication configured.
112
- demo.launch(mcp_server=True)
 
 
 
 
 
 
 
1
  # app.py
2
  import json
3
  import logging
 
 
4
  from datetime import datetime, timezone
5
+ from typing import Dict
6
+
7
  import gradio as gr
8
 
9
+ # --------------------------
10
+ # Logging setup (quiet + precise)
11
+ # --------------------------
12
  class JsonFormatter(logging.Formatter):
 
 
 
13
  def format(self, record: logging.LogRecord) -> str:
14
+ payload = {
15
+ "ts": datetime.now(timezone.utc).isoformat(timespec="milliseconds"),
16
  "level": record.levelname,
17
  "logger": record.name,
18
  "message": record.getMessage(),
19
  }
20
+ # Attach any extra dict passed via logger.extra
21
+ for k, v in getattr(record, "__dict__", {}).items():
22
+ if k not in payload and k not in (
23
+ "args", "asctime", "created", "exc_info", "exc_text", "filename",
24
+ "funcName", "levelno", "lineno", "module", "msecs", "msg",
25
+ "name", "pathname", "process", "processName", "relativeCreated",
26
+ "stack_info", "thread", "threadName"
27
+ ):
28
+ payload[k] = v
29
+ return json.dumps(payload, ensure_ascii=False)
30
+
31
+ def setup_logging() -> logging.Logger:
32
+ root = logging.getLogger()
33
+ root.setLevel(logging.WARNING) # default higher; opt-in with our logger
34
+
35
+ # Silence noisy libs
36
+ logging.getLogger("httpx").setLevel(logging.ERROR) # suppress asset GETs
37
+ logging.getLogger("uvicorn").setLevel(logging.WARNING)
38
+ logging.getLogger("uvicorn.access").setLevel(logging.ERROR) # access logs off/noisy
39
 
 
40
  handler = logging.StreamHandler()
41
  handler.setFormatter(JsonFormatter())
 
 
 
 
42
 
43
+ app_logger = logging.getLogger("app")
44
+ app_logger.setLevel(logging.INFO)
45
+ # Avoid duplicate handlers if reloaded
46
+ if not any(isinstance(h, logging.StreamHandler) for h in app_logger.handlers):
47
+ app_logger.addHandler(handler)
48
+ app_logger.propagate = False
49
+ return app_logger
50
 
51
+ log = setup_logging()
52
+
53
+ # --------------------------
54
+ # Time function (LLM-friendly JSON)
55
+ # --------------------------
56
+ def _time_payload_utc() -> Dict[str, object]:
57
  """
58
+ Return multiple canonical time representations for LLMs/tools.
59
+ All times are UTC.
60
  """
61
+ now = datetime.now(timezone.utc)
62
+ iso8601 = now.isoformat(timespec="milliseconds").replace("+00:00", "Z")
63
+
64
+ # ISO week/date parts
65
+ iso_year, iso_week, iso_weekday = now.isocalendar()
66
+ yday = int(now.strftime("%j"))
67
+
68
+ return {
69
+ # Canonical machine formats
70
+ "utc_iso8601": iso8601, # e.g., 2025-11-11T04:15:30.123Z
71
+ "unix_seconds": int(now.timestamp()), # e.g., 1762824930
72
+ "unix_milliseconds": int(now.timestamp() * 1000),
73
+
74
+ # Human-friendly hints (still machine-parseable)
75
+ "date_utc": now.strftime("%Y-%m-%d"), # 2025-11-11
76
+ "time_utc": now.strftime("%H:%M:%S.%f")[:-3], # 04:15:30.123
77
+ "weekday_utc": now.strftime("%A"), # Tuesday
78
+ "rfc_1123_utc": now.strftime("%a, %d %b %Y %H:%M:%S GMT"),
79
+
80
+ # Components
81
+ "year": now.year,
82
+ "month": now.month,
83
+ "day": now.day,
84
+ "hour": now.hour,
85
+ "minute": now.minute,
86
+ "second": now.second,
87
+ "millisecond": int(now.microsecond / 1000),
88
+
89
+ # ISO calendar context
90
+ "iso_year": iso_year,
91
+ "iso_week": iso_week,
92
+ "iso_weekday": iso_weekday, # 1=Mon ... 7=Sun
93
+ "yearday": yday,
94
+
95
+ # Provenance
96
  "timezone": "UTC",
97
+ "source": "Datetime MCP Server",
98
+ "note": "All values are UTC. Prefer 'utc_iso8601' for portability."
 
 
99
  }
 
100
 
 
101
  def now_utc():
102
  """
103
+ Return current time (UTC) as a rich JSON object.
104
+ Designed to be easily consumed by LLMs/tools.
105
  """
106
+ payload = _time_payload_utc()
107
+ # Application-level audit log (one line per query)
108
+ log.info("time_requested", extra={"route": "predict", "out_keys": list(payload.keys())})
109
+ return payload
 
 
 
 
 
 
 
 
 
 
 
110
 
111
+ # --------------------------
112
+ # Gradio Interface
113
+ # --------------------------
114
  demo = gr.Interface(
115
  fn=now_utc,
116
  inputs=None,
117
+ outputs=gr.JSON(label="UTC time (LLM-friendly JSON)"),
118
  title="Datetime MCP Server",
119
+ description="Returns the current UTC datetime in multiple canonical formats."
120
  )
121
 
122
+ # --------------------------
123
+ # Middleware to log only meaningful hits
124
+ # --------------------------
125
+ def attach_selective_logging_middleware(app):
126
+ """
127
+ Add a lightweight middleware that logs only when an API/predict-like
128
+ endpoint is hit; skips UI asset routes (/_app, /assets, /favicon, etc).
129
+ """
130
+ from fastapi import Request
131
+ from starlette.responses import Response
132
+
133
+ ASSET_PREFIXES = ("/_app", "/assets", "/static", "/favicon", "/logo", "/file=")
134
+ INTERESTING_PREFIXES = ("/run", "/queue", "/predict", "/api", "/chat", "/submit")
135
+
136
+ @app.middleware("http")
137
+ async def selective_access_log(request: Request, call_next):
138
+ path = request.url.path or "/"
139
+ # Skip common asset paths
140
+ if path.startswith(ASSET_PREFIXES):
141
+ return await call_next(request)
142
+
143
+ # Log only interesting interactive endpoints
144
+ if path.startswith(INTERESTING_PREFIXES) or path == "/":
145
+ ua = request.headers.get("user-agent", "")
146
+ client_ip = getattr(request.client, "host", None)
147
+ log.info(
148
+ "incoming_request",
149
+ extra={
150
+ "method": request.method,
151
+ "path": path,
152
+ "client_ip": client_ip,
153
+ "user_agent": ua[:300],
154
+ },
155
+ )
156
+ response: Response = await call_next(request)
157
+ return response
158
+
159
+ # Attach middleware to the FastAPI app Gradio runs under
160
+ # Gradio 4.x exposes the FastAPI app at demo.server.app
161
+ try:
162
+ attach_selective_logging_middleware(demo.server.app) # type: ignore[attr-defined]
163
+ except Exception:
164
+ # Fallback for older/newer Gradio internals; safe no-op if unavailable.
165
+ pass
166
+
167
  if __name__ == "__main__":
168
+ # quiet=True reduces banner/noise; mcp_server=True exposes the MCP SSE endpoint.
169
+ demo.launch(
170
+ mcp_server=True,
171
+ quiet=True, # suppresses gradio banner/extra logs
172
+ show_error=True, # still useful while developing
173
+ server_name="0.0.0.0",
174
+ server_port=7861,
175
+ )