Skip to content

Commit 5a91c94

Browse files
committed
feat(GeminiResponseParser): use UUID for fallback ID generation and improve response handling
Signed-off-by: liuhy <liuhongyu@apache.org>
1 parent 9975da6 commit 5a91c94

7 files changed

Lines changed: 505 additions & 3 deletions

File tree

agentscope-core/src/main/java/io/agentscope/core/formatter/gemini/GeminiResponseParser.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,7 @@ protected void parseToolCall(
265265
try {
266266
String id = functionCall.getId();
267267
if (id == null || id.isEmpty()) {
268-
id = "tool_call_" + System.currentTimeMillis(); // Fallback if ID is missing
268+
id = "tool_call_" + UUID.randomUUID(); // Fallback if ID is missing
269269
}
270270
String name = functionCall.getName() != null ? functionCall.getName() : "";
271271

agentscope-core/src/main/java/io/agentscope/core/model/GeminiTransport.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,8 @@ final class GeminiTransport {
5858

5959
Flux<ChatResponse> handleUnaryResponse(Request request, Instant startTime) {
6060
try {
61-
Response response = httpClient.newCall(request).execute();
62-
try (ResponseBody responseBody = response.body()) {
61+
try (Response response = httpClient.newCall(request).execute();
62+
ResponseBody responseBody = response.body()) {
6363
String bodyString = responseBody != null ? responseBody.string() : null;
6464
if (!response.isSuccessful() || bodyString == null) {
6565
String errorBody = bodyString != null ? bodyString : "null";

agentscope-core/src/test/java/io/agentscope/core/formatter/gemini/GeminiResponseParserTest.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,30 @@ void testParseToolCallWithoutId() {
384384
assertEquals("search", toolUse.getName());
385385
}
386386

387+
@Test
388+
void testParseToolCallWithoutIdGeneratesUniqueFallbackIds() {
389+
GeminiFunctionCall firstCall = new GeminiFunctionCall();
390+
firstCall.setName("search");
391+
firstCall.setArgs(Map.of("query", "first"));
392+
393+
GeminiFunctionCall secondCall = new GeminiFunctionCall();
394+
secondCall.setName("search");
395+
secondCall.setArgs(Map.of("query", "second"));
396+
397+
List<ContentBlock> firstBlocks = new ArrayList<>();
398+
List<ContentBlock> secondBlocks = new ArrayList<>();
399+
400+
parser.parseToolCall(firstCall, null, firstBlocks);
401+
parser.parseToolCall(secondCall, null, secondBlocks);
402+
403+
ToolUseBlock firstToolUse = (ToolUseBlock) firstBlocks.get(0);
404+
ToolUseBlock secondToolUse = (ToolUseBlock) secondBlocks.get(0);
405+
406+
assertTrue(firstToolUse.getId().startsWith("tool_call_"));
407+
assertTrue(secondToolUse.getId().startsWith("tool_call_"));
408+
assertTrue(!firstToolUse.getId().equals(secondToolUse.getId()));
409+
}
410+
387411
@Test
388412
void testParseThinkingResponseWithSignature() {
389413
// Build response with thinking content and signature
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Copyright 2024-2026 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.agentscope.core.formatter.gemini.dto;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertTrue;
20+
21+
import io.agentscope.core.util.JsonUtils;
22+
import java.util.List;
23+
import java.util.Map;
24+
import org.junit.jupiter.api.DisplayName;
25+
import org.junit.jupiter.api.Tag;
26+
import org.junit.jupiter.api.Test;
27+
28+
@Tag("unit")
29+
@DisplayName("GeminiGenerationConfig Unit Tests")
30+
class GeminiGenerationConfigTest {
31+
32+
@Test
33+
@DisplayName("Should set all fields through builder")
34+
void testBuilder() {
35+
GeminiGenerationConfig.GeminiThinkingConfig thinkingConfig =
36+
GeminiGenerationConfig.GeminiThinkingConfig.builder()
37+
.includeThoughts(true)
38+
.thinkingBudget(256)
39+
.thinkingLevel("medium")
40+
.build();
41+
42+
GeminiGenerationConfig config =
43+
GeminiGenerationConfig.builder()
44+
.stopSequences(List.of("END"))
45+
.responseMimeType("application/json")
46+
.responseSchema(Map.of("type", "object"))
47+
.candidateCount(2)
48+
.maxOutputTokens(512)
49+
.temperature(0.2)
50+
.topP(0.8)
51+
.topK(20.0)
52+
.presencePenalty(0.1)
53+
.frequencyPenalty(0.2)
54+
.seed(7)
55+
.thinkingConfig(thinkingConfig)
56+
.build();
57+
58+
assertEquals(List.of("END"), config.getStopSequences());
59+
assertEquals("application/json", config.getResponseMimeType());
60+
assertEquals(Map.of("type", "object"), config.getResponseSchema());
61+
assertEquals(2, config.getCandidateCount());
62+
assertEquals(512, config.getMaxOutputTokens());
63+
assertEquals(0.2, config.getTemperature());
64+
assertEquals(0.8, config.getTopP());
65+
assertEquals(20.0, config.getTopK());
66+
assertEquals(0.1, config.getPresencePenalty());
67+
assertEquals(0.2, config.getFrequencyPenalty());
68+
assertEquals(7, config.getSeed());
69+
assertEquals(thinkingConfig, config.getThinkingConfig());
70+
}
71+
72+
@Test
73+
@DisplayName("Should set fields through setters")
74+
void testSetters() {
75+
GeminiGenerationConfig config = new GeminiGenerationConfig();
76+
GeminiGenerationConfig.GeminiThinkingConfig thinkingConfig =
77+
new GeminiGenerationConfig.GeminiThinkingConfig();
78+
79+
config.setStopSequences(List.of("S1", "S2"));
80+
config.setResponseMimeType("text/plain");
81+
config.setResponseSchema(Map.of("k", "v"));
82+
config.setCandidateCount(1);
83+
config.setMaxOutputTokens(1024);
84+
config.setTemperature(0.7);
85+
config.setTopP(0.9);
86+
config.setTopK(5.0);
87+
config.setPresencePenalty(0.3);
88+
config.setFrequencyPenalty(0.4);
89+
config.setSeed(42);
90+
config.setThinkingConfig(thinkingConfig);
91+
92+
assertEquals(List.of("S1", "S2"), config.getStopSequences());
93+
assertEquals("text/plain", config.getResponseMimeType());
94+
assertEquals(Map.of("k", "v"), config.getResponseSchema());
95+
assertEquals(1, config.getCandidateCount());
96+
assertEquals(1024, config.getMaxOutputTokens());
97+
assertEquals(0.7, config.getTemperature());
98+
assertEquals(0.9, config.getTopP());
99+
assertEquals(5.0, config.getTopK());
100+
assertEquals(0.3, config.getPresencePenalty());
101+
assertEquals(0.4, config.getFrequencyPenalty());
102+
assertEquals(42, config.getSeed());
103+
assertEquals(thinkingConfig, config.getThinkingConfig());
104+
}
105+
106+
@Test
107+
@DisplayName("Should set nested thinking config fields")
108+
void testThinkingConfigSetters() {
109+
GeminiGenerationConfig.GeminiThinkingConfig config =
110+
new GeminiGenerationConfig.GeminiThinkingConfig();
111+
config.setIncludeThoughts(false);
112+
config.setThinkingBudget(128);
113+
config.setThinkingLevel("low");
114+
115+
assertEquals(false, config.getIncludeThoughts());
116+
assertEquals(128, config.getThinkingBudget());
117+
assertEquals("low", config.getThinkingLevel());
118+
}
119+
120+
@Test
121+
@DisplayName("Should serialize using Gemini field names and omit null fields")
122+
void testSerialization() {
123+
GeminiGenerationConfig config = new GeminiGenerationConfig();
124+
config.setResponseMimeType("application/json");
125+
config.setTopK(30.0);
126+
config.setThinkingConfig(
127+
GeminiGenerationConfig.GeminiThinkingConfig.builder()
128+
.includeThoughts(true)
129+
.thinkingBudget(32)
130+
.build());
131+
132+
String json = JsonUtils.getJsonCodec().toJson(config);
133+
134+
assertTrue(json.contains("\"responseMimeType\":\"application/json\""));
135+
assertTrue(json.contains("\"topK\":30.0"));
136+
assertTrue(json.contains("\"thinkingConfig\""));
137+
assertTrue(json.contains("\"includeThoughts\":true"));
138+
assertTrue(json.contains("\"thinkingBudget\":32"));
139+
assertTrue(!json.contains("\"frequencyPenalty\""));
140+
}
141+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
/*
2+
* Copyright 2024-2026 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
package io.agentscope.core.formatter.gemini.dto;
17+
18+
import static org.junit.jupiter.api.Assertions.assertEquals;
19+
import static org.junit.jupiter.api.Assertions.assertTrue;
20+
21+
import io.agentscope.core.util.JsonUtils;
22+
import java.util.Map;
23+
import org.junit.jupiter.api.DisplayName;
24+
import org.junit.jupiter.api.Tag;
25+
import org.junit.jupiter.api.Test;
26+
27+
@Tag("unit")
28+
@DisplayName("GeminiPart Unit Tests")
29+
class GeminiPartTest {
30+
31+
@Test
32+
@DisplayName("Should set and get top level fields")
33+
void testTopLevelFields() {
34+
GeminiPart part = new GeminiPart();
35+
GeminiPart.GeminiFunctionCall functionCall =
36+
new GeminiPart.GeminiFunctionCall("call_1", "lookup", Map.of("k", "v"));
37+
GeminiPart.GeminiFunctionResponse functionResponse =
38+
new GeminiPart.GeminiFunctionResponse("resp_1", "lookup", Map.of("output", "ok"));
39+
GeminiPart.GeminiBlob blob = new GeminiPart.GeminiBlob("image/png", "ZmFrZQ==");
40+
GeminiPart.GeminiFileData fileData =
41+
new GeminiPart.GeminiFileData("text/plain", "gs://bucket/file.txt");
42+
43+
part.setText("hello");
44+
part.setFunctionCall(functionCall);
45+
part.setFunctionResponse(functionResponse);
46+
part.setInlineData(blob);
47+
part.setFileData(fileData);
48+
part.setThought(true);
49+
part.setSignature("sig");
50+
part.setThoughtSignature("thought-sig");
51+
52+
assertEquals("hello", part.getText());
53+
assertEquals(functionCall, part.getFunctionCall());
54+
assertEquals(functionResponse, part.getFunctionResponse());
55+
assertEquals(blob, part.getInlineData());
56+
assertEquals(fileData, part.getFileData());
57+
assertEquals(true, part.getThought());
58+
assertEquals("sig", part.getSignature());
59+
assertEquals("thought-sig", part.getThoughtSignature());
60+
}
61+
62+
@Test
63+
@DisplayName("Should support function call constructors and setters")
64+
void testFunctionCallConstructors() {
65+
GeminiPart.GeminiFunctionCall fromTwoArgs =
66+
new GeminiPart.GeminiFunctionCall("get_weather", Map.of("city", "SF"));
67+
GeminiPart.GeminiFunctionCall fromThreeArgs =
68+
new GeminiPart.GeminiFunctionCall("call_2", "get_weather", Map.of("city", "LA"));
69+
70+
fromTwoArgs.setId("call_3");
71+
fromTwoArgs.setName("weather");
72+
fromTwoArgs.setArgs(Map.of("city", "Tokyo"));
73+
74+
assertEquals("call_3", fromTwoArgs.getId());
75+
assertEquals("weather", fromTwoArgs.getName());
76+
assertEquals(Map.of("city", "Tokyo"), fromTwoArgs.getArgs());
77+
assertEquals("call_2", fromThreeArgs.getId());
78+
assertEquals("get_weather", fromThreeArgs.getName());
79+
assertEquals(Map.of("city", "LA"), fromThreeArgs.getArgs());
80+
}
81+
82+
@Test
83+
@DisplayName("Should support function response constructors and setters")
84+
void testFunctionResponseConstructors() {
85+
GeminiPart.GeminiFunctionResponse fromTwoArgs =
86+
new GeminiPart.GeminiFunctionResponse("lookup", Map.of("output", "Paris"));
87+
GeminiPart.GeminiFunctionResponse fromThreeArgs =
88+
new GeminiPart.GeminiFunctionResponse("id_1", "lookup", Map.of("output", "Berlin"));
89+
90+
fromTwoArgs.setId("id_2");
91+
fromTwoArgs.setName("lookup_city");
92+
fromTwoArgs.setResponse(Map.of("output", "Rome"));
93+
94+
assertEquals("id_2", fromTwoArgs.getId());
95+
assertEquals("lookup_city", fromTwoArgs.getName());
96+
assertEquals(Map.of("output", "Rome"), fromTwoArgs.getResponse());
97+
assertEquals("id_1", fromThreeArgs.getId());
98+
assertEquals("lookup", fromThreeArgs.getName());
99+
assertEquals(Map.of("output", "Berlin"), fromThreeArgs.getResponse());
100+
}
101+
102+
@Test
103+
@DisplayName("Should support blob and file data constructors and setters")
104+
void testBlobAndFileData() {
105+
GeminiPart.GeminiBlob blob = new GeminiPart.GeminiBlob();
106+
blob.setMimeType("audio/mp3");
107+
blob.setData("YXVkaW8=");
108+
assertEquals("audio/mp3", blob.getMimeType());
109+
assertEquals("YXVkaW8=", blob.getData());
110+
111+
GeminiPart.GeminiFileData fileData = new GeminiPart.GeminiFileData();
112+
fileData.setMimeType("application/pdf");
113+
fileData.setFileUri("https://example.com/a.pdf");
114+
assertEquals("application/pdf", fileData.getMimeType());
115+
assertEquals("https://example.com/a.pdf", fileData.getFileUri());
116+
}
117+
118+
@Test
119+
@DisplayName("Should serialize functionCall without id field")
120+
void testFunctionCallSerializationIgnoresId() {
121+
GeminiPart part = new GeminiPart();
122+
part.setFunctionCall(
123+
new GeminiPart.GeminiFunctionCall(
124+
"internal-id", "get_capital", Map.of("country", "Japan")));
125+
126+
String json = JsonUtils.getJsonCodec().toJson(part);
127+
128+
assertTrue(json.contains("\"functionCall\""));
129+
assertTrue(json.contains("\"name\":\"get_capital\""));
130+
assertTrue(json.contains("\"args\":{\"country\":\"Japan\"}"));
131+
assertTrue(!json.contains("internal-id"));
132+
}
133+
}

0 commit comments

Comments
 (0)