Skip to content

fix: validate Bedrock conversation messages - #857

Merged
fdelbrayelle merged 1 commit into
kestra-io:mainfrom
Pavani3001:fix-bedrock-messages-validation
Oct 23, 2026
Merged

fdelbrayelle merged 1 commit into
kestra-io:mainfrom
Pavani3001:fix-bedrock-messages-validation

Conversation

@Pavani3001

@Pavani3001 Pavani3001 commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

What changes are being made and why?

Fixes #819

This PR fixes the HV000030 validation error in the Bedrock Converse and ConverseStream tasks.

The messages field is a Kestra Property<List<Map<String, String>>>. Using @NotEmpty directly on the Property causes Hibernate Validator to fail because @NotEmpty is not supported for this type.

Changes:

  • Replace @NotEmpty with @NotNull on messages.
  • Add an explicit check after rendering to reject an empty messages list.
  • Add tests for Converse and ConverseStream covering empty and valid messages.
  • Verify that valid messages no longer produce the HV000030 error.

How the changes have been QAed?

The focused tests were executed successfully:

ConverseTest
ConverseStreamTest

7 tests passed
0 failed
0 skipped

BUILD SUCCESSFUL

The tests cover:

  • Valid conversation messages.
  • Empty message lists being rejected.
  • The HV000030 validation error no longer occurring.
  • Existing Converse and ConverseStream behavior remaining functional.

Example flow:

id: bedrock-converse-test
namespace: io.kestra.tests

tasks:
  - id: converse
    type: io.kestra.plugin.aws.bedrock.Converse
    region: us-east-1
    modelId: anthropic.claude-3-5-sonnet-20241022-v2:0
    messages:
      - role: user
        content: "Hello!"

Setup Instructions

No additional setup requirements beyond the existing AWS Bedrock configuration for the plugin.


Contributor Checklist ✅

  • I have read and followed the plugin contribution guidelines

@kestrabot kestrabot Bot added area/plugin Plugin-related issue or feature request kind/external Pull requests raised by community contributors labels Oct 23, 2026
@github-project-automation github-project-automation Bot moved this to To review in Pull Requests Oct 23, 2026
@kestrabot
kestrabot Bot requested a review from a team October 23, 2026 13:43
@kestrabot
kestrabot Bot requested a review from fdelbrayelle October 23, 2026 13:43

@fdelbrayelle fdelbrayelle left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Kestra Plugin Code Review

Business Requirements: met

Fixes #819: @NotEmpty is removed from messages in both Converse and ConverseStream (the only two Property fields in the repo carrying it), replaced by @NotNull plus an empty check after rendering, as the issue suggests. The new validation tests fail with HV000030 against the main code and pass on this head, so they guard the regression.

Kestra Guidelines: 5 findings

  • 🟡 Converse.java:117 [Guidelines] Empty check duplicated in both tasks
  • 🟡 Converse.java:118 [Guidelines] Error message does not say what to do
  • 🟡 ConverseTest.java:111 [Tests] Hand-rolled validator instead of ModelValidator
  • 🟢 ConverseTest.java:120 [Guidelines] Fully qualified java.util.Locale
  • 🟢 ConverseTest.java:86 [Guidelines] Explicit local types and fixed task ids

Security (OWASP Top 10:2025 + KPS): 0 findings

✅ No issues found

Performance: 0 findings

✅ No issues found

Tests: ConverseTest and ConverseStreamTest run locally on 90a41a1, 7 passed, 0 failed. CI green.

Confidence: 83% (100 - 5 Suggestion - 5 Suggestion - 5 Suggestion - 1 Nit - 1 Nit)

Verdict: APPROVE; recommendation only, posted as COMMENT, a maintainer decides.

} catch (ClassCastException e) {
throw new IllegalArgumentException("messages must be a list of {role, content} maps", e);
}
if (rawMessages.isEmpty()) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🟡 [Guidelines] Empty check duplicated in both tasks
Problem: The same rawMessages.isEmpty() guard is copied into Converse and ConverseStream (line 120), so the two can drift apart; the guidelines ask for shared behaviour to be extracted (DRY).
Fix: Move the check to the top of BedrockUtils.buildMessages(...), which both tasks already call with the rendered list, and drop it from the two run() methods.

throw new IllegalArgumentException("messages must be a list of {role, content} maps", e);
}
if (rawMessages.isEmpty()) {
throw new IllegalArgumentException("messages must not be empty");

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🟡 [Guidelines] Error message does not say what to do
Problem: messages must not be empty states the failure but not the fix, and since @NotEmpty is gone the property docs no longer signal that at least one entry is required (Error Messages: say what went wrong AND what to do).
Fix: Use something like "messages must contain at least one {role, content} entry, for example a 'user' turn", and add "At least one entry is required." to the messages @Schema description in both tasks.

.messages(Property.ofValue(List.of(Map.of("role", "user", "content", "Hello!"))))
.build();

var violations = Validation.byDefaultProvider()

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🟡 [Tests] Hand-rolled validator instead of ModelValidator
Problem: The test builds its own Validation.byDefaultProvider() with an anonymous MessageInterpolator, duplicated in ConverseStreamTest, rather than the validator Kestra uses when a flow is saved.
Fix: @Inject ModelValidator modelValidator; (io.kestra.core.models.validations.ModelValidator) and assert assertThat(modelValidator.isValid(task).isEmpty(), is(true)); in both test classes; this also removes the interpolator boilerplate.

}

@Override
public String interpolate(String messageTemplate, Context context, java.util.Locale locale) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🟢 [Guidelines] Fully qualified java.util.Locale
Problem: java.util.Locale is written inline here and in ConverseStreamTest.java:171 with no naming conflict (Java Style: import classes instead of using fully qualified names).
Fix: Add import java.util.Locale; in both test files, or drop the anonymous interpolator entirely if you switch to ModelValidator.


@Test
void givenEmptyMessages_whenConverse_thenThrowsExpectedException() {
RunContext runContext = runContextFactory.of();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

🟢 [Guidelines] Explicit local types and fixed task ids
Problem: New tests declare RunContext runContext and Converse task / ConverseStream task instead of var, and use fixed ids like test-converse-empty-messages (Java Style: var for locals; Flaky Test Prevention: randomize task ids). Same in ConverseStreamTest.
Fix: Use var runContext = ... and var task = ..., and build ids with IdUtils.create() or a random suffix.

@fdelbrayelle

Copy link
Copy Markdown
Member

QA report

Commit: 90a41a1
Edition: OSS | Docker tag: v1.3.44 and v2.0.7 (same plugin JAR, shadowJar built from the PR head, run one instance at a time)
Instance: http://localhost:18857 (local, left running on v2.0.7 for review; docker rm -f qa-aws857-kestra qa-aws857-mock to stop)
Screenshots: https://claude.ai/artifact/WfVNTxM4VZqBpjcU4JVGCq
Confidence: 100% (16 PASS of 16 scenarios, no deductions)

Setup: Bedrock calls go to a local mock Bedrock runtime (endpointOverride: http://qa-aws857-mock:8080, dummy credentials, no AWS account, no external host). All flows were saved and run from the UI. Fixes #819.

Summary

Scenario 1.3.44 2.0.7
Converse, templated messages: save in UI PASS PASS
ConverseStream, templated messages: save in UI PASS PASS
Converse, templated, non-empty list: run PASS (SUCCESS) PASS (SUCCESS)
ConverseStream, templated, non-empty list: run PASS (SUCCESS) PASS (SUCCESS)
Converse, templated, empty list: run PASS (FAILED, messages must not be empty) PASS (same)
ConverseStream, templated, empty list: run PASS (FAILED, messages must not be empty) PASS (same)
Non-regression: Converse, literal messages PASS (SUCCESS) PASS (SUCCESS)
Non-regression: ConverseStream, literal messages PASS (SUCCESS) PASS (SUCCESS)

Per-flow results (identical on both versions)

All four templated and literal success runs: task chat SUCCESS (0.1s to 1.2s for Converse, about 2.3s for ConverseStream). Outputs: modelId: anthropic.claude-3-haiku-20240307-v1:0, content: Hello from mock, stopReason: end_turn, inputTokens: 7, outputTokens: 3. The mock received the request with the rendered message list.

Empty list runs: chat FAILED in under 0.3s, the only ERROR log is messages must not be empty, and the mock received no request (the check happens before any call).

Save step: the flows save with messages: "{{ inputs.messages }}" and no validation error. The only editor warnings are about plain-text accessKeyId and secretKeyId (dummy values in this test). Screenshots: saved 1.3, saved 2.0, empty logs 1.3, empty logs 2.0.

Flow: tpl_converse

Flow YAML: tpl_converse
id: tpl_converse
namespace: qa.aws857

inputs:
  - id: messages
    type: JSON
    defaults: '[{"role":"user","content":"Hello"}]'

tasks:
  - id: chat
    type: io.kestra.plugin.aws.bedrock.Converse
    region: us-east-1
    accessKeyId: test
    secretKeyId: test
    endpointOverride: http://qa-aws857-mock:8080
    modelId: anthropic.claude-3-haiku-20240307-v1:0
    messages: "{{ inputs.messages }}"

Gantt: 1.3, 2.0

Flow: tpl_stream

Flow YAML: tpl_stream
id: tpl_stream
namespace: qa.aws857

inputs:
  - id: messages
    type: JSON
    defaults: '[{"role":"user","content":"Hello"}]'

tasks:
  - id: chat
    type: io.kestra.plugin.aws.bedrock.ConverseStream
    region: us-east-1
    accessKeyId: test
    secretKeyId: test
    endpointOverride: http://qa-aws857-mock:8080
    modelId: anthropic.claude-3-haiku-20240307-v1:0
    messages: "{{ inputs.messages }}"

Gantt: 1.3, 2.0

Flow: tpl_converse_empty

Flow YAML: tpl_converse_empty
id: tpl_converse_empty
namespace: qa.aws857

inputs:
  - id: messages
    type: JSON
    defaults: '[]'

tasks:
  - id: chat
    type: io.kestra.plugin.aws.bedrock.Converse
    region: us-east-1
    accessKeyId: test
    secretKeyId: test
    endpointOverride: http://qa-aws857-mock:8080
    modelId: anthropic.claude-3-haiku-20240307-v1:0
    messages: "{{ inputs.messages }}"

Gantt: 1.3, 2.0

Flow: tpl_stream_empty

Flow YAML: tpl_stream_empty
id: tpl_stream_empty
namespace: qa.aws857

inputs:
  - id: messages
    type: JSON
    defaults: '[]'

tasks:
  - id: chat
    type: io.kestra.plugin.aws.bedrock.ConverseStream
    region: us-east-1
    accessKeyId: test
    secretKeyId: test
    endpointOverride: http://qa-aws857-mock:8080
    modelId: anthropic.claude-3-haiku-20240307-v1:0
    messages: "{{ inputs.messages }}"

Gantt: 1.3, 2.0

Flow: lit_converse

Flow YAML: lit_converse
id: lit_converse
namespace: qa.aws857

tasks:
  - id: chat
    type: io.kestra.plugin.aws.bedrock.Converse
    region: us-east-1
    accessKeyId: test
    secretKeyId: test
    endpointOverride: http://qa-aws857-mock:8080
    modelId: anthropic.claude-3-haiku-20240307-v1:0
    messages:
      - role: user
        content: Hello

Gantt: 1.3, 2.0

Flow: lit_stream

Flow YAML: lit_stream
id: lit_stream
namespace: qa.aws857

tasks:
  - id: chat
    type: io.kestra.plugin.aws.bedrock.ConverseStream
    region: us-east-1
    accessKeyId: test
    secretKeyId: test
    endpointOverride: http://qa-aws857-mock:8080
    modelId: anthropic.claude-3-haiku-20240307-v1:0
    messages:
      - role: user
        content: Hello

Gantt: 1.3, 2.0

Notes

  • Not run: a pre-fix comparison against a JAR built from main (the "before the fix it failed" claim was not reproduced here).
  • Empty-list scenarios use a flow input default of [] (separate flows) because the execute dialog JSON field could not be edited reliably through browser automation.
  • The 2.0.7 image installs its default plugin set on startup by itself; this is image behavior, not part of the test flows.
  • No timeouts. No new findings.

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

Labels

area/plugin Plugin-related issue or feature request kind/external Pull requests raised by community contributors

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

bedrock.Converse and ConverseStream flows can't be saved: @NotEmpty on Property<List> crashes validation (HV000030)

2 participants