Skip to content

fix: bound embedder input and make message writes resilient to embedding failures - #175

Open
steveonjava wants to merge 3 commits into
neo4j-labs:mainfrom
steveonjava:feat/bug2-oversize-input-resilience
Open

fix: bound embedder input and make message writes resilient to embedding failures#175
steveonjava wants to merge 3 commits into
neo4j-labs:mainfrom
steveonjava:feat/bug2-oversize-input-resilience

Conversation

@steveonjava

@steveonjava steveonjava commented Aug 6, 2026

Copy link
Copy Markdown

Summary

Oversize input to the OpenAI embedder caused a silent data loss bug: if the input exceeded the token budget, the API returned a 400 error, the message write aborted, and the extracted entities were lost. This PR closes the gap with a three-layer fix.

Root cause

OpenAIEmbedder.embed() and embed_batch() passed raw text to the OpenAI API with no token budget check. When the input was too long, the API returned a 400 error, which bubbled up through add_message() and add_messages_batch(), aborting the whole write. Same for the batch path: one failing batch meant all messages in it were dropped.

Fix

Three layers, each addressing a different failure mode:

Layer 1: Input-bounded embedder. _truncate_to_tokens() truncates text to the model token budget before calling the API. Uses tiktoken when available for an accurate count, falls back to a 4-char-per-token estimate otherwise. Called in both embed() and embed_batch().

Layer 2: Resilient add_message(). Wraps the single-message embed in a try/except. On failure, stores the message with a null vector and continues to entity extraction. The entities carry their own embeddings, so the memory stays fully recallable.

Layer 3: Batch-level degradation in add_messages_batch(). Same pattern at batch level: if the whole batch embed fails, all messages in the batch get null vectors and insertion continues.

Verification

  • Unit tests: 1518/1518 pass (1514 implementer + 4 verifier-added tests for the real tiktoken code path that was never exercised before).
  • Integration tests: 444/444 pass (133 pre-existing unrelated skips).
  • Lint/format/typecheck: mypy strict, ty, ruff lint, and formatting all clean.
  • Build: uv build produces a wheel that installs and imports cleanly.
  • Spec checklist: model token budget with headroom confirmed, add_message degrade-to-null confirmed, add_messages_batch same-guard confirmed, truncate called in both embed paths confirmed.

Files changed

  • src/neo4j_agent_memory/embeddings/openai.py (added _truncate_to_tokens(), MODEL_MAX_INPUT_TOKENS, _max_tokens_for(), _DEFAULT_MAX_INPUT_TOKENS, _TOKEN_HEADROOM; wired truncation into embed() and embed_batch())
  • src/neo4j_agent_memory/memory/short_term.py (try/except wrappers around embed() and embed_batch() in add_message() and add_messages_batch(), with warning logs on failure)
  • tests/unit/embeddings/test_openai.py (new file: 93 lines covering the char-fallback path, the real tiktoken path, embedder truncation in embed() and embed_batch(), and empty-list short-circuit)
  • tests/unit/test_relation_storage.py (new tests for embedding resilience: add_message survives embed failure with entity extraction intact, add_messages_batch survives embed_batch failure, happy-path vector flow confirmed)

Fixes #104

I hit this on real oversize messages: the OpenAI API rejects any input
over the model's token ceiling with a raw 400, and that error was
propagating straight up to the caller. I add a token-budget truncation
step in embed() and embed_batch() so oversize text gets truncated
instead of rejected.

The truncation uses tiktoken when it is available for an accurate
count, and falls back to a 4-chars-per-token estimate when it is not,
since tiktoken is an optional dependency of this package.

Related to neo4j-labs#104.
add_message embedded content before creating the message node. When
the embed call raised, for example on oversize input against the
model's context limit, the exception aborted the whole write and the
message plus its extracted entities were silently dropped. I wrap the
embed call in try/except and fall back to a null vector on failure so
the node write and entity extraction always proceed.

I apply the same fallback to the batch path in add_messages_batch, so
a single bad message in a batch does not sink the rest of the batch.

Related to neo4j-labs#104.
@vercel

vercel Bot commented Aug 6, 2026

Copy link
Copy Markdown

@steveonjava is attempting to deploy a commit to the lyonwj's projects Team on Vercel.

A member of the Team first needs to authorize it.

@steveonjava
steveonjava marked this pull request as ready for review August 7, 2026 03:21
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.

Bug: Embedding model maximum context length

1 participant