Skip to content

fix: handle datetime/date in to_prompt_json#1527

Open
jalaliamirreza wants to merge 1 commit into
getzep:mainfrom
jalaliamirreza:fix/to-prompt-json-datetime
Open

fix: handle datetime/date in to_prompt_json#1527
jalaliamirreza wants to merge 1 commit into
getzep:mainfrom
jalaliamirreza:fix/to-prompt-json-datetime

Conversation

@jalaliamirreza

Copy link
Copy Markdown

Problem

Fixes #1526.

to_prompt_json (graphiti_core/prompts/prompt_helpers.py) calls json.dumps(data, ...) with no default= handler, so any datetime/date value in the serialized data raises:

TypeError: Object of type datetime is not JSON serializable

This surfaces during entity-summary regeneration —
extract_attributes_from_nodes → _extract_entity_summaries_batch → extract_summaries_batch → to_prompt_json(context['attributes']) — whenever an entity attribute holds a date (common with custom entity types that declare datetime/date fields, e.g. a contract signed_at or a person date_of_birth). The first ingest of a brand-new entity succeeds (no summary regen, so the dump never runs); the second ingest touching the same entity triggers the regen and crashes.

Repro

from datetime import datetime, timezone
from graphiti_core.prompts.prompt_helpers import to_prompt_json

to_prompt_json({"signed_at": datetime(2024, 3, 15, tzinfo=timezone.utc)})
# TypeError: Object of type datetime is not JSON serializable

Fix

Add a default= fallback to to_prompt_json:

  • date / datetime → ISO-8601 (.isoformat())
  • anything else json can't encode → str()

Output is unchanged for already-serializable data (the default= callable only fires for values the stock encoder rejects). Since this serializes prompt content for an LLM, ISO-8601 dates are also more readable than the raw error path.

Tests

Adds tests/test_prompt_helpers.py (pure unit, no Neo4j/LLM, runs under make test):

  • datetime → ISO-8601
  • date → ISO-8601
  • datetime nested in lists/dicts
  • plain data round-trips unchanged
  • ensure_ascii=False unicode + indent preserved
make lint   # ruff check + pyright: clean
make format # clean
pytest tests/test_prompt_helpers.py  # 5 passed

Notes

  • No new dependencies (stdlib datetime only).
  • Bug fix to existing functionality; < 20 LOC, no RFC needed per CONTRIBUTING.

to_prompt_json called json.dumps without a default= handler, so any
datetime/date value in the serialized data raised
'TypeError: Object of type datetime is not JSON serializable'. This
surfaces during entity-summary regeneration (extract_attributes_from_nodes
-> _extract_entity_summaries_batch -> to_prompt_json) when an entity
attribute holds a date, crashing the second ingest of such an entity.

Add a default= fallback: date/datetime serialize as ISO-8601, anything
else falls back to str(). Output is unchanged for already-serializable
data. Adds unit tests in tests/test_prompt_helpers.py.
jalaliamirreza added a commit to Mayia-App/mayia that referenced this pull request Jun 3, 2026
…as shim retirement gate (#169)

Filed the upstream root-cause fix (option C): getzep/graphiti#1527 adds a
default= handler to to_prompt_json (issue getzep/graphiti#1526). Wire the
concrete reference into the shim docstring + journey so the retirement
gate is unambiguous when the fix lands in a release.
jalaliamirreza added a commit to Mayia-App/mayia that referenced this pull request Jun 3, 2026
…line identity check (#169)

claude-review round 2:
- _json_default now renders date/datetime via .isoformat() (RFC-3339)
  instead of default=str (space separator). Matches the upstream fix
  (getzep/graphiti#1527) + the rest of apps/memory (search.py/episodes.py),
  so the shim emits the same datetime form graphiti-core will once the
  upstream lands. Decimal/UUID/etc. still fall back to str().
- WARN test uses record.getMessage() (not .message — only set by
  Formatter.format(); raw LogRecords may lack it).
- Inlined _is_original into the sweep to mirror _reset_for_testing's
  inverse identity check (symmetry).
- Added test_patched_falls_back_to_str_for_other_unserializable (Decimal +
  UUID) — covers the str() fallback branch, back to 100%.

Declined again: generator-fixture `-> None` annotation (round-1 #2) —
matches the file convention, no type-checker enforces it, Ruff green.
jalaliamirreza added a commit to Mayia-App/mayia that referenced this pull request Jun 3, 2026
…502 (#169) (#172)

* fix(memory): datetime-safe to_prompt_json shim (#169)

graphiti-core 0.29.0's to_prompt_json calls json.dumps with no default=
handler, so entity-summary regen on the second ingest of any entity
carrying a LAW datetime attribute (Person.date_of_birth,
Matter.engagement_letter_date, Signed.signed_at, ...) raises
TypeError: Object of type datetime is not JSON serializable -> 502.

Option B (short-term shim): install_datetime_safe_to_prompt_json()
rebinds every loaded graphiti_core to_prompt_json reference (a
sys.modules sweep, since `from ... import` copies the reference into
~7 modules) onto a default=str wrapper. Identity-idempotent and
re-runnable. Wired into _build_graphiti() before any add_episode.

Also docs: cloudbuild-deploy.md now describes tag-triggered (server-v* /
memory-v*) deploys instead of push-to-main.

TDD: graphiti_patches.journey.md + test_graphiti_patches.py (8 tests:
bug characterization, full import-site sweep, summarize_nodes crash
site, unicode/indent preservation, idempotency) + J5 wiring test.
100% coverage on the new module; memory suite 225 passed, 92.9% total.

* fix(memory): WARN on rebound-0 sweep + explicit shim upgrade gate (#169)

Pre-PR architect review (2 MEDIUM, both applied):

- _build_graphiti now WARNs when the to_prompt_json sweep rebinds 0
  references. Production builds Graphiti once, so a 0 means graphiti-core
  moved the bind site on an in-range bump — the #169 502 would otherwise
  return silently. Converts a silent prod regression into an alertable log.
- pyproject.toml graphiti-core pin documents that bumping requires
  re-running test_install_rebinds_summarize_nodes_crash_site as the
  go/no-go that the shim still covers the crash path.

Security review: APPROVE, no findings.

* test(memory): bot round 1 — happy-path wiring mock + explicit rebound-0 WARN test (#169)

claude-review round 1:
- test_build_graphiti_installs_datetime_patch mocked install to return 0,
  incidentally firing the rebound-0 WARN branch. Use return_value=7 (happy
  path) and add test_build_graphiti_warns_when_sweep_rebinds_nothing to
  cover the WARN branch deterministically via caplog (no longer order-
  dependent on real-build tests).
- journey.md: corrected stale "guard flag" wording — idempotency is
  identity comparison against the stock function, not a flag.

Declined: generator-fixture `-> None` annotation — matches the file's
existing fixture convention (_reset_graphiti_singleton, _set_default_env)
and Ruff is clean; changing only the new fixture breaks consistency.

* docs(memory): cite upstream graphiti-core fix (getzep/graphiti#1527) as shim retirement gate (#169)

Filed the upstream root-cause fix (option C): getzep/graphiti#1527 adds a
default= handler to to_prompt_json (issue getzep/graphiti#1526). Wire the
concrete reference into the shim docstring + journey so the retirement
gate is unambiguous when the fix lands in a release.

* fix(memory): bot round 2 — isoformat default + caplog getMessage + inline identity check (#169)

claude-review round 2:
- _json_default now renders date/datetime via .isoformat() (RFC-3339)
  instead of default=str (space separator). Matches the upstream fix
  (getzep/graphiti#1527) + the rest of apps/memory (search.py/episodes.py),
  so the shim emits the same datetime form graphiti-core will once the
  upstream lands. Decimal/UUID/etc. still fall back to str().
- WARN test uses record.getMessage() (not .message — only set by
  Formatter.format(); raw LogRecords may lack it).
- Inlined _is_original into the sweep to mirror _reset_for_testing's
  inverse identity check (symmetry).
- Added test_patched_falls_back_to_str_for_other_unserializable (Decimal +
  UUID) — covers the str() fallback branch, back to 100%.

Declined again: generator-fixture `-> None` annotation (round-1 #2) —
matches the file convention, no type-checker enforces it, Ruff green.

* test(memory): bot round 3 — drop unused monkeypatch fixture param (#169)

claude-review round 3: test_build_graphiti_installs_datetime_patch
declared monkeypatch but patches exclusively via patch.object context
managers — dead dependency. Removed.

* fix(memory): bot round 4 — no false WARN on idempotent patch re-run + assert install ordering (#169)

claude-review round 4:
- MEDIUM: install runs before env validation, so a failed cold-start
  (missing env, or build_indices_and_constraints raising) left the patch
  installed but the singleton uncached; the retry's sweep rebound 0 and
  fired the "bind site moved / 502 risk" WARN falsely, burying the real
  error. Added is_datetime_safe_to_prompt_json_installed() predicate and a
  3-way branch: rebound>0 -> INFO; rebound==0 & active -> DEBUG (benign
  idempotent re-run); rebound==0 & not active -> WARN (genuine regression).
- LOW: J5 wiring test now asserts install ran BEFORE Graphiti() via a
  Graphiti side-effect, not just call_count.
- Added unit test for the predicate + an idempotent-reinstall-is-quiet test.
  229 passed, 100% on graphiti_patches.py, 92.98% total.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

to_prompt_json crashes on datetime attribute values (Object of type datetime is not JSON serializable)

1 participant