1
+ package ai.koog.integration.tests
2
+
3
+ import ai.koog.agents.core.agent.AIAgent
4
+ import ai.koog.agents.core.tools.SimpleTool
5
+ import ai.koog.agents.core.tools.ToolArgs
6
+ import ai.koog.agents.core.tools.ToolDescriptor
7
+ import ai.koog.agents.core.tools.ToolParameterDescriptor
8
+ import ai.koog.agents.core.tools.ToolParameterType
9
+ import ai.koog.agents.core.tools.ToolRegistry
10
+ import ai.koog.agents.features.eventHandler.feature.EventHandler
11
+ import ai.koog.integration.tests.utils.TestUtils.readTestAnthropicKeyFromEnv
12
+ import ai.koog.prompt.executor.clients.anthropic.AnthropicModels
13
+ import ai.koog.prompt.executor.llms.all.simpleAnthropicExecutor
14
+ import kotlinx.coroutines.runBlocking
15
+ import kotlinx.serialization.Serializable
16
+ import org.junit.jupiter.api.BeforeAll
17
+ import org.junit.jupiter.api.Test
18
+ import org.junit.jupiter.api.Assumptions.assumeTrue
19
+ import kotlin.test.assertNotNull
20
+ import kotlin.test.assertTrue
21
+
22
+ /* *
23
+ * Integration test for verifying the fix for the Anthropic API JSON schema validation error
24
+ * when using complex nested structures in tool parameters.
25
+ *
26
+ * The issue was in the AnthropicLLMClient.kt file, specifically in the getTypeMapForParameter() function
27
+ * that converts ToolDescriptor objects to JSON schemas for the Anthropic API.
28
+ *
29
+ * The problem was that when processing ToolParameterType.Object, the function created invalid nested structures
30
+ * by placing type information under a "type" key, resulting in invalid schema structures like:
31
+ * {
32
+ * "type": {"type": "string"} // Invalid nesting
33
+ * }
34
+ *
35
+ * This test verifies that the fix works by creating an agent with the Anthropic API and a tool
36
+ * with complex nested structures, and then running it with a sample input.
37
+ */
38
+ class AnthropicSchemaValidationIntegrationTest {
39
+
40
+ companion object {
41
+ private var anthropicApiKey: String? = null
42
+ private var apiKeyAvailable = false
43
+
44
+ @BeforeAll
45
+ @JvmStatic
46
+ fun setup () {
47
+ try {
48
+ anthropicApiKey = readTestAnthropicKeyFromEnv()
49
+ // Check that the API key is not empty or blank
50
+ apiKeyAvailable = ! anthropicApiKey.isNullOrBlank()
51
+ if (! apiKeyAvailable) {
52
+ println (" Anthropic API key is empty or blank" )
53
+ println (" Tests requiring Anthropic API will be skipped" )
54
+ }
55
+ } catch (e: Exception ) {
56
+ println (" Anthropic API key not available: ${e.message} " )
57
+ println (" Tests requiring Anthropic API will be skipped" )
58
+ apiKeyAvailable = false
59
+ }
60
+ }
61
+ }
62
+
63
+ /* *
64
+ * Address type enum.
65
+ */
66
+ @Serializable
67
+ enum class AddressType {
68
+ HOME , WORK , OTHER
69
+ }
70
+
71
+ /* *
72
+ * An address with multiple fields.
73
+ */
74
+ @Serializable
75
+ data class Address (
76
+ val type : AddressType ,
77
+ val street : String ,
78
+ val city : String ,
79
+ val state : String ,
80
+ val zipCode : String
81
+ )
82
+
83
+ /* *
84
+ * A user profile with nested structures.
85
+ */
86
+ @Serializable
87
+ data class UserProfile (
88
+ val name : String ,
89
+ val email : String ,
90
+ val addresses : List <Address >
91
+ )
92
+
93
+ /* *
94
+ * Arguments for the complex nested tool.
95
+ */
96
+ @Serializable
97
+ data class ComplexNestedToolArgs (
98
+ val profile : UserProfile
99
+ ) : ToolArgs
100
+
101
+ /* *
102
+ * A complex nested tool that demonstrates the JSON schema validation error.
103
+ * This tool has parameters with complex nested structures that would trigger
104
+ * the error in the Anthropic API before the fix.
105
+ */
106
+ object ComplexNestedTool : SimpleTool<ComplexNestedToolArgs>() {
107
+ override val argsSerializer = ComplexNestedToolArgs .serializer()
108
+
109
+ override val descriptor = ToolDescriptor (
110
+ name = " complex_nested_tool" ,
111
+ description = " A tool that processes user profiles with complex nested structures." ,
112
+ requiredParameters = listOf (
113
+ ToolParameterDescriptor (
114
+ name = " profile" ,
115
+ description = " The user profile to process" ,
116
+ type = ToolParameterType .Object (
117
+ properties = listOf (
118
+ ToolParameterDescriptor (
119
+ name = " name" ,
120
+ description = " The user's full name" ,
121
+ type = ToolParameterType .String
122
+ ),
123
+ ToolParameterDescriptor (
124
+ name = " email" ,
125
+ description = " The user's email address" ,
126
+ type = ToolParameterType .String
127
+ ),
128
+ ToolParameterDescriptor (
129
+ name = " addresses" ,
130
+ description = " The user's addresses" ,
131
+ type = ToolParameterType .List (
132
+ ToolParameterType .Object (
133
+ properties = listOf (
134
+ ToolParameterDescriptor (
135
+ name = " type" ,
136
+ description = " The type of address (HOME, WORK, or OTHER)" ,
137
+ type = ToolParameterType .Enum (AddressType .entries.map { it.name }.toTypedArray())
138
+ ),
139
+ ToolParameterDescriptor (
140
+ name = " street" ,
141
+ description = " The street address" ,
142
+ type = ToolParameterType .String
143
+ ),
144
+ ToolParameterDescriptor (
145
+ name = " city" ,
146
+ description = " The city" ,
147
+ type = ToolParameterType .String
148
+ ),
149
+ ToolParameterDescriptor (
150
+ name = " state" ,
151
+ description = " The state or province" ,
152
+ type = ToolParameterType .String
153
+ ),
154
+ ToolParameterDescriptor (
155
+ name = " zipCode" ,
156
+ description = " The ZIP or postal code" ,
157
+ type = ToolParameterType .String
158
+ )
159
+ ),
160
+ requiredProperties = listOf (" type" , " street" , " city" , " state" , " zipCode" )
161
+ )
162
+ )
163
+ )
164
+ ),
165
+ requiredProperties = listOf (" name" , " email" , " addresses" )
166
+ )
167
+ )
168
+ )
169
+ )
170
+
171
+ override suspend fun doExecute (args : ComplexNestedToolArgs ): String {
172
+ // Process the user profile
173
+ val profile = args.profile
174
+ val addressesInfo = profile.addresses.joinToString(" \n " ) { address ->
175
+ " - ${address.type} Address: ${address.street} , ${address.city} , ${address.state} ${address.zipCode} "
176
+ }
177
+
178
+ return """
179
+ Successfully processed user profile:
180
+ Name: ${profile.name}
181
+ Email: ${profile.email}
182
+ Addresses:
183
+ $addressesInfo
184
+ """ .trimIndent()
185
+ }
186
+ }
187
+
188
+ /* *
189
+ * Test that verifies the fix for the Anthropic API JSON schema validation error
190
+ * when using complex nested structures in tool parameters.
191
+ *
192
+ * Before the fix, this test would fail with an error like:
193
+ * "tools.0.custom.input_schema: JSON schema is invalid. It must match JSON Schema draft 2020-12"
194
+ *
195
+ * After the fix, the test should pass, demonstrating that the Anthropic API
196
+ * can now correctly handle complex nested structures in tool parameters.
197
+ *
198
+ * Note: This test requires a valid Anthropic API key to be set in the environment variable
199
+ * ANTHROPIC_API_TEST_KEY. If the key is not available, the test will be skipped.
200
+ */
201
+ @Test
202
+ fun integration_testAnthropicComplexNestedStructures () {
203
+ // Skip the test if the Anthropic API key is not available
204
+ assumeTrue(apiKeyAvailable, " Anthropic API key is not available" )
205
+
206
+ runBlocking<Unit > {
207
+ // Create an agent with the Anthropic API and the complex nested tool
208
+ val agent = AIAgent (
209
+ executor = simpleAnthropicExecutor(anthropicApiKey!! ),
210
+ llmModel = AnthropicModels .Sonnet_3_7 ,
211
+ systemPrompt = " You are a helpful assistant that can process user profiles. Please use the complex_nested_tool to process the user profile I provide." ,
212
+ toolRegistry = ToolRegistry {
213
+ tool(ComplexNestedTool )
214
+ },
215
+ installFeatures = {
216
+ install(EventHandler ) {
217
+ onAgentRunError { eventContext ->
218
+ println (" ERROR: ${eventContext.throwable.javaClass.simpleName} (${eventContext.throwable.message} )" )
219
+ println (eventContext.throwable.stackTraceToString())
220
+ true
221
+ }
222
+ onToolCall { eventContext ->
223
+ println (" Calling tool: ${eventContext.tool.name} " )
224
+ println (" Arguments: ${eventContext.toolArgs.toString().take(100 )} ..." )
225
+ }
226
+ }
227
+ }
228
+ )
229
+
230
+ // Run the agent with a request to process a user profile
231
+ val result = agent.run ("""
232
+ Please process this user profile:
233
+
234
+ Name: John Doe
235
+
236
+ Addresses:
237
+ 1. HOME: 123 Main St, Springfield, IL 62701
238
+ 2. WORK: 456 Business Ave, Springfield, IL 62701
239
+ """ .trimIndent())
240
+
241
+ // Verify the result
242
+ println (" \n Result: $result " )
243
+ assertNotNull(result, " Result should not be null" )
244
+ assertTrue(result.isNotBlank(), " Result should not be empty or blank" )
245
+
246
+ // Check that the result contains expected information
247
+ assertTrue(result.lowercase().contains(" john doe" ), " Result should contain the user's name" )
248
+ assertTrue(result.lowercase().contains(
" [email protected] " ),
" Result should contain the user's email" )
249
+ assertTrue(result.lowercase().contains(" main st" ), " Result should contain the home address street" )
250
+ assertTrue(result.lowercase().contains(" business ave" ), " Result should contain the work address street" )
251
+ }
252
+ }
253
+ }
0 commit comments