Skip to content

fix(deriver): coerce bare-string explicit observations from json_object providers#900

Open
faithpricejp-source wants to merge 1 commit into
plastic-labs:mainfrom
faithpricejp-source:fix/893-explicit-string-coercion
Open

fix(deriver): coerce bare-string explicit observations from json_object providers#900
faithpricejp-source wants to merge 1 commit into
plastic-labs:mainfrom
faithpricejp-source:fix/893-explicit-string-coercion

Conversation

@faithpricejp-source

@faithpricejp-source faithpricejp-source commented Jul 12, 2026

Copy link
Copy Markdown

Linked Issue

Closes #893

Description

minimal_deriver_prompt()'s examples and the PromptRepresentation.explicit field description both present explicit facts as bare strings, but the field type is list[ExplicitObservationBase] (objects with a content key). Providers without json_schema structured-output support (DeepSeek via LiteLLM, some vLLM configs) run in json_object mode and infer the JSON shape from that prompt/schema text, so they emit:

{"explicit": ["alice is 25 years old", "alice has a dog"]}

That fails PromptRepresentation validation and, via the structured-output fallback in src/llm/structured_output.py, is silently turned into PromptRepresentation(explicit=[])zero observations, no error surfaced.

Fix

  • Extend the existing explicit mode="before" validator to coerce bare strings into {"content": ...}. Dicts/objects pass through unchanged; non-string scalars (e.g. a stray int) still raise, so genuine shape errors aren't masked. This mirrors the precedent already in structured_output.py, which repairs deductive shape drift.
  • Fix the misleading explicit field-description example so the schema injected in json_object mode no longer advertises the wrong (bare-string) shape.

Load-bearing decision: coercion lives on the model boundary (not only in the repair path) so it applies whether the payload arrives via json_object repair or direct validation.

Type of Change

  • Bug fix (non-breaking change that fixes an issue)

Breaking Changes

N/A — objects and None continue to validate exactly as before; only the previously-failing bare-string shape now succeeds.

Test Coverage

  • I added/updated unit tests

Added tests/utils/test_representation.py (pure model-validation, no DB — registered in the conftest runtime-mock blocklist alongside the other tests/utils unit tests). Covers: bare strings coerced, objects still accepted, mixed input normalized, None[], and non-string scalar still rejected.

Verified locally: with the fix all 5 pass; reverting only the validator makes the two coercion tests fail with the exact ValidationError that becomes a silent explicit=[] in production. ruff check and ruff format --check clean.

Checklist

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have added tests that prove my fix works
  • New and existing tests pass locally
  • I have updated documentation if needed

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of explicit observations when provided as plain text values.
    • Continued support for object-form observations and mixed input formats.
    • Empty or missing explicit observations now normalize consistently.
    • Invalid non-text values remain rejected with clear validation errors.
  • Documentation

    • Updated field examples to reflect the supported object format.

…ct providers

`minimal_deriver_prompt()` examples and the `PromptRepresentation.explicit`
field description both show explicit facts as bare strings, but the field type
is `list[ExplicitObservationBase]` (objects with a `content` key). Providers
without `json_schema` support (DeepSeek via LiteLLM, some vLLM configs) run in
`json_object` mode and infer shape from that text, so they emit
`{"explicit": ["fact", ...]}`. That fails `PromptRepresentation` validation and,
via the structured-output fallback, is silently turned into `explicit=[]` —
zero observations with no error (plastic-labs#893).

Extend the existing `explicit` before-validator to coerce bare strings into
`{"content": ...}` (dicts/objects pass through unchanged; non-string scalars
still raise). Fix the misleading field-description example so the injected
json_object schema no longer advertises the wrong shape. Add pure
model-validation tests (no DB) covering bare strings, objects, mixed input,
None, and the non-string reject path.

Closes plastic-labs#893

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3847fdb6-75ab-4a98-9732-e2f2ff93e384

📥 Commits

Reviewing files that changed from the base of the PR and between 73453f8 and 2a69642.

📒 Files selected for processing (3)
  • src/utils/representation.py
  • tests/conftest.py
  • tests/utils/test_representation.py

Walkthrough

PromptRepresentation.explicit now accepts bare strings by converting them to content objects, preserves object inputs, maps None to an empty list, and rejects invalid scalar elements. Dedicated unit tests cover these cases without runtime mocks.

Changes

Explicit observation normalization

Layer / File(s) Summary
Normalize explicit observations
src/utils/representation.py
The explicit field example now uses content objects, and its pre-validator normalizes string items and None values.
Validate normalization behavior
tests/utils/test_representation.py, tests/conftest.py
Tests cover string, object, mixed, null, and invalid inputs; the test module is excluded from runtime-mock setup.

Estimated code review effort: 2 (Simple) | ~10 minutes

Poem

I’m a rabbit with strings in my tray,
Now content-shaped carrots lead the way.
Mixed or empty, the model knows,
Bad little numbers meet validation woes.
Hop, hop—tests keep the pathway bright!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly states the main change: coercing bare-string explicit observations from json_object providers.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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.

Deriver prompt instructs LLM to return wrong JSON shape for explicit, causing zero observations with OpenAI-compatible providers (DeepSeek, vLLM)

1 participant