Skip to content

Commit 1983041

Browse files
authored
Merge branch 'main' into plan-notebook-ui-optimize
2 parents 8349c79 + 9a529c3 commit 1983041

3 files changed

Lines changed: 172 additions & 53 deletions

File tree

agentscope-core/src/main/java/io/agentscope/core/agent/AgentBase.java

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -675,6 +675,19 @@ public final Flux<Event> stream(
675675
return createEventStream(options, () -> call(msgs, structuredModel));
676676
}
677677

678+
/**
679+
* Stream with multiple input messages using a JSON schema.
680+
*
681+
* @param msgs Input messages
682+
* @param options Stream configuration options
683+
* @param schema JSON schema defining the structure of the response
684+
* @return Flux of events emitted during execution
685+
*/
686+
@Override
687+
public final Flux<Event> stream(List<Msg> msgs, StreamOptions options, JsonNode schema) {
688+
return createEventStream(options, () -> call(msgs, schema));
689+
}
690+
678691
/**
679692
* Helper method to create an event stream with proper hook lifecycle management.
680693
*

agentscope-core/src/main/java/io/agentscope/core/agent/StreamableAgent.java

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
package io.agentscope.core.agent;
1717

18+
import com.fasterxml.jackson.databind.JsonNode;
1819
import io.agentscope.core.message.Msg;
1920
import java.util.List;
2021
import reactor.core.publisher.Flux;
@@ -98,6 +99,18 @@ default Flux<Event> stream(Msg msg, StreamOptions options, Class<?> structuredMo
9899
return stream(List.of(msg), options, structuredModel);
99100
}
100101

102+
/**
103+
* Stream execution events for a single message with JSON schema support.
104+
*
105+
* @param msg Input message
106+
* @param options Stream configuration options
107+
* @param schema JSON schema defining the structure
108+
* @return Flux of events emitted during execution
109+
*/
110+
default Flux<Event> stream(Msg msg, StreamOptions options, JsonNode schema) {
111+
return stream(List.of(msg), options, schema);
112+
}
113+
101114
/**
102115
* Stream execution events for multiple messages with default options.
103116
*
@@ -126,4 +139,14 @@ default Flux<Event> stream(List<Msg> msgs) {
126139
* @return Flux of events emitted during execution
127140
*/
128141
Flux<Event> stream(List<Msg> msgs, StreamOptions options, Class<?> structuredModel);
142+
143+
/**
144+
* Stream execution events with JSON schema support.
145+
*
146+
* @param msgs Input messages
147+
* @param options Stream configuration options
148+
* @param schema JSON schema defining the structure
149+
* @return Flux of events emitted during execution
150+
*/
151+
Flux<Event> stream(List<Msg> msgs, StreamOptions options, JsonNode schema);
129152
}

agentscope-core/src/test/java/io/agentscope/core/agent/StructuredOutputDynamicDefineTest.java

Lines changed: 136 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,14 @@
3535
import java.util.List;
3636
import java.util.Map;
3737
import org.junit.jupiter.api.BeforeEach;
38+
import org.junit.jupiter.api.DisplayName;
3839
import 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+
*/
4146
public 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

Comments
 (0)