Skip to content

Commit ee69341

Browse files
committed
refactor: cleanup
1 parent b373618 commit ee69341

File tree

7 files changed

+223
-142
lines changed

7 files changed

+223
-142
lines changed

vaadin-ai-components-flow-parent/vaadin-ai-components-flow/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,12 @@
3333
<version>${langchain4j.version}</version>
3434
<optional>true</optional>
3535
</dependency>
36+
<dependency>
37+
<groupId>dev.langchain4j</groupId>
38+
<artifactId>langchain4j-open-ai</artifactId>
39+
<version>${langchain4j.version}</version>
40+
<optional>true</optional>
41+
</dependency>
3642
<dependency>
3743
<groupId>io.projectreactor</groupId>
3844
<artifactId>reactor-core</artifactId>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright 2000-2026 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.component.ai.provider;
17+
18+
/**
19+
* Supported content type categories for attachments.
20+
* <p>
21+
* Intended only for internal use and can be removed in the future.
22+
*/
23+
enum AttachmentContentType {
24+
/** Image content types (image/*). */
25+
IMAGE,
26+
/** Text content types (text/*). */
27+
TEXT,
28+
/** PDF content types (application/pdf, application/x-pdf). */
29+
PDF,
30+
/** Audio content types (audio/*). */
31+
AUDIO,
32+
/** Video content types (video/*). */
33+
VIDEO,
34+
/** Unsupported or unknown content types. */
35+
UNSUPPORTED;
36+
37+
/**
38+
* Detects the content type category from a MIME type string.
39+
*
40+
* @param contentType
41+
* the MIME content type
42+
* @return the corresponding category, or {@code UNSUPPORTED} if not
43+
* recognized
44+
*/
45+
public static AttachmentContentType fromMimeType(String contentType) {
46+
if (contentType != null) {
47+
if (contentType.startsWith("image/")) {
48+
return IMAGE;
49+
}
50+
if (contentType.startsWith("text/")) {
51+
return TEXT;
52+
}
53+
if (contentType.startsWith("application/pdf")
54+
|| contentType.startsWith("application/x-pdf")) {
55+
return PDF;
56+
}
57+
if (contentType.startsWith("audio/")) {
58+
return AUDIO;
59+
}
60+
if (contentType.startsWith("video/")) {
61+
return VIDEO;
62+
}
63+
}
64+
return UNSUPPORTED;
65+
}
66+
}

vaadin-ai-components-flow-parent/vaadin-ai-components-flow/src/main/java/com/vaadin/flow/component/ai/provider/LLMProviderHelpers.java

Lines changed: 6 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -27,44 +27,7 @@
2727
* <p>
2828
* Intended only for internal use and can be removed in the future.
2929
*/
30-
public final class LLMProviderHelpers {
31-
32-
/**
33-
* Supported content type categories for attachments.
34-
*/
35-
public enum AttachmentContentType {
36-
/** Image content types (image/*). */
37-
IMAGE,
38-
/** Text content types (text/*). */
39-
TEXT,
40-
/** PDF content types (application/pdf, application/x-pdf). */
41-
PDF,
42-
/** Unsupported or unknown content types. */
43-
UNSUPPORTED;
44-
45-
/**
46-
* Detects the content type category from a MIME type string.
47-
*
48-
* @param contentType
49-
* the MIME content type
50-
* @return the corresponding category, or {@code UNSUPPORTED} if not
51-
* recognized
52-
*/
53-
public static AttachmentContentType fromMimeType(String contentType) {
54-
if (contentType != null) {
55-
if (contentType.startsWith("image/")) {
56-
return IMAGE;
57-
}
58-
if (contentType.contains("text")) {
59-
return TEXT;
60-
}
61-
if (contentType.contains("pdf")) {
62-
return PDF;
63-
}
64-
}
65-
return UNSUPPORTED;
66-
}
67-
}
30+
final class LLMProviderHelpers {
6831

6932
/**
7033
* Decodes byte array as UTF-8 text.
@@ -107,8 +70,11 @@ public static String decodeAsUtf8(byte[] data, String fileName,
10770
* @return a data URL in the format "data:{contentType};base64,{data}"
10871
*/
10972
public static String toBase64DataUrl(byte[] data, String contentType) {
110-
var base64 = Base64.getEncoder().encodeToString(data);
111-
return "data:" + contentType + ";base64," + base64;
73+
return "data:" + contentType + ";base64," + getBase64Data(data);
74+
}
75+
76+
public static String getBase64Data(byte[] data) {
77+
return Base64.getEncoder().encodeToString(data);
11278
}
11379

11480
/**

vaadin-ai-components-flow-parent/vaadin-ai-components-flow/src/main/java/com/vaadin/flow/component/ai/provider/langchain4j/LangChain4JLLMProvider.java renamed to vaadin-ai-components-flow-parent/vaadin-ai-components-flow/src/main/java/com/vaadin/flow/component/ai/provider/LangChain4JLLMProvider.java

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@
1313
* License for the specific language governing permissions and limitations under
1414
* the License.
1515
*/
16-
package com.vaadin.flow.component.ai.provider.langchain4j;
16+
package com.vaadin.flow.component.ai.provider;
1717

18-
import java.lang.reflect.Method;
1918
import java.util.ArrayList;
2019
import java.util.Arrays;
2120
import java.util.Collections;
@@ -32,21 +31,22 @@
3231
import org.slf4j.LoggerFactory;
3332

3433
import com.vaadin.flow.component.UI;
35-
import com.vaadin.flow.component.ai.provider.LLMProvider;
36-
import com.vaadin.flow.component.ai.provider.LLMProviderHelpers;
3734

3835
import dev.langchain4j.agent.tool.Tool;
3936
import dev.langchain4j.agent.tool.ToolExecutionRequest;
4037
import dev.langchain4j.agent.tool.ToolSpecification;
4138
import dev.langchain4j.agent.tool.ToolSpecifications;
4239
import dev.langchain4j.data.message.AiMessage;
40+
import dev.langchain4j.data.message.AudioContent;
4341
import dev.langchain4j.data.message.ChatMessage;
4442
import dev.langchain4j.data.message.Content;
4543
import dev.langchain4j.data.message.ImageContent;
44+
import dev.langchain4j.data.message.PdfFileContent;
4645
import dev.langchain4j.data.message.SystemMessage;
4746
import dev.langchain4j.data.message.TextContent;
4847
import dev.langchain4j.data.message.ToolExecutionResultMessage;
4948
import dev.langchain4j.data.message.UserMessage;
49+
import dev.langchain4j.data.message.VideoContent;
5050
import dev.langchain4j.memory.ChatMemory;
5151
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
5252
import dev.langchain4j.model.chat.ChatModel;
@@ -124,7 +124,8 @@ public Flux<String> stream(LLMRequest request) {
124124
"User message must not be null");
125125
return Flux.create(sink -> {
126126
try {
127-
chatMemory.add(buildUserMessage(request));
127+
var userMessage = buildUserMessage(request);
128+
chatMemory.add(userMessage);
128129
var toolContext = new ToolContext(prepareToolExecutors(request),
129130
prepareToolSpecifications(request));
130131
var context = new ChatExecutionContext(request, sink,
@@ -137,7 +138,11 @@ public Flux<String> stream(LLMRequest request) {
137138
}
138139

139140
/**
140-
* Only for testing purposes
141+
* Sets the timeout for tool execution in seconds. Only for testing
142+
* purposes.
143+
*
144+
* @param toolExecutionTimeoutSeconds
145+
* the timeout in seconds
141146
*/
142147
void setToolExecutionTimeoutSeconds(int toolExecutionTimeoutSeconds) {
143148
this.toolExecutionTimeoutSeconds = toolExecutionTimeoutSeconds;
@@ -155,11 +160,15 @@ private Map<String, ToolExecutor> prepareToolExecutors(LLMRequest request) {
155160
Arrays.stream(toolObject.getClass().getDeclaredMethods())
156161
.filter(method -> method.isAnnotationPresent(Tool.class))
157162
.forEach(method -> {
163+
// This can fail in environments where the module
164+
// doesn't allow
165+
// reflective access, in which case we skip the method.
158166
try {
159167
method.setAccessible(true);
160168
} catch (Exception e) {
161169
LOGGER.warn(
162-
"Failed to make tool method accessible: {}",
170+
"Failed to make tool method accessible: {}. "
171+
+ "Ensure the method is public or the module allows reflective access.",
163172
method.getName(), e);
164173
return;
165174
}
@@ -172,7 +181,8 @@ private Map<String, ToolExecutor> prepareToolExecutors(LLMRequest request) {
172181
return toolExecutors;
173182
}
174183

175-
private ToolExecutor getToolExecutor(Object toolObject, Method method) {
184+
private ToolExecutor getToolExecutor(Object toolObject,
185+
java.lang.reflect.Method method) {
176186
var baseExecutor = new DefaultToolExecutor(toolObject, method);
177187
return (toolRequest, memoryId) -> {
178188
var currentUI = UI.getCurrent();
@@ -358,12 +368,14 @@ private boolean isStreaming() {
358368
private static Optional<Content> getAttachmentContent(
359369
Attachment attachment) {
360370
LLMProviderHelpers.validateAttachment(attachment);
361-
var contentType = LLMProviderHelpers.AttachmentContentType
371+
var contentType = AttachmentContentType
362372
.fromMimeType(attachment.contentType());
363373
return switch (contentType) {
364374
case IMAGE -> Optional.of(getImageAttachmentContent(attachment));
365375
case TEXT -> Optional.of(getTextAttachmentContent(attachment));
366376
case PDF -> Optional.of(getPdfAttachmentContent(attachment));
377+
case AUDIO -> Optional.of(getAudioAttachmentContent(attachment));
378+
case VIDEO -> Optional.of(getVideoAttachmentContent(attachment));
367379
case UNSUPPORTED -> Optional.empty();
368380
};
369381
}
@@ -375,18 +387,28 @@ private static TextContent getTextAttachmentContent(Attachment attachment) {
375387
.formatTextAttachment(attachment.fileName(), textContent));
376388
}
377389

378-
private static TextContent getPdfAttachmentContent(Attachment attachment) {
379-
var textContent = LLMProviderHelpers.decodeAsUtf8(attachment.data(),
380-
attachment.fileName(), true);
381-
return TextContent.from(LLMProviderHelpers
382-
.formatTextAttachment(attachment.fileName(), textContent));
390+
private static PdfFileContent getPdfAttachmentContent(
391+
Attachment attachment) {
392+
var base64 = LLMProviderHelpers.getBase64Data(attachment.data());
393+
return PdfFileContent.from(base64, attachment.contentType());
383394
}
384395

385396
private static ImageContent getImageAttachmentContent(
386397
Attachment attachment) {
387-
var dataUrl = LLMProviderHelpers.toBase64DataUrl(attachment.data(),
388-
attachment.contentType());
389-
return ImageContent.from(dataUrl, ImageContent.DetailLevel.AUTO);
398+
var base64 = LLMProviderHelpers.getBase64Data(attachment.data());
399+
return ImageContent.from(base64, attachment.contentType());
400+
}
401+
402+
private static AudioContent getAudioAttachmentContent(
403+
Attachment attachment) {
404+
var base64 = LLMProviderHelpers.getBase64Data(attachment.data());
405+
return AudioContent.from(base64, attachment.contentType());
406+
}
407+
408+
private static VideoContent getVideoAttachmentContent(
409+
Attachment attachment) {
410+
var base64 = LLMProviderHelpers.getBase64Data(attachment.data());
411+
return VideoContent.from(base64, attachment.contentType());
390412
}
391413

392414
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
/*
2+
* Copyright 2000-2026 Vaadin Ltd.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
5+
* use this file except in compliance with the License. You may obtain a copy of
6+
* the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12+
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
13+
* License for the specific language governing permissions and limitations under
14+
* the License.
15+
*/
16+
package com.vaadin.flow.component.ai.provider;
17+
18+
import org.junit.Assert;
19+
import org.junit.Test;
20+
21+
public class AttachmentContentTypeTest {
22+
23+
@Test
24+
public void supportedContentType_fromMimeType_withImageTypes_returnsImage() {
25+
Assert.assertEquals(AttachmentContentType.IMAGE,
26+
AttachmentContentType.fromMimeType("image/png"));
27+
Assert.assertEquals(AttachmentContentType.IMAGE,
28+
AttachmentContentType.fromMimeType("image/jpeg"));
29+
Assert.assertEquals(AttachmentContentType.IMAGE,
30+
AttachmentContentType.fromMimeType("image/gif"));
31+
}
32+
33+
@Test
34+
public void supportedContentType_fromMimeType_withTextTypes_returnsText() {
35+
Assert.assertEquals(AttachmentContentType.TEXT,
36+
AttachmentContentType.fromMimeType("text/plain"));
37+
Assert.assertEquals(AttachmentContentType.TEXT,
38+
AttachmentContentType.fromMimeType("text/html"));
39+
}
40+
41+
@Test
42+
public void supportedContentType_fromMimeType_withPdfTypes_returnsPdf() {
43+
Assert.assertEquals(AttachmentContentType.PDF,
44+
AttachmentContentType.fromMimeType("application/pdf"));
45+
Assert.assertEquals(AttachmentContentType.PDF,
46+
AttachmentContentType.fromMimeType("application/x-pdf"));
47+
}
48+
49+
@Test
50+
public void supportedContentType_fromMimeType_withAudioTypes_returnsAudio() {
51+
Assert.assertEquals(AttachmentContentType.AUDIO,
52+
AttachmentContentType.fromMimeType("audio/mpeg"));
53+
Assert.assertEquals(AttachmentContentType.AUDIO,
54+
AttachmentContentType.fromMimeType("audio/wav"));
55+
Assert.assertEquals(AttachmentContentType.AUDIO,
56+
AttachmentContentType.fromMimeType("audio/ogg"));
57+
}
58+
59+
@Test
60+
public void supportedContentType_fromMimeType_withVideoTypes_returnsVideo() {
61+
Assert.assertEquals(AttachmentContentType.VIDEO,
62+
AttachmentContentType.fromMimeType("video/mp4"));
63+
Assert.assertEquals(AttachmentContentType.VIDEO,
64+
AttachmentContentType.fromMimeType("video/webm"));
65+
Assert.assertEquals(AttachmentContentType.VIDEO,
66+
AttachmentContentType.fromMimeType("video/ogg"));
67+
}
68+
69+
@Test
70+
public void supportedContentType_fromMimeType_withUnsupportedTypes_returnsUnsupported() {
71+
Assert.assertEquals(AttachmentContentType.UNSUPPORTED,
72+
AttachmentContentType.fromMimeType("application/octet-stream"));
73+
Assert.assertEquals(AttachmentContentType.UNSUPPORTED,
74+
AttachmentContentType.fromMimeType("application/json"));
75+
}
76+
77+
@Test
78+
public void supportedContentType_fromMimeType_withNull_returnsUnsupported() {
79+
Assert.assertEquals(AttachmentContentType.UNSUPPORTED,
80+
AttachmentContentType.fromMimeType(null));
81+
}
82+
}

0 commit comments

Comments
 (0)