From 800a348897297892df99ebf0eb03393fcc3d3c02 Mon Sep 17 00:00:00 2001 From: Linar Abzaltdinov Date: Thu, 19 Jun 2025 23:48:35 +0300 Subject: [PATCH 1/4] =?UTF-8?q?=D0=A1=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D0=B5=20id=20=D1=84=D0=B0=D0=B9=D0=BB=D0=BE=D0=B2?= =?UTF-8?q?=20(media)=20=D0=B2=20=D1=81=D0=BF=D0=B8=D1=81=D0=BE=D0=BA=20?= =?UTF-8?q?=D1=81=D0=BE=D0=BE=D0=B1=D1=89=D0=B5=D0=BD=D0=B8=D0=B9=20=D0=B2?= =?UTF-8?q?=D0=BD=D1=83=D1=82=D1=80=D0=B8=20Prompt=20=D0=B4=D0=BB=D1=8F=20?= =?UTF-8?q?=D0=BF=D0=BE=D1=81=D0=BB=D0=B5=D0=B4=D1=83=D1=8E=D1=89=D0=B5?= =?UTF-8?q?=D0=B3=D0=BE=20=D0=B4=D0=BE=D1=81=D1=82=D1=83=D0=BF=D0=B0=20?= =?UTF-8?q?=D0=BA=20=D0=BD=D0=B8=D0=BC?= 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 | 47 +++++++++++++++++-- 1 file changed, 42 insertions(+), 5 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 74771f0..ef1ea7f 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 @@ -28,6 +28,7 @@ import org.springframework.ai.chat.observation.DefaultChatModelObservationConvention; import org.springframework.ai.chat.prompt.ChatOptions; import org.springframework.ai.chat.prompt.Prompt; +import org.springframework.ai.content.Media; import org.springframework.ai.model.ModelOptionsUtils; import org.springframework.ai.model.tool.*; import org.springframework.ai.retry.RetryUtils; @@ -286,7 +287,10 @@ Prompt buildRequestPrompt(Prompt prompt) { ToolCallingChatOptions.validateToolCallbacks(requestOptions.getToolCallbacks()); - return new Prompt(prompt.getInstructions(), requestOptions); + // Uploads media and sets an id to media + List messagesWithUploadedMediaIds = uploadMedia(prompt.getInstructions()); + + return new Prompt(messagesWithUploadedMediaIds, requestOptions); } private CompletionRequest createRequest(Prompt prompt, boolean stream) { @@ -295,10 +299,7 @@ private CompletionRequest createRequest(Prompt prompt, boolean stream) { if (message instanceof UserMessage userMessage) { if (!CollectionUtils.isEmpty(userMessage.getMedia())) { List filesIds = userMessage.getMedia().stream() - .map(media -> gigaChatApi - .uploadFile(media) - .getBody() - .id()) + .map(media -> UUID.fromString(media.getId())) .toList(); return List.of(new CompletionRequest.Message( @@ -363,6 +364,42 @@ private CompletionRequest createRequest(Prompt prompt, boolean stream) { return request; } + /** + * Загружает медиа файлы, если они переданы в UserMessage, и проставляет к ним id. + * @param messages - исходные сообщения + * @return - обновленные сообщения с проставленными id для media + */ + private List uploadMedia(List messages) { + return messages.stream() + .map(message -> { + if (message instanceof UserMessage userMessage + && !CollectionUtils.isEmpty(userMessage.getMedia())) { + var mediaWithIds = userMessage.getMedia().stream() + .map(media -> media.getId() != null + ? media + : Media.builder() + .id(gigaChatApi + .uploadFile(media) + .getBody() + .id() + .toString()) + .name(media.getName()) + .data(media.getData()) + .mimeType(media.getMimeType()) + .build()) + .toList(); + return UserMessage.builder() + .text(userMessage.getText()) + .metadata(userMessage.getMetadata()) + .media(mediaWithIds) + .build(); + } else { + return message; + } + }) + .toList(); + } + private Object getFunctionCall(GigaChatOptions requestOptions, List toolDefinitions) { var callMode = requestOptions.getFunctionCallMode(); From e14ff7736bfb85ae19f1cbc1f2ecf3182cd2a845 Mon Sep 17 00:00:00 2001 From: Linar Abzaltdinov Date: Fri, 20 Jun 2025 12:45:08 +0300 Subject: [PATCH 2/4] =?UTF-8?q?*=20=D0=A1=D0=BE=D1=85=D1=80=D0=B0=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20id=20=D0=B7=D0=B0=D0=B3=D1=80=D1=83?= =?UTF-8?q?=D0=B6=D0=B5=D0=BD=D0=BD=D1=8B=D1=85=20=D1=84=D0=B0=D0=B9=D0=BB?= =?UTF-8?q?=D0=BE=D0=B2=20=D0=B2=20ChatResponseMetadata=20*=20=D0=98=D1=81?= =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81?= =?UTF-8?q?=D0=BE=D1=85=D1=80=D0=B0=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B2?= =?UTF-8?q?=D0=BD=D1=83=D1=82=D1=80=D0=B5=D0=BD=D0=BD=D0=B8=D1=85=20=D1=81?= =?UTF-8?q?=D0=BE=D0=BE=D0=B1=D0=B7=D0=B5=D0=BD=D0=B8=D0=B9=20Spring=20AI?= =?UTF-8?q?=20=D0=B2=20ChatResponseMetadata?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Linar Abzaltdinov --- docs/response-metadata.md | 36 +++++++++++- .../example/MultimodalityController.java | 17 +++++- .../example/WeatherToolController.java | 2 +- .../chat/giga/springai/GigaChatModel.java | 55 +++++++++++++++---- .../support/GigaChatResponseUtils.java | 16 ++++-- .../support/GigaChatResponseUtilsTest.java | 52 +++++++++++++++--- 6 files changed, 147 insertions(+), 31 deletions(-) diff --git a/docs/response-metadata.md b/docs/response-metadata.md index bbe4cfd..f3c1531 100644 --- a/docs/response-metadata.md +++ b/docs/response-metadata.md @@ -3,12 +3,12 @@ Для получения дополнительной информации об ответе создан утилитный класс [GigaChatResponseUtils](../spring-ai-gigachat/src/main/java/chat/giga/springai/support/GigaChatResponseUtils.java) -### Получение всей истории переписки с GigaChat +### Получение всей переписки с GigaChat под капотом Spring AI Если Вам необходимо получить информацию обо всех сообщениях, которые были получены и отправлены под капотом фреймфорка Spring AI (например - какие тулы были вызваны, с какими параметрами, каковы результаты вызова тулов), -можно воспользоваться утилитным методом `GigaChatResponseUtils.getConversationHistory(chatResponse)`. +можно воспользоваться утилитным методом `GigaChatResponseUtils.getInternalMessages(chatResponse)`. Пример: @@ -18,10 +18,40 @@ ChatResponse chatResponse = chatClient .toolCallbacks(GigaTools.from(new WeatherTools())) .call() .chatResponse(); -List toolResponseMessages = GigaChatResponseUtils.getConversationHistory(chatResponse) +List toolResponseMessages = GigaChatResponseUtils.getInternalMessages(chatResponse) .stream() .filter(msg -> MessageType.TOOL.equals(msg.getMessageType())) .toList(); log.info("Было вызвано {} функций", toolResponseMessages.size()); ``` +### Получение иденификаторов загруженных файлов при использовании Multimodality + +Если Вам необходимо получить идентификаторы загруженных файлов, +(например - чтобы затем повторно использовать их в запросе или наоборот удалить), +можно воспользоваться утилитным методом `GigaChatResponseUtils.getUploadedMediaIds(chatResponse)`. + +Пример: + +```java +ChatResponse chatResponse = chatClient + .prompt() + .user(u -> u.text("Какая порода кота на фото?") + .media(new Media(MimeType.valueOf(multipartFile.getContentType()), multipartFile.getResource()))) + .call() + .chatResponse(); + +String mediaId = GigaChatResponseUtils.getUploadedMediaIds(chatResponse).get(0); + +// переиспользуем загруженные файлы в последующем запросе +chatClient.prompt() + .user(u -> u.text("Какого цвета шерсть у кота на фото?") + // при повторном использовании mimeType и data не важны, но не должны быть null + .media(Media.builder().id(mediaId).mimeType(MediaType.ALL).data("").build())) + .call() + .chatResponse(); + +// удаляем файлы +uploadedMediaIds.forEach(gigaChatApi::deleteFile); +``` + diff --git a/spring-ai-gigachat-example/src/main/java/chat/giga/springai/example/MultimodalityController.java b/spring-ai-gigachat-example/src/main/java/chat/giga/springai/example/MultimodalityController.java index 203180a..d1e4105 100644 --- a/spring-ai-gigachat-example/src/main/java/chat/giga/springai/example/MultimodalityController.java +++ b/spring-ai-gigachat-example/src/main/java/chat/giga/springai/example/MultimodalityController.java @@ -3,8 +3,12 @@ import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE; import chat.giga.springai.GigaChatOptions; +import chat.giga.springai.api.chat.GigaChatApi; +import chat.giga.springai.support.GigaChatResponseUtils; +import java.util.List; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.SimpleLoggerAdvisor; +import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.content.Media; import org.springframework.http.MediaType; import org.springframework.util.MimeType; @@ -16,8 +20,10 @@ public class MultimodalityController { private final ChatClient chatClient; + private final GigaChatApi gigaChatApi; - public MultimodalityController(ChatClient.Builder chatClientBuilder) { + public MultimodalityController(ChatClient.Builder chatClientBuilder, GigaChatApi gigaChatApi) { + this.gigaChatApi = gigaChatApi; this.chatClient = chatClientBuilder .defaultAdvisors(new SimpleLoggerAdvisor()) .defaultOptions( @@ -28,12 +34,17 @@ public MultimodalityController(ChatClient.Builder chatClientBuilder) { @PostMapping(value = "/multimodality/chat", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public String chatWithMultimodality( @RequestParam String userMessage, @RequestParam("file") MultipartFile multipartFile) { - return chatClient + ChatResponse chatResponse = chatClient .prompt() .user(u -> u.text(userMessage) .media(new Media( MimeType.valueOf(multipartFile.getContentType()), multipartFile.getResource()))) .call() - .content(); + .chatResponse(); + + List uploadedMediaIds = GigaChatResponseUtils.getUploadedMediaIds(chatResponse); + uploadedMediaIds.forEach(gigaChatApi::deleteFile); + + return chatResponse.getResult().getOutput().getText(); } } 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 90faa0c..f7eee5d 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 @@ -162,7 +162,7 @@ public String weatherToolAnnotation(@RequestBody String question) { .toolCallbacks(GigaTools.from(new WeatherTools())) .call() .chatResponse(); - List toolResponseMessages = GigaChatResponseUtils.getConversationHistory(chatResponse).stream() + List toolResponseMessages = GigaChatResponseUtils.getInternalMessages(chatResponse).stream() .filter(msg -> MessageType.TOOL.equals(msg.getMessageType())) .toList(); log.info("Было вызвано {} функций", toolResponseMessages.size()); 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 ef1ea7f..a94409b 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 @@ -48,7 +48,8 @@ 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"; + public static final String INTERNAL_CONVERSATION_HISTORY = "GigaChatInternalConversationHistory"; + public static final String UPLOADED_MEDIA_IDS = "GigaChatUploadedMediaIds"; private static final ToolCallingManager DEFAULT_TOOL_CALLING_MANAGER = ToolCallingManager.builder().build(); @@ -151,8 +152,7 @@ public ChatResponse internalCall(Prompt prompt, ChatResponse previousChatRespons Usage accumulatedUsage = UsageCalculator.getCumulativeUsage(currentChatResponseUsage, previousChatResponse); - ChatResponse chatResponse = - toChatResponse(completionResponse, accumulatedUsage, false, prompt.getInstructions()); + ChatResponse chatResponse = toChatResponse(completionResponse, accumulatedUsage, false); observationContext.setResponse(chatResponse); return chatResponse; @@ -173,7 +173,7 @@ public ChatResponse internalCall(Prompt prompt, ChatResponse previousChatRespons } } - return response; + return buildChatResponseWithCustomMetadata(prompt, response); } @Override @@ -214,8 +214,7 @@ public Flux internalStream(Prompt prompt, ChatResponse previousCha Usage accumulatedUsage = UsageCalculator.getCumulativeUsage(currentChatResponseUsage, previousChatResponse); - ChatResponse chatResponse = - toChatResponse(completionResponse, accumulatedUsage, true, prompt.getInstructions()); + ChatResponse chatResponse = toChatResponse(completionResponse, accumulatedUsage, true); if (this.toolExecutionEligibilityPredicate.isToolExecutionRequired( prompt.getOptions(), chatResponse)) { @@ -234,7 +233,7 @@ public Flux internalStream(Prompt prompt, ChatResponse previousCha } } - return Flux.just(chatResponse); + return Flux.just(buildChatResponseWithCustomMetadata(prompt, chatResponse)); }) .doOnError(observation::error) .doFinally(s -> observation.stop()) @@ -452,13 +451,11 @@ private List getFunctionDescriptions(List .toList(); } - private ChatResponse toChatResponse( - CompletionResponse completionResponse, Usage usage, boolean streaming, List conversationHistory) { + private ChatResponse toChatResponse(CompletionResponse completionResponse, Usage usage, boolean streaming) { List generations = completionResponse.getChoices().stream() .map(choice -> buildGeneration(completionResponse.getId(), choice, streaming)) .toList(); - return new ChatResponse( - generations, from(completionResponse, usage, Map.of(CONVERSATION_HISTORY, conversationHistory))); + return new ChatResponse(generations, from(completionResponse, usage)); } private Generation buildGeneration(String id, CompletionResponse.Choice choice, boolean streaming) { @@ -492,6 +489,42 @@ private Generation buildGeneration(String id, CompletionResponse.Choice choice, return new Generation(assistantMessage, generationMetadata); } + private ChatResponse buildChatResponseWithCustomMetadata(Prompt prompt, ChatResponse originalResponse) { + // т.к. этот метод вызывается при обратном проходе из рекурсии internalCall/internalStream, + // то нужно заполнять метаданные только один раз при первом вызове + if (originalResponse.getMetadata().containsKey(INTERNAL_CONVERSATION_HISTORY)) { + return originalResponse; + } + + List messages = prompt.getInstructions(); + + // ищем индекс последнего пользовательского сообщения, т.к. здесь могут быть сообщения из ChatMemory + int lastUserMessageIndex = -1; + for (int i = messages.size() - 1; i >= 0; i--) { + if (messages.get(i) instanceof UserMessage) { + lastUserMessageIndex = i; + break; + } + } + + // Должен включать только AssistantMessage и ToolResponseMessage, + // т.е. внутренние сообщения от GigaChat с параметрами вызова функции, а результаты вызова функций + var internalConversationHistory = new ArrayList<>(messages.subList(lastUserMessageIndex + 1, messages.size())); + var chatResponseBuilder = ChatResponse.builder() + .from(originalResponse) + .metadata(INTERNAL_CONVERSATION_HISTORY, internalConversationHistory); + + // ID загруженных медиа файлов + UserMessage lastUserMessage = (UserMessage) messages.get(lastUserMessageIndex); + if (!CollectionUtils.isEmpty(lastUserMessage.getMedia())) { + chatResponseBuilder.metadata( + UPLOADED_MEDIA_IDS, + lastUserMessage.getMedia().stream().map(Media::getId).toList()); + } + + return chatResponseBuilder.build(); + } + private ChatResponseMetadata from(CompletionResponse completionResponse) { return from(completionResponse, buildUsage(completionResponse.getUsage())); } 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 5a80640..f7114c7 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 @@ -15,11 +15,19 @@ @UtilityClass @Slf4j public class GigaChatResponseUtils { - public static List getConversationHistory(ChatResponse chatResponse) { + public static List getInternalMessages(ChatResponse chatResponse) { + return getFromMetadata(chatResponse, GigaChatModel.INTERNAL_CONVERSATION_HISTORY, List.of()); + } + + public static List getUploadedMediaIds(ChatResponse chatResponse) { + return getFromMetadata(chatResponse, GigaChatModel.UPLOADED_MEDIA_IDS, List.of()); + } + + private static T getFromMetadata(ChatResponse chatResponse, String key, T defaultValue) { if (chatResponse != null && chatResponse.getMetadata() != null) { - List messages = chatResponse.getMetadata().get(GigaChatModel.CONVERSATION_HISTORY); - return messages == null ? List.of() : messages; + T data = chatResponse.getMetadata().get(key); + return data == null ? defaultValue : data; } - return List.of(); + return defaultValue; } } 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 index 3c04ff1..9e1baf6 100644 --- 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 @@ -6,6 +6,8 @@ import chat.giga.springai.GigaChatModel; import java.util.Collections; import java.util.List; +import java.util.UUID; + import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; @@ -22,34 +24,66 @@ public class GigaChatResponseUtilsTest { private ChatResponse chatResponse; @Test - public void testGetConversationHistoryWithNullChatResponse() { - List result = GigaChatResponseUtils.getConversationHistory(null); + public void testGetInternalMessagesWithNullChatResponse() { + var result = GigaChatResponseUtils.getInternalMessages(null); assertEquals(Collections.emptyList(), result); } @Test - public void testGetConversationHistoryWithNullMetadata() { + public void testGetInternalMessagesWithNullMetadata() { when(chatResponse.getMetadata()).thenReturn(null); - List result = GigaChatResponseUtils.getConversationHistory(chatResponse); + var result = GigaChatResponseUtils.getInternalMessages(chatResponse); assertEquals(Collections.emptyList(), result); } @Test - public void testGetConversationHistoryWithEmptyMetadata() { + public void testGetInternalMessagesWithEmptyMetadata() { when(chatResponse.getMetadata()) .thenReturn(ChatResponseMetadata.builder().build()); - List result = GigaChatResponseUtils.getConversationHistory(chatResponse); + var result = GigaChatResponseUtils.getInternalMessages(chatResponse); assertEquals(Collections.emptyList(), result); } @Test - public void testGetConversationHistoryWithConversationHistoryInMetadata() { + public void testGetInternalMessages() { Message message = new UserMessage("Hello, world!"); var metadata = ChatResponseMetadata.builder() - .keyValue(GigaChatModel.CONVERSATION_HISTORY, List.of(message)) + .keyValue(GigaChatModel.INTERNAL_CONVERSATION_HISTORY, List.of(message)) .build(); when(chatResponse.getMetadata()).thenReturn(metadata); - List result = GigaChatResponseUtils.getConversationHistory(chatResponse); + var result = GigaChatResponseUtils.getInternalMessages(chatResponse); assertEquals(Collections.singletonList(message), result); } + + @Test + public void testGetUploadedMediaIdsWithNullChatResponse() { + var result = GigaChatResponseUtils.getUploadedMediaIds(null); + assertEquals(Collections.emptyList(), result); + } + + @Test + public void testGetUploadedMediaIdsWithNullMetadata() { + when(chatResponse.getMetadata()).thenReturn(null); + var result = GigaChatResponseUtils.getUploadedMediaIds(chatResponse); + assertEquals(Collections.emptyList(), result); + } + + @Test + public void testGetUploadedMediaIdsWithEmptyMetadata() { + when(chatResponse.getMetadata()) + .thenReturn(ChatResponseMetadata.builder().build()); + var result = GigaChatResponseUtils.getUploadedMediaIds(chatResponse); + assertEquals(Collections.emptyList(), result); + } + + @Test + public void testGetUploadedMediaIds() { + String mediaId = UUID.randomUUID().toString(); + var metadata = ChatResponseMetadata.builder() + .keyValue(GigaChatModel.UPLOADED_MEDIA_IDS, List.of(mediaId)) + .build(); + when(chatResponse.getMetadata()).thenReturn(metadata); + var result = GigaChatResponseUtils.getUploadedMediaIds(chatResponse); + assertEquals(Collections.singletonList(mediaId), result); + } } From 30060b8406e90342d48a7eb2cf5264fe1fb3ce5a Mon Sep 17 00:00:00 2001 From: Linar Abzaltdinov Date: Fri, 20 Jun 2025 12:51:09 +0300 Subject: [PATCH 3/4] spotless Signed-off-by: Linar Abzaltdinov --- .../chat/giga/springai/support/GigaChatResponseUtilsTest.java | 1 - 1 file changed, 1 deletion(-) 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 index 9e1baf6..8d6b1da 100644 --- 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 @@ -7,7 +7,6 @@ import java.util.Collections; import java.util.List; import java.util.UUID; - import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; From 911ff621425d4e0730892bff7c96375f15b7419b Mon Sep 17 00:00:00 2001 From: Linar Abzaltdinov Date: Fri, 20 Jun 2025 20:18:39 +0300 Subject: [PATCH 4/4] =?UTF-8?q?=D1=80=D0=B5=D1=84=D0=B0=D0=BA=D1=82=D0=BE?= =?UTF-8?q?=D1=80=D0=B8=D0=BD=D0=B3?= 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 | 73 ++++++++++++------- 1 file changed, 45 insertions(+), 28 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 a94409b..3afa421 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 @@ -371,27 +371,8 @@ private CompletionRequest createRequest(Prompt prompt, boolean stream) { private List uploadMedia(List messages) { return messages.stream() .map(message -> { - if (message instanceof UserMessage userMessage - && !CollectionUtils.isEmpty(userMessage.getMedia())) { - var mediaWithIds = userMessage.getMedia().stream() - .map(media -> media.getId() != null - ? media - : Media.builder() - .id(gigaChatApi - .uploadFile(media) - .getBody() - .id() - .toString()) - .name(media.getName()) - .data(media.getData()) - .mimeType(media.getMimeType()) - .build()) - .toList(); - return UserMessage.builder() - .text(userMessage.getText()) - .metadata(userMessage.getMetadata()) - .media(mediaWithIds) - .build(); + if (message instanceof UserMessage userMessage) { + return buildUserMessageWithUploadedMedia(userMessage); } else { return message; } @@ -399,6 +380,38 @@ private List uploadMedia(List messages) { .toList(); } + private UserMessage buildUserMessageWithUploadedMedia(UserMessage userMessage) { + List mediaList = userMessage.getMedia(); + + // Если нет медиа, то ничего не меняем + if (CollectionUtils.isEmpty(mediaList)) { + return userMessage; + } + var mediaWithIds = mediaList.stream().map(this::uploadMediaAndSetId).toList(); + return UserMessage.builder() + .text(userMessage.getText()) + .metadata(userMessage.getMetadata()) + .media(mediaWithIds) + .build(); + } + + // Загрузка файла в GigaChat, если у media нет id. + private Media uploadMediaAndSetId(Media media) { + // если id указан - значит файл уже загружен и можно возвращать media как есть + if (media.getId() != null) { + return media; + } + // иначе загружаем файл и получаем его id + String mediaId = gigaChatApi.uploadFile(media).getBody().id().toString(); + + return Media.builder() + .id(mediaId) + .name(media.getName()) + .data(media.getData()) + .mimeType(media.getMimeType()) + .build(); + } + private Object getFunctionCall(GigaChatOptions requestOptions, List toolDefinitions) { var callMode = requestOptions.getFunctionCallMode(); @@ -499,13 +512,7 @@ private ChatResponse buildChatResponseWithCustomMetadata(Prompt prompt, ChatResp List messages = prompt.getInstructions(); // ищем индекс последнего пользовательского сообщения, т.к. здесь могут быть сообщения из ChatMemory - int lastUserMessageIndex = -1; - for (int i = messages.size() - 1; i >= 0; i--) { - if (messages.get(i) instanceof UserMessage) { - lastUserMessageIndex = i; - break; - } - } + int lastUserMessageIndex = getIndexOfLastUserMessage(messages); // Должен включать только AssistantMessage и ToolResponseMessage, // т.е. внутренние сообщения от GigaChat с параметрами вызова функции, а результаты вызова функций @@ -525,6 +532,16 @@ private ChatResponse buildChatResponseWithCustomMetadata(Prompt prompt, ChatResp return chatResponseBuilder.build(); } + // Возвращает индекс последнего пользовательского сообщения, или -1, если их нет + private int getIndexOfLastUserMessage(List messages) { + for (int i = messages.size() - 1; i >= 0; i--) { + if (messages.get(i) instanceof UserMessage) { + return i; + } + } + return -1; + } + private ChatResponseMetadata from(CompletionResponse completionResponse) { return from(completionResponse, buildUsage(completionResponse.getUsage())); }