Skip to content

Commit 25b3468

Browse files
committed
fix: Resolve IllegalArgumentException for text MIME types in LangChain4j adapter
1 parent 0447bf6 commit 25b3468

2 files changed

Lines changed: 175 additions & 5 deletions

File tree

contrib/langchain4j/src/main/java/com/google/adk/models/langchain4j/LangChain4j.java

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,8 @@
7070
import dev.langchain4j.model.output.TokenUsage;
7171
import io.reactivex.rxjava3.core.BackpressureStrategy;
7272
import io.reactivex.rxjava3.core.Flowable;
73+
74+
import java.nio.charset.Charset;
7375
import java.util.ArrayList;
7476
import java.util.Base64;
7577
import java.util.HashMap;
@@ -388,13 +390,12 @@ private List<ChatMessage> toUserOrToolResultMessage(Content content) {
388390
.mimeType(mimeType)
389391
.build());
390392
} else if (mimeType.startsWith("text/")
391-
|| "application/json".equals(mimeType)
392-
|| mimeType.endsWith("+json")
393-
|| mimeType.endsWith("+xml")) {
393+
|| mimeType.startsWith("application/json")
394+
|| mimeType.contains("+json")
395+
|| mimeType.contains("+xml")) {
394396
// TODO are there missing text based mime types?
395397
// TODO should we assume UTF_8?
396-
lc4jContents.add(
397-
TextContent.from(new String(bytes, java.nio.charset.StandardCharsets.UTF_8)));
398+
lc4jContent = TextContent.from(new String(bytes, extractCharset(mimeType)));
398399
}
399400

400401
if (lc4jContent != null) {
@@ -420,6 +421,20 @@ private List<ChatMessage> toUserOrToolResultMessage(Content content) {
420421
}
421422
}
422423

424+
private Charset extractCharset(String mimeType) {
425+
String charSetString="charset=";
426+
if (mimeType == null || !mimeType.toLowerCase().contains(charSetString)) {
427+
return java.nio.charset.StandardCharsets.UTF_8;
428+
}
429+
try {
430+
String[] parts = mimeType.toLowerCase().split(charSetString);
431+
String charsetName = parts[1].split(";")[0].trim().replace("\"", "").replace("'", "");
432+
return java.nio.charset.Charset.forName(charsetName);
433+
} catch (IllegalArgumentException e) {
434+
return java.nio.charset.StandardCharsets.UTF_8;
435+
}
436+
}
437+
423438
private AiMessage toAiMessage(Content content) {
424439
List<String> texts = new ArrayList<>();
425440
List<ToolExecutionRequest> toolExecutionRequests = new ArrayList<>();

contrib/langchain4j/src/test/java/com/google/adk/models/langchain4j/LangChain4jTest.java

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -993,4 +993,159 @@ void testGenerateContentWithNullAiMessageText() {
993993
assertThat(response.content()).isPresent();
994994
assertThat(response.content().get().parts().orElse(List.of())).isEmpty();
995995
}
996+
997+
@Test
998+
@DisplayName("Should parse text/plain inlineData as TextContent without exception")
999+
void testGenerateContentWithTextPlainInlineData() {
1000+
final String textPayload = "Hello, plain text.";
1001+
final Blob textBlob =
1002+
Blob.builder()
1003+
.mimeType("text/plain")
1004+
.data(textPayload.getBytes(java.nio.charset.StandardCharsets.UTF_8))
1005+
.build();
1006+
final Part textPart = Part.builder().inlineData(textBlob).build();
1007+
1008+
final LlmRequest llmRequest =
1009+
LlmRequest.builder().contents(List.of(Content.fromParts(textPart))).build();
1010+
1011+
final ChatResponse chatResponse = mock(ChatResponse.class);
1012+
final AiMessage aiMessage = AiMessage.from("Acknowledged.");
1013+
when(chatResponse.aiMessage()).thenReturn(aiMessage);
1014+
when(chatModel.chat(any(ChatRequest.class))).thenReturn(chatResponse);
1015+
1016+
langChain4j.generateContent(llmRequest, false).blockingFirst();
1017+
1018+
final ArgumentCaptor<ChatRequest> requestCaptor = ArgumentCaptor.forClass(ChatRequest.class);
1019+
verify(chatModel).chat(requestCaptor.capture());
1020+
final ChatRequest capturedRequest = requestCaptor.getValue();
1021+
1022+
assertThat(capturedRequest.messages()).hasSize(1);
1023+
assertThat(capturedRequest.messages().get(0)).isInstanceOf(UserMessage.class);
1024+
final UserMessage userMessage = (UserMessage) capturedRequest.messages().get(0);
1025+
1026+
assertThat(userMessage.contents()).hasSize(1);
1027+
assertThat(userMessage.contents().get(0))
1028+
.isInstanceOf(dev.langchain4j.data.message.TextContent.class);
1029+
1030+
final dev.langchain4j.data.message.TextContent textContent =
1031+
(dev.langchain4j.data.message.TextContent) userMessage.contents().get(0);
1032+
assertThat(textContent.text()).isEqualTo(textPayload);
1033+
}
1034+
1035+
@Test
1036+
@DisplayName("Should parse application/json inlineData as TextContent without exception")
1037+
void testGenerateContentWithApplicationJsonInlineData() {
1038+
final String jsonPayload = "{\"key\":\"value\"}";
1039+
final Blob jsonBlob =
1040+
Blob.builder()
1041+
.mimeType("application/json")
1042+
.data(jsonPayload.getBytes(java.nio.charset.StandardCharsets.UTF_8))
1043+
.build();
1044+
final Part jsonPart = Part.builder().inlineData(jsonBlob).build();
1045+
1046+
final LlmRequest llmRequest =
1047+
LlmRequest.builder().contents(List.of(Content.fromParts(jsonPart))).build();
1048+
1049+
final ChatResponse chatResponse = mock(ChatResponse.class);
1050+
final AiMessage aiMessage = AiMessage.from("Parsed JSON.");
1051+
when(chatResponse.aiMessage()).thenReturn(aiMessage);
1052+
when(chatModel.chat(any(ChatRequest.class))).thenReturn(chatResponse);
1053+
1054+
langChain4j.generateContent(llmRequest, false).blockingFirst();
1055+
1056+
final ArgumentCaptor<ChatRequest> requestCaptor = ArgumentCaptor.forClass(ChatRequest.class);
1057+
verify(chatModel).chat(requestCaptor.capture());
1058+
final ChatRequest capturedRequest = requestCaptor.getValue();
1059+
1060+
final UserMessage userMessage = (UserMessage) capturedRequest.messages().get(0);
1061+
final dev.langchain4j.data.message.TextContent textContent =
1062+
(dev.langchain4j.data.message.TextContent) userMessage.contents().get(0);
1063+
assertThat(textContent.text()).isEqualTo(jsonPayload);
1064+
}
1065+
1066+
@Test
1067+
@DisplayName(
1068+
"Should throw IllegalArgumentException for genuinely unsupported inlineData mime types")
1069+
void testGenerateContentWithUnsupportedMimeType() {
1070+
final Blob unsupportedBlob =
1071+
Blob.builder().mimeType("application/x-yaml").data(new byte[] {1, 2, 3, 4}).build();
1072+
final Part unsupportedPart = Part.builder().inlineData(unsupportedBlob).build();
1073+
1074+
final LlmRequest llmRequest =
1075+
LlmRequest.builder().contents(List.of(Content.fromParts(unsupportedPart))).build();
1076+
1077+
assertThatThrownBy(() -> langChain4j.generateContent(llmRequest, false).blockingFirst())
1078+
.isInstanceOf(IllegalArgumentException.class)
1079+
.hasMessageContaining("Unknown or unhandled mime type: application/x-yaml");
1080+
}
1081+
1082+
@Test
1083+
@DisplayName("Should extract and apply explicit charset from mimeType (e.g., UTF-16)")
1084+
void testGenerateContentWithExplicitCharset() {
1085+
final String textPayload = "Hello, this is strictly UTF-16 encoded text.";
1086+
1087+
final byte[] utf16Bytes = textPayload.getBytes(java.nio.charset.StandardCharsets.UTF_16);
1088+
1089+
final Blob textBlob =
1090+
Blob.builder()
1091+
.mimeType("text/plain; charset=utf-16")
1092+
.data(utf16Bytes)
1093+
.build();
1094+
final Part textPart = Part.builder().inlineData(textBlob).build();
1095+
1096+
final LlmRequest llmRequest =
1097+
LlmRequest.builder().contents(List.of(Content.fromParts(textPart))).build();
1098+
1099+
final ChatResponse chatResponse = mock(ChatResponse.class);
1100+
final AiMessage aiMessage = AiMessage.from("Acknowledged.");
1101+
when(chatResponse.aiMessage()).thenReturn(aiMessage);
1102+
when(chatModel.chat(any(ChatRequest.class))).thenReturn(chatResponse);
1103+
1104+
langChain4j.generateContent(llmRequest, false).blockingFirst();
1105+
1106+
final ArgumentCaptor<ChatRequest> requestCaptor = ArgumentCaptor.forClass(ChatRequest.class);
1107+
verify(chatModel).chat(requestCaptor.capture());
1108+
final ChatRequest capturedRequest = requestCaptor.getValue();
1109+
1110+
final UserMessage userMessage = (UserMessage) capturedRequest.messages().get(0);
1111+
final dev.langchain4j.data.message.TextContent textContent =
1112+
(dev.langchain4j.data.message.TextContent) userMessage.contents().get(0);
1113+
1114+
assertThat(textContent.text()).isEqualTo(textPayload);
1115+
}
1116+
1117+
@Test
1118+
@DisplayName("Should safely fallback to UTF-8 if provided charset is malformed or unsupported")
1119+
void testGenerateContentWithMalformedCharsetFallback() {
1120+
final String textPayload = "{\"status\": \"fallback to UTF-8 successful\"}";
1121+
1122+
final byte[] utf8Bytes = textPayload.getBytes(java.nio.charset.StandardCharsets.UTF_8);
1123+
1124+
final Blob jsonBlob =
1125+
Blob.builder()
1126+
.mimeType("application/json; charset=not-a-real-charset-12345")
1127+
.data(utf8Bytes)
1128+
.build();
1129+
final Part jsonPart = Part.builder().inlineData(jsonBlob).build();
1130+
1131+
final LlmRequest llmRequest =
1132+
LlmRequest.builder().contents(List.of(Content.fromParts(jsonPart))).build();
1133+
1134+
final ChatResponse chatResponse = mock(ChatResponse.class);
1135+
final AiMessage aiMessage = AiMessage.from("Fallback verified.");
1136+
when(chatResponse.aiMessage()).thenReturn(aiMessage);
1137+
when(chatModel.chat(any(ChatRequest.class))).thenReturn(chatResponse);
1138+
1139+
langChain4j.generateContent(llmRequest, false).blockingFirst();
1140+
1141+
final ArgumentCaptor<ChatRequest> requestCaptor = ArgumentCaptor.forClass(ChatRequest.class);
1142+
verify(chatModel).chat(requestCaptor.capture());
1143+
final ChatRequest capturedRequest = requestCaptor.getValue();
1144+
1145+
final UserMessage userMessage = (UserMessage) capturedRequest.messages().get(0);
1146+
final dev.langchain4j.data.message.TextContent textContent =
1147+
(dev.langchain4j.data.message.TextContent) userMessage.contents().get(0);
1148+
1149+
assertThat(textContent.text()).isEqualTo(textPayload);
1150+
}
9961151
}

0 commit comments

Comments
 (0)