3535import java .util .List ;
3636import java .util .Map ;
3737import org .junit .jupiter .api .BeforeEach ;
38+ import org .junit .jupiter .api .DisplayName ;
3839import org .junit .jupiter .api .Test ;
40+ import reactor .core .publisher .Flux ;
41+ import reactor .test .StepVerifier ;
3942
40- /** Tests for dynamic define json schema in StructuredOutputCapableAgent (deferred forcing mode). */
43+ /**
44+ * Tests for dynamically defined JSON schema in StructuredOutputCapableAgent (deferred forcing mode).
45+ */
4146public class StructuredOutputDynamicDefineTest {
4247
4348 private Toolkit toolkit ;
@@ -50,58 +55,7 @@ void setUp() {
5055 @ Test
5156 void testDynamicComplexNestedStructure () {
5257 Memory memory = new InMemoryMemory ();
53- Map <String , Object > toolInput =
54- Map .of (
55- "response" ,
56- Map .of (
57- "location" ,
58- "San Francisco" ,
59- "temperature" ,
60- "72°F" ,
61- "condition" ,
62- "Sunny" ));
63-
64- MockModel mockModel =
65- new MockModel (
66- msgs -> {
67- // Check if we have any TOOL role messages (tool execution results)
68- boolean hasToolResults =
69- msgs .stream ().anyMatch (m -> m .getRole () == MsgRole .TOOL );
70-
71- if (!hasToolResults ) {
72- // First call: return tool use for generate_response
73- return List .of (
74- ChatResponse .builder ()
75- .id ("msg_1" )
76- .content (
77- List .of (
78- ToolUseBlock .builder ()
79- .id ("call_123" )
80- .name ("generate_response" )
81- .input (toolInput )
82- .content (
83- JsonUtils
84- .getJsonCodec ()
85- .toJson (
86- toolInput ))
87- .build ()))
88- .usage (new ChatUsage (10 , 20 , 30 ))
89- .build ());
90- } else {
91- // Second call (after tool execution): return simple text
92- // (no more tool calls, indicating we're done)
93- return List .of (
94- ChatResponse .builder ()
95- .id ("msg_2" )
96- .content (
97- List .of (
98- TextBlock .builder ()
99- .text ("Response generated" )
100- .build ()))
101- .usage (new ChatUsage (5 , 10 , 15 ))
102- .build ());
103- }
104- });
58+ MockModel mockModel = getMockModel ();
10559
10660 // Create agent with TOOL_BASED strategy
10761 ReActAgent agent =
@@ -158,4 +112,133 @@ void testDynamicComplexNestedStructure() {
158112 assertEquals ("72°F" , result .get ("temperature" ));
159113 assertEquals ("Sunny" , result .get ("condition" ));
160114 }
115+
116+ @ Test
117+ @ DisplayName ("Stream execution events with JSON schema structured support" )
118+ void testDynamicComplexNestedStructureStreamingMode () {
119+ Memory memory = new InMemoryMemory ();
120+ MockModel mockModel = getMockModel ();
121+
122+ // Create agent with TOOL_BASED strategy
123+ ReActAgent agent =
124+ ReActAgent .builder ()
125+ .name ("weather-agent" )
126+ .sysPrompt ("You are a weather assistant" )
127+ .model (mockModel )
128+ .toolkit (toolkit )
129+ .memory (memory )
130+ .build ();
131+
132+ // Execute structured output call
133+ Msg inputMsg =
134+ Msg .builder ()
135+ .name ("user" )
136+ .role (MsgRole .USER )
137+ .content (
138+ TextBlock .builder ()
139+ .text ("What's the weather in San Francisco?" )
140+ .build ())
141+ .build ();
142+ String json =
143+ """
144+ {
145+ "type": "object",
146+ "properties": {
147+ "location": {
148+ "type": "string"
149+ },
150+ "temperature": {
151+ "type": "string"
152+ },
153+ "condition": {
154+ "type": "string"
155+ }
156+ },
157+ "required": ["location", "temperature", "condition"],
158+ "additionalProperties": false
159+ }
160+ """ ;
161+ // Streaming agent and extract structured data from response message
162+ Flux <Event > eventFlux =
163+ agent .stream (
164+ inputMsg ,
165+ StreamOptions .defaults (),
166+ JsonUtils .getJsonCodec ().fromJson (json , JsonNode .class ));
167+
168+ StepVerifier .create (eventFlux )
169+ .thenConsumeWhile (
170+ event -> !(event .isLast () && event .getType () == EventType .AGENT_RESULT ))
171+ .assertNext (
172+ event -> {
173+ Msg responseMsg = event .getMessage ();
174+ assertNotNull (responseMsg );
175+ assertNotNull (responseMsg .getMetadata ());
176+
177+ // Extract structured data from metadata
178+ Map <String , Object > result = responseMsg .getStructuredData (false );
179+
180+ // Verify
181+ assertNotNull (result );
182+ assertEquals ("San Francisco" , result .get ("location" ));
183+ assertEquals ("72°F" , result .get ("temperature" ));
184+ assertEquals ("Sunny" , result .get ("condition" ));
185+ })
186+ .verifyComplete ();
187+ }
188+
189+ private static MockModel getMockModel () {
190+ Map <String , Object > toolInput =
191+ Map .of (
192+ "response" ,
193+ Map .of (
194+ "location" ,
195+ "San Francisco" ,
196+ "temperature" ,
197+ "72°F" ,
198+ "condition" ,
199+ "Sunny" ));
200+
201+ MockModel mockModel =
202+ new MockModel (
203+ msgs -> {
204+ // Check if we have any TOOL role messages (tool execution results)
205+ boolean hasToolResults =
206+ msgs .stream ().anyMatch (m -> m .getRole () == MsgRole .TOOL );
207+
208+ if (!hasToolResults ) {
209+ // First call: return tool use for generate_response
210+ return List .of (
211+ ChatResponse .builder ()
212+ .id ("msg_1" )
213+ .content (
214+ List .of (
215+ ToolUseBlock .builder ()
216+ .id ("call_123" )
217+ .name ("generate_response" )
218+ .input (toolInput )
219+ .content (
220+ JsonUtils
221+ .getJsonCodec ()
222+ .toJson (
223+ toolInput ))
224+ .build ()))
225+ .usage (new ChatUsage (10 , 20 , 30 ))
226+ .build ());
227+ } else {
228+ // Second call (after tool execution): return simple text
229+ // (no more tool calls, indicating we're done)
230+ return List .of (
231+ ChatResponse .builder ()
232+ .id ("msg_2" )
233+ .content (
234+ List .of (
235+ TextBlock .builder ()
236+ .text ("Response generated" )
237+ .build ()))
238+ .usage (new ChatUsage (5 , 10 , 15 ))
239+ .build ());
240+ }
241+ });
242+ return mockModel ;
243+ }
161244}
0 commit comments