mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-05-10 18:58:21 +00:00
* fix(gateway): return ISO 8601 timestamps from threads endpoints (#2594) ThreadResponse documents created_at / updated_at as ISO timestamps, matching the LangGraph Platform schema (langgraph_sdk.schema.Thread exposes them as datetime, JSON-encoded as ISO 8601). The gateway threads router was instead emitting str(time.time()) — unix-second floats — breaking frontend new Date() parsing and producing a mixed ISO/unix wire format that also corrupted the search sort order. Centralize timestamp generation in deerflow.utils.time: - now_iso() — datetime.now(UTC).isoformat() - coerce_iso(x) — heals legacy unix-timestamp strings on read so the store converges to ISO without a one-shot migration threads.py: replace 6 time.time() call sites with now_iso(); wrap all read paths and Phase-2 checkpoint metadata with coerce_iso(); _store_upsert opportunistically heals legacy created_at on update; drop unused time import. thread_runs.py: reuse now_iso() instead of a private duplicate _now_iso(), preventing future drift between the two timestamp call sites. Tests: 9 unit tests for the helper; 5 integration tests pinning the ISO contract for create/get/patch/search and the legacy-healing path on the internal store upsert. Full suite: 2144 passed, 15 skipped, 0 failed. Closes #2594 * fix(gateway): coerce checkpoint metadata timestamps to ISO on read After the merge with main, three additional read paths in ``threads.py`` were still emitting raw ``str(metadata.get("created_at", ""))`` — ``get_thread_state``, ``update_thread_state``, and ``get_thread_history``. Same root cause as #2594: when the checkpoint metadata's ``created_at`` is a unix-second float (legacy data, or a checkpoint written by an older Gateway version), ``str(float)`` produces ``"1777252410.411327"`` and the frontend's ``new Date(...)`` returns ``Invalid Date``. The fix on the ``/threads/{id}`` GET path was already in place; these three sibling endpoints needed the same treatment. All four call sites now flow through ``coerce_iso``, so: - legacy float metadata heals to ISO on the way out, - ISO metadata passes through unchanged, - ``datetime`` instances (which the new ``coerce_iso`` branch handles explicitly) emit with the ``T`` separator instead of falling through to the space-separated ``str(datetime)`` form. Coverage added for the two endpoints not already pinned by the merge: - ``test_get_thread_state_returns_iso_for_legacy_checkpoint_metadata`` - ``test_get_thread_history_returns_iso_for_legacy_checkpoint_metadata`` Both pre-seed a checkpoint whose metadata carries the literal float from the issue body and assert the wire format is ISO.
76 lines
2.9 KiB
Python
76 lines
2.9 KiB
Python
"""ISO 8601 timestamp helpers for the Gateway and embedded runtime.
|
|
|
|
DeerFlow stores and serializes thread/run timestamps as ISO 8601 UTC
|
|
strings to match the LangGraph Platform schema (see
|
|
``langgraph_sdk.schema.Thread``, where ``created_at`` / ``updated_at``
|
|
are ``datetime`` and JSON-encode to ISO 8601). All timestamp generation
|
|
should funnel through :func:`now_iso` so the wire format stays
|
|
consistent across endpoints, the embedded ``RunManager``, and the
|
|
checkpoint metadata written by the Gateway.
|
|
|
|
:func:`coerce_iso` provides a forward-compatible read path for legacy
|
|
records that historically stored ``str(time.time())`` floats.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from datetime import UTC, datetime
|
|
|
|
__all__ = ["coerce_iso", "now_iso"]
|
|
|
|
_UNIX_TIMESTAMP_PATTERN = re.compile(r"^\d{10}(?:\.\d+)?$")
|
|
"""Matches the unix-timestamp string shape historically written by
|
|
``str(time.time())`` (10-digit seconds with optional fractional part).
|
|
The 10-digit anchor avoids accidentally rewriting ISO years like
|
|
``"2026"`` and stays valid until the year 2286.
|
|
"""
|
|
|
|
|
|
def now_iso() -> str:
|
|
"""Return the current UTC time as an ISO 8601 string.
|
|
|
|
Example: ``"2026-04-27T03:19:46.511479+00:00"``.
|
|
"""
|
|
return datetime.now(UTC).isoformat()
|
|
|
|
|
|
def coerce_iso(value: object) -> str:
|
|
"""Best-effort coerce a stored timestamp to an ISO 8601 string.
|
|
|
|
Translates legacy unix-timestamp floats / strings written by older
|
|
DeerFlow versions into ISO without a one-shot migration. ISO strings
|
|
pass through unchanged; ``datetime`` instances are normalised to UTC
|
|
(tz-naive values are assumed to be UTC) and emitted via
|
|
``isoformat()`` so the wire format always uses the ``T`` separator;
|
|
empty values become ``""``; unrecognised values are stringified as a
|
|
last resort.
|
|
"""
|
|
if value is None or value == "":
|
|
return ""
|
|
if isinstance(value, bool):
|
|
# ``bool`` is a subclass of ``int`` — treat as garbage, not 0/1.
|
|
return str(value)
|
|
if isinstance(value, datetime):
|
|
# ``datetime`` must be handled before the ``int``/``float`` check;
|
|
# str(datetime) would produce ``"YYYY-MM-DD HH:MM:SS+00:00"``
|
|
# (space separator), which breaks strict ISO 8601 consumers.
|
|
if value.tzinfo is None:
|
|
value = value.replace(tzinfo=UTC)
|
|
else:
|
|
value = value.astimezone(UTC)
|
|
return value.isoformat()
|
|
if isinstance(value, (int, float)):
|
|
try:
|
|
return datetime.fromtimestamp(float(value), UTC).isoformat()
|
|
except (ValueError, OverflowError, OSError):
|
|
return str(value)
|
|
if isinstance(value, str):
|
|
if _UNIX_TIMESTAMP_PATTERN.match(value):
|
|
try:
|
|
return datetime.fromtimestamp(float(value), UTC).isoformat()
|
|
except (ValueError, OverflowError, OSError):
|
|
return value
|
|
return value
|
|
return str(value)
|