Skip to content

Python: Fix array input in Chat Completions format losing user message content#5133

Open
giles17 wants to merge 7 commits intomicrosoft:mainfrom
giles17:agent/fix-5112-1
Open

Python: Fix array input in Chat Completions format losing user message content#5133
giles17 wants to merge 7 commits intomicrosoft:mainfrom
giles17:agent/fix-5112-1

Conversation

@giles17
Copy link
Copy Markdown
Contributor

@giles17 giles17 commented Apr 6, 2026

Motivation and Context

When users send input to the /responses endpoint as an array of message objects using Chat Completions format ({"role": "user", "content": "..."} without a "type" field), the executor fails to recognize it as structured input. This causes the agent to ignore the user's actual query and return unfiltered tool output instead of properly analyzing and filtering results per the user's request.

Fixes #5112

Description

The root cause is that _is_openai_multimodal_format and _convert_input_to_chat_message only recognized message objects containing an explicit "type": "message" field (Responses API format), but Chat Completions format messages use {"role": "..", "content": "..."} without a type field. The fix extends both methods to also detect and handle the Chat Completions format by checking for the presence of a "role" key when "type" is absent. This ensures that array-based input is correctly parsed into a Message object with the user's text preserved, so the agent processes the query identically regardless of whether input is sent as a plain string or a message array. Regression tests verify detection and conversion of Chat Completions format messages with both string and list content.

Contribution Checklist

  • The code builds clean without any errors or warnings
  • The PR follows the Contribution Guidelines
  • All unit tests pass, and I have added new tests where possible
  • Is this a breaking change? If yes, add "[BREAKING]" prefix to the title of the PR.

Note: PR autogenerated by giles17's agent

Copilot and others added 3 commits April 6, 2026 23:08
…icrosoft#5112)

When input was sent as an array without an explicit "type": "message"
field (Chat Completions format), _is_openai_multimodal_format returned
False and _convert_openai_input_to_chat_message silently skipped the
item. This caused the user's query to be replaced with an empty string,
making the agent return unfiltered tool results.

Broaden both _is_openai_multimodal_format and the item-level check in
_convert_openai_input_to_chat_message to also accept items that have a
"role" field but no explicit "type" field.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Fix ruff SIM103 lint error by replacing if/return True/return False
pattern with direct return of bool expression in
_is_openai_multimodal_format. Also accept formatter-applied style
changes in tests.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@giles17 giles17 self-assigned this Apr 6, 2026
Copilot AI review requested due to automatic review settings April 6, 2026 23:13
@moonbox3 moonbox3 added the python label Apr 6, 2026
@github-actions github-actions bot changed the title Fix array input in Chat Completions format losing user message content Python: Fix array input in Chat Completions format losing user message content Apr 6, 2026
Copy link
Copy Markdown
Contributor Author

@giles17 giles17 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 4 | Confidence: 94%

✓ Correctness

This PR only deletes a REPRODUCTION_REPORT.md file that was accidentally committed to the repository root during investigation of issue #5112. The file contained internal debugging notes, raw analysis text, and repository-local paths — none of which belong in the committed codebase. The deletion is correct and has no impact on code behavior. However, this PR does not include the actual bug fix for issue #5112 (the missing Chat Completions format handling in _executor.py). That fix should be addressed in a separate or follow-up PR.

✓ Security Reliability

This PR only deletes a REPRODUCTION_REPORT.md file that was previously committed to the repository root. The file contained investigation notes, internal file paths, and analysis details for issue #5112. Removing it is a cleanup action with no security or reliability concerns — the file contained no secrets, and its removal does not affect any code paths, tests, or runtime behavior. No code changes are present in this diff.

✓ Test Coverage

This PR only deletes REPRODUCTION_REPORT.md, a temporary investigation artifact that should not be committed to the repository. The actual bug fix (handling Chat Completions format in _executor.py) and its corresponding tests were added in prior commits on this branch. The existing tests on the branch adequately cover the fix: they test detection of Chat Completions format in _is_openai_multimodal_format, conversion with string content, and conversion with list content. The deletion of the report has no impact on test coverage.

✓ Design Approach

This PR deletes REPRODUCTION_REPORT.md, a temporary investigation artifact that should not have been committed to the repository. The file contained notes from a bug reproduction session for issue #5112 but no actual code fix. Deleting it is the correct cleanup action. There are no design concerns with this change — removing stale investigation artifacts from the repo is appropriate housekeeping.

Suggestions

  • Ensure the actual bug fix for issue #5112 (broadening the item_type check in _convert_openai_input_to_chat_message at _executor.py line 634) is included in this or a follow-up PR, since this diff only removes the investigation report.

Automated review by giles17's agents

@moonbox3
Copy link
Copy Markdown
Contributor

moonbox3 commented Apr 6, 2026

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
TOTAL27057318888% 
report-only-changed-files is enabled. No files were changed during this commit :)

Python Unit Test Overview

Tests Skipped Failures Errors Time
5373 20 💤 0 ❌ 0 🔥 1m 22s ⏱️

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates the DevUI /v1/responses executor’s input parsing so that message arrays sent in Chat Completions-style shape ({"role": "...", "content": ...} without a top-level "type": "message") are recognized and converted into an Agent Framework Message, preventing loss of user prompt text.

Changes:

  • Extend _is_openai_multimodal_format to also detect Chat Completions-style message dicts (no "type", but has "role").
  • Extend _convert_openai_input_to_chat_message to treat "role"-bearing dicts as message items, including when content is a plain string.
  • Add regression tests covering detection and conversion for Chat Completions-style inputs.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 4 comments.

File Description
python/packages/devui/agent_framework_devui/_executor.py Broadens OpenAI input detection and conversion logic to accept Chat Completions-style message objects.
python/packages/devui/tests/devui/test_multimodal_workflow.py Adds tests to validate the new detection/conversion behavior for Chat Completions-style inputs.

…ns parsing

- Tighten _is_openai_multimodal_format to validate role is a known
  string value and content is str|list, reducing false positives
- Skip non-user messages (system/assistant/tool/developer) during
  conversion to prevent them being treated as user text
- Rename test to clarify it covers Chat Completions envelope with
  Responses API content parts
- Add end-to-end regression test through _parse_workflow_input with
  JSON-stringified Chat Completions array
- Add tests for non-user message skipping and malformed input rejection

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

@giles17 giles17 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 4 | Confidence: 86%

✓ Correctness

The fix correctly addresses the reported bug where Chat Completions format messages (without an explicit "type": "message" field) had their user content silently dropped. The two changes—(1) skipping non-user messages in _convert_openai_input_to_chat_message and (2) tightening _is_openai_multimodal_format to validate role, content key, and content type—are logically sound and well-tested. The isinstance(content, str | list) union syntax is safe given the project's requires-python >= 3.10 constraint. No correctness bugs, race conditions, or regressions are introduced.

✓ Security Reliability

The fix correctly addresses the root cause: Chat Completions-format input items without an explicit "type": "message" field were silently skipped, causing user content to be lost. The PR (1) broadens _is_openai_multimodal_format to accept Chat Completions-style dicts with proper validation (role allowlist, content type check), and (2) skips non-user messages in the converter to prevent system/assistant content from being injected as user content. Both changes are sound from a security perspective. One minor reliability gap: when every item in the array is non-user (e.g., only system messages), all items are skipped and the fallback at line 784 silently produces an empty text message sent as role="user", which could cause confusing model behavior with no diagnostic signal beyond a debug log. Tests cover the key scenarios well.

✗ Test Coverage

New tests cover the main fix paths well: Chat Completions format detection, non-user message skipping, JSON-stringified regression path, and malformed input rejection. However, there is a notable gap—there is no test for the edge case where ALL messages in the input array are non-user roles (e.g., only system messages). The new skip logic on lines 635-638 would skip every item, resulting in an empty contents list that triggers the fallback at line 783 producing a Message with empty text. This is an important behavioral consequence of the change that should be explicitly covered and asserted. Additionally, the Responses API format path (type: message with non-user role) is not tested for the skip behavior, only the Chat Completions path is.

✓ Design Approach

The fix correctly addresses the root cause: Chat Completions format items lacking an explicit "type": "message" field were silently skipped, causing the user's query to be lost. Both changes — stricter detection in _is_openai_multimodal_format and role-filtering in _convert_input_to_chat_message — are sound. One design concern worth flagging: the non-user role skipping (role != "user") is applied unconditionally to both Responses API (type == "message") items and Chat Completions items. This is a behavior change for Responses API arrays containing assistant-turn messages, but it is actually correct since the output is always Message(role="user", ...). A subtler issue is that _is_openai_multimodal_format now accepts arrays whose first element has role system, assistant, or tool (all are in valid_roles), which means a system-only or assistant-only message list is routed into _convert_input_to_chat_message, all items get skipped, and the fallback Content.from_text(text="") silently produces an empty user message rather than signalling an error or falling through to another handler. This is a pre-existing silent-failure pattern but the new routing makes it reachable for more inputs.

Flagged Issues

  • No test covers the edge case where all input messages are non-user roles (e.g., [{"role": "system", "content": ".."}]). The new skip logic causes all items to be skipped, falling through to line 783-784 which creates a Message with empty text content. This is a significant behavioral change that needs an explicit test to document and verify the expected outcome.

Suggestions

  • Consider whether _is_openai_multimodal_format should look beyond the first item for a user-role message. Currently, an input like [{"role": "tool", "content": "result"}] is classified as multimodal format and processed into an empty-text fallback message, which may be surprising.
  • When all input items are skipped (non-user), the fallback at line 783-784 silently creates an empty user message. Consider logging a warning here (e.g., "All input items were non-user; no user content extracted") so operators can diagnose unexpected empty responses in production.
  • The _is_openai_multimodal_format check only validates the first item in the list. A malformed second item (e.g., with content set to an integer) would pass format detection but be silently ignored during conversion. This is pre-existing behavior and low-risk, but worth noting.
  • Add a test for the Responses API format ({"type": "message", "role": "system", ...}) to verify non-user messages are also skipped when using that envelope, not just the Chat Completions format.
  • Consider adding positive cases to test_is_openai_multimodal_format_rejects_malformed_input to verify all valid roles (system, assistant, tool, developer) are accepted, since the new valid_roles set is part of the contract.
  • The valid_roles set used in _is_openai_multimodal_format includes system, assistant, and tool. Combined with the new non-user skipping logic, a list whose first (or only) item is a non-user role will be detected as multimodal format and routed into _convert_input_to_chat_message, but then every item will be skipped, silently producing an empty Message. Consider either (a) restricting _is_openai_multimodal_format to return True only when at least the first element has role == "user", or (b) emitting a warning (not just a debug log) when _convert_input_to_chat_message produces a zero-content result after skipping non-user items, so the silent data loss is visible in production logs.

Automated review by giles17's agents

Copilot and others added 3 commits April 6, 2026 23:26
…case tests

- Add logger.warning when all input items are non-user and the empty
  content fallback is triggered, making silent data loss visible in
  production logs.
- Add test for all-non-user messages (Chat Completions format) verifying
  the empty-text fallback behavior.
- Add test for non-user messages in Responses API format (type: message
  envelope) verifying they are skipped correctly.
- Add test verifying all valid roles (user, system, assistant, tool,
  developer) are accepted by _is_openai_multimodal_format.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Address review feedback: _is_openai_multimodal_format now requires at
least one item with role='user' before classifying input as multimodal
format. This prevents non-user-only arrays (e.g., system-only) from
being routed into the conversion path where all items would be skipped,
silently producing an empty message.

Changes:
- Restructure _is_openai_multimodal_format to check both Responses API
  and Chat Completions paths, then verify user-role presence
- Update test_is_openai_multimodal_format_accepts_all_valid_roles to
  test non-user roles alongside a user companion item
- Add test_is_openai_multimodal_format_rejects_non_user_only for both
  Chat Completions and Responses API format arrays

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
…ves differently with input when sent as string vs array - Difference is consistent
Copy link
Copy Markdown
Contributor Author

@giles17 giles17 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 4 | Confidence: 92%

✓ Correctness

This diff contains only cosmetic changes: (1) an unnecessary cast() added to _is_openai_multimodal_format where isinstance(item, dict) already narows the type at runtime, and (2) reformatting of test assertions from single-line to parenthesized multi-line style. The actual bug fix (line 634, broadening the item_type check in _convert_openai_input_to_chat_message) was applied in a prior commit and is not part of this diff. There are no correctness issues.

✓ Security Reliability

This PR contains only cosmetic changes: (1) adding a cast() call in _is_openai_multimodal_format that is a no-op at runtime (Python's cast does nothing), and (2) reformatting test assertions for line length. There are no functional, security, or reliability changes. The underlying bug fix for parsing Chat Completions format input (the item_type is None and "role" in item_dict condition at line 634) appears to have already been applied in a prior commit and is not part of this diff. No new security or reliability issues are introduced.

✓ Test Coverage

This PR contains only cosmetic changes: (1) an unnecessary cast() wrapping in _is_openai_multimodal_format on _executor.py line 862 — the isinstance(item, dict) guard already narows the type, making the cast redundant; (2) reformatting of multi-line assert statements in the test file for style. No new tests are added and no behavioral changes are made. The core bug fix (line 634, handling Chat Completions format without type) was already landed in a prior PR and is present in this branch. Since there are no behavior changes, there are no test coverage gaps introduced by this diff.

✓ Design Approach

The diff makes two narrow changes: (1) adds a cast(dict[str, Any], item) call in the _is_openai_multimodal_format generator expression for type-checker satisfaction, and (2) reformats multi-line test assertions for readability. Neither change alters runtime behaviour. The cast is consistent with the existing pattern used at line 632 of the same file. The test changes are purely cosmetic. Notably, the actual behavioural bug fix described in the issue — broadening the item_type check at line 634 to accept Chat Completions format items that lack an explicit "type" field — is already present in the local file but is not part of this diff, meaning it was committed separately. There are no design, abstraction, or correctness issues introduced by what is in this diff.

Suggestions

  • The cast(dict[str, Any], item) on line 862 of _executor.py is redundant — isinstance(item, dict) already short-circuits the and expression, so .get("role") is only called on dicts. If added purely for type-checker satisfaction (e.g., pyright strict mode), consider adding a brief comment explaining why; otherwise, it's unnecessary noise and item.get("role") can be used directly as before.

Automated review by giles17's agents

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: Agent behaves differently with input when sent as string vs array - Difference is consistent

3 participants