diff --git a/spring-ai-model/src/test/java/org/springframework/ai/tool/observation/DefaultToolCallingObservationConventionTests.java b/spring-ai-model/src/test/java/org/springframework/ai/tool/observation/DefaultToolCallingObservationConventionTests.java index 788472384a4..46727be7ab0 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/tool/observation/DefaultToolCallingObservationConventionTests.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/tool/observation/DefaultToolCallingObservationConventionTests.java @@ -98,4 +98,47 @@ void shouldHaveHighCardinalityKeyValues() { "{}")); } + @Test + void shouldHaveAllStandardLowCardinalityKeys() { + ToolCallingObservationContext observationContext = ToolCallingObservationContext.builder() + .toolDefinition(ToolDefinition.builder().name("tool").description("Tool").inputSchema("{}").build()) + .toolCallArguments("args") + .build(); + + var lowCardinalityKeys = this.observationConvention.getLowCardinalityKeyValues(observationContext); + + // Verify all expected low cardinality keys are present + assertThat(lowCardinalityKeys).extracting(KeyValue::getKey) + .contains(ToolCallingObservationDocumentation.LowCardinalityKeyNames.TOOL_DEFINITION_NAME.asString(), + ToolCallingObservationDocumentation.LowCardinalityKeyNames.AI_OPERATION_TYPE.asString(), + ToolCallingObservationDocumentation.LowCardinalityKeyNames.AI_PROVIDER.asString(), + ToolCallingObservationDocumentation.LowCardinalityKeyNames.SPRING_AI_KIND.asString()); + } + + @Test + void shouldHandleNullContext() { + assertThat(this.observationConvention.supportsContext(null)).isFalse(); + } + + @Test + void shouldBeConsistentAcrossMultipleCalls() { + ToolCallingObservationContext observationContext = ToolCallingObservationContext.builder() + .toolDefinition(ToolDefinition.builder() + .name("consistentTool") + .description("Consistent description") + .inputSchema("{}") + .build()) + .toolCallArguments("args") + .build(); + + // Call multiple times and verify consistency + String name1 = this.observationConvention.getContextualName(observationContext); + String name2 = this.observationConvention.getContextualName(observationContext); + var lowCard1 = this.observationConvention.getLowCardinalityKeyValues(observationContext); + var lowCard2 = this.observationConvention.getLowCardinalityKeyValues(observationContext); + + assertThat(name1).isEqualTo(name2); + assertThat(lowCard1).isEqualTo(lowCard2); + } + } diff --git a/spring-ai-model/src/test/java/org/springframework/ai/tool/observation/ToolCallingContentObservationFilterTests.java b/spring-ai-model/src/test/java/org/springframework/ai/tool/observation/ToolCallingContentObservationFilterTests.java index f64ab4010ed..c10a144b9e4 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/tool/observation/ToolCallingContentObservationFilterTests.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/tool/observation/ToolCallingContentObservationFilterTests.java @@ -74,4 +74,62 @@ void augmentContextWhenNullResult() { .isEmpty(); } + @Test + void whenToolCallArgumentsIsEmptyStringThenHighCardinalityKeyValueIsEmpty() { + var originalContext = ToolCallingObservationContext.builder() + .toolDefinition(ToolDefinition.builder().name("toolA").description("description").inputSchema("{}").build()) + .toolCallArguments("") + .toolCallResult("result") + .build(); + var augmentedContext = this.observationFilter.map(originalContext); + + assertThat(augmentedContext.getHighCardinalityKeyValues()).contains(KeyValue + .of(ToolCallingObservationDocumentation.HighCardinalityKeyNames.TOOL_CALL_ARGUMENTS.asString(), "")); + assertThat(augmentedContext.getHighCardinalityKeyValues()).contains(KeyValue + .of(ToolCallingObservationDocumentation.HighCardinalityKeyNames.TOOL_CALL_RESULT.asString(), "result")); + } + + @Test + void whenToolCallResultIsEmptyStringThenHighCardinalityKeyValueIsEmpty() { + var originalContext = ToolCallingObservationContext.builder() + .toolDefinition(ToolDefinition.builder().name("toolA").description("description").inputSchema("{}").build()) + .toolCallArguments("input") + .toolCallResult("") + .build(); + var augmentedContext = this.observationFilter.map(originalContext); + + assertThat(augmentedContext.getHighCardinalityKeyValues()).contains(KeyValue + .of(ToolCallingObservationDocumentation.HighCardinalityKeyNames.TOOL_CALL_ARGUMENTS.asString(), "input")); + assertThat(augmentedContext.getHighCardinalityKeyValues()).contains(KeyValue + .of(ToolCallingObservationDocumentation.HighCardinalityKeyNames.TOOL_CALL_RESULT.asString(), "")); + } + + @Test + void whenFilterAppliedMultipleTimesThenIdempotent() { + var originalContext = ToolCallingObservationContext.builder() + .toolDefinition(ToolDefinition.builder().name("toolA").description("description").inputSchema("{}").build()) + .toolCallArguments("input") + .toolCallResult("result") + .build(); + + var augmentedOnce = this.observationFilter.map(originalContext); + var augmentedTwice = this.observationFilter.map(augmentedOnce); + + // Count occurrences of each key + long argumentsCount = augmentedTwice.getHighCardinalityKeyValues() + .stream() + .filter(kv -> kv.getKey() + .equals(ToolCallingObservationDocumentation.HighCardinalityKeyNames.TOOL_CALL_ARGUMENTS.asString())) + .count(); + long resultCount = augmentedTwice.getHighCardinalityKeyValues() + .stream() + .filter(kv -> kv.getKey() + .equals(ToolCallingObservationDocumentation.HighCardinalityKeyNames.TOOL_CALL_RESULT.asString())) + .count(); + + // Should not duplicate keys + assertThat(argumentsCount).isEqualTo(1); + assertThat(resultCount).isEqualTo(1); + } + } diff --git a/spring-ai-model/src/test/java/org/springframework/ai/tool/observation/ToolCallingObservationContextTests.java b/spring-ai-model/src/test/java/org/springframework/ai/tool/observation/ToolCallingObservationContextTests.java index 44f3aabaf6d..f888cce6703 100644 --- a/spring-ai-model/src/test/java/org/springframework/ai/tool/observation/ToolCallingObservationContextTests.java +++ b/spring-ai-model/src/test/java/org/springframework/ai/tool/observation/ToolCallingObservationContextTests.java @@ -74,4 +74,50 @@ void whenToolMetadataIsNullThenThrow() { .build()).isInstanceOf(IllegalArgumentException.class).hasMessageContaining("toolMetadata cannot be null"); } + @Test + void whenToolArgumentsIsEmptyStringThenReturnEmptyString() { + var observationContext = ToolCallingObservationContext.builder() + .toolDefinition(ToolDefinition.builder().name("toolA").description("description").inputSchema("{}").build()) + .toolCallArguments("") + .build(); + assertThat(observationContext).isNotNull(); + assertThat(observationContext.getToolCallArguments()).isEqualTo(""); + } + + @Test + void whenToolCallResultIsNullThenReturnNull() { + var observationContext = ToolCallingObservationContext.builder() + .toolDefinition(ToolDefinition.builder().name("toolA").description("description").inputSchema("{}").build()) + .toolCallResult(null) + .build(); + assertThat(observationContext).isNotNull(); + assertThat(observationContext.getToolCallResult()).isNull(); + } + + @Test + void whenToolCallResultIsEmptyStringThenReturnEmptyString() { + var observationContext = ToolCallingObservationContext.builder() + .toolDefinition(ToolDefinition.builder().name("toolA").description("description").inputSchema("{}").build()) + .toolCallResult("") + .build(); + assertThat(observationContext).isNotNull(); + assertThat(observationContext.getToolCallResult()).isEqualTo(""); + } + + @Test + void whenToolDefinitionIsSetThenGetReturnsIt() { + var toolDef = ToolDefinition.builder() + .name("testTool") + .description("Test description") + .inputSchema("{\"type\": \"object\"}") + .build(); + + var observationContext = ToolCallingObservationContext.builder().toolDefinition(toolDef).build(); + + assertThat(observationContext.getToolDefinition()).isEqualTo(toolDef); + assertThat(observationContext.getToolDefinition().name()).isEqualTo("testTool"); + assertThat(observationContext.getToolDefinition().description()).isEqualTo("Test description"); + assertThat(observationContext.getToolDefinition().inputSchema()).isEqualTo("{\"type\": \"object\"}"); + } + }