Skip to content

Fix parse_response/2 marker leak + drive handle_ai_response/2 via real test - #562

Closed
mdon wants to merge 2 commits into
BeamLabEU:devfrom
mdon:followup-translate-parser-boundary
Closed

Fix parse_response/2 marker leak + drive handle_ai_response/2 via real test#562
mdon wants to merge 2 commits into
BeamLabEU:devfrom
mdon:followup-translate-parser-boundary

Conversation

@mdon

@mdonmdon commented May 22, 2026

Copy link
Copy Markdown
Contributor

Summary

Follow-up to PR #560 — two commits that landed on mdon/fix-translation-response-shape after the merge.

1. Parser marker-leak bug (real bug, present on dev today)

parse_response/2's field-capture boundary regex only terminated at REQUESTED markers. A model that emits a marker the caller didn't ask for silently rolls that block's content into the previous requested field.

Real-world trigger: projects-side prompt template referenced {{title}} literally (no title variable bound for a template resource that only has name + description), so the AI emitted ---TITLE---{{title}} between ---NAME--- and ---DESCRIPTION---. The parser, asked for only name and description, returned name as "Mitarbeiter-Onboarding---TITLE---{{title}}" — corrupted.

Fix: widen the boundary regex to terminate at any ---[A-Z0-9_]+--- marker (the character class matches what marker/1 already produces). Unrequested markers' content is dropped silently — they're not in the requested fields list, so the caller never gets them either way.

Verified end-to-end against real deepseek-v3.2: DE-DE and FR-FR translations of the Employee onboarding template were clean after this fix, dirty before.

2. handle_ai_response/2 testability

Codex final-review of #560 caught that the original "OpenAI-shaped response map" test manually extracted message.content and only drove parse_response/2 directly — it would still have passed against the pre-fix broken implementation. Reworked:

  • Promoted handle_ai_response/2 from defp to @doc false def so unit tests can drive it directly.
  • Replaced the cross-module PhoenixKitAI.Completion.extract_content/1 call with an inline pattern-match on the OpenAI shape. Drops the cross-module dep — works in core's test env (plugin absent) and any host (plugin present, any version).

Test plan

  • New regression test pins the unrequested-marker boundary behaviour
  • New handle_ai_response/2 tests cover the OpenAI map / raw binary / malformed shape branches
  • All 21 translation tests pass
  • mix credo --strict clean

mdon added 2 commits May 22, 2026 03:54
The boundary regex for each requested field only included OTHER
requested markers as terminators. When a model emitted a marker
the caller didn't ask for, that block's content silently
appended to the preceding requested field's capture.
Real-world trigger: the projects prompt template referenced
`{{title}}` literally (no `title` variable was bound for a
template resource that only has name + description), so the AI
dutifully emitted `---TITLE---{{title}}` between `---NAME---`
and `---DESCRIPTION---`. The parser, asked for only `name` and
`description`, set name to "Mitarbeiter-Onboarding---TITLE---{{title}}"
because nothing in its boundary regex matched `---TITLE---`.
Fix: bound captures at any `---[A-Z0-9_]+---` marker, not just
the explicitly-requested ones. The character class matches what
`marker/1` already produces. The `i` flag still tolerates
lowercased markers in the response. Unrequested markers'
content is dropped silently — they're not in the requested
fields list, so the caller never gets them either way.
Regression test pins the exact real-world response we saw.
Codex final-review findings on PR BeamLabEU#560:
CONCERN: the original "handles OpenAI-shaped response map" test
manually extracted message.content and then called parse_response/2.
The test would still have passed against the pre-fix broken
implementation because it never drove the new handle_ai_response/2
branch. Fixed by:
- Exposing handle_ai_response/2 as @doc false def so the unit
test can drive it directly (instead of going through the
private path).
- Rewrote the test to call Translation.handle_ai_response(map, fields)
with a realistic OpenAI response shape. Now actually fails if
the map-handling branch regresses.
- Added two more handle_ai_response/2 tests covering the raw-binary
legacy/stub path and the malformed-shape error path.
CONCERN: the earlier implementation called
PhoenixKitAI.Completion.extract_content/1, which raises
UndefinedFunctionError in core's test env where the plugin isn't
loaded. Replaced the cross-module call with an inline pattern match
on the OpenAI shape (choices[0].message.content). The shape is
stable (OpenAI-standard), so the inline match is safe; and removing
the dep means the helper works in core's test env and in any host
regardless of which PhoenixKitAI version they pin.
NIT: the test comment now matches the test behavior.
@mdon

mdon commented May 22, 2026

Copy link
Copy Markdown
ContributorAuthor

Superseded by #565 — consolidated translation follow-ups into one PR per module per @mdon convention. The same commits are cherry-picked onto the new branch verbatim.

@mdonmdon closed this May 22, 2026
Sign up for freeto 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.

1 participant

@mdon