diff --git a/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatModel.java b/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatModel.java index fc4da3e8bd6..d167bcce1bc 100644 --- a/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatModel.java +++ b/models/spring-ai-anthropic/src/main/java/org/springframework/ai/anthropic/AnthropicChatModel.java @@ -23,12 +23,14 @@ import java.util.Map; import java.util.Set; import java.util.stream.Collectors; +import java.util.stream.Stream; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ai.agent.Agent; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -235,6 +237,22 @@ public ChatResponse call(Prompt prompt) { if (!isProxyToolCalls(prompt, this.defaultOptions) && response != null && this.isToolCall(response, Set.of("tool_use"))) { var toolCallConversation = handleToolCalls(prompt, response); + // process agent transfer + ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolCallConversation + .get(toolCallConversation.size() - 1); + List agentList = toolResponseMessage.getResponses().stream().flatMap(toolResponse -> { + if (null == toolResponse.agent()) { + return Stream.empty(); + } + else { + return Stream.of(toolResponse.agent()); + } + }).toList(); + if (!CollectionUtils.isEmpty(agentList)) { + Agent agent = agentList.get(0); + // transfer task to other agent + return agent.call(prompt); + } return this.call(new Prompt(toolCallConversation, prompt.getOptions())); } @@ -266,6 +284,22 @@ public Flux stream(Prompt prompt) { if (!isProxyToolCalls(prompt, this.defaultOptions) && this.isToolCall(chatResponse, Set.of("tool_use"))) { var toolCallConversation = handleToolCalls(prompt, chatResponse); +// process agent transfer + ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolCallConversation.get(toolCallConversation.size() - 1); + List agentList = toolResponseMessage.getResponses() + .stream() + .flatMap(toolResponse -> { + if (null == toolResponse.agent()) { + return Stream.empty(); + } else { + return Stream.of(toolResponse.agent()); + } + }).toList(); + if(!CollectionUtils.isEmpty(agentList)) { + Agent agent = agentList.get(0); +// transfer task to other agent + return agent.stream(prompt); + } return this.stream(new Prompt(toolCallConversation, prompt.getOptions())); } diff --git a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatModel.java b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatModel.java index 9add97589e6..659ca27c1c7 100644 --- a/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatModel.java +++ b/models/spring-ai-azure-openai/src/main/java/org/springframework/ai/azure/openai/AzureOpenAiChatModel.java @@ -26,6 +26,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; +import java.util.stream.Stream; import com.azure.ai.openai.OpenAIAsyncClient; import com.azure.ai.openai.OpenAIClient; @@ -57,6 +58,7 @@ import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; +import org.springframework.ai.agent.Agent; import reactor.core.publisher.Flux; import org.springframework.ai.azure.openai.metadata.AzureOpenAiUsage; @@ -218,6 +220,22 @@ public ChatResponse call(Prompt prompt) { if (!isProxyToolCalls(prompt, this.defaultOptions) && isToolCall(response, Set.of(String.valueOf(CompletionsFinishReason.TOOL_CALLS).toLowerCase()))) { var toolCallConversation = handleToolCalls(prompt, response); + // process agent transfer + ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolCallConversation + .get(toolCallConversation.size() - 1); + List agentList = toolResponseMessage.getResponses().stream().flatMap(toolResponse -> { + if (null == toolResponse.agent()) { + return Stream.empty(); + } + else { + return Stream.of(toolResponse.agent()); + } + }).toList(); + if (!CollectionUtils.isEmpty(agentList)) { + Agent agent = agentList.get(0); + // transfer task to other agent + return agent.call(prompt); + } // Recursively call the call method with the tool call message // conversation that contains the call responses. return this.call(new Prompt(toolCallConversation, prompt.getOptions())); @@ -282,6 +300,22 @@ public Flux stream(Prompt prompt) { if (!isProxyToolCalls(prompt, this.defaultOptions) && isToolCall(chatResponse, Set.of(String.valueOf(CompletionsFinishReason.TOOL_CALLS).toLowerCase()))) { var toolCallConversation = handleToolCalls(prompt, chatResponse); + // process agent transfer + ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolCallConversation + .get(toolCallConversation.size() - 1); + List agentList = toolResponseMessage.getResponses().stream().flatMap(toolResponse -> { + if (null == toolResponse.agent()) { + return Stream.empty(); + } + else { + return Stream.of(toolResponse.agent()); + } + }).toList(); + if (!CollectionUtils.isEmpty(agentList)) { + Agent agent = agentList.get(0); + // transfer task to other agent + return agent.stream(prompt); + } // Recursively call the call method with the tool call message // conversation that contains the call responses. return this.stream(new Prompt(toolCallConversation, prompt.getOptions())); diff --git a/models/spring-ai-bedrock-converse/src/main/java/org/springframework/ai/bedrock/converse/BedrockProxyChatModel.java b/models/spring-ai-bedrock-converse/src/main/java/org/springframework/ai/bedrock/converse/BedrockProxyChatModel.java index c437786dd5e..482d0d18eda 100644 --- a/models/spring-ai-bedrock-converse/src/main/java/org/springframework/ai/bedrock/converse/BedrockProxyChatModel.java +++ b/models/spring-ai-bedrock-converse/src/main/java/org/springframework/ai/bedrock/converse/BedrockProxyChatModel.java @@ -26,12 +26,14 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Stream; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ai.agent.Agent; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; import reactor.core.publisher.Sinks; @@ -195,6 +197,22 @@ public ChatResponse call(Prompt prompt) { if (!this.isProxyToolCalls(prompt, this.defaultOptions) && chatResponse != null && this.isToolCall(chatResponse, Set.of("tool_use"))) { var toolCallConversation = this.handleToolCalls(prompt, chatResponse); + // process agent transfer + ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolCallConversation + .get(toolCallConversation.size() - 1); + List agentList = toolResponseMessage.getResponses().stream().flatMap(toolResponse -> { + if (null == toolResponse.agent()) { + return Stream.empty(); + } + else { + return Stream.of(toolResponse.agent()); + } + }).toList(); + if (!CollectionUtils.isEmpty(agentList)) { + Agent agent = agentList.get(0); + // transfer task to other agent + return agent.call(prompt); + } return this.call(new Prompt(toolCallConversation, prompt.getOptions())); } @@ -510,6 +528,22 @@ public Flux stream(Prompt prompt) { if (!this.isProxyToolCalls(prompt, this.defaultOptions) && chatResponse != null && this.isToolCall(chatResponse, Set.of("tool_use"))) { var toolCallConversation = this.handleToolCalls(prompt, chatResponse); + // process agent transfer + ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolCallConversation + .get(toolCallConversation.size() - 1); + List agentList = toolResponseMessage.getResponses().stream().flatMap(toolResponse -> { + if (null == toolResponse.agent()) { + return Stream.empty(); + } + else { + return Stream.of(toolResponse.agent()); + } + }).toList(); + if (!CollectionUtils.isEmpty(agentList)) { + Agent agent = agentList.get(0); + // transfer task to other agent + return agent.stream(prompt); + } return this.stream(new Prompt(toolCallConversation, prompt.getOptions())); } return Mono.just(chatResponse); diff --git a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxChatModel.java b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxChatModel.java index f32fe5d3c5a..5b557a8fde2 100644 --- a/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxChatModel.java +++ b/models/spring-ai-minimax/src/main/java/org/springframework/ai/minimax/MiniMaxChatModel.java @@ -22,12 +22,14 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ai.agent.Agent; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -269,6 +271,22 @@ else if (!CollectionUtils.isEmpty(choice.messages())) { if (!isProxyToolCalls(prompt, this.defaultOptions) && isToolCall(response, Set.of(ChatCompletionFinishReason.TOOL_CALLS.name(), ChatCompletionFinishReason.STOP.name()))) { var toolCallConversation = handleToolCalls(prompt, response); + // process agent transfer + ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolCallConversation + .get(toolCallConversation.size() - 1); + List agentList = toolResponseMessage.getResponses().stream().flatMap(toolResponse -> { + if (null == toolResponse.agent()) { + return Stream.empty(); + } + else { + return Stream.of(toolResponse.agent()); + } + }).toList(); + if (!CollectionUtils.isEmpty(agentList)) { + Agent agent = agentList.get(0); + // transfer task to other agent + return agent.call(prompt); + } // Recursively call the call method with the tool call message // conversation that contains the call responses. return this.call(new Prompt(toolCallConversation, prompt.getOptions())); @@ -337,6 +355,22 @@ public Flux stream(Prompt prompt) { if (!isProxyToolCalls(prompt, this.defaultOptions) && isToolCall(response, Set.of(ChatCompletionFinishReason.TOOL_CALLS.name(), ChatCompletionFinishReason.STOP.name()))) { var toolCallConversation = handleToolCalls(prompt, response); + // process agent transfer + ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolCallConversation.get(toolCallConversation.size() - 1); + List agentList = toolResponseMessage.getResponses() + .stream() + .flatMap(toolResponse -> { + if (null == toolResponse.agent()) { + return Stream.empty(); + } else { + return Stream.of(toolResponse.agent()); + } + }).toList(); + if(!CollectionUtils.isEmpty(agentList)) { + Agent agent = agentList.get(0); + // transfer task to other agent + return agent.stream(prompt); + } // Recursively call the stream method with the tool call message // conversation that contains the call responses. return this.stream(new Prompt(toolCallConversation, prompt.getOptions())); diff --git a/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiChatModel.java b/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiChatModel.java index 25657ec39b1..3d7cc92896f 100644 --- a/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiChatModel.java +++ b/models/spring-ai-mistral-ai/src/main/java/org/springframework/ai/mistralai/MistralAiChatModel.java @@ -21,12 +21,14 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ai.agent.Agent; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -204,6 +206,22 @@ public ChatResponse call(Prompt prompt) { && isToolCall(response, Set.of(MistralAiApi.ChatCompletionFinishReason.TOOL_CALLS.name(), MistralAiApi.ChatCompletionFinishReason.STOP.name()))) { var toolCallConversation = handleToolCalls(prompt, response); + // process agent transfer + ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolCallConversation + .get(toolCallConversation.size() - 1); + List agentList = toolResponseMessage.getResponses().stream().flatMap(toolResponse -> { + if (null == toolResponse.agent()) { + return Stream.empty(); + } + else { + return Stream.of(toolResponse.agent()); + } + }).toList(); + if (!CollectionUtils.isEmpty(agentList)) { + Agent agent = agentList.get(0); + // transfer task to other agent + return agent.call(prompt); + } // Recursively call the call method with the tool call message // conversation that contains the call responses. return this.call(new Prompt(toolCallConversation, prompt.getOptions())); @@ -275,6 +293,22 @@ public Flux stream(Prompt prompt) { Flux chatResponseFlux = chatResponse.flatMap(response -> { if (!isProxyToolCalls(prompt, this.defaultOptions) && isToolCall(response, Set.of(MistralAiApi.ChatCompletionFinishReason.TOOL_CALLS.name()))) { var toolCallConversation = handleToolCalls(prompt, response); + // process agent transfer + ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolCallConversation.get(toolCallConversation.size()-1); + List agentList = toolResponseMessage.getResponses() + .stream() + .flatMap(toolResponse -> { + if (null == toolResponse.agent()) { + return Stream.empty(); + } else { + return Stream.of(toolResponse.agent()); + } + }).toList(); + if(!CollectionUtils.isEmpty(agentList)) { + Agent agent = agentList.get(0); + // transfer task to other agent + return agent.stream(prompt); + } // Recursively call the stream method with the tool call message // conversation that contains the call responses. return this.stream(new Prompt(toolCallConversation, prompt.getOptions())); diff --git a/models/spring-ai-moonshot/src/main/java/org/springframework/ai/moonshot/MoonshotChatModel.java b/models/spring-ai-moonshot/src/main/java/org/springframework/ai/moonshot/MoonshotChatModel.java index 09c18bdd54b..32375d8042d 100644 --- a/models/spring-ai-moonshot/src/main/java/org/springframework/ai/moonshot/MoonshotChatModel.java +++ b/models/spring-ai-moonshot/src/main/java/org/springframework/ai/moonshot/MoonshotChatModel.java @@ -21,12 +21,14 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ai.agent.Agent; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -229,6 +231,22 @@ public ChatResponse call(Prompt prompt) { && isToolCall(response, Set.of(MoonshotApi.ChatCompletionFinishReason.TOOL_CALLS.name(), MoonshotApi.ChatCompletionFinishReason.STOP.name()))) { var toolCallConversation = handleToolCalls(prompt, response); + // process agent transfer + ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolCallConversation + .get(toolCallConversation.size() - 1); + List agentList = toolResponseMessage.getResponses().stream().flatMap(toolResponse -> { + if (null == toolResponse.agent()) { + return Stream.empty(); + } + else { + return Stream.of(toolResponse.agent()); + } + }).toList(); + if (!CollectionUtils.isEmpty(agentList)) { + Agent agent = agentList.get(0); + // transfer task to other agent + return agent.call(prompt); + } // Recursively call the call method with the tool call message // conversation that contains the call responses. return this.call(new Prompt(toolCallConversation, prompt.getOptions())); @@ -300,6 +318,22 @@ public Flux stream(Prompt prompt) { if (!isProxyToolCalls(prompt, this.defaultOptions) && isToolCall(response, Set.of(ChatCompletionFinishReason.TOOL_CALLS.name(), ChatCompletionFinishReason.STOP.name()))) { var toolCallConversation = handleToolCalls(prompt, response); + // process agent transfer + ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolCallConversation + .get(toolCallConversation.size() - 1); + List agentList = toolResponseMessage.getResponses().stream().flatMap(toolResponse -> { + if (null == toolResponse.agent()) { + return Stream.empty(); + } + else { + return Stream.of(toolResponse.agent()); + } + }).toList(); + if (!CollectionUtils.isEmpty(agentList)) { + Agent agent = agentList.get(0); + // transfer task to other agent + return agent.stream(prompt); + } // Recursively call the stream method with the tool call message // conversation that contains the call responses. return this.stream(new Prompt(toolCallConversation, prompt.getOptions())); diff --git a/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/OllamaChatModel.java b/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/OllamaChatModel.java index f4fcd722f15..9051dc6b957 100644 --- a/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/OllamaChatModel.java +++ b/models/spring-ai-ollama/src/main/java/org/springframework/ai/ollama/OllamaChatModel.java @@ -21,16 +21,15 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Stream; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; +import org.springframework.ai.agent.Agent; +import org.springframework.ai.chat.messages.*; import reactor.core.publisher.Flux; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.SystemMessage; -import org.springframework.ai.chat.messages.ToolResponseMessage; -import org.springframework.ai.chat.messages.UserMessage; import org.springframework.ai.chat.metadata.ChatGenerationMetadata; import org.springframework.ai.chat.metadata.ChatResponseMetadata; import org.springframework.ai.chat.model.AbstractToolCallSupport; @@ -170,6 +169,23 @@ public ChatResponse call(Prompt prompt) { if (!isProxyToolCalls(prompt, this.defaultOptions) && response != null && isToolCall(response, Set.of("stop"))) { var toolCallConversation = handleToolCalls(prompt, response); + + // process agent transfer + ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolCallConversation + .get(toolCallConversation.size() - 1); + List agentList = toolResponseMessage.getResponses().stream().flatMap(toolResponse -> { + if (null == toolResponse.agent()) { + return Stream.empty(); + } + else { + return Stream.of(toolResponse.agent()); + } + }).toList(); + if (!CollectionUtils.isEmpty(agentList)) { + Agent agent = agentList.get(0); + // transfer task to other agent + return agent.call(prompt); + } // Recursively call the call method with the tool call message // conversation that contains the call responses. return this.call(new Prompt(toolCallConversation, prompt.getOptions())); @@ -227,6 +243,22 @@ public Flux stream(Prompt prompt) { Flux chatResponseFlux = chatResponse.flatMap(response -> { if (isToolCall(response, Set.of("stop"))) { var toolCallConversation = handleToolCalls(prompt, response); + // process agent transfer + ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolCallConversation + .get(toolCallConversation.size() - 1); + List agentList = toolResponseMessage.getResponses().stream().flatMap(toolResponse -> { + if (null == toolResponse.agent()) { + return Stream.empty(); + } + else { + return Stream.of(toolResponse.agent()); + } + }).toList(); + if (!CollectionUtils.isEmpty(agentList)) { + Agent agent = agentList.get(0); + // transfer task to other agent + return agent.stream(prompt); + } // Recursively call the stream method with the tool call message // conversation that contains the call responses. return this.stream(new Prompt(toolCallConversation, prompt.getOptions())); diff --git a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatModel.java b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatModel.java index f21a064ee6b..ae1cafb1645 100644 --- a/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatModel.java +++ b/models/spring-ai-openai/src/main/java/org/springframework/ai/openai/OpenAiChatModel.java @@ -25,12 +25,14 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.stream.Collectors; +import java.util.stream.Stream; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ai.agent.Agent; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -267,6 +269,22 @@ public ChatResponse call(Prompt prompt) { && isToolCall(response, Set.of(OpenAiApi.ChatCompletionFinishReason.TOOL_CALLS.name(), OpenAiApi.ChatCompletionFinishReason.STOP.name()))) { var toolCallConversation = handleToolCalls(prompt, response); + // process agent transfer + ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolCallConversation + .get(toolCallConversation.size() - 1); + List agentList = toolResponseMessage.getResponses().stream().flatMap(toolResponse -> { + if (null == toolResponse.agent()) { + return Stream.empty(); + } + else { + return Stream.of(toolResponse.agent()); + } + }).toList(); + if (!CollectionUtils.isEmpty(agentList)) { + Agent agent = agentList.get(0); + // transfer task to other agent + return agent.call(prompt); + } // Recursively call the call method with the tool call message // conversation that contains the call responses. return this.call(new Prompt(toolCallConversation, prompt.getOptions())); @@ -337,6 +355,22 @@ public Flux stream(Prompt prompt) { if (!isProxyToolCalls(prompt, this.defaultOptions) && isToolCall(response, Set.of(OpenAiApi.ChatCompletionFinishReason.TOOL_CALLS.name(), OpenAiApi.ChatCompletionFinishReason.STOP.name()))) { var toolCallConversation = handleToolCalls(prompt, response); +// process agent transfer + ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolCallConversation.get(toolCallConversation.size()-1); + List agentList = toolResponseMessage.getResponses() + .stream() + .flatMap(toolResponse -> { + if (null == toolResponse.agent()) { + return Stream.empty(); + } else { + return Stream.of(toolResponse.agent()); + } + }).toList(); + if(!CollectionUtils.isEmpty(agentList)) { + Agent agent = agentList.get(0); +// transfer task to other agent + return agent.stream(prompt); + } // Recursively call the stream method with the tool call message // conversation that contains the call responses. return this.stream(new Prompt(toolCallConversation, prompt.getOptions())); diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelProxyToolCallsIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelProxyToolCallsIT.java index acaca68361b..cf8d3652475 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelProxyToolCallsIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/OpenAiChatModelProxyToolCallsIT.java @@ -171,7 +171,7 @@ void functionCall() throws JsonMappingException, JsonProcessingException { argumentsMap.get("unit").toString()); toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), functionName, - ModelOptionsUtils.toJsonString(functionResponse))); + ModelOptionsUtils.toJsonString(functionResponse), null)); } ToolResponseMessage toolMessageResponse = new ToolResponseMessage(toolResponses, Map.of()); @@ -256,7 +256,7 @@ private Flux processToolCall(Prompt prompt, Set finishReas String functionResponse = customFunction.apply(toolCall); toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), toolCall.name(), - ModelOptionsUtils.toJsonString(functionResponse))); + ModelOptionsUtils.toJsonString(functionResponse), null)); } ToolResponseMessage toolMessageResponse = new ToolResponseMessage(toolResponses, Map.of()); diff --git a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientProxyFunctionCallsIT.java b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientProxyFunctionCallsIT.java index b02892a5138..ed3ad7129bb 100644 --- a/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientProxyFunctionCallsIT.java +++ b/models/spring-ai-openai/src/test/java/org/springframework/ai/openai/chat/client/OpenAiChatClientProxyFunctionCallsIT.java @@ -162,7 +162,7 @@ void toolProxyFunctionCall() throws JsonMappingException, JsonProcessingExceptio argumentsMap.get("unit").toString()); toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), functionName, - ModelOptionsUtils.toJsonString(functionResponse))); + ModelOptionsUtils.toJsonString(functionResponse), null)); } ToolResponseMessage toolMessageResponse = new ToolResponseMessage(toolResponses, Map.of()); diff --git a/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModel.java b/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModel.java index b347d551871..1493af427bc 100644 --- a/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModel.java +++ b/models/spring-ai-vertex-ai-gemini/src/main/java/org/springframework/ai/vertexai/gemini/VertexAiGeminiChatModel.java @@ -22,6 +22,7 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.stream.Stream; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonInclude.Include; @@ -46,6 +47,7 @@ import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; +import org.springframework.ai.agent.Agent; import reactor.core.publisher.Flux; import org.springframework.ai.chat.messages.AssistantMessage; @@ -317,6 +319,22 @@ public ChatResponse call(Prompt prompt) { if (!isProxyToolCalls(prompt, this.defaultOptions) && isToolCall(response, Set.of(FinishReason.STOP.name()))) { var toolCallConversation = handleToolCalls(prompt, response); + // process agent transfer + ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolCallConversation + .get(toolCallConversation.size() - 1); + List agentList = toolResponseMessage.getResponses().stream().flatMap(toolResponse -> { + if (null == toolResponse.agent()) { + return Stream.empty(); + } + else { + return Stream.of(toolResponse.agent()); + } + }).toList(); + if (!CollectionUtils.isEmpty(agentList)) { + Agent agent = agentList.get(0); + // transfer task to other agent + return agent.call(prompt); + } // Recursively call the call method with the tool call message // conversation that contains the call responses. return this.call(new Prompt(toolCallConversation, prompt.getOptions())); @@ -361,6 +379,22 @@ public Flux stream(Prompt prompt) { if (!isProxyToolCalls(prompt, this.defaultOptions) && isToolCall(chatResponse, Set.of(FinishReason.STOP.name(), FinishReason.FINISH_REASON_UNSPECIFIED.name()))) { var toolCallConversation = handleToolCalls(prompt, chatResponse); + // process agent transfer + ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolCallConversation + .get(toolCallConversation.size() - 1); + List agentList = toolResponseMessage.getResponses().stream().flatMap(toolResponse -> { + if (null == toolResponse.agent()) { + return Stream.empty(); + } + else { + return Stream.of(toolResponse.agent()); + } + }).toList(); + if (!CollectionUtils.isEmpty(agentList)) { + Agent agent = agentList.get(0); + // transfer task to other agent + return agent.stream(prompt); + } // Recursively call the stream method with the tool call message // conversation that contains the call responses. return this.stream(new Prompt(toolCallConversation, prompt.getOptions())); diff --git a/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiChatModel.java b/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiChatModel.java index 7da150c62e7..c36cc13dbac 100644 --- a/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiChatModel.java +++ b/models/spring-ai-zhipuai/src/main/java/org/springframework/ai/zhipuai/ZhiPuAiChatModel.java @@ -23,12 +23,14 @@ import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.stream.Stream; import io.micrometer.observation.Observation; import io.micrometer.observation.ObservationRegistry; import io.micrometer.observation.contextpropagation.ObservationThreadLocalAccessor; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.ai.agent.Agent; import reactor.core.publisher.Flux; import reactor.core.publisher.Mono; @@ -239,6 +241,22 @@ public ChatResponse call(Prompt prompt) { if (!isProxyToolCalls(prompt, this.defaultOptions) && isToolCall(response, Set.of(ChatCompletionFinishReason.TOOL_CALLS.name(), ChatCompletionFinishReason.STOP.name()))) { var toolCallConversation = handleToolCalls(prompt, response); + // process agent transfer + ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolCallConversation + .get(toolCallConversation.size() - 1); + List agentList = toolResponseMessage.getResponses().stream().flatMap(toolResponse -> { + if (null == toolResponse.agent()) { + return Stream.empty(); + } + else { + return Stream.of(toolResponse.agent()); + } + }).toList(); + if (!CollectionUtils.isEmpty(agentList)) { + Agent agent = agentList.get(0); + // transfer task to other agent + return agent.call(prompt); + } // Recursively call the call method with the tool call message // conversation that contains the call responses. return this.call(new Prompt(toolCallConversation, prompt.getOptions())); @@ -308,6 +326,22 @@ public Flux stream(Prompt prompt) { Flux flux = chatResponse.flatMap(response -> { if (!isProxyToolCalls(prompt, this.defaultOptions) && isToolCall(response, Set.of(ChatCompletionFinishReason.TOOL_CALLS.name(), ChatCompletionFinishReason.STOP.name()))) { var toolCallConversation = handleToolCalls(prompt, response); + // process agent transfer + ToolResponseMessage toolResponseMessage = (ToolResponseMessage) toolCallConversation + .get(toolCallConversation.size() - 1); + List agentList = toolResponseMessage.getResponses().stream().flatMap(toolResponse -> { + if (null == toolResponse.agent()) { + return Stream.empty(); + } + else { + return Stream.of(toolResponse.agent()); + } + }).toList(); + if (!CollectionUtils.isEmpty(agentList)) { + Agent agent = agentList.get(0); + // transfer task to other agent + return agent.stream(prompt); + } // Recursively call the stream method with the tool call message // conversation that contains the call responses. return this.stream(new Prompt(toolCallConversation, prompt.getOptions())); diff --git a/spring-ai-core/src/main/java/org/springframework/ai/agent/Agent.java b/spring-ai-core/src/main/java/org/springframework/ai/agent/Agent.java new file mode 100644 index 00000000000..054854b032d --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/agent/Agent.java @@ -0,0 +1,187 @@ +package org.springframework.ai.agent; + +import org.springframework.ai.chat.messages.InstructionMessage; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.model.ChatModel; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.prompt.ChatOptions; +import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.model.function.FunctionCallback; +import org.springframework.ai.model.function.FunctionCallingOptions; +import reactor.core.publisher.Flux; + +import java.util.List; + +/** + * The Agent class references the openAI swarm open source framework, which can call tool + * functions and transfer tasks to other AI agents. + *

+ * for example : transfer singing task to agentB + *

+ * + * Agent agentA = new Agent("agentA", new InstructionMessage("You are an intelligent butler, skilled at getting professionals to do professional things. by the way agentB skilled in singing"), + * ollamaChatModel); + + * Agent agentB = new Agent("agentB", new InstructionMessage("You are a singer, skilled in singing"), + * ollamaChatModel); + + * ArrayList functionCallbacks = new ArrayList<>(); + * functionCallbacks.add(FunctionCallbackWrapper.builder(new Function() { + * public Agent apply(Request request) { + * return agentB; + * } + * }).withDescription("You can call this function to sing") + * .withName("assign task to agentB") + * .build()); + * agentA.setFunctionCallbacks(functionCallbacks); + + * String response = agentA.call("Hello, sing me a song"); + * + * + * @author wanglei + * @since 1.0.0 M1 + */ +public class Agent implements ChatModel { + + /** + * agent name + */ + private String agentName; + + /** + * every agent can have a first instruction to describe what can it do + */ + private InstructionMessage instructionMessage; + + /** + * chatModel + */ + private ChatModel chatModel; + + /** + * functionCallback + */ + private List functionCallbacks; + + /** + * agent call for chatResponse + * @param prompt the request object to be sent to the AI model + * @return + */ + @Override + public ChatResponse call(Prompt prompt) { + + Prompt copyPrompt = prompt.copy(); + if (instructionMessage != null) { + List instructions = copyPrompt.getInstructions(); + // remove other agent instruction + instructions.removeIf(InstructionMessage.class::isInstance); + instructions.add(0, instructionMessage); + } + + ChatOptions chatModelOptions = chatModel.getDefaultOptions(); + if (chatModelOptions instanceof FunctionCallingOptions) { + // support functionCalling + // add functionCallBacks to conversion + ChatOptions copyChatOptions = copyPrompt.getOptions(); + if (null == copyChatOptions) { + ChatOptions copyChatModelOption = chatModelOptions.copy(); + FunctionCallingOptions copyChatModelFunctionOptions = (FunctionCallingOptions) copyChatModelOption + .copy(); + copyChatModelFunctionOptions.setFunctionCallbacks(functionCallbacks); + copyPrompt = new Prompt(copyPrompt.getInstructions(), copyChatModelFunctionOptions); + + } + else if (copyChatOptions instanceof FunctionCallingOptions copyChatFunctionCallingOptions) { + copyChatFunctionCallingOptions.setFunctionCallbacks(functionCallbacks); + } + } + + return chatModel.call(copyPrompt); + } + + @Override + public ChatOptions getDefaultOptions() { + return chatModel.getDefaultOptions(); + } + + /** + * agent stream method + * @param prompt the request object to be sent to the AI model + * @return + */ + @Override + public Flux stream(Prompt prompt) { + Prompt copyPrompt = prompt.copy(); + if (instructionMessage != null) { + List instructions = copyPrompt.getInstructions(); + // remove other agent instruction + instructions.removeIf(InstructionMessage.class::isInstance); + instructions.add(0, instructionMessage); + } + + ChatOptions chatModelOptions = chatModel.getDefaultOptions(); + if (chatModelOptions instanceof FunctionCallingOptions) { + // support functionCalling + // add functionCallBacks to conversion + ChatOptions copyChatOptions = copyPrompt.getOptions(); + if (null == copyChatOptions) { + ChatOptions copyChatModelOption = chatModelOptions.copy(); + FunctionCallingOptions copyChatModelFunctionOptions = (FunctionCallingOptions) copyChatModelOption + .copy(); + copyChatModelFunctionOptions.setFunctionCallbacks(functionCallbacks); + copyPrompt = new Prompt(copyPrompt.getInstructions(), copyChatModelFunctionOptions); + + } + else if (copyChatOptions instanceof FunctionCallingOptions copyChatFunctionCallingOptions) { + copyChatFunctionCallingOptions.setFunctionCallbacks(functionCallbacks); + } + } + + return chatModel.stream(copyPrompt); + } + + public Agent() { + } + + public Agent(String agentName, InstructionMessage instructionMessage, ChatModel chatModel, + List functionCallbacks) { + this.agentName = agentName; + this.instructionMessage = instructionMessage; + this.chatModel = chatModel; + this.functionCallbacks = functionCallbacks; + } + + public Agent(String agentName, InstructionMessage instructionMessage, ChatModel chatModel) { + this(agentName, instructionMessage, chatModel, null); + } + + public Agent(String agentName, ChatModel chatModel) { + this(agentName, null, chatModel); + } + + public String getAgentName() { + return agentName; + } + + public void setAgentName(String agentName) { + this.agentName = agentName; + } + + public ChatModel getChatModel() { + return chatModel; + } + + public void setChatModel(ChatModel chatModel) { + this.chatModel = chatModel; + } + + public List getFunctionCallbacks() { + return functionCallbacks; + } + + public void setFunctionCallbacks(List functionCallbacks) { + this.functionCallbacks = functionCallbacks; + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/messages/InstructionMessage.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/messages/InstructionMessage.java new file mode 100644 index 00000000000..a06a670170b --- /dev/null +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/messages/InstructionMessage.java @@ -0,0 +1,21 @@ +package org.springframework.ai.chat.messages; + +/** + * Instructions are a subtype of user messages that will be added to each conversation in + * AI agent scenarios, allowing AI agents to clearly understand their responsibilities + * + * @author wanglei + */ +public class InstructionMessage extends UserMessage { + + public InstructionMessage(String instruction) { + super(instruction); + } + + @Override + public String toString() { + return "InstructionMessage{" + "messageType=" + messageType + ", textContent='" + textContent + '\'' + + ", metadata=" + metadata + '}'; + } + +} diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/messages/ToolResponseMessage.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/messages/ToolResponseMessage.java index 47da252180f..ee70a3e7370 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/chat/messages/ToolResponseMessage.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/messages/ToolResponseMessage.java @@ -16,6 +16,8 @@ package org.springframework.ai.chat.messages; +import org.springframework.ai.agent.Agent; + import java.util.List; import java.util.Map; import java.util.Objects; @@ -69,7 +71,7 @@ public String toString() { + ", metadata=" + this.metadata + '}'; } - public record ToolResponse(String id, String name, String responseData) { + public record ToolResponse(String id, String name, String responseData, Agent agent) { } diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/model/AbstractToolCallSupport.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/model/AbstractToolCallSupport.java index b78a8ba7b6d..68865ff8d0d 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/chat/model/AbstractToolCallSupport.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/model/AbstractToolCallSupport.java @@ -24,6 +24,7 @@ import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import org.springframework.ai.agent.Agent; import org.springframework.ai.chat.messages.AssistantMessage; import org.springframework.ai.chat.messages.Message; import org.springframework.ai.chat.messages.ToolResponseMessage; @@ -207,10 +208,19 @@ protected ToolResponseMessage executeFunctions(AssistantMessage assistantMessage throw new IllegalStateException("No function callback found for function name: " + functionName); } - String functionResponse = this.functionCallbackRegister.get(functionName) - .call(functionArguments, toolContext); + FunctionCallback functionCallback = this.functionCallbackRegister.get(functionName); + Object returnRawObject = functionCallback.callReturnRaw(functionArguments, toolContext); + // transfer task to agent + if (returnRawObject instanceof Agent) { + toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), functionName, "ok", + (Agent) returnRawObject)); + } + else { + String functionResponse = functionCallback.convertResultObject2String(returnRawObject); + toolResponses + .add(new ToolResponseMessage.ToolResponse(toolCall.id(), functionName, functionResponse, null)); + } - toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), functionName, functionResponse)); } return new ToolResponseMessage(toolResponses, Map.of()); diff --git a/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/Prompt.java b/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/Prompt.java index deb35cb9b17..a9054556e5a 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/Prompt.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/chat/prompt/Prompt.java @@ -23,11 +23,7 @@ import java.util.List; import java.util.Objects; -import org.springframework.ai.chat.messages.AssistantMessage; -import org.springframework.ai.chat.messages.Message; -import org.springframework.ai.chat.messages.SystemMessage; -import org.springframework.ai.chat.messages.ToolResponseMessage; -import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.messages.*; import org.springframework.ai.model.ModelRequest; /** @@ -118,7 +114,10 @@ public Prompt copy() { private List instructionsCopy() { List messagesCopy = new ArrayList<>(); this.messages.forEach(message -> { - if (message instanceof UserMessage userMessage) { + if (message instanceof InstructionMessage instructionMessage) { + messagesCopy.add(new InstructionMessage(instructionMessage.getContent())); + } + else if (message instanceof UserMessage userMessage) { messagesCopy .add(new UserMessage(userMessage.getContent(), userMessage.getMedia(), message.getMetadata())); } diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/function/AbstractFunctionCallback.java b/spring-ai-core/src/main/java/org/springframework/ai/model/function/AbstractFunctionCallback.java index c4ddfbb219e..7fbd6b29ab6 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/model/function/AbstractFunctionCallback.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/function/AbstractFunctionCallback.java @@ -117,6 +117,23 @@ public String call(String functionArguments) { return this.andThen(this.responseConverter).apply(request, null); } + @Override + public O callReturnRaw(String functionInput, ToolContext toolContext) { + I request = fromJson(functionInput, this.inputType); + return this.apply(request, toolContext); + } + + @Override + public O callReturnRaw(String functionInput) { + I request = fromJson(functionInput, this.inputType); + return this.apply(request, null); + } + + @Override + public String convertResultObject2String(Object rowObjectResult) { + return this.responseConverter.apply((O) rowObjectResult); + } + private T fromJson(String json, Type targetType) { try { return this.objectMapper.readValue(json, this.objectMapper.constructType(targetType)); diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallback.java b/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallback.java index b7432753565..dcd6cd52acb 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallback.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallback.java @@ -220,4 +220,37 @@ interface MethodInvokingSpec { } + + /** + * @param functionInput JSON string with the function arguments to be passed to the + * function. The arguments are defined as JSON schema usually registered with the the + * model. Arguments are provided by the AI model. + * @return raw object return by function call + */ + Object callReturnRaw(String functionInput); + + /** + * @param functionInput JSON string with the function arguments to be passed to the + * function. The arguments are defined as JSON schema usually registered with the the + * model. Arguments are provided by the AI model. + * @param toolContext Map with the function context. The context is used to pass + * additional user provided state in addition to the arguments provided by the AI + * model. + * @return raw object return by function call + */ + default Object callReturnRaw(String functionInput, ToolContext toolContext) { + if (toolContext != null) { + throw new UnsupportedOperationException("Function context is not supported!"); + } + return callReturnRaw(functionInput); + } + + /** + * After calling the callReturnRaw function, serialize the returned raw object into a + * String type + * @param rowObjectResult row object return by function call + * @return Serialize to string + */ + String convertResultObject2String(Object rowObjectResult); + } diff --git a/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallingHelper.java b/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallingHelper.java index 42db38485b4..476c93b5139 100644 --- a/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallingHelper.java +++ b/spring-ai-core/src/main/java/org/springframework/ai/model/function/FunctionCallingHelper.java @@ -22,7 +22,9 @@ import java.util.Optional; import java.util.Set; import java.util.function.Function; +import java.util.stream.Stream; +import org.springframework.ai.agent.Agent; import reactor.core.publisher.Flux; import org.springframework.ai.chat.messages.AssistantMessage; @@ -94,7 +96,7 @@ public Flux processStream(ChatModel chatModel, Prompt prompt, Set< String functionResponse = customFunction.apply(toolCall); toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), toolCall.name(), - ModelOptionsUtils.toJsonString(functionResponse))); + ModelOptionsUtils.toJsonString(functionResponse), null)); } ToolResponseMessage toolMessageResponse = new ToolResponseMessage(toolResponses, Map.of()); @@ -136,7 +138,7 @@ public ChatResponse processCall(ChatModel chatModel, Prompt prompt, Set String functionResponse = customFunction.apply(toolCall); toolResponses.add(new ToolResponseMessage.ToolResponse(toolCall.id(), toolCall.name(), - ModelOptionsUtils.toJsonString(functionResponse))); + ModelOptionsUtils.toJsonString(functionResponse), null)); } ToolResponseMessage toolMessageResponse = new ToolResponseMessage(toolResponses, Map.of()); @@ -181,6 +183,16 @@ public String call(String functionInput) { "FunctionDefinition provides only metadata. It doesn't implement the call method."); } + @Override + public Object callReturnRaw(String functionInput) { + return null; + } + + @Override + public String convertResultObject2String(Object rowObjectResult) { + return ""; + } + } } diff --git a/spring-ai-core/src/test/java/org/springframework/ai/chat/client/observation/DefaultChatClientObservationConventionTests.java b/spring-ai-core/src/test/java/org/springframework/ai/chat/client/observation/DefaultChatClientObservationConventionTests.java index 0f1e4814277..f9ee227461e 100644 --- a/spring-ai-core/src/test/java/org/springframework/ai/chat/client/observation/DefaultChatClientObservationConventionTests.java +++ b/spring-ai-core/src/test/java/org/springframework/ai/chat/client/observation/DefaultChatClientObservationConventionTests.java @@ -108,6 +108,16 @@ public String call(String functionInput) { // TODO Auto-generated method stub throw new UnsupportedOperationException("Unimplemented method 'call'"); } + + @Override + public Object callReturnRaw(String functionInput) { + return null; + } + + @Override + public String convertResultObject2String(Object rowObjectResult) { + return ""; + } }; }