Merge pull request #596 from lawrenceakka/tz-fix

Fix for missing timezone info on ingested docs. mcp schema requires TZ
This commit is contained in:
Yiorgis Gozadinos 2026-09-03 09:59:14 -05:00 committed by GitHub
commit 376dcb2e6f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 57 additions and 12 deletions

View file

@ -2,6 +2,13 @@
## [Unreleased]
### Fixed
- Document timestamps include timezone information and satisfy the MCP
date-time output schema. Stored timestamps without an offset are read as
host-local time; a database written under a different host timezone shifts
by that offset.
## [0.82.0] - 2026-09-03
### Removed

View file

@ -2,7 +2,7 @@ import asyncio
import json
import logging
from collections.abc import AsyncGenerator
from datetime import datetime
from datetime import UTC, datetime
from typing import TYPE_CHECKING
from docling_core.types.doc.document import DescriptionMetaField, PictureMeta
@ -531,7 +531,7 @@ async def _flush_rebuild_batch(
if not documents:
return
now = datetime.now().isoformat()
now = datetime.now(UTC).isoformat()
# Batch update documents and document_meta using merge_insert (one LanceDB
# version per table). Content+blobs go to documents; mutable attributes go

View file

@ -1,8 +1,8 @@
import json
from datetime import datetime
from datetime import UTC, datetime
from typing import TYPE_CHECKING
from pydantic import BaseModel, Field
from pydantic import BaseModel, Field, field_validator
from haiku.rag.store.compression import compress_docling_split, decompress_json
@ -29,8 +29,13 @@ class Document(BaseModel):
docling_document: bytes | None = Field(default=None, exclude=True)
docling_pages: bytes | None = Field(default=None, exclude=True)
docling_version: str | None = Field(default=None, exclude=True)
created_at: datetime = Field(default_factory=datetime.now)
updated_at: datetime = Field(default_factory=datetime.now)
created_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
updated_at: datetime = Field(default_factory=lambda: datetime.now(UTC))
@field_validator("created_at", "updated_at")
@classmethod
def _to_utc(cls, value: datetime) -> datetime:
return value if value.tzinfo else value.astimezone(UTC)
def set_docling(self, docling_doc: "DoclingDocument") -> None:
"""Serialize and store a DoclingDocument, splitting structure and pages.

View file

@ -1,5 +1,5 @@
import json
from datetime import datetime
from datetime import UTC, datetime
from typing import overload
from uuid import uuid4
@ -77,8 +77,12 @@ class DocumentRepository:
docling_document=doc.docling_document,
docling_pages=doc.docling_pages,
docling_version=doc.docling_version,
created_at=datetime.fromisoformat(created) if created else datetime.now(),
updated_at=datetime.fromisoformat(updated) if updated else datetime.now(),
created_at=datetime.fromisoformat(created)
if created
else datetime.now(UTC),
updated_at=datetime.fromisoformat(updated)
if updated
else datetime.now(UTC),
)
def _to_documents_record(self, entity: Document, doc_id: str) -> DocumentRecord:
@ -138,7 +142,7 @@ class DocumentRepository:
# document_meta) would surface.
if isinstance(entity, Document):
doc_id = str(uuid4())
now = datetime.now().isoformat()
now = datetime.now(UTC).isoformat()
await self.store.document_meta_table.add(
[self._to_meta_record(entity, doc_id, now, now)]
)
@ -159,7 +163,7 @@ class DocumentRepository:
if not documents:
return []
now = datetime.now().isoformat()
now = datetime.now(UTC).isoformat()
created_at = datetime.fromisoformat(now)
doc_records = []
meta_records = []
@ -272,7 +276,7 @@ class DocumentRepository:
self.store._assert_writable()
assert entity.id, "Document ID is required for update"
now = datetime.now().isoformat()
now = datetime.now(UTC).isoformat()
entity.updated_at = datetime.fromisoformat(now)
created = entity.created_at.isoformat() if entity.created_at else now
record = self._to_meta_record(entity, entity.id, created, now)

View file

@ -1,3 +1,7 @@
import os
import time
from datetime import UTC, datetime
import pytest
from haiku.rag.store.engine import Store
@ -549,3 +553,28 @@ async def test_document_get_by_uri_with_special_characters(
assert retrieved is not None
assert retrieved.id == created_doc.id
assert retrieved.uri == "Hamish and Andy's Gap Year"
@pytest.mark.skipif(not hasattr(time, "tzset"), reason="time.tzset is POSIX-only")
def test_naive_timestamp_is_converted_from_local_time():
original_tz = os.environ.get("TZ")
os.environ["TZ"] = "Europe/Athens"
time.tzset()
try:
created = datetime(2026, 1, 1, 14, 0, 0) # January is EET, UTC+2
updated = datetime(2026, 1, 1, 14, 30, 0)
doc = Document(content="x", created_at=created, updated_at=updated)
assert doc.created_at == datetime(2026, 1, 1, 12, 0, 0, tzinfo=UTC)
assert doc.updated_at == datetime(2026, 1, 1, 12, 30, 0, tzinfo=UTC)
finally:
if original_tz is None:
os.environ.pop("TZ", None)
else:
os.environ["TZ"] = original_tz
time.tzset()
def test_created_at_serializes_with_timezone_offset():
doc = Document(content="x")
value = doc.model_dump(mode="json")["created_at"]
assert datetime.fromisoformat(value).utcoffset() is not None