Skip to content

Commit f6ab9d9

Browse files
Merge pull request #1076 from ddobrin:issues
PiperOrigin-RevId: 888626578
2 parents 5640c58 + ce4b642 commit f6ab9d9

5 files changed

Lines changed: 190 additions & 128 deletions

File tree

contrib/spring-ai/DOCUMENT-GEMINI.md

Lines changed: 0 additions & 86 deletions
This file was deleted.

contrib/spring-ai/README.md

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,21 @@ To use ADK Java with the Spring AI integration in your application, add the foll
1818
<dependency>
1919
<groupId>com.google.adk</groupId>
2020
<artifactId>google-adk</artifactId>
21-
<version>0.3.1-SNAPSHOT</version>
21+
<version>1.0.1-rc.1-SNAPSHOT</version>
2222
</dependency>
2323

2424
<!-- ADK Spring AI Integration -->
2525
<dependency>
2626
<groupId>com.google.adk</groupId>
2727
<artifactId>google-adk-spring-ai</artifactId>
28-
<version>0.3.1-SNAPSHOT</version>
28+
<version>1.0.1-rc.1-SNAPSHOT</version>
2929
</dependency>
3030

3131
<!-- Spring AI BOM for version management -->
3232
<dependency>
3333
<groupId>org.springframework.ai</groupId>
3434
<artifactId>spring-ai-bom</artifactId>
35-
<version>1.1.0-M3</version>
35+
<version>2.0.0-M3</version>
3636
<type>pom</type>
3737
<scope>import</scope>
3838
</dependency>
@@ -109,14 +109,14 @@ Add the Spring AI provider dependencies for the AI services you want to use:
109109
<parent>
110110
<groupId>org.springframework.boot</groupId>
111111
<artifactId>spring-boot-starter-parent</artifactId>
112-
<version>3.2.0</version>
112+
<version>4.0.2</version>
113113
<relativePath/>
114114
</parent>
115115

116116
<properties>
117117
<java.version>17</java.version>
118-
<spring-ai.version>1.1.0-M3</spring-ai.version>
119-
<adk.version>0.3.1-SNAPSHOT</adk.version>
118+
<spring-ai.version>2.0.0-M3</spring-ai.version>
119+
<adk.version>1.0.1-rc.1-SNAPSHOT</adk.version>
120120
</properties>
121121

122122
<dependencyManagement>
@@ -271,7 +271,7 @@ public class MyAdkSpringAiApplication {
271271
.anthropicApi(anthropicApi)
272272
.build();
273273

274-
return new SpringAI(chatModel, "claude-3-5-sonnet-20241022");
274+
return new SpringAI(chatModel, "claude-sonnet-4-6");
275275
}
276276

277277
@Bean
@@ -312,7 +312,7 @@ spring:
312312
api-key: ${ANTHROPIC_API_KEY}
313313
chat:
314314
options:
315-
model: claude-3-5-sonnet-20241022
315+
model: claude-sonnet-4-6
316316
temperature: 0.7
317317

318318
# ADK Spring AI Configuration
@@ -365,13 +365,13 @@ The main adapter class that implements `BaseLlm` and wraps Spring AI `ChatModel`
365365
**Usage:**
366366
```java
367367
// With ChatModel only
368-
SpringAI springAI = new SpringAI(chatModel, "claude-sonnet-4-20250514");
368+
SpringAI springAI = new SpringAI(chatModel, "claude-sonnet-4-6");
369369

370370
// With both ChatModel and StreamingChatModel
371-
SpringAI springAI = new SpringAI(chatModel, streamingChatModel, "claude-sonnet-4-20250514");
371+
SpringAI springAI = new SpringAI(chatModel, streamingChatModel, "claude-sonnet-4-6");
372372

373373
// With observability configuration
374-
SpringAI springAI = new SpringAI(chatModel, "claude-sonnet-4-20250514", observabilityConfig);
374+
SpringAI springAI = new SpringAI(chatModel, "claude-sonnet-4-6", observabilityConfig);
375375
```
376376

377377
#### 2. MessageConverter (MessageConverter.java)
@@ -533,7 +533,7 @@ The library works with any Spring AI provider:
533533
- Features: Chat, streaming, function calling, embeddings
534534

535535
2. **Anthropic** (`spring-ai-anthropic`)
536-
- Models: Claude 3.5 Sonnet, Claude 3 Haiku
536+
- Models: Claude 4.x Sonnet, Claude 4.x Haiku
537537
- Features: Chat, streaming, function calling
538538
- **Note:** Requires proper function schema registration
539539

@@ -563,7 +563,7 @@ The library works with any Spring AI provider:
563563

564564
#### Anthropic
565565
- **Function Calling:** Requires explicit schema registration using `inputSchema()` method
566-
- **Model Names:** Use full model names like `claude-3-5-sonnet-20241022`
566+
- **Model Names:** Use full model names like `claude-sonnet-4-6`
567567
- **API Key:** Requires `ANTHROPIC_API_KEY` environment variable
568568

569569
#### OpenAI

contrib/spring-ai/src/main/java/com/google/adk/models/springai/ToolConverter.java

Lines changed: 59 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.util.HashMap;
2424
import java.util.List;
2525
import java.util.Map;
26+
import java.util.Set;
2627
import org.slf4j.Logger;
2728
import org.slf4j.LoggerFactory;
2829
import org.springframework.ai.tool.ToolCallback;
@@ -172,6 +173,17 @@ public List<ToolCallback> convertToSpringAiTools(Map<String, BaseTool> tools) {
172173
} catch (Exception e) {
173174
logger.error("Error serializing schema to JSON: {}", e.getMessage(), e);
174175
}
176+
} else if (declaration.parametersJsonSchema().isPresent()) {
177+
callbackBuilder.inputType(Map.class);
178+
try {
179+
String schemaJson =
180+
new com.fasterxml.jackson.databind.ObjectMapper()
181+
.writeValueAsString(declaration.parametersJsonSchema().get());
182+
callbackBuilder.inputSchema(schemaJson);
183+
logger.debug("Set input schema JSON from parametersJsonSchema: {}", schemaJson);
184+
} catch (Exception e) {
185+
logger.error("Error serializing parametersJsonSchema to JSON: {}", e.getMessage(), e);
186+
}
175187
}
176188

177189
toolCallbacks.add(callbackBuilder.build());
@@ -187,45 +199,63 @@ public List<ToolCallback> convertToSpringAiTools(Map<String, BaseTool> tools) {
187199
*/
188200
private Map<String, Object> processArguments(
189201
Map<String, Object> args, FunctionDeclaration declaration) {
190-
// If the arguments already match the expected format, return as-is
191202
if (declaration.parameters().isPresent()) {
192203
var schema = declaration.parameters().get();
193204
if (schema.properties().isPresent()) {
194-
var expectedParams = schema.properties().get().keySet();
195-
196-
// Check if all expected parameters are present at the top level
197-
boolean allParamsPresent = expectedParams.stream().allMatch(args::containsKey);
198-
if (allParamsPresent) {
199-
return args;
205+
return normalizeArguments(args, schema.properties().get().keySet());
206+
}
207+
} else if (declaration.parametersJsonSchema().isPresent()) {
208+
try {
209+
@SuppressWarnings("unchecked")
210+
Map<String, Object> schemaMap =
211+
new com.fasterxml.jackson.databind.ObjectMapper()
212+
.convertValue(declaration.parametersJsonSchema().get(), Map.class);
213+
Object propertiesObj = schemaMap.get("properties");
214+
if (propertiesObj instanceof Map) {
215+
@SuppressWarnings("unchecked")
216+
Set<String> expectedParams = ((Map<String, Object>) propertiesObj).keySet();
217+
return normalizeArguments(args, expectedParams);
200218
}
219+
} catch (Exception e) {
220+
logger.warn(
221+
"Error processing parametersJsonSchema for argument mapping: {}", e.getMessage());
222+
}
223+
}
201224

202-
// Check if arguments are nested under a single key (common pattern)
203-
if (args.size() == 1) {
204-
var singleValue = args.values().iterator().next();
205-
if (singleValue instanceof Map) {
206-
@SuppressWarnings("unchecked")
207-
Map<String, Object> nestedArgs = (Map<String, Object>) singleValue;
208-
boolean allNestedParamsPresent =
209-
expectedParams.stream().allMatch(nestedArgs::containsKey);
210-
if (allNestedParamsPresent) {
211-
return nestedArgs;
212-
}
213-
}
214-
}
225+
// If no processing worked, return original args and let ADK handle the error
226+
return args;
227+
}
215228

216-
// Check if we have a single parameter function and got a direct value
217-
if (expectedParams.size() == 1) {
218-
String expectedParam = expectedParams.iterator().next();
219-
if (args.size() == 1 && !args.containsKey(expectedParam)) {
220-
// Try to map the single value to the expected parameter name
221-
Object singleValue = args.values().iterator().next();
222-
return Map.of(expectedParam, singleValue);
223-
}
229+
private Map<String, Object> normalizeArguments(
230+
Map<String, Object> args, Set<String> expectedParams) {
231+
// Check if all expected parameters are present at the top level
232+
boolean allParamsPresent = expectedParams.stream().allMatch(args::containsKey);
233+
if (allParamsPresent) {
234+
return args;
235+
}
236+
237+
// Check if arguments are nested under a single key (common pattern)
238+
if (args.size() == 1) {
239+
var singleValue = args.values().iterator().next();
240+
if (singleValue instanceof Map) {
241+
@SuppressWarnings("unchecked")
242+
Map<String, Object> nestedArgs = (Map<String, Object>) singleValue;
243+
boolean allNestedParamsPresent = expectedParams.stream().allMatch(nestedArgs::containsKey);
244+
if (allNestedParamsPresent) {
245+
return nestedArgs;
224246
}
225247
}
226248
}
227249

228-
// If no processing worked, return original args and let ADK handle the error
250+
// Check if we have a single parameter function and got a direct value
251+
if (expectedParams.size() == 1) {
252+
String expectedParam = expectedParams.iterator().next();
253+
if (args.size() == 1 && !args.containsKey(expectedParam)) {
254+
Object singleValue = args.values().iterator().next();
255+
return Map.of(expectedParam, singleValue);
256+
}
257+
}
258+
229259
return args;
230260
}
231261

contrib/spring-ai/src/test/java/com/google/adk/models/springai/ToolConverterArgumentProcessingTest.java

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,90 @@ private Map<String, Object> invokeProcessArguments(
115115
return (Map<String, Object>) method.invoke(converter, args, declaration);
116116
}
117117

118+
@Test
119+
void testArgumentProcessingWithParametersJsonSchema_correctFormat() throws Exception {
120+
ToolConverter converter = new ToolConverter();
121+
Method processArguments = getProcessArgumentsMethod(converter);
122+
123+
com.google.genai.types.FunctionDeclaration declaration =
124+
com.google.genai.types.FunctionDeclaration.builder()
125+
.name("getWeatherInfo")
126+
.description("Get weather information")
127+
.parametersJsonSchema(
128+
Map.of(
129+
"type", "object", "properties", Map.of("location", Map.of("type", "string"))))
130+
.build();
131+
132+
Map<String, Object> correctArgs = Map.of("location", "San Francisco");
133+
Map<String, Object> processedArgs =
134+
invokeProcessArguments(processArguments, converter, correctArgs, declaration);
135+
136+
assertThat(processedArgs).isEqualTo(correctArgs);
137+
}
138+
139+
@Test
140+
void testArgumentProcessingWithParametersJsonSchema_nestedFormat() throws Exception {
141+
ToolConverter converter = new ToolConverter();
142+
Method processArguments = getProcessArgumentsMethod(converter);
143+
144+
com.google.genai.types.FunctionDeclaration declaration =
145+
com.google.genai.types.FunctionDeclaration.builder()
146+
.name("getWeatherInfo")
147+
.description("Get weather information")
148+
.parametersJsonSchema(
149+
Map.of(
150+
"type", "object", "properties", Map.of("location", Map.of("type", "string"))))
151+
.build();
152+
153+
Map<String, Object> nestedArgs = Map.of("args", Map.of("location", "San Francisco"));
154+
Map<String, Object> processedArgs =
155+
invokeProcessArguments(processArguments, converter, nestedArgs, declaration);
156+
157+
assertThat(processedArgs).containsEntry("location", "San Francisco");
158+
}
159+
160+
@Test
161+
void testArgumentProcessingWithParametersJsonSchema_directValue() throws Exception {
162+
ToolConverter converter = new ToolConverter();
163+
Method processArguments = getProcessArgumentsMethod(converter);
164+
165+
com.google.genai.types.FunctionDeclaration declaration =
166+
com.google.genai.types.FunctionDeclaration.builder()
167+
.name("getWeatherInfo")
168+
.description("Get weather information")
169+
.parametersJsonSchema(
170+
Map.of(
171+
"type", "object", "properties", Map.of("location", Map.of("type", "string"))))
172+
.build();
173+
174+
Map<String, Object> directValueArgs = Map.of("value", "San Francisco");
175+
Map<String, Object> processedArgs =
176+
invokeProcessArguments(processArguments, converter, directValueArgs, declaration);
177+
178+
assertThat(processedArgs).containsEntry("location", "San Francisco");
179+
}
180+
181+
@Test
182+
void testArgumentProcessingWithParametersJsonSchema_noMatch() throws Exception {
183+
ToolConverter converter = new ToolConverter();
184+
Method processArguments = getProcessArgumentsMethod(converter);
185+
186+
com.google.genai.types.FunctionDeclaration declaration =
187+
com.google.genai.types.FunctionDeclaration.builder()
188+
.name("getWeatherInfo")
189+
.description("Get weather information")
190+
.parametersJsonSchema(
191+
Map.of(
192+
"type", "object", "properties", Map.of("location", Map.of("type", "string"))))
193+
.build();
194+
195+
Map<String, Object> wrongArgs = Map.of("city", "San Francisco", "country", "USA");
196+
Map<String, Object> processedArgs =
197+
invokeProcessArguments(processArguments, converter, wrongArgs, declaration);
198+
199+
assertThat(processedArgs).isEqualTo(wrongArgs);
200+
}
201+
118202
public static class WeatherTools {
119203
public static Map<String, Object> getWeatherInfo(String location) {
120204
return Map.of(

0 commit comments

Comments
 (0)