From a5f1d8d8292dbe5580d7cce8a8a14181709b3088 Mon Sep 17 00:00:00 2001 From: Linar Abzaltdinov Date: Wed, 18 Jun 2025 17:57:48 +0300 Subject: [PATCH 1/4] =?UTF-8?q?=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20=D0=B2=D0=BE=D0=B7=D0=BC=D0=BE=D0=B6=D0=BD?= =?UTF-8?q?=D0=BE=D1=81=D1=82=D0=B8=20=D0=BF=D1=80=D0=BE=D1=81=D0=BC=D0=BE?= =?UTF-8?q?=D1=82=D1=80=D0=B0=20=D0=B2=D1=81=D0=B5=D1=85=20=D1=81=D0=BE?= =?UTF-8?q?=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9=20(=D1=81=20=D0=BF?= =?UTF-8?q?=D0=B0=D1=80=D0=B0=D0=BC=D0=B5=D1=82=D1=80=D1=8B=D0=BC=D0=B8=20?= =?UTF-8?q?=D0=B2=D1=8B=D0=B7=D0=BE=D0=B2=D0=BE=D0=B2=20=D1=82=D1=83=D0=BB?= =?UTF-8?q?=D0=BE=D0=B2,=20=D1=81=20=D1=80=D0=B5=D0=B7=D1=83=D0=BB=D1=8C?= =?UTF-8?q?=D1=82=D0=B0=D1=82=D0=B0=D0=BC=D0=B8=20=D0=B8=D1=85=20=D0=B2?= =?UTF-8?q?=D1=8B=D0=B7=D0=BE=D0=B2=D0=B0)=20=D0=B8=D0=B7=20ChatResponse.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Linar Abzaltdinov --- docs/response-metadata.md | 26 +++++++++ docs/tools.md | 2 +- spring-ai-gigachat-example/README.md | 2 +- .../example/WeatherToolController.java | 15 ++++- .../chat/giga/springai/GigaChatModel.java | 28 +++++++--- .../support/GigaChatResponseUtils.java | 26 +++++++++ .../support/GigaChatResponseUtilsTest.java | 55 +++++++++++++++++++ 7 files changed, 142 insertions(+), 12 deletions(-) create mode 100644 docs/response-metadata.md create mode 100644 spring-ai-gigachat/src/main/java/chat/giga/springai/support/GigaChatResponseUtils.java create mode 100644 spring-ai-gigachat/src/test/java/chat/giga/springai/support/GigaChatResponseUtilsTest.java diff --git a/docs/response-metadata.md b/docs/response-metadata.md new file mode 100644 index 0000000..361bbad --- /dev/null +++ b/docs/response-metadata.md @@ -0,0 +1,26 @@ +## Дополнительная метаинформация в ответе ChatResponse + +Для получения дополнительной информации об ответе создан утилитный класс +[GigaChatResponseUtils](../spring-ai-gigachat/src/main/java/chat/giga/springai/support/GigaChatResponseUtils.java) + +### Получение всей истории переписки с GigaChat + +Если Вам необходимо получить информацию обо всех сообщениях, +которые были получены и отправлены под капотом фреймфорка Spring AI +(например - какие тулы были вызваны, с какими параметрами, каковы результаты вызова тулов), +можно воспользоваться утилитным методом `GigaChatResponseUtils.getConversationHistory(chatResponse)`. + +Пример: +```java +ChatResponse chatResponse = chatClient + .prompt(question) + .toolCallbacks(GigaTools.from(new WeatherTools())) + .call() + .chatResponse(); +List toolResponseMessages = GigaChatResponseUtils.getConversationHistory(chatResponse) + .stream() + .filter(msg -> MessageType.TOOL.equals(msg.getMessageType())) + .toList(); +log.info("Было вызвано {} функций", toolResponseMessages.size()); +``` + diff --git a/docs/tools.md b/docs/tools.md index 43f420b..b4b4d71 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -1,4 +1,4 @@ -### Вызов пользовательских функций +## Вызов пользовательских функций Для вызова внешних функций пользуйтесь официальной документацией Spring AI - https://docs.spring.io/spring-ai/reference/api/tools.html. diff --git a/spring-ai-gigachat-example/README.md b/spring-ai-gigachat-example/README.md index 27d26c4..af49340 100644 --- a/spring-ai-gigachat-example/README.md +++ b/spring-ai-gigachat-example/README.md @@ -75,7 +75,7 @@ curl localhost:8080/tool/v1/weather -d "Какая температура в К curl localhost:8080/tool/v2/weather -d "Сколько градусов в Спб?" -H "content-type:application/json" curl localhost:8080/tool/v3/weather -d "Сколько градусов в Москве?" -H "content-type:application/json" curl localhost:8080/tool/v4/weather -d "Сколько градусов в Сочи будет завтра?" -H "content-type:application/json" -curl localhost:8080/tool/v4/weather -d "Какое давление в Сочи будет завтра?" -H "content-type:application/json" +curl localhost:8080/tool/v4/weather -d "Сколько градусов и какое давление в Сочи будет завтра?" -H "content-type:application/json" ``` ## Примеры использования RAG diff --git a/spring-ai-gigachat-example/src/main/java/chat/giga/springai/example/WeatherToolController.java b/spring-ai-gigachat-example/src/main/java/chat/giga/springai/example/WeatherToolController.java index 21974c7..90faa0c 100644 --- a/spring-ai-gigachat-example/src/main/java/chat/giga/springai/example/WeatherToolController.java +++ b/spring-ai-gigachat-example/src/main/java/chat/giga/springai/example/WeatherToolController.java @@ -2,15 +2,20 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; +import chat.giga.springai.support.GigaChatResponseUtils; import chat.giga.springai.tool.GigaTools; import chat.giga.springai.tool.annotation.FewShotExample; import chat.giga.springai.tool.annotation.GigaTool; import chat.giga.springai.tool.function.GigaFunctionToolCallback; import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import java.util.List; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.MessageType; +import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.tool.annotation.Tool; import org.springframework.ai.tool.annotation.ToolParam; import org.springframework.ai.tool.function.FunctionToolCallback; @@ -150,12 +155,18 @@ String getTemperature( */ @PostMapping("tool/v4/weather") public String weatherToolAnnotation(@RequestBody String question) { - return chatClient + ChatResponse chatResponse = chatClient .prompt(question) // Важно использовать .toolCallbacks(GigaTools.from()), чтобы обрабатывались аннотации @GigaTool и @Tool // Если использовать конструкцию .tools(new WeatherTools()), то будет использоваться только @Tool .toolCallbacks(GigaTools.from(new WeatherTools())) .call() - .content(); + .chatResponse(); + List toolResponseMessages = GigaChatResponseUtils.getConversationHistory(chatResponse).stream() + .filter(msg -> MessageType.TOOL.equals(msg.getMessageType())) + .toList(); + log.info("Было вызвано {} функций", toolResponseMessages.size()); + toolResponseMessages.forEach(msg -> log.info(msg.toString())); + return chatResponse.getResult().getOutput().getText(); } } diff --git a/spring-ai-gigachat/src/main/java/chat/giga/springai/GigaChatModel.java b/spring-ai-gigachat/src/main/java/chat/giga/springai/GigaChatModel.java index ebf580c..bd2e80d 100644 --- a/spring-ai-gigachat/src/main/java/chat/giga/springai/GigaChatModel.java +++ b/spring-ai-gigachat/src/main/java/chat/giga/springai/GigaChatModel.java @@ -16,10 +16,7 @@ import java.util.stream.Collectors; import lombok.Setter; import lombok.extern.slf4j.Slf4j; -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.messages.*; import org.springframework.ai.chat.metadata.*; import org.springframework.ai.chat.model.ChatModel; import org.springframework.ai.chat.model.ChatResponse; @@ -38,6 +35,7 @@ import org.springframework.ai.tool.definition.ToolDefinition; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; +import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; @@ -50,6 +48,7 @@ public class GigaChatModel implements ChatModel { public static final String DEFAULT_MODEL_NAME = GigaChatApi.ChatModel.GIGA_CHAT_2.getName(); public static final ChatModelObservationConvention DEFAULT_OBSERVATION_CONVENTION = new DefaultChatModelObservationConvention(); + public static final String CONVERSATION_HISTORY = "conversationHistory"; private static final ToolCallingManager DEFAULT_TOOL_CALLING_MANAGER = ToolCallingManager.builder().build(); @@ -146,7 +145,8 @@ public ChatResponse internalCall(Prompt prompt, ChatResponse previousChatRespons Usage accumulatedUsage = UsageCalculator.getCumulativeUsage(currentChatResponseUsage, previousChatResponse); - ChatResponse chatResponse = toChatResponse(completionResponse, accumulatedUsage, false); + ChatResponse chatResponse = + toChatResponse(completionResponse, accumulatedUsage, false, prompt.getInstructions()); observationContext.setResponse(chatResponse); return chatResponse; @@ -204,7 +204,8 @@ public Flux internalStream(Prompt prompt, ChatResponse previousCha Usage accumulatedUsage = UsageCalculator.getCumulativeUsage(currentChatResponseUsage, previousChatResponse); - ChatResponse chatResponse = toChatResponse(completionResponse, accumulatedUsage, true); + ChatResponse chatResponse = + toChatResponse(completionResponse, accumulatedUsage, true, prompt.getInstructions()); if (this.toolExecutionEligibilityPredicate.isToolExecutionRequired( prompt.getOptions(), chatResponse)) { @@ -405,7 +406,11 @@ private List getFunctionDescriptions(List .toList(); } - private ChatResponse toChatResponse(CompletionResponse completionResponse, Usage usage, boolean streaming) { + private ChatResponse toChatResponse( + CompletionResponse completionResponse, + Usage usage, + boolean streaming, + @NonNull List conversationHistory) { if (completionResponse == null) { log.warn("Null completion response"); return new ChatResponse(List.of()); @@ -414,7 +419,8 @@ private ChatResponse toChatResponse(CompletionResponse completionResponse, Usage List generations = completionResponse.getChoices().stream() .map(choice -> buildGeneration(completionResponse.getId(), choice, streaming)) .toList(); - return new ChatResponse(generations, from(completionResponse, usage)); + return new ChatResponse( + generations, from(completionResponse, usage, Map.of(CONVERSATION_HISTORY, conversationHistory))); } private Generation buildGeneration(String id, CompletionResponse.Choice choice, boolean streaming) { @@ -453,6 +459,11 @@ private ChatResponseMetadata from(CompletionResponse completionResponse) { } private ChatResponseMetadata from(CompletionResponse completionResponse, Usage usage) { + return from(completionResponse, usage, Map.of()); + } + + private ChatResponseMetadata from( + CompletionResponse completionResponse, Usage usage, Map metadata) { Assert.notNull(completionResponse, "GigaChat CompletionResponse must not be null"); return ChatResponseMetadata.builder() .id(completionResponse.getId()) @@ -460,6 +471,7 @@ private ChatResponseMetadata from(CompletionResponse completionResponse, Usage u .usage(usage) .keyValue("created", completionResponse.getCreated()) .keyValue("object", completionResponse.getObject()) + .metadata(metadata) .build(); } diff --git a/spring-ai-gigachat/src/main/java/chat/giga/springai/support/GigaChatResponseUtils.java b/spring-ai-gigachat/src/main/java/chat/giga/springai/support/GigaChatResponseUtils.java new file mode 100644 index 0000000..edfc9c3 --- /dev/null +++ b/spring-ai-gigachat/src/main/java/chat/giga/springai/support/GigaChatResponseUtils.java @@ -0,0 +1,26 @@ +package chat.giga.springai.support; + +import chat.giga.springai.GigaChatModel; +import java.util.List; +import lombok.experimental.UtilityClass; +import lombok.extern.slf4j.Slf4j; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.model.ChatResponse; + +/** + * Utils for providing type-safe access to ChatResponse metadata properties from GigaChat model response. + * + * @author Linar Abzaltdinov + */ +@UtilityClass +@Slf4j +public class GigaChatResponseUtils { + public static List getConversationHistory(ChatResponse chatResponse) { + if (chatResponse != null + && chatResponse.getMetadata() != null + && chatResponse.getMetadata().containsKey(GigaChatModel.CONVERSATION_HISTORY)) { + return chatResponse.getMetadata().get(GigaChatModel.CONVERSATION_HISTORY); + } + return List.of(); + } +} diff --git a/spring-ai-gigachat/src/test/java/chat/giga/springai/support/GigaChatResponseUtilsTest.java b/spring-ai-gigachat/src/test/java/chat/giga/springai/support/GigaChatResponseUtilsTest.java new file mode 100644 index 0000000..3c04ff1 --- /dev/null +++ b/spring-ai-gigachat/src/test/java/chat/giga/springai/support/GigaChatResponseUtilsTest.java @@ -0,0 +1,55 @@ +package chat.giga.springai.support; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +import chat.giga.springai.GigaChatModel; +import java.util.Collections; +import java.util.List; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.ai.chat.messages.Message; +import org.springframework.ai.chat.messages.UserMessage; +import org.springframework.ai.chat.metadata.ChatResponseMetadata; +import org.springframework.ai.chat.model.ChatResponse; + +@ExtendWith(MockitoExtension.class) +public class GigaChatResponseUtilsTest { + + @Mock + private ChatResponse chatResponse; + + @Test + public void testGetConversationHistoryWithNullChatResponse() { + List result = GigaChatResponseUtils.getConversationHistory(null); + assertEquals(Collections.emptyList(), result); + } + + @Test + public void testGetConversationHistoryWithNullMetadata() { + when(chatResponse.getMetadata()).thenReturn(null); + List result = GigaChatResponseUtils.getConversationHistory(chatResponse); + assertEquals(Collections.emptyList(), result); + } + + @Test + public void testGetConversationHistoryWithEmptyMetadata() { + when(chatResponse.getMetadata()) + .thenReturn(ChatResponseMetadata.builder().build()); + List result = GigaChatResponseUtils.getConversationHistory(chatResponse); + assertEquals(Collections.emptyList(), result); + } + + @Test + public void testGetConversationHistoryWithConversationHistoryInMetadata() { + Message message = new UserMessage("Hello, world!"); + var metadata = ChatResponseMetadata.builder() + .keyValue(GigaChatModel.CONVERSATION_HISTORY, List.of(message)) + .build(); + when(chatResponse.getMetadata()).thenReturn(metadata); + List result = GigaChatResponseUtils.getConversationHistory(chatResponse); + assertEquals(Collections.singletonList(message), result); + } +} From 687cab3d545d568d2060e71d6f6510f7fb33e371 Mon Sep 17 00:00:00 2001 From: Linar Abzaltdinov Date: Thu, 19 Jun 2025 10:10:45 +0300 Subject: [PATCH 2/4] spotless Signed-off-by: Linar Abzaltdinov --- docs/response-metadata.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/response-metadata.md b/docs/response-metadata.md index 361bbad..bbe4cfd 100644 --- a/docs/response-metadata.md +++ b/docs/response-metadata.md @@ -11,6 +11,7 @@ можно воспользоваться утилитным методом `GigaChatResponseUtils.getConversationHistory(chatResponse)`. Пример: + ```java ChatResponse chatResponse = chatClient .prompt(question) From f6c605ff0d9d328c4980da47f7387451b115d75e Mon Sep 17 00:00:00 2001 From: Linar Abzaltdinov Date: Thu, 19 Jun 2025 16:50:34 +0300 Subject: [PATCH 3/4] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BF=D1=80=D0=BE=D0=B2=D0=B5=D1=80=D0=BA=D0=B8=20cha?= =?UTF-8?q?tCompletionResponse=20=D0=BD=D0=B0=20null=20=D0=98=D1=81=D0=BF?= =?UTF-8?q?=D1=80=D0=B0=D0=B2=D0=B8=D0=BB=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8=D1=81=D1=82=D0=BE=D1=80=D0=B8?= =?UTF-8?q?=D0=B8=20=D0=BF=D0=B5=D1=80=D0=B5=D0=BF=D0=B8=D1=81=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Linar Abzaltdinov --- .../chat/giga/springai/GigaChatModel.java | 21 ++++++++++--------- .../support/GigaChatResponseUtils.java | 10 +++++---- 2 files changed, 17 insertions(+), 14 deletions(-) diff --git a/spring-ai-gigachat/src/main/java/chat/giga/springai/GigaChatModel.java b/spring-ai-gigachat/src/main/java/chat/giga/springai/GigaChatModel.java index bd2e80d..74771f0 100644 --- a/spring-ai-gigachat/src/main/java/chat/giga/springai/GigaChatModel.java +++ b/spring-ai-gigachat/src/main/java/chat/giga/springai/GigaChatModel.java @@ -35,7 +35,6 @@ import org.springframework.ai.tool.definition.ToolDefinition; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseEntity; -import org.springframework.lang.NonNull; import org.springframework.lang.Nullable; import org.springframework.retry.support.RetryTemplate; import org.springframework.util.Assert; @@ -139,6 +138,12 @@ public ChatResponse internalCall(Prompt prompt, ChatResponse previousChatRespons ctx -> this.gigaChatApi.chatCompletionEntity(request, buildHeaders(prompt.getOptions()))); CompletionResponse completionResponse = completionEntity.getBody(); + + if (completionResponse == null) { + log.warn("No chat completion returned for prompt: {}", prompt); + return new ChatResponse(List.of()); + } + completionResponse.setId(completionEntity.getHeaders().getFirst(X_REQUEST_ID)); Usage currentChatResponseUsage = buildUsage(completionResponse.getUsage()); @@ -200,6 +205,10 @@ public Flux internalStream(Prompt prompt, ChatResponse previousCha ctx -> this.gigaChatApi.chatCompletionStream(request, buildHeaders(prompt.getOptions()))); Flux chatResponseFlux = response.switchMap(completionResponse -> { + if (completionResponse == null) { + log.warn("No chat completion returned for prompt: {}", prompt); + return Flux.just(new ChatResponse(List.of())); + } Usage currentChatResponseUsage = buildUsage(completionResponse.getUsage()); Usage accumulatedUsage = UsageCalculator.getCumulativeUsage(currentChatResponseUsage, previousChatResponse); @@ -407,15 +416,7 @@ private List getFunctionDescriptions(List } private ChatResponse toChatResponse( - CompletionResponse completionResponse, - Usage usage, - boolean streaming, - @NonNull List conversationHistory) { - if (completionResponse == null) { - log.warn("Null completion response"); - return new ChatResponse(List.of()); - } - + CompletionResponse completionResponse, Usage usage, boolean streaming, List conversationHistory) { List generations = completionResponse.getChoices().stream() .map(choice -> buildGeneration(completionResponse.getId(), choice, streaming)) .toList(); diff --git a/spring-ai-gigachat/src/main/java/chat/giga/springai/support/GigaChatResponseUtils.java b/spring-ai-gigachat/src/main/java/chat/giga/springai/support/GigaChatResponseUtils.java index edfc9c3..3df3af2 100644 --- a/spring-ai-gigachat/src/main/java/chat/giga/springai/support/GigaChatResponseUtils.java +++ b/spring-ai-gigachat/src/main/java/chat/giga/springai/support/GigaChatResponseUtils.java @@ -16,10 +16,12 @@ @Slf4j public class GigaChatResponseUtils { public static List getConversationHistory(ChatResponse chatResponse) { - if (chatResponse != null - && chatResponse.getMetadata() != null - && chatResponse.getMetadata().containsKey(GigaChatModel.CONVERSATION_HISTORY)) { - return chatResponse.getMetadata().get(GigaChatModel.CONVERSATION_HISTORY); + if (chatResponse != null && chatResponse.getMetadata() != null) { + List messages = chatResponse.getMetadata().get(GigaChatModel.CONVERSATION_HISTORY); + if (messages == null) { + messages = List.of(); + } + return messages; } return List.of(); } From 5d0826fb6050421561383b0e4fd5f3d37fb4e4d3 Mon Sep 17 00:00:00 2001 From: Linar Abzaltdinov Date: Thu, 19 Jun 2025 16:52:13 +0300 Subject: [PATCH 4/4] =?UTF-8?q?=D0=98=D1=81=D0=BF=D1=80=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D0=BB=20=D0=BF=D0=BE=D0=BB=D1=83=D1=87=D0=B5=D0=BD=D0=B8=D0=B5?= =?UTF-8?q?=20=D0=B8=D1=81=D1=82=D0=BE=D1=80=D0=B8=D0=B8=20=D0=BF=D0=B5?= =?UTF-8?q?=D1=80=D0=B5=D0=BF=D0=B8=D1=81=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Linar Abzaltdinov --- .../chat/giga/springai/support/GigaChatResponseUtils.java | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/spring-ai-gigachat/src/main/java/chat/giga/springai/support/GigaChatResponseUtils.java b/spring-ai-gigachat/src/main/java/chat/giga/springai/support/GigaChatResponseUtils.java index 3df3af2..5a80640 100644 --- a/spring-ai-gigachat/src/main/java/chat/giga/springai/support/GigaChatResponseUtils.java +++ b/spring-ai-gigachat/src/main/java/chat/giga/springai/support/GigaChatResponseUtils.java @@ -18,10 +18,7 @@ public class GigaChatResponseUtils { public static List getConversationHistory(ChatResponse chatResponse) { if (chatResponse != null && chatResponse.getMetadata() != null) { List messages = chatResponse.getMetadata().get(GigaChatModel.CONVERSATION_HISTORY); - if (messages == null) { - messages = List.of(); - } - return messages; + return messages == null ? List.of() : messages; } return List.of(); }