From 859359a7acf5013863eb86a4d45663aaacfa8485 Mon Sep 17 00:00:00 2001 From: jujn <2087687391@qq.com> Date: Sun, 9 Aug 2026 22:36:03 +0800 Subject: [PATCH 1/2] fix(agui): parse request bodies with Jackson 2 codec --- .../agui/common/AguiRequestBodyParser.java | 43 ++++++++++ .../boot/agui/mvc/AguiRestController.java | 11 ++- .../boot/agui/webflux/AguiWebFluxHandler.java | 7 +- .../common/AguiRequestBodyParserTest.java | 86 +++++++++++++++++++ 4 files changed, 141 insertions(+), 6 deletions(-) create mode 100644 agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/common/AguiRequestBodyParser.java create mode 100644 agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/test/java/io/agentscope/spring/boot/agui/common/AguiRequestBodyParserTest.java diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/common/AguiRequestBodyParser.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/common/AguiRequestBodyParser.java new file mode 100644 index 0000000000..b15ae03c08 --- /dev/null +++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/common/AguiRequestBodyParser.java @@ -0,0 +1,43 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.spring.boot.agui.common; + +import io.agentscope.core.agui.model.RunAgentInput; +import io.agentscope.core.util.JsonUtils; +import java.util.Objects; + +/** + * Parses AG-UI request bodies with AgentScope's JSON codec. + * + *

The default AgentScope codec is Jackson 2. Keeping this conversion at the AG-UI boundary + * avoids selecting Spring Boot 4's Jackson 3 codec for AG-UI's Jackson 2-annotated models, while + * leaving the application's other HTTP converters unchanged. + */ +public final class AguiRequestBodyParser { + + private AguiRequestBodyParser() {} + + /** + * Parses a JSON request body into {@link RunAgentInput}. + * + * @param body the raw JSON request body + * @return the parsed AG-UI input + */ + public static RunAgentInput parse(String body) { + Objects.requireNonNull(body, "body cannot be null"); + return JsonUtils.getJsonCodec().fromJson(body, RunAgentInput.class); + } +} diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/mvc/AguiRestController.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/mvc/AguiRestController.java index 2b4feef6bf..9c169dee3f 100644 --- a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/mvc/AguiRestController.java +++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/mvc/AguiRestController.java @@ -16,6 +16,7 @@ package io.agentscope.spring.boot.agui.mvc; import io.agentscope.core.agui.model.RunAgentInput; +import io.agentscope.spring.boot.agui.common.AguiRequestBodyParser; import jakarta.servlet.http.HttpServletRequest; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.PathVariable; @@ -63,7 +64,7 @@ public AguiRestController( *

  • "default"
  • * * - * @param input The run agent input + * @param body The run agent input * @param agentIdHeader The agent ID from HTTP header (optional) * @return An SseEmitter for streaming AG-UI events */ @@ -72,12 +73,13 @@ public AguiRestController( consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter run( - @RequestBody RunAgentInput input, + @RequestBody String body, @RequestHeader( value = "${agentscope.agui.agent-id-header:X-Agent-Id}", required = false) String agentIdHeader, HttpServletRequest request) { + RunAgentInput input = AguiRequestBodyParser.parse(body); return aguiMvcController.handle(input, agentIdHeader, request); } @@ -87,7 +89,7 @@ public SseEmitter run( *

    The path variable takes highest priority for agent resolution. * * @param agentId The agent ID from path variable - * @param input The run agent input + * @param body The run agent input * @param agentIdHeader The agent ID from HTTP header (optional) * @return An SseEmitter for streaming AG-UI events */ @@ -97,12 +99,13 @@ public SseEmitter run( produces = MediaType.TEXT_EVENT_STREAM_VALUE) public SseEmitter runWithAgentId( @PathVariable String agentId, - @RequestBody RunAgentInput input, + @RequestBody String body, @RequestHeader( value = "${agentscope.agui.agent-id-header:X-Agent-Id}", required = false) String agentIdHeader, HttpServletRequest request) { + RunAgentInput input = AguiRequestBodyParser.parse(body); return aguiMvcController.handleWithAgentId(input, agentIdHeader, agentId, request); } } diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/webflux/AguiWebFluxHandler.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/webflux/AguiWebFluxHandler.java index 54b0777885..6a69ae1ca4 100644 --- a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/webflux/AguiWebFluxHandler.java +++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/webflux/AguiWebFluxHandler.java @@ -24,6 +24,7 @@ import io.agentscope.core.agui.model.RunAgentInput; import io.agentscope.core.agui.processor.AguiRequestProcessor; import io.agentscope.core.agui.registry.AguiAgentRegistry; +import io.agentscope.spring.boot.agui.common.AguiRequestBodyParser; import io.agentscope.spring.boot.agui.common.AguiRuntimeContextRequest; import io.agentscope.spring.boot.agui.common.AguiRuntimeContextResolver; import io.agentscope.spring.boot.agui.common.DefaultAgentResolver; @@ -112,7 +113,8 @@ private AguiWebFluxHandler(Builder builder) { * @return A Mono containing the server response with SSE stream */ public Mono handle(ServerRequest request) { - return request.bodyToMono(RunAgentInput.class) + return request.bodyToMono(String.class) + .map(AguiRequestBodyParser::parse) .flatMap(input -> processInput(input, request, null)) .onErrorResume(this::handleParseError); } @@ -128,7 +130,8 @@ public Mono handle(ServerRequest request) { */ public Mono handleWithAgentId(ServerRequest request) { String pathAgentId = request.pathVariable(AGENT_ID_PATH_VARIABLE); - return request.bodyToMono(RunAgentInput.class) + return request.bodyToMono(String.class) + .map(AguiRequestBodyParser::parse) .flatMap(input -> processInput(input, request, pathAgentId)) .onErrorResume(this::handleParseError); } diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/test/java/io/agentscope/spring/boot/agui/common/AguiRequestBodyParserTest.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/test/java/io/agentscope/spring/boot/agui/common/AguiRequestBodyParserTest.java new file mode 100644 index 0000000000..295b88038b --- /dev/null +++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/test/java/io/agentscope/spring/boot/agui/common/AguiRequestBodyParserTest.java @@ -0,0 +1,86 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.spring.boot.agui.common; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.agentscope.core.agui.model.MessageContent; +import io.agentscope.core.agui.model.RunAgentInput; +import org.junit.jupiter.api.Test; + +class AguiRequestBodyParserTest { + + @Test + void shouldParseTextContentWithAgentScopeCodec() { + String body = + """ + { + "threadId": "thread-1", + "runId": "run-1", + "messages": [ + {"id": "message-1", "role": "user", "content": "Hello!"} + ] + } + """; + + RunAgentInput input = AguiRequestBodyParser.parse(body); + + assertEquals("Hello!", input.getMessages().get(0).getTextContent()); + } + + @Test + void shouldParseMultimodalContentWithAgentScopeCodec() { + String body = + """ + { + "threadId": "thread-1", + "runId": "run-1", + "messages": [ + { + "id": "message-1", + "role": "user", + "content": [ + {"type": "text", "text": "Describe this"}, + { + "type": "image", + "source": { + "type": "url", + "value": "https://example.com/image.png" + } + } + ] + } + ] + } + """; + + RunAgentInput input = AguiRequestBodyParser.parse(body); + MessageContent.Blocks content = + assertInstanceOf( + MessageContent.Blocks.class, input.getMessages().get(0).getContent()); + + assertEquals(2, content.parts().size()); + assertTrue(input.getMessages().get(0).hasBlocks()); + } + + @Test + void shouldRejectNullBody() { + assertThrows(NullPointerException.class, () -> AguiRequestBodyParser.parse(null)); + } +} From 0578fe7310771d0271270462c806faf7d442c64b Mon Sep 17 00:00:00 2001 From: jujn <2087687391@qq.com> Date: Mon, 10 Aug 2026 00:09:00 +0800 Subject: [PATCH 2/2] fix: improve --- .../boot/agui/mvc/AguiRestController.java | 35 ++++++++++++++- .../boot/agui/mvc/AguiRestControllerTest.java | 43 +++++++++++++++++++ 2 files changed, 76 insertions(+), 2 deletions(-) create mode 100644 agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/test/java/io/agentscope/spring/boot/agui/mvc/AguiRestControllerTest.java diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/mvc/AguiRestController.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/mvc/AguiRestController.java index 9c169dee3f..8cfac18022 100644 --- a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/mvc/AguiRestController.java +++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/main/java/io/agentscope/spring/boot/agui/mvc/AguiRestController.java @@ -15,10 +15,16 @@ */ package io.agentscope.spring.boot.agui.mvc; +import io.agentscope.core.agui.encoder.AguiEventEncoder; +import io.agentscope.core.agui.event.AguiEvent; import io.agentscope.core.agui.model.RunAgentInput; +import io.agentscope.core.util.JsonException; import io.agentscope.spring.boot.agui.common.AguiRequestBodyParser; import jakarta.servlet.http.HttpServletRequest; +import java.util.Map; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; @@ -36,6 +42,7 @@ public class AguiRestController { private final AguiMvcController aguiMvcController; + private final AguiEventEncoder encoder = new AguiEventEncoder(); private final String pathPrefix; private final boolean enablePathRouting; @@ -64,7 +71,7 @@ public AguiRestController( *

  • "default"
  • * * - * @param body The run agent input + * @param body The raw run agent input JSON * @param agentIdHeader The agent ID from HTTP header (optional) * @return An SseEmitter for streaming AG-UI events */ @@ -89,7 +96,7 @@ public SseEmitter run( *

    The path variable takes highest priority for agent resolution. * * @param agentId The agent ID from path variable - * @param body The run agent input + * @param body The raw run agent input JSON * @param agentIdHeader The agent ID from HTTP header (optional) * @return An SseEmitter for streaming AG-UI events */ @@ -108,4 +115,28 @@ public SseEmitter runWithAgentId( RunAgentInput input = AguiRequestBodyParser.parse(body); return aguiMvcController.handleWithAgentId(input, agentIdHeader, agentId, request); } + + /** + * Return HTTP 400 for AG-UI request body parsing failures. + * + * @param error the JSON parse failure + * @return an SSE-compatible bad request response + */ + @ExceptionHandler(JsonException.class) + public ResponseEntity handleParseError(JsonException error) { + String errorEvent = + encoder.encodeToJson( + new AguiEvent.Raw( + "unknown", + "unknown", + Map.of( + "error", + "Failed to parse request: " + error.getMessage()))) + .trim(); + String finishEvent = + encoder.encodeToJson(new AguiEvent.RunFinished("unknown", "unknown")).trim(); + return ResponseEntity.badRequest() + .contentType(MediaType.TEXT_EVENT_STREAM) + .body("data: " + errorEvent + "\n\n" + "data: " + finishEvent + "\n\n"); + } } diff --git a/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/test/java/io/agentscope/spring/boot/agui/mvc/AguiRestControllerTest.java b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/test/java/io/agentscope/spring/boot/agui/mvc/AguiRestControllerTest.java new file mode 100644 index 0000000000..fd6f2507dd --- /dev/null +++ b/agentscope-extensions/agentscope-spring-boot-starters/agentscope-agui-spring-boot-starter/src/test/java/io/agentscope/spring/boot/agui/mvc/AguiRestControllerTest.java @@ -0,0 +1,43 @@ +/* + * Copyright 2024-2026 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.agentscope.spring.boot.agui.mvc; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.agentscope.core.util.JsonException; +import org.junit.jupiter.api.Test; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; + +class AguiRestControllerTest { + + @Test + void shouldReturnBadRequestSseForParseErrors() { + AguiRestController controller = new AguiRestController(null, "/agui", true); + + ResponseEntity response = + controller.handleParseError(new JsonException("bad json")); + + assertEquals(HttpStatus.BAD_REQUEST, response.getStatusCode()); + assertEquals(MediaType.TEXT_EVENT_STREAM, response.getHeaders().getContentType()); + assertNotNull(response.getBody()); + assertTrue(response.getBody().contains("Failed to parse request: bad json")); + assertTrue(response.getBody().contains("data: ")); + } +}