Skip to content

Добавление возможности просмотра всех сообщений внутри Spring AI #17

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 19, 2025
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 27 additions & 0 deletions docs/response-metadata.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
## Дополнительная метаинформация в ответе 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<Message> toolResponseMessages = GigaChatResponseUtils.getConversationHistory(chatResponse)
.stream()
.filter(msg -> MessageType.TOOL.equals(msg.getMessageType()))
.toList();
log.info("Было вызвано {} функций", toolResponseMessages.size());
```

2 changes: 1 addition & 1 deletion docs/tools.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
### Вызов пользовательских функций
## Вызов пользовательских функций

Для вызова внешних функций пользуйтесь официальной документацией
Spring AI - https://docs.spring.io/spring-ai/reference/api/tools.html.
Expand Down
2 changes: 1 addition & 1 deletion spring-ai-gigachat-example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<Message> 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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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();

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -204,7 +204,8 @@ public Flux<ChatResponse> 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)) {
Expand Down Expand Up @@ -405,7 +406,11 @@ private List<CompletionRequest.FunctionDescription> getFunctionDescriptions(List
.toList();
}

private ChatResponse toChatResponse(CompletionResponse completionResponse, Usage usage, boolean streaming) {
private ChatResponse toChatResponse(
CompletionResponse completionResponse,
Usage usage,
boolean streaming,
@NonNull List<Message> conversationHistory) {
if (completionResponse == null) {
log.warn("Null completion response");
return new ChatResponse(List.of());
Expand All @@ -414,7 +419,8 @@ private ChatResponse toChatResponse(CompletionResponse completionResponse, Usage
List<Generation> 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) {
Expand Down Expand Up @@ -453,13 +459,19 @@ 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<String, Object> metadata) {
Assert.notNull(completionResponse, "GigaChat CompletionResponse must not be null");
return ChatResponseMetadata.builder()
.id(completionResponse.getId())
.model(completionResponse.getModel())
.usage(usage)
.keyValue("created", completionResponse.getCreated())
.keyValue("object", completionResponse.getObject())
.metadata(metadata)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -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<Message> 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();
}
}
Original file line number Diff line number Diff line change
@@ -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<Message> result = GigaChatResponseUtils.getConversationHistory(null);
assertEquals(Collections.emptyList(), result);
}

@Test
public void testGetConversationHistoryWithNullMetadata() {
when(chatResponse.getMetadata()).thenReturn(null);
List<Message> result = GigaChatResponseUtils.getConversationHistory(chatResponse);
assertEquals(Collections.emptyList(), result);
}

@Test
public void testGetConversationHistoryWithEmptyMetadata() {
when(chatResponse.getMetadata())
.thenReturn(ChatResponseMetadata.builder().build());
List<Message> 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<Message> result = GigaChatResponseUtils.getConversationHistory(chatResponse);
assertEquals(Collections.singletonList(message), result);
}
}