diff --git a/.gitignore b/.gitignore index 7879fe1..3d5de90 100644 --- a/.gitignore +++ b/.gitignore @@ -16,7 +16,7 @@ buildNumber.properties .project # JDT-specific (Eclipse Java Development Tools) .classpath - +nb-configuration.xml .idea diff --git a/examples/glassfish-car-booking/pom.xml b/examples/glassfish-car-booking/pom.xml index 1dc46e0..9373056 100644 --- a/examples/glassfish-car-booking/pom.xml +++ b/examples/glassfish-car-booking/pom.xml @@ -25,10 +25,13 @@ ${jakarta.jakartaee-api.version} provided + + io.smallrye.llm + mp-ai-api + io.smallrye.llm smallrye-llm-langchain4j-portable-extension - 1.0.0-SNAPSHOT jakarta.enterprise @@ -66,7 +69,6 @@ dev.langchain4j langchain4j - ${dev.langchain4j.version} dev.langchain4j @@ -88,7 +90,7 @@ org.slf4j slf4j-jdk14 runtime - 2.0.9 + 2.0.16 diff --git a/examples/glassfish-car-booking/src/main/java/io/jefrajames/booking/BookingService.java b/examples/glassfish-car-booking/src/main/java/io/jefrajames/booking/BookingService.java index 4ee5ff5..3e38e4c 100644 --- a/examples/glassfish-car-booking/src/main/java/io/jefrajames/booking/BookingService.java +++ b/examples/glassfish-car-booking/src/main/java/io/jefrajames/booking/BookingService.java @@ -6,9 +6,9 @@ import java.util.Map; import java.util.stream.Collectors; -import jakarta.enterprise.context.ApplicationScoped; +import org.eclipse.microprofile.ai.llm.Tool; -import dev.langchain4j.agent.tool.Tool; +import jakarta.enterprise.context.ApplicationScoped; import lombok.extern.java.Log; @ApplicationScoped diff --git a/examples/glassfish-car-booking/src/main/java/io/jefrajames/booking/ChatAiService.java b/examples/glassfish-car-booking/src/main/java/io/jefrajames/booking/ChatAiService.java index d35c5c3..594b8f1 100644 --- a/examples/glassfish-car-booking/src/main/java/io/jefrajames/booking/ChatAiService.java +++ b/examples/glassfish-car-booking/src/main/java/io/jefrajames/booking/ChatAiService.java @@ -1,9 +1,7 @@ package io.jefrajames.booking; -import dev.langchain4j.service.SystemMessage; -import io.smallrye.llm.spi.RegisterAIService; - -import java.time.temporal.ChronoUnit; +import org.eclipse.microprofile.ai.llm.RegisterAIService; +import org.eclipse.microprofile.ai.llm.SystemMessage; @SuppressWarnings("CdiManagedBeanInconsistencyInspection") @RegisterAIService(tools = BookingService.class, chatMemoryMaxMessages = 10, chatLanguageModelName = "chat-model") diff --git a/examples/glassfish-car-booking/src/main/java/io/jefrajames/booking/FraudAiService.java b/examples/glassfish-car-booking/src/main/java/io/jefrajames/booking/FraudAiService.java index 68488de..021bf3c 100644 --- a/examples/glassfish-car-booking/src/main/java/io/jefrajames/booking/FraudAiService.java +++ b/examples/glassfish-car-booking/src/main/java/io/jefrajames/booking/FraudAiService.java @@ -1,13 +1,12 @@ package io.jefrajames.booking; -import dev.langchain4j.service.SystemMessage; -import dev.langchain4j.service.UserMessage; -import dev.langchain4j.service.V; -import io.smallrye.llm.spi.RegisterAIService; +import org.eclipse.microprofile.ai.llm.RegisterAIService; +import org.eclipse.microprofile.ai.llm.SystemMessage; +import org.eclipse.microprofile.ai.llm.UserMessage; +import org.eclipse.microprofile.ai.llm.V; @SuppressWarnings("CdiManagedBeanInconsistencyInspection") @RegisterAIService(chatMemoryMaxMessages = 5, - chatLanguageModelName = "chat-model") public interface FraudAiService { diff --git a/examples/helidon-car-booking-portable-ext/pom.xml b/examples/helidon-car-booking-portable-ext/pom.xml index b9a2307..afe82b9 100644 --- a/examples/helidon-car-booking-portable-ext/pom.xml +++ b/examples/helidon-car-booking-portable-ext/pom.xml @@ -11,8 +11,8 @@ SmallRye LLM Examples: Helidon Car Booking Portable Extension + 4.0.7 io.helidon.Main - 0.34.0 @@ -20,14 +20,14 @@ io.helidon helidon-bom - 4.0.7 + ${version.io.helidon} pom import io.helidon helidon-dependencies - 4.0.7 + ${version.io.helidon} pom import @@ -35,15 +35,17 @@ + + io.smallrye.llm + mp-ai-api + io.smallrye.llm smallrye-llm-langchain4j-portable-extension - 1.0.0-SNAPSHOT io.smallrye.llm smallrye-llm-langchain4j-config-mpconfig - 1.0.0-SNAPSHOT diff --git a/examples/helidon-car-booking-portable-ext/src/main/java/io/jefrajames/booking/ChatAiService.java b/examples/helidon-car-booking-portable-ext/src/main/java/io/jefrajames/booking/ChatAiService.java index 143dd93..407ee89 100644 --- a/examples/helidon-car-booking-portable-ext/src/main/java/io/jefrajames/booking/ChatAiService.java +++ b/examples/helidon-car-booking-portable-ext/src/main/java/io/jefrajames/booking/ChatAiService.java @@ -2,13 +2,12 @@ import java.time.temporal.ChronoUnit; +import org.eclipse.microprofile.ai.llm.RegisterAIService; +import org.eclipse.microprofile.ai.llm.SystemMessage; import org.eclipse.microprofile.faulttolerance.Fallback; import org.eclipse.microprofile.faulttolerance.Retry; import org.eclipse.microprofile.faulttolerance.Timeout; -import dev.langchain4j.service.SystemMessage; -import io.smallrye.llm.spi.RegisterAIService; - @SuppressWarnings("CdiManagedBeanInconsistencyInspection") @RegisterAIService(tools = BookingService.class, chatMemoryMaxMessages = 10, chatLanguageModelName = "chat-model") public interface ChatAiService { diff --git a/examples/helidon-car-booking-portable-ext/src/main/java/io/jefrajames/booking/FraudAiService.java b/examples/helidon-car-booking-portable-ext/src/main/java/io/jefrajames/booking/FraudAiService.java index 185b006..a22deaa 100644 --- a/examples/helidon-car-booking-portable-ext/src/main/java/io/jefrajames/booking/FraudAiService.java +++ b/examples/helidon-car-booking-portable-ext/src/main/java/io/jefrajames/booking/FraudAiService.java @@ -2,19 +2,16 @@ import java.time.temporal.ChronoUnit; +import org.eclipse.microprofile.ai.llm.RegisterAIService; +import org.eclipse.microprofile.ai.llm.SystemMessage; +import org.eclipse.microprofile.ai.llm.UserMessage; +import org.eclipse.microprofile.ai.llm.V; import org.eclipse.microprofile.faulttolerance.Fallback; import org.eclipse.microprofile.faulttolerance.Retry; import org.eclipse.microprofile.faulttolerance.Timeout; -import dev.langchain4j.service.SystemMessage; -import dev.langchain4j.service.UserMessage; -import dev.langchain4j.service.V; -import io.smallrye.llm.spi.RegisterAIService; - @SuppressWarnings("CdiManagedBeanInconsistencyInspection") -@RegisterAIService(chatMemoryMaxMessages = 5, - - chatLanguageModelName = "chat-model") +@RegisterAIService(chatMemoryMaxMessages = 5, chatLanguageModelName = "chat-model") public interface FraudAiService { @SystemMessage(""" diff --git a/examples/helidon-car-booking/pom.xml b/examples/helidon-car-booking/pom.xml index 47ee4b0..2b42388 100644 --- a/examples/helidon-car-booking/pom.xml +++ b/examples/helidon-car-booking/pom.xml @@ -11,8 +11,8 @@ SmallRye LLM Examples: Helidon Car Booking + 4.0.7 io.helidon.Main - 0.34.0 @@ -20,14 +20,14 @@ io.helidon helidon-bom - 4.0.7 + ${version.io.helidon} pom import io.helidon helidon-dependencies - 4.0.7 + ${version.io.helidon} pom import @@ -35,15 +35,17 @@ + + io.smallrye.llm + mp-ai-api + io.smallrye.llm smallrye-llm-langchain4j-buildcompatible-extension - 1.0.0-SNAPSHOT io.smallrye.llm smallrye-llm-langchain4j-config-mpconfig - 1.0.0-SNAPSHOT diff --git a/examples/helidon-car-booking/src/main/java/io/jefrajames/booking/BookingService.java b/examples/helidon-car-booking/src/main/java/io/jefrajames/booking/BookingService.java index 4ee5ff5..8704807 100644 --- a/examples/helidon-car-booking/src/main/java/io/jefrajames/booking/BookingService.java +++ b/examples/helidon-car-booking/src/main/java/io/jefrajames/booking/BookingService.java @@ -8,7 +8,8 @@ import jakarta.enterprise.context.ApplicationScoped; -import dev.langchain4j.agent.tool.Tool; +import org.eclipse.microprofile.ai.llm.Tool; + import lombok.extern.java.Log; @ApplicationScoped diff --git a/examples/helidon-car-booking/src/main/java/io/jefrajames/booking/ChatAiService.java b/examples/helidon-car-booking/src/main/java/io/jefrajames/booking/ChatAiService.java index 143dd93..407ee89 100644 --- a/examples/helidon-car-booking/src/main/java/io/jefrajames/booking/ChatAiService.java +++ b/examples/helidon-car-booking/src/main/java/io/jefrajames/booking/ChatAiService.java @@ -2,13 +2,12 @@ import java.time.temporal.ChronoUnit; +import org.eclipse.microprofile.ai.llm.RegisterAIService; +import org.eclipse.microprofile.ai.llm.SystemMessage; import org.eclipse.microprofile.faulttolerance.Fallback; import org.eclipse.microprofile.faulttolerance.Retry; import org.eclipse.microprofile.faulttolerance.Timeout; -import dev.langchain4j.service.SystemMessage; -import io.smallrye.llm.spi.RegisterAIService; - @SuppressWarnings("CdiManagedBeanInconsistencyInspection") @RegisterAIService(tools = BookingService.class, chatMemoryMaxMessages = 10, chatLanguageModelName = "chat-model") public interface ChatAiService { diff --git a/examples/helidon-car-booking/src/main/java/io/jefrajames/booking/FraudAiService.java b/examples/helidon-car-booking/src/main/java/io/jefrajames/booking/FraudAiService.java index 185b006..a16e425 100644 --- a/examples/helidon-car-booking/src/main/java/io/jefrajames/booking/FraudAiService.java +++ b/examples/helidon-car-booking/src/main/java/io/jefrajames/booking/FraudAiService.java @@ -2,15 +2,14 @@ import java.time.temporal.ChronoUnit; +import org.eclipse.microprofile.ai.llm.RegisterAIService; +import org.eclipse.microprofile.ai.llm.SystemMessage; +import org.eclipse.microprofile.ai.llm.UserMessage; +import org.eclipse.microprofile.ai.llm.V; import org.eclipse.microprofile.faulttolerance.Fallback; import org.eclipse.microprofile.faulttolerance.Retry; import org.eclipse.microprofile.faulttolerance.Timeout; -import dev.langchain4j.service.SystemMessage; -import dev.langchain4j.service.UserMessage; -import dev.langchain4j.service.V; -import io.smallrye.llm.spi.RegisterAIService; - @SuppressWarnings("CdiManagedBeanInconsistencyInspection") @RegisterAIService(chatMemoryMaxMessages = 5, diff --git a/examples/liberty-car-booking/README.md b/examples/liberty-car-booking/README.md index 92561c7..7eb643f 100644 --- a/examples/liberty-car-booking/README.md +++ b/examples/liberty-car-booking/README.md @@ -10,7 +10,7 @@ These are the steps to run this service. ## Application requirements: - JDK 17 and higher - Maven 3.9.9 and higher -- LangChain4j 0.33.0 or higher. +- LangChain4j 0.35.0 or higher. - Testing against GPT 3.5 and 4.0 on a dedicated Azure instance (to be customized in your context). Then you can access the application through the browser of your choice. @@ -39,7 +39,7 @@ To package the application in JVM mode run: `mvn package`. ## Configuration -All configuration is centralized in `microprofile-config.properties`(found is `resources\META-INF` folder) and can be redefined using environment variables. +All configuration is centralized in `microprofile-config.properties` (found is `resources\META-INF` folder) and can be redefined using environment variables. ## Running the application diff --git a/examples/liberty-car-booking/pom.xml b/examples/liberty-car-booking/pom.xml index 89923cd..3d4b008 100644 --- a/examples/liberty-car-booking/pom.xml +++ b/examples/liberty-car-booking/pom.xml @@ -1,175 +1,120 @@ - 4.0.0 - - io.smallrye.llm.examples - examples - 1.0.0-SNAPSHOT - + 4.0.0 + + io.smallrye.llm.examples + examples + 1.0.0-SNAPSHOT + ../pom.xml + - liberty-car-booking - war - SmallRye LLM Examples: Liberty Car Booking + liberty-car-booking + war + SmallRye LLM Examples: Liberty Car Booking - - - Buhake Sindi - +2 - - PROJECT LEAD - - - + + + Buhake Sindi + +2 + + PROJECT LEAD + + + - - UTF-8 - UTF-8 - 10.0.0 - 6.1 - 3.13.0 - 3.4.0 - 0.34.0 + + UTF-8 + UTF-8 + 10.0.0 + 6.1 + 3.4.0 + + + ${project.build.directory}/liberty/wlp/usr/shared/resources/lib/ + - - ${project.build.directory}/liberty/wlp/usr/shared/resources/lib/ - - - - - - jakarta.platform - jakarta.jakartaee-api - ${jakartaee-api.version} - provided - - - - org.eclipse.microprofile - microprofile - ${microprofile-api.version} - pom - provided - - - + + + + + jakarta.platform + jakarta.jakartaee-api + ${jakartaee-api.version} + provided + - - io.smallrye.llm - smallrye-llm-langchain4j-config-mpconfig - 1.0.0-SNAPSHOT - + + org.eclipse.microprofile + microprofile + ${microprofile-api.version} + pom + provided + - - io.smallrye.llm - smallrye-llm-langchain4j-portable-extension - 1.0.0-SNAPSHOT - - - - - dev.langchain4j - langchain4j - ${dev.langchain4j.version} - - - - dev.langchain4j - langchain4j-hugging-face - ${dev.langchain4j.version} - + + dev.langchain4j + langchain4j-hugging-face + ${dev.langchain4j.version} + - - - dev.langchain4j - langchain4j-azure-open-ai - ${dev.langchain4j.version} - + + + dev.langchain4j + langchain4j-azure-open-ai + ${dev.langchain4j.version} + - - dev.langchain4j - langchain4j-open-ai - ${dev.langchain4j.version} - + + dev.langchain4j + langchain4j-open-ai + ${dev.langchain4j.version} + - - dev.langchain4j - langchain4j-embeddings-all-minilm-l6-v2 - ${dev.langchain4j.version} - + + dev.langchain4j + langchain4j-embeddings-all-minilm-l6-v2 + ${dev.langchain4j.version} + - - - ai.djl.huggingface - tokenizers - 0.30.0 - + + + ai.djl.huggingface + tokenizers + 0.30.0 + - - - org.slf4j - slf4j-jdk14 - runtime - 2.0.9 - - - + + + org.slf4j + slf4j-jdk14 + runtime + 2.0.9 + + + - - - + + + org.eclipse.microprofile + microprofile + pom + + + io.smallrye.llm + mp-ai-api + - - org.eclipse.microprofile - microprofile - pom - io.smallrye.llm smallrye-llm-langchain4j-config-mpconfig - - io.smallrye.llm - smallrye-llm-langchain4j-portable-extension - - - - - - + + io.smallrye.llm + smallrye-llm-langchain4j-portable-extension + + org.projectlombok lombok @@ -189,10 +134,10 @@ - - dev.langchain4j - langchain4j-azure-open-ai - + + dev.langchain4j + langchain4j-azure-open-ai + dev.langchain4j @@ -205,10 +150,10 @@ - - ai.djl.huggingface - tokenizers - + + ai.djl.huggingface + tokenizers + @@ -216,43 +161,38 @@ slf4j-jdk14 runtime - + - - ${project.artifactId} - - - - - io.openliberty.tools - liberty-maven-plugin - 3.10.3 - - - ${project.build.finalName} - ${project.basedir}/docs-for-rag - - - - - + + ${project.artifactId} + + + + + io.openliberty.tools + liberty-maven-plugin + 3.10.3 + + + ${project.build.finalName} + ${project.basedir}/docs-for-rag + + + + + - - - org.apache.maven.plugins - maven-compiler-plugin - - - - io.openliberty.tools - liberty-maven-plugin - + + + io.openliberty.tools + liberty-maven-plugin + - - org.apache.maven.plugins - maven-war-plugin - ${war-plugin.version} - - - + + org.apache.maven.plugins + maven-war-plugin + ${war-plugin.version} + + + \ No newline at end of file diff --git a/examples/liberty-car-booking/src/main/java/io/jefrajames/booking/ChatAiService.java b/examples/liberty-car-booking/src/main/java/io/jefrajames/booking/ChatAiService.java index bc6892f..6bdb4e6 100644 --- a/examples/liberty-car-booking/src/main/java/io/jefrajames/booking/ChatAiService.java +++ b/examples/liberty-car-booking/src/main/java/io/jefrajames/booking/ChatAiService.java @@ -2,13 +2,12 @@ import java.time.temporal.ChronoUnit; +import org.eclipse.microprofile.ai.llm.RegisterAIService; +import org.eclipse.microprofile.ai.llm.SystemMessage; import org.eclipse.microprofile.faulttolerance.Fallback; import org.eclipse.microprofile.faulttolerance.Retry; import org.eclipse.microprofile.faulttolerance.Timeout; -import dev.langchain4j.service.SystemMessage; -import io.smallrye.llm.spi.RegisterAIService; - //@SuppressWarnings("CdiManagedBeanInconsistencyInspection") @RegisterAIService(tools = BookingService.class, chatMemoryMaxMessages = 10) public interface ChatAiService { @@ -38,5 +37,4 @@ default String chatFallback(String question) { "Sorry, I am not able to answer your request %s at the moment. Please try again later.", question); } - } diff --git a/examples/liberty-car-booking/src/main/java/io/jefrajames/booking/FraudAiService.java b/examples/liberty-car-booking/src/main/java/io/jefrajames/booking/FraudAiService.java index 68488de..0376b0e 100644 --- a/examples/liberty-car-booking/src/main/java/io/jefrajames/booking/FraudAiService.java +++ b/examples/liberty-car-booking/src/main/java/io/jefrajames/booking/FraudAiService.java @@ -1,9 +1,9 @@ package io.jefrajames.booking; -import dev.langchain4j.service.SystemMessage; -import dev.langchain4j.service.UserMessage; -import dev.langchain4j.service.V; -import io.smallrye.llm.spi.RegisterAIService; +import org.eclipse.microprofile.ai.llm.RegisterAIService; +import org.eclipse.microprofile.ai.llm.SystemMessage; +import org.eclipse.microprofile.ai.llm.UserMessage; +import org.eclipse.microprofile.ai.llm.V; @SuppressWarnings("CdiManagedBeanInconsistencyInspection") @RegisterAIService(chatMemoryMaxMessages = 5, diff --git a/examples/quarkus-car-booking/pom.xml b/examples/quarkus-car-booking/pom.xml index 54a9889..c300926 100644 --- a/examples/quarkus-car-booking/pom.xml +++ b/examples/quarkus-car-booking/pom.xml @@ -5,6 +5,7 @@ examples io.smallrye.llm.examples 1.0.0-SNAPSHOT + ../pom.xml quarkus-car-booking @@ -34,15 +35,17 @@ + + io.smallrye.llm + mp-ai-api + io.smallrye.llm smallrye-llm-langchain4j-buildcompatible-extension - 1.0.0-SNAPSHOT io.smallrye.llm smallrye-llm-langchain4j-config-mpconfig - 1.0.0-SNAPSHOT org.projectlombok @@ -53,7 +56,6 @@ dev.langchain4j langchain4j - ${dev.langchain4j.version} dev.langchain4j diff --git a/examples/quarkus-car-booking/src/main/java/io/jefrajames/booking/BookingService.java b/examples/quarkus-car-booking/src/main/java/io/jefrajames/booking/BookingService.java index 4ee5ff5..8704807 100644 --- a/examples/quarkus-car-booking/src/main/java/io/jefrajames/booking/BookingService.java +++ b/examples/quarkus-car-booking/src/main/java/io/jefrajames/booking/BookingService.java @@ -8,7 +8,8 @@ import jakarta.enterprise.context.ApplicationScoped; -import dev.langchain4j.agent.tool.Tool; +import org.eclipse.microprofile.ai.llm.Tool; + import lombok.extern.java.Log; @ApplicationScoped diff --git a/examples/quarkus-car-booking/src/main/java/io/jefrajames/booking/ChatAiService.java b/examples/quarkus-car-booking/src/main/java/io/jefrajames/booking/ChatAiService.java index 7e00c9b..7ddaa0b 100644 --- a/examples/quarkus-car-booking/src/main/java/io/jefrajames/booking/ChatAiService.java +++ b/examples/quarkus-car-booking/src/main/java/io/jefrajames/booking/ChatAiService.java @@ -2,13 +2,12 @@ import java.time.temporal.ChronoUnit; +import org.eclipse.microprofile.ai.llm.RegisterAIService; +import org.eclipse.microprofile.ai.llm.SystemMessage; import org.eclipse.microprofile.faulttolerance.Fallback; import org.eclipse.microprofile.faulttolerance.Retry; import org.eclipse.microprofile.faulttolerance.Timeout; -import dev.langchain4j.service.SystemMessage; -import io.smallrye.llm.spi.RegisterAIService; - @SuppressWarnings("CdiManagedBeanInconsistencyInspection") @RegisterAIService(tools = BookingService.class, chatMemoryMaxMessages = 10) public interface ChatAiService { diff --git a/examples/quarkus-car-booking/src/main/java/io/jefrajames/booking/FraudAiService.java b/examples/quarkus-car-booking/src/main/java/io/jefrajames/booking/FraudAiService.java index 61fdeef..143b440 100644 --- a/examples/quarkus-car-booking/src/main/java/io/jefrajames/booking/FraudAiService.java +++ b/examples/quarkus-car-booking/src/main/java/io/jefrajames/booking/FraudAiService.java @@ -2,15 +2,14 @@ import java.time.temporal.ChronoUnit; +import org.eclipse.microprofile.ai.llm.RegisterAIService; +import org.eclipse.microprofile.ai.llm.SystemMessage; +import org.eclipse.microprofile.ai.llm.UserMessage; +import org.eclipse.microprofile.ai.llm.V; import org.eclipse.microprofile.faulttolerance.Fallback; import org.eclipse.microprofile.faulttolerance.Retry; import org.eclipse.microprofile.faulttolerance.Timeout; -import dev.langchain4j.service.SystemMessage; -import dev.langchain4j.service.UserMessage; -import dev.langchain4j.service.V; -import io.smallrye.llm.spi.RegisterAIService; - @SuppressWarnings("CdiManagedBeanInconsistencyInspection") @RegisterAIService(chatMemoryMaxMessages = 5) public interface FraudAiService { diff --git a/mp-ai-api/pom.xml b/mp-ai-api/pom.xml new file mode 100644 index 0000000..4ba2ccd --- /dev/null +++ b/mp-ai-api/pom.xml @@ -0,0 +1,20 @@ + + 4.0.0 + + io.smallrye.llm + smallrye-llm-parent + 1.0.0-SNAPSHOT + ../pom.xml + + mp-ai-api + + + + jakarta.enterprise + jakarta.enterprise.cdi-api + provided + + + \ No newline at end of file diff --git a/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/MemoryId.java b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/MemoryId.java new file mode 100644 index 0000000..f1cbea5 --- /dev/null +++ b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/MemoryId.java @@ -0,0 +1,20 @@ +package org.eclipse.microprofile.ai.llm; + +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * The value of a method parameter annotated with @MemoryId will be used to find the memory belonging to that user/conversation. + * A parameter annotated with @MemoryId can be of any type, provided it has properly implemented equals() and hashCode() + * methods. + */ +@Documented +@Retention(RUNTIME) +@Target(PARAMETER) +public @interface MemoryId { + +} diff --git a/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/Moderate.java b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/Moderate.java new file mode 100644 index 0000000..58628a5 --- /dev/null +++ b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/Moderate.java @@ -0,0 +1,24 @@ +package org.eclipse.microprofile.ai.llm; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * When a method in the AI Service is annotated with @Moderate, each invocation of this method will call not only the LLM, + * but also the moderation model (which must be provided during the construction of the AI Service) in parallel. + * This ensures that no malicious content is supplied by the user. + * Before the method returns an answer from the LLM, it will wait until the moderation model returns a result. + * If the moderation model flags the content, a ModerationException will be thrown. + * There is also an option to moderate user input *before* sending it to the LLM. If you require this functionality, + * please open an issue. + */ +@Documented +@Retention(RUNTIME) +@Target(METHOD) +public @interface Moderate { + +} diff --git a/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/P.java b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/P.java new file mode 100644 index 0000000..c261d1c --- /dev/null +++ b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/P.java @@ -0,0 +1,32 @@ +package org.eclipse.microprofile.ai.llm; + +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Parameter of a Tool + */ +@Documented +@Retention(RUNTIME) +@Target({ PARAMETER }) +public @interface P { + + /** + * Description of a parameter + * + * @return the description of a parameter + */ + String value(); + + /** + * Whether the parameter is required + * + * @return true if the parameter is required, false otherwise + * Default is true. + */ + boolean required() default true; +} diff --git a/smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/spi/RegisterAIService.java b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/RegisterAIService.java similarity index 95% rename from smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/spi/RegisterAIService.java rename to mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/RegisterAIService.java index b48605f..2c3f8c5 100644 --- a/smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/spi/RegisterAIService.java +++ b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/RegisterAIService.java @@ -1,4 +1,4 @@ -package io.smallrye.llm.spi; +package org.eclipse.microprofile.ai.llm; import static java.lang.annotation.RetentionPolicy.RUNTIME; diff --git a/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/StructuredPrompt.java b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/StructuredPrompt.java new file mode 100644 index 0000000..4c0d469 --- /dev/null +++ b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/StructuredPrompt.java @@ -0,0 +1,32 @@ +package org.eclipse.microprofile.ai.llm; + +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Represents a structured prompt. + */ +@Documented +@Retention(RUNTIME) +@Target(TYPE) +public @interface StructuredPrompt { + + /** + * Prompt template can be defined in one line or multiple lines. + * If the template is defined in multiple lines, the lines will be joined with a delimiter defined below. + * + * @return the prompt template lines. + */ + String[] value(); + + /** + * The delimiter to join the lines of the prompt template. + * + * @return the delimiter. + */ + String delimiter() default "\n"; +} diff --git a/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/SystemMessage.java b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/SystemMessage.java new file mode 100644 index 0000000..1f050c3 --- /dev/null +++ b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/SystemMessage.java @@ -0,0 +1,62 @@ +package org.eclipse.microprofile.ai.llm; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.TYPE; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Specifies either a complete system message (prompt) or a system message template to be used each time an AI service is + * invoked. + *
+ * An example: + * + *
+ * interface Assistant {
+ *
+ *     {@code @SystemMessage}("You are a helpful assistant")
+ *     String chat(String userMessage);
+ * }
+ * 
+ * + * The system message can contain template variables, + * which will be resolved with values from method parameters annotated with @{@link V}. + *
+ * An example: + * + *
+ * interface Assistant {
+ *
+ *     {@code @SystemMessage}("You are a {{characteristic}} assistant")
+ *     String chat(@UserMessage String userMessage, @V("characteristic") String characteristic);
+ * }
+ * 
+ * + * @see UserMessage + */ +@Documented +@Retention(RUNTIME) +@Target({ TYPE, METHOD }) +public @interface SystemMessage { + + /** + * Prompt template can be defined in one line or multiple lines. + * If the template is defined in multiple lines, the lines will be joined with a delimiter defined below. + */ + String[] value() default ""; + + String delimiter() default "\n"; + + /** + * The resource from which to read the prompt template. + * If no resource is specified, the prompt template is taken from {@link #value()}. + * If the resource is not found, an {@link IllegalConfigurationException} is thrown. + *

+ * The resource will be read by calling {@link Class#getResourceAsStream(String)} + * on the AI Service class (interface). + */ + String fromResource() default ""; +} diff --git a/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/TokenStream.java b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/TokenStream.java new file mode 100644 index 0000000..76e9713 --- /dev/null +++ b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/TokenStream.java @@ -0,0 +1,59 @@ +package org.eclipse.microprofile.ai.llm; + +import java.util.function.Consumer; + +/** + * Represents a token stream from language model to which you can subscribe and receive updates + * when a new token is available, when language model finishes streaming, or when an error occurs during streaming. + * It is intended to be used as a return type in AI Service. + */ +public interface TokenStream { + + /** + * The provided consumer will be invoked when/if contents have been retrieved using {@link RetrievalAugmentor}. + *

+ * The invocation happens before any call is made to the language model. + * + * @param contentHandler lambda that consumes all retrieved contents + * @return token stream instance used to configure or start stream processing + */ + // TokenStream onRetrieved(Consumer> contentHandler); + + /** + * The provided consumer will be invoked every time a new token from a language model is available. + * + * @param tokenHandler lambda that consumes tokens of the response + * @return token stream instance used to configure or start stream processing + */ + TokenStream onNext(Consumer tokenHandler); + + /** + * The provided consumer will be invoked when a language model finishes streaming a response. + * + * @param completionHandler lambda that will be invoked when language model finishes streaming + * @return token stream instance used to configure or start stream processing + */ + // TokenStream onComplete(Consumer> completionHandler); + + /** + * The provided consumer will be invoked when an error occurs during streaming. + * + * @param errorHandler lambda that will be invoked when an error occurs + * @return token stream instance used to configure or start stream processing + */ + TokenStream onError(Consumer errorHandler); + + /** + * All errors during streaming will be ignored (but will be logged with a WARN log level). + * + * @return token stream instance used to configure or start stream processing + */ + TokenStream ignoreErrors(); + + /** + * Completes the current token stream building and starts processing. + *

+ * Will send a request to LLM and start response streaming. + */ + void start(); +} diff --git a/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/TokenStreamAdapter.java b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/TokenStreamAdapter.java new file mode 100644 index 0000000..fd7a70b --- /dev/null +++ b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/TokenStreamAdapter.java @@ -0,0 +1,14 @@ +package org.eclipse.microprofile.ai.llm; + +import java.lang.reflect.Type; + +/** + * @author Buhake Sindi + * @since 11 October 2024 + */ +public interface TokenStreamAdapter { + + boolean canAdaptTokenStreamTo(Type type); + + Object adapt(TokenStream tokenStream); +} diff --git a/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/Tool.java b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/Tool.java new file mode 100644 index 0000000..8655b33 --- /dev/null +++ b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/Tool.java @@ -0,0 +1,36 @@ +package org.eclipse.microprofile.ai.llm; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Java methods annotated with {@code @Tool} are considered tools/functions that language model can execute/call. + * Tool/function calling LLM capability (e.g., see OpenAI + * function calling documentation) + * is used under the hood. + * If LLM decides to call the tool, the arguments are automatically parsed and injected as method arguments. + */ +@Documented +@Retention(RUNTIME) +@Target(METHOD) +public @interface Tool { + + /** + * Name of the tool. If not provided, method name will be used. + * + * @return name of the tool. + */ + String name() default ""; + + /** + * Description of the tool. + * It should be clear and descriptive to allow language model to understand the tool's purpose and its intended use. + * + * @return description of the tool. + */ + String[] value() default ""; +} diff --git a/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/ToolMemoryId.java b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/ToolMemoryId.java new file mode 100644 index 0000000..af46700 --- /dev/null +++ b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/ToolMemoryId.java @@ -0,0 +1,19 @@ +package org.eclipse.microprofile.ai.llm; + +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * If a {@link Tool} method parameter is annotated with this annotation, + * memory id (parameter annotated with @MemoryId in AI Service) will be injected automatically. + */ +@Documented +@Retention(RUNTIME) +@Target(PARAMETER) +public @interface ToolMemoryId { + +} diff --git a/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/UserMessage.java b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/UserMessage.java new file mode 100644 index 0000000..172d081 --- /dev/null +++ b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/UserMessage.java @@ -0,0 +1,63 @@ +package org.eclipse.microprofile.ai.llm; + +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * Specifies either a complete user message or a user message template to be used each time an AI service is invoked. + * The user message can contain template variables, + * which will be resolved with values from method parameters annotated with @{@link V}. + *
+ * An example: + * + *

+ * interface Assistant {
+ *
+ *     {@code @UserMessage}("Say hello to {{name}}")
+ *     String greet(@V("name") String name);
+ * }
+ * 
+ * + * {@code @UserMessage} can also be used with method parameters: + * + *
+ * interface Assistant {
+ *
+ *     {@code @SystemMessage}("You are a {{characteristic}} assistant")
+ *     String chat(@UserMessage String userMessage, @V("characteristic") String characteristic);
+ * }
+ * 
+ * + * In this case {@code String userMessage} can contain unresolved template variables (e.g. "{{characteristic}}"), + * which will be resolved using the values of method parameters annotated with @{@link V}. + * + * @see SystemMessage + */ +@Documented +@Retention(RUNTIME) +@Target({ METHOD, PARAMETER }) +public @interface UserMessage { + + /** + * Prompt template can be defined in one line or multiple lines. + * If the template is defined in multiple lines, the lines will be joined with a delimiter defined below. + */ + String[] value() default ""; + + String delimiter() default "\n"; + + /** + * The resource from which to read the prompt template. + * If no resource is specified, the prompt template is taken from {@link #value()}. + * If the resource is not found, an {@link IllegalConfigurationException} is thrown. + *

+ * The resource will be read by calling {@link Class#getResourceAsStream(String)} + * on the AI Service class (interface). + */ + String fromResource() default ""; +} diff --git a/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/UserName.java b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/UserName.java new file mode 100644 index 0000000..4e93838 --- /dev/null +++ b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/UserName.java @@ -0,0 +1,18 @@ +package org.eclipse.microprofile.ai.llm; + +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * The value of a method parameter annotated with @UserName will be injected into the field 'name' of a UserMessage. + */ +@Documented +@Retention(RUNTIME) +@Target(PARAMETER) +public @interface UserName { + +} diff --git a/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/V.java b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/V.java new file mode 100644 index 0000000..4110a4a --- /dev/null +++ b/mp-ai-api/src/main/java/org/eclipse/microprofile/ai/llm/V.java @@ -0,0 +1,42 @@ +package org.eclipse.microprofile.ai.llm; + +import static java.lang.annotation.ElementType.PARAMETER; +import static java.lang.annotation.RetentionPolicy.RUNTIME; + +import java.lang.annotation.Documented; +import java.lang.annotation.Retention; +import java.lang.annotation.Target; + +/** + * When a parameter of a method in an AI Service is annotated with {@code @V}, + * it becomes a prompt template variable. Its value will be injected into prompt templates defined + * via @{@link UserMessage}, @{@link SystemMessage}. + *

+ * Example: + * + *

+ * {@code @UserMessage("Hello, my name is {{name}}. I am {{age}} years old.")}
+ * String chat(@V("name") String name, @V("age") int age);
+ * 
+ *

+ * Example: + * + *

+ * {@code @UserMessage("Hello, my name is {{name}}. I am {{age}} years old.")}
+ * String chat(@V String name, @V int age);
+ * 
+ *

+ * + * @see UserMessage + * @see SystemMessage + */ +@Documented +@Retention(RUNTIME) +@Target(PARAMETER) +public @interface V { + + /** + * Name of a variable (placeholder) in a prompt template. + */ + String value(); +} diff --git a/pom.xml b/pom.xml index 881e44b..1b24b2d 100644 --- a/pom.xml +++ b/pom.xml @@ -1,20 +1,20 @@ +~ Copyright 2017 Red Hat, Inc. +~ +~ Licensed under the Apache License, Version 2.0 (the "License"); +~ you may not use this file except in compliance with the License. +~ You may obtain a copy of the License at +~ +~ http://www.apache.org/licenses/LICENSE-2.0 +~ +~ Unless required by applicable law or agreed to in writing, software +~ distributed under the License is distributed on an "AS IS" BASIS, +~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +~ See the License for the specific language governing permissions and +~ limitations under the License. +--> 4.0.0 @@ -42,14 +42,9 @@ 4.0.0.Final 5.1.3.Final 3.5.3.Final - 0.34.0 - - 17 - 17 - ${maven.compiler.target} - ${maven.compiler.source} + 0.35.0 17 - + ${maven.compiler.release} SmallRye LLM @@ -79,6 +74,7 @@ smallrye-llm-langchain4j-buildcompatible-extension smallrye-llm-langchain4j-core smallrye-llm-langchain4j-config-mpconfig + mp-ai-api @@ -95,15 +91,43 @@ import pom + + org.jboss.logging + jboss-logging + ${version.org.jboss.logging} + + + dev.langchain4j + langchain4j + ${dev.langchain4j.version} + - - io.smallrye.testing - smallrye-testing-bom - ${version.smallrye.testing} - import - pom + io.smallrye.llm + mp-ai-api + ${project.version} + + io.smallrye.llm + smallrye-llm-langchain4j-buildcompatible-extension + ${project.version} + + + io.smallrye.llm + smallrye-llm-langchain4j-config-mpconfig + ${project.version} + + + io.smallrye.llm + smallrye-llm-langchain4j-core + ${project.version} + + + io.smallrye.llm + smallrye-llm-langchain4j-portable-extension + ${project.version} + + jakarta.enterprise jakarta.enterprise.cdi-api @@ -116,44 +140,49 @@ ${version.weld} runtime - - org.jboss.weld - weld-junit5 - ${weld-junit5.version} - test - org.jboss.weld.se weld-se-core ${version.weld} runtime + + - org.jboss.logging - jboss-logging - ${version.org.jboss.logging} + io.smallrye.testing + smallrye-testing-bom + ${version.smallrye.testing} + import + pom - dev.langchain4j - langchain4j - ${dev.langchain4j.version} + org.jboss.weld + weld-junit5 + ${weld-junit5.version} + test + + + io.smallrye.config + smallrye-config + 3.8.1 + test - - - - org.apache.maven.plugins - maven-compiler-plugin - + + + + org.apache.maven.plugins + maven-compiler-plugin + - - org.apache.maven.plugins - maven-surefire-plugin - + + org.apache.maven.plugins + maven-surefire-plugin + - + diff --git a/smallrye-llm-langchain4j-buildcompatible-extension/pom.xml b/smallrye-llm-langchain4j-buildcompatible-extension/pom.xml index bb24078..33a17bb 100644 --- a/smallrye-llm-langchain4j-buildcompatible-extension/pom.xml +++ b/smallrye-llm-langchain4j-buildcompatible-extension/pom.xml @@ -15,7 +15,6 @@ org.jacoco jacoco-maven-plugin - 0.8.12 @@ -63,20 +62,17 @@ io.smallrye.llm smallrye-llm-langchain4j-core - ${project.version} io.smallrye.llm smallrye-llm-langchain4j-config-mpconfig - ${project.version} test io.smallrye.config smallrye-config - 3.8.1 test diff --git a/smallrye-llm-langchain4j-buildcompatible-extension/src/main/java/io/smallrye/llm/aiservice/Langchain4JAIServiceBuildCompatibleExtension.java b/smallrye-llm-langchain4j-buildcompatible-extension/src/main/java/io/smallrye/llm/aiservice/Langchain4JAIServiceBuildCompatibleExtension.java index 2cdbeb2..24c6d90 100644 --- a/smallrye-llm-langchain4j-buildcompatible-extension/src/main/java/io/smallrye/llm/aiservice/Langchain4JAIServiceBuildCompatibleExtension.java +++ b/smallrye-llm-langchain4j-buildcompatible-extension/src/main/java/io/smallrye/llm/aiservice/Langchain4JAIServiceBuildCompatibleExtension.java @@ -20,10 +20,9 @@ import jakarta.enterprise.lang.model.types.ClassType; import jakarta.inject.Named; +import org.eclipse.microprofile.ai.llm.RegisterAIService; import org.jboss.logging.Logger; -import io.smallrye.llm.spi.RegisterAIService; - public class Langchain4JAIServiceBuildCompatibleExtension implements BuildCompatibleExtension { private static final Logger LOGGER = Logger.getLogger(Langchain4JAIServiceBuildCompatibleExtension.class); private static final Set> detectedAIServicesDeclaredInterfaces = new HashSet<>(); diff --git a/smallrye-llm-langchain4j-buildcompatible-extension/src/test/java/io/smallrye/llm/core/MyDummyAIService.java b/smallrye-llm-langchain4j-buildcompatible-extension/src/test/java/io/smallrye/llm/core/MyDummyAIService.java index fb87b1f..5d656c1 100644 --- a/smallrye-llm-langchain4j-buildcompatible-extension/src/test/java/io/smallrye/llm/core/MyDummyAIService.java +++ b/smallrye-llm-langchain4j-buildcompatible-extension/src/test/java/io/smallrye/llm/core/MyDummyAIService.java @@ -1,9 +1,9 @@ package io.smallrye.llm.core; -import dev.langchain4j.service.SystemMessage; -import dev.langchain4j.service.UserMessage; -import dev.langchain4j.service.V; -import io.smallrye.llm.spi.RegisterAIService; +import org.eclipse.microprofile.ai.llm.RegisterAIService; +import org.eclipse.microprofile.ai.llm.SystemMessage; +import org.eclipse.microprofile.ai.llm.UserMessage; +import org.eclipse.microprofile.ai.llm.V; @SuppressWarnings("CdiManagedBeanInconsistencyInspection") @RegisterAIService diff --git a/smallrye-llm-langchain4j-buildcompatible-extension/src/test/java/io/smallrye/llm/core/MyDummyApplicationScopedAIService.java b/smallrye-llm-langchain4j-buildcompatible-extension/src/test/java/io/smallrye/llm/core/MyDummyApplicationScopedAIService.java index 81deb7e..c86cf42 100644 --- a/smallrye-llm-langchain4j-buildcompatible-extension/src/test/java/io/smallrye/llm/core/MyDummyApplicationScopedAIService.java +++ b/smallrye-llm-langchain4j-buildcompatible-extension/src/test/java/io/smallrye/llm/core/MyDummyApplicationScopedAIService.java @@ -2,7 +2,7 @@ import jakarta.enterprise.context.ApplicationScoped; -import io.smallrye.llm.spi.RegisterAIService; +import org.eclipse.microprofile.ai.llm.RegisterAIService; @SuppressWarnings("CdiManagedBeanInconsistencyInspection") @RegisterAIService(scope = ApplicationScoped.class) diff --git a/smallrye-llm-langchain4j-config-mpconfig/pom.xml b/smallrye-llm-langchain4j-config-mpconfig/pom.xml index d80a581..9c60d2c 100644 --- a/smallrye-llm-langchain4j-config-mpconfig/pom.xml +++ b/smallrye-llm-langchain4j-config-mpconfig/pom.xml @@ -14,7 +14,6 @@ io.smallrye.config smallrye-config - 3.8.1 test @@ -25,7 +24,6 @@ io.smallrye.llm smallrye-llm-langchain4j-core - ${project.version} diff --git a/smallrye-llm-langchain4j-core/pom.xml b/smallrye-llm-langchain4j-core/pom.xml index aee9cb2..aac899e 100644 --- a/smallrye-llm-langchain4j-core/pom.xml +++ b/smallrye-llm-langchain4j-core/pom.xml @@ -11,6 +11,10 @@ SmallRye LLM: LangChain4j Core + + io.smallrye.llm + mp-ai-api + org.jboss.logging jboss-logging diff --git a/smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/aiservice/CommonAIServiceCreator.java b/smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/aiservice/CommonAIServiceCreator.java index 35546d9..a2a4faf 100644 --- a/smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/aiservice/CommonAIServiceCreator.java +++ b/smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/aiservice/CommonAIServiceCreator.java @@ -7,13 +7,13 @@ import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.literal.NamedLiteral; +import org.eclipse.microprofile.ai.llm.RegisterAIService; import org.jboss.logging.Logger; import dev.langchain4j.model.chat.ChatLanguageModel; import dev.langchain4j.rag.content.retriever.ContentRetriever; import dev.langchain4j.service.AiServices; import io.smallrye.llm.core.langchain4j.core.config.spi.ChatMemoryFactoryProvider; -import io.smallrye.llm.spi.RegisterAIService; public class CommonAIServiceCreator { diff --git a/smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/core/langchain4j/services/SmallRyeAiServiceTokenStream.java b/smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/core/langchain4j/services/SmallRyeAiServiceTokenStream.java new file mode 100644 index 0000000..775d9dc --- /dev/null +++ b/smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/core/langchain4j/services/SmallRyeAiServiceTokenStream.java @@ -0,0 +1,89 @@ +package io.smallrye.llm.core.langchain4j.services; + +import java.util.function.Consumer; + +import org.eclipse.microprofile.ai.llm.TokenStream; + +/** + * @author Buhake Sindi + * @since 10 October 2024 + */ +public class SmallRyeAiServiceTokenStream implements TokenStream { + + private dev.langchain4j.service.TokenStream delegateTokenStream; + + /** + * @param delegateTokenStream + */ + public SmallRyeAiServiceTokenStream(dev.langchain4j.service.TokenStream delegateTokenStream) { + super(); + this.delegateTokenStream = delegateTokenStream; + } + + // /* (non-Javadoc) + // * @see io.smallrye.llm.service.TokenStream#onRetrieved(java.util.function.Consumer) + // */ + // @Override + // public TokenStream onRetrieved(Consumer> contentHandler) { + // // TODO Auto-generated method stub + // delegateTokenStream.onRetrieved(contentHandler); + // return this; + // } + + /* + * (non-Javadoc) + * + * @see io.smallrye.llm.service.TokenStream#onNext(java.util.function.Consumer) + */ + @Override + public TokenStream onNext(Consumer tokenHandler) { + // TODO Auto-generated method stub + delegateTokenStream.onNext(tokenHandler); + return this; + } + + // /* (non-Javadoc) + // * @see io.smallrye.llm.service.TokenStream#onComplete(java.util.function.Consumer) + // */ + // @Override + // public TokenStream onComplete(Consumer> completionHandler) { + // // TODO Auto-generated method stub + // delegateTokenStream.onComplete(completionHandler); + // return this; + // } + + /* + * (non-Javadoc) + * + * @see io.smallrye.llm.service.TokenStream#onError(java.util.function.Consumer) + */ + @Override + public TokenStream onError(Consumer errorHandler) { + // TODO Auto-generated method stub + delegateTokenStream.onError(errorHandler); + return this; + } + + /* + * (non-Javadoc) + * + * @see io.smallrye.llm.service.TokenStream#ignoreErrors() + */ + @Override + public TokenStream ignoreErrors() { + // TODO Auto-generated method stub + delegateTokenStream.ignoreErrors(); + return this; + } + + /* + * (non-Javadoc) + * + * @see io.smallrye.llm.service.TokenStream#start() + */ + @Override + public void start() { + // TODO Auto-generated method stub + delegateTokenStream.start(); + } +} diff --git a/smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/core/langchain4j/services/SmallRyeLang4JAiServices.java b/smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/core/langchain4j/services/SmallRyeLang4JAiServices.java new file mode 100644 index 0000000..b4296c2 --- /dev/null +++ b/smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/core/langchain4j/services/SmallRyeLang4JAiServices.java @@ -0,0 +1,612 @@ +package io.smallrye.llm.core.langchain4j.services; + +import static dev.langchain4j.exception.IllegalConfigurationException.illegalConfiguration; +import static dev.langchain4j.internal.Exceptions.illegalArgument; +import static dev.langchain4j.internal.Exceptions.runtime; +import static dev.langchain4j.internal.Utils.isNotNullOrBlank; +import static dev.langchain4j.model.chat.Capability.RESPONSE_FORMAT_JSON_SCHEMA; +import static dev.langchain4j.model.chat.request.ResponseFormatType.JSON; +import static dev.langchain4j.service.TypeUtils.typeHasRawClass; +import static dev.langchain4j.service.output.JsonSchemas.jsonSchemaFrom; +import static dev.langchain4j.spi.ServiceHelper.loadFactories; + +import java.io.InputStream; +import java.lang.reflect.Array; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.Proxy; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Scanner; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import org.eclipse.microprofile.ai.llm.MemoryId; +import org.eclipse.microprofile.ai.llm.Moderate; +import org.eclipse.microprofile.ai.llm.StructuredPrompt; +import org.eclipse.microprofile.ai.llm.TokenStream; +import org.eclipse.microprofile.ai.llm.TokenStreamAdapter; +import org.eclipse.microprofile.ai.llm.Tool; +import org.eclipse.microprofile.ai.llm.UserName; +import org.eclipse.microprofile.ai.llm.V; +import org.jboss.logging.Logger; + +import dev.langchain4j.agent.tool.ToolExecutionRequest; +import dev.langchain4j.agent.tool.ToolSpecification; +import dev.langchain4j.data.message.AiMessage; +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.data.message.SystemMessage; +import dev.langchain4j.data.message.ToolExecutionResultMessage; +import dev.langchain4j.data.message.UserMessage; +import dev.langchain4j.memory.ChatMemory; +import dev.langchain4j.model.chat.request.ChatRequest; +import dev.langchain4j.model.chat.request.ResponseFormat; +import dev.langchain4j.model.chat.request.json.JsonSchema; +import dev.langchain4j.model.chat.response.ChatResponse; +import dev.langchain4j.model.input.Prompt; +import dev.langchain4j.model.input.PromptTemplate; +import dev.langchain4j.model.input.structured.StructuredPromptProcessor; +import dev.langchain4j.model.moderation.Moderation; +import dev.langchain4j.model.output.Response; +import dev.langchain4j.model.output.TokenUsage; +import dev.langchain4j.rag.AugmentationRequest; +import dev.langchain4j.rag.AugmentationResult; +import dev.langchain4j.rag.query.Metadata; +import dev.langchain4j.service.AiServiceContext; +import dev.langchain4j.service.AiServiceTokenStream; +import dev.langchain4j.service.AiServices; +import dev.langchain4j.service.Result; +import dev.langchain4j.service.TypeUtils; +import dev.langchain4j.service.output.ServiceOutputParser; +import dev.langchain4j.service.tool.DefaultToolExecutor; +import dev.langchain4j.service.tool.ToolExecution; +import dev.langchain4j.service.tool.ToolExecutor; +import dev.langchain4j.service.tool.ToolProviderRequest; +import dev.langchain4j.service.tool.ToolProviderResult; + +/** + * @author Buhake Sindi + * @since 10 October 2024 + */ +public class SmallRyeLang4JAiServices extends AiServices { + + private static final Logger LOGGER = Logger.getLogger(SmallRyeLang4JAiServices.class); + + private final ServiceOutputParser serviceOutputParser = new ServiceOutputParser(); + private final Collection tokenStreamAdapters = loadFactories(TokenStreamAdapter.class); + private static final int MAX_SEQUENTIAL_TOOL_EXECUTIONS = 10; + + /** + * @param context + */ + public SmallRyeLang4JAiServices(AiServiceContext context) { + super(context); + //TODO Auto-generated constructor stub + } + + /* + * (non-Javadoc) + * + * @see dev.langchain4j.service.AiServices#tools(java.util.List) + */ + @Override + public AiServices tools(List objectsWithTools) { + // TODO Auto-generated method stub + if (objectsWithTools != null) { + if (context.toolSpecifications == null) { + context.toolSpecifications = new ArrayList<>(); + } + if (context.toolExecutors == null) { + context.toolExecutors = new HashMap<>(); + } + + for (Object objectWithTool : objectsWithTools) { + if (objectWithTool instanceof Class) { + throw illegalConfiguration("Tool '%s' must be an object, not a class", objectWithTool); + } + + for (Method method : objectWithTool.getClass().getDeclaredMethods()) { + if (method.isAnnotationPresent(Tool.class)) { + ToolSpecification toolSpecification = ToolSpecifications.toolSpecificationFrom(method); + context.toolSpecifications.add(toolSpecification); + context.toolExecutors.put(toolSpecification.name(), new DefaultToolExecutor(objectWithTool, method)); + } + } + } + + return super.tools(objectsWithTools); + } + + return this; + } + + @SuppressWarnings("unchecked") + @Override + public T build() { + // TODO Auto-generated method stub + performBasicValidation(); + + for (Method method : context.aiServiceClass.getMethods()) { + if (method.isAnnotationPresent(Moderate.class) && context.moderationModel == null) { + throw illegalConfiguration("The @Moderate annotation is present, but the moderationModel is not set up. " + + "Please ensure a valid moderationModel is configured before using the @Moderate annotation."); + } + if (method.getReturnType() == Result.class || + method.getReturnType() == List.class || + method.getReturnType() == Set.class) { + TypeUtils.validateReturnTypesAreProperlyParametrized(method.getName(), method.getGenericReturnType()); + } + } + + Object proxyInstance = Proxy.newProxyInstance( + context.aiServiceClass.getClassLoader(), + new Class[] { context.aiServiceClass }, + new InvocationHandler() { + + private final ExecutorService executor = Executors.newCachedThreadPool(); + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Exception { + + if (method.getDeclaringClass() == Object.class) { + // methods like equals(), hashCode() and toString() should not be handled by this proxy + return method.invoke(this, args); + } + + validateParameters(method); + + Object memoryId = findMemoryId(method, args).orElse(DEFAULT); + + Optional systemMessage = prepareSystemMessage(memoryId, method, args); + UserMessage userMessage = prepareUserMessage(method, args); + AugmentationResult augmentationResult = null; + if (context.retrievalAugmentor != null) { + List chatMemory = context.hasChatMemory() + ? context.chatMemory(memoryId).messages() + : null; + Metadata metadata = Metadata.from(userMessage, memoryId, chatMemory); + AugmentationRequest augmentationRequest = new AugmentationRequest(userMessage, metadata); + augmentationResult = context.retrievalAugmentor.augment(augmentationRequest); + userMessage = (UserMessage) augmentationResult.chatMessage(); + } + + // TODO give user ability to provide custom OutputParser + Type returnType = method.getGenericReturnType(); + + boolean streaming = returnType == TokenStream.class || canAdaptTokenStreamTo(returnType); + + boolean supportsJsonSchema = supportsJsonSchema(); + Optional jsonSchema = Optional.empty(); + if (supportsJsonSchema && !streaming) { + jsonSchema = jsonSchemaFrom(returnType); + } + + if ((!supportsJsonSchema || !jsonSchema.isPresent()) && !streaming) { + // TODO append after storing in the memory? + userMessage = appendOutputFormatInstructions(returnType, userMessage); + } + + if (context.hasChatMemory()) { + ChatMemory chatMemory = context.chatMemory(memoryId); + systemMessage.ifPresent(chatMemory::add); + chatMemory.add(userMessage); + } + + List messages; + if (context.hasChatMemory()) { + messages = context.chatMemory(memoryId).messages(); + } else { + messages = new ArrayList<>(); + systemMessage.ifPresent(messages::add); + messages.add(userMessage); + } + + Future moderationFuture = triggerModerationIfNeeded(method, messages); + + List toolSpecifications = context.toolSpecifications; + Map toolExecutors = context.toolExecutors; + + if (context.toolProvider != null) { + toolSpecifications = new ArrayList<>(); + toolExecutors = new HashMap<>(); + ToolProviderRequest toolProviderRequest = new ToolProviderRequest(memoryId, userMessage); + ToolProviderResult toolProviderResult = context.toolProvider.provideTools(toolProviderRequest); + if (toolProviderResult != null) { + Map tools = toolProviderResult.tools(); + for (ToolSpecification toolSpecification : tools.keySet()) { + toolSpecifications.add(toolSpecification); + toolExecutors.put(toolSpecification.name(), tools.get(toolSpecification)); + } + } + } + + if (streaming) { + TokenStream tokenStream = new SmallRyeAiServiceTokenStream(new AiServiceTokenStream( + messages, + toolSpecifications, + toolExecutors, + augmentationResult != null ? augmentationResult.contents() : null, + context, + memoryId)); + // TODO moderation + if (returnType == TokenStream.class) { + return tokenStream; + } else { + return adapt(tokenStream, returnType); + } + } + + Response response; + if (supportsJsonSchema && jsonSchema.isPresent()) { + ChatRequest chatRequest = ChatRequest.builder() + .messages(messages) + .toolSpecifications(toolSpecifications) + .responseFormat(ResponseFormat.builder() + .type(JSON) + .jsonSchema(jsonSchema.get()) + .build()) + .build(); + + ChatResponse chatResponse = context.chatModel.chat(chatRequest); + + response = new Response<>( + chatResponse.aiMessage(), + chatResponse.tokenUsage(), + chatResponse.finishReason()); + } else { + // TODO migrate to new API + response = toolSpecifications == null || toolSpecifications.isEmpty() + ? context.chatModel.generate(messages) + : context.chatModel.generate(messages, toolSpecifications); + } + + TokenUsage tokenUsageAccumulator = response.tokenUsage(); + + verifyModerationIfNeeded(moderationFuture); + + int executionsLeft = MAX_SEQUENTIAL_TOOL_EXECUTIONS; + List toolExecutions = new ArrayList<>(); + while (true) { + + if (executionsLeft-- == 0) { + throw runtime("Something is wrong, exceeded %s sequential tool executions", + MAX_SEQUENTIAL_TOOL_EXECUTIONS); + } + + AiMessage aiMessage = response.content(); + + if (context.hasChatMemory()) { + context.chatMemory(memoryId).add(aiMessage); + } else { + messages = new ArrayList<>(messages); + messages.add(aiMessage); + } + + if (!aiMessage.hasToolExecutionRequests()) { + break; + } + + for (ToolExecutionRequest toolExecutionRequest : aiMessage.toolExecutionRequests()) { + ToolExecutor toolExecutor = toolExecutors.get(toolExecutionRequest.name()); + String toolExecutionResult = toolExecutor.execute(toolExecutionRequest, memoryId); + toolExecutions.add(ToolExecution.builder() + .request(toolExecutionRequest) + .result(toolExecutionResult) + .build()); + ToolExecutionResultMessage toolExecutionResultMessage = ToolExecutionResultMessage.from( + toolExecutionRequest, + toolExecutionResult); + if (context.hasChatMemory()) { + context.chatMemory(memoryId).add(toolExecutionResultMessage); + } else { + messages.add(toolExecutionResultMessage); + } + } + + if (context.hasChatMemory()) { + messages = context.chatMemory(memoryId).messages(); + } + + response = context.chatModel.generate(messages, toolSpecifications); + tokenUsageAccumulator = TokenUsage.sum(tokenUsageAccumulator, response.tokenUsage()); + } + + response = Response.from(response.content(), tokenUsageAccumulator, response.finishReason()); + + Object parsedResponse = serviceOutputParser.parse(response, returnType); + if (typeHasRawClass(returnType, Result.class)) { + return Result.builder() + .content(parsedResponse) + .tokenUsage(tokenUsageAccumulator) + .sources(augmentationResult == null ? null : augmentationResult.contents()) + .finishReason(response.finishReason()) + .toolExecutions(toolExecutions) + .build(); + } else { + return parsedResponse; + } + } + + private boolean canAdaptTokenStreamTo(Type returnType) { + for (TokenStreamAdapter tokenStreamAdapter : tokenStreamAdapters) { + if (tokenStreamAdapter.canAdaptTokenStreamTo(returnType)) { + return true; + } + } + return false; + } + + private Object adapt(TokenStream tokenStream, Type returnType) { + for (TokenStreamAdapter tokenStreamAdapter : tokenStreamAdapters) { + if (tokenStreamAdapter.canAdaptTokenStreamTo(returnType)) { + return tokenStreamAdapter.adapt(tokenStream); + } + } + throw new IllegalStateException("Can't find suitable TokenStreamAdapter"); + } + + private boolean supportsJsonSchema() { + return context.chatModel != null + && context.chatModel.supportedCapabilities().contains(RESPONSE_FORMAT_JSON_SCHEMA); + } + + private UserMessage appendOutputFormatInstructions(Type returnType, UserMessage userMessage) { + String outputFormatInstructions = serviceOutputParser.outputFormatInstructions(returnType); + String text = userMessage.singleText() + outputFormatInstructions; + if (isNotNullOrBlank(userMessage.name())) { + userMessage = UserMessage.from(userMessage.name(), text); + } else { + userMessage = UserMessage.from(text); + } + return userMessage; + } + + private Future triggerModerationIfNeeded(Method method, List messages) { + if (method.isAnnotationPresent(Moderate.class)) { + return executor.submit(() -> { + List messagesToModerate = removeToolMessages(messages); + return context.moderationModel.moderate(messagesToModerate).content(); + }); + } + return null; + } + }); + + return (T) proxyInstance; + } + + static void validateParameters(Method method) { + Parameter[] parameters = method.getParameters(); + if (parameters == null || parameters.length < 2) { + return; + } + + for (Parameter parameter : parameters) { + V v = parameter.getAnnotation(V.class); + org.eclipse.microprofile.ai.llm.UserMessage userMessage = parameter + .getAnnotation(org.eclipse.microprofile.ai.llm.UserMessage.class); + MemoryId memoryId = parameter.getAnnotation(MemoryId.class); + UserName userName = parameter.getAnnotation(UserName.class); + if (v == null && userMessage == null && memoryId == null && userName == null) { + throw illegalConfiguration( + "Parameter '%s' of method '%s' should be annotated with @V or @UserMessage " + + "or @UserName or @MemoryId", + parameter.getName(), method.getName()); + } + } + } + + private static Optional findMemoryId(Method method, Object[] args) { + Parameter[] parameters = method.getParameters(); + for (int i = 0; i < parameters.length; i++) { + if (parameters[i].isAnnotationPresent(MemoryId.class)) { + Object memoryId = args[i]; + if (memoryId == null) { + throw illegalArgument( + "The value of parameter '%s' annotated with @MemoryId in method '%s' must not be null", + parameters[i].getName(), method.getName()); + } + return Optional.of(memoryId); + } + } + return Optional.empty(); + } + + private Optional prepareSystemMessage(Object memoryId, Method method, Object[] args) { + return findSystemMessageTemplate(memoryId, method) + .map(systemMessageTemplate -> PromptTemplate.from(systemMessageTemplate) + .apply(findTemplateVariables(systemMessageTemplate, method, args)) + .toSystemMessage()); + } + + private Optional findSystemMessageTemplate(Object memoryId, Method method) { + org.eclipse.microprofile.ai.llm.SystemMessage annotation = method + .getAnnotation(org.eclipse.microprofile.ai.llm.SystemMessage.class); + if (annotation != null) { + return Optional + .of(getTemplate(method, "System", annotation.fromResource(), annotation.value(), annotation.delimiter())); + } + + return context.systemMessageProvider.apply(memoryId); + } + + private static Map findTemplateVariables(String template, Method method, Object[] args) { + Parameter[] parameters = method.getParameters(); + + Map variables = new HashMap<>(); + for (int i = 0; i < parameters.length; i++) { + V annotation = parameters[i].getAnnotation(V.class); + if (annotation != null) { + String variableName = annotation.value(); + Object variableValue = args[i]; + variables.put(variableName, variableValue); + } + } + + if (template.contains("{{it}}") && !variables.containsKey("it")) { + String itValue = getValueOfVariableIt(parameters, args); + variables.put("it", itValue); + } + + return variables; + } + + private static String getValueOfVariableIt(Parameter[] parameters, Object[] args) { + if (parameters.length == 1) { + Parameter parameter = parameters[0]; + if (!parameter.isAnnotationPresent(MemoryId.class) + && !parameter.isAnnotationPresent(org.eclipse.microprofile.ai.llm.UserMessage.class) + && !parameter.isAnnotationPresent(UserName.class) + && (!parameter.isAnnotationPresent(V.class) || isAnnotatedWithIt(parameter))) { + return toString(args[0]); + } + } + + for (int i = 0; i < parameters.length; i++) { + if (isAnnotatedWithIt(parameters[i])) { + return toString(args[i]); + } + } + + throw illegalConfiguration("Error: cannot find the value of the prompt template variable \"{{it}}\"."); + } + + private static boolean isAnnotatedWithIt(Parameter parameter) { + V annotation = parameter.getAnnotation(V.class); + return annotation != null && "it".equals(annotation.value()); + } + + private static UserMessage prepareUserMessage(Method method, Object[] args) { + + String template = getUserMessageTemplate(method, args); + Map variables = findTemplateVariables(template, method, args); + + Prompt prompt = PromptTemplate.from(template).apply(variables); + + Optional maybeUserName = findUserName(method.getParameters(), args); + return maybeUserName.map(userName -> UserMessage.from(userName, prompt.text())) + .orElseGet(prompt::toUserMessage); + } + + private static String getUserMessageTemplate(Method method, Object[] args) { + + Optional templateFromMethodAnnotation = findUserMessageTemplateFromMethodAnnotation(method); + Optional templateFromParameterAnnotation = findUserMessageTemplateFromAnnotatedParameter(method.getParameters(), + args); + + if (templateFromMethodAnnotation.isPresent() && templateFromParameterAnnotation.isPresent()) { + throw illegalConfiguration( + "Error: The method '%s' has multiple @UserMessage annotations. Please use only one.", + method.getName()); + } + + if (templateFromMethodAnnotation.isPresent()) { + return templateFromMethodAnnotation.get(); + } + if (templateFromParameterAnnotation.isPresent()) { + return templateFromParameterAnnotation.get(); + } + + Optional templateFromTheOnlyArgument = findUserMessageTemplateFromTheOnlyArgument(method.getParameters(), args); + if (templateFromTheOnlyArgument.isPresent()) { + return templateFromTheOnlyArgument.get(); + } + + throw illegalConfiguration("Error: The method '%s' does not have a user message defined.", method.getName()); + } + + private static Optional findUserMessageTemplateFromMethodAnnotation(Method method) { + return Optional.ofNullable(method.getAnnotation(org.eclipse.microprofile.ai.llm.UserMessage.class)) + .map(a -> getTemplate(method, "User", a.fromResource(), a.value(), a.delimiter())); + } + + private static Optional findUserMessageTemplateFromAnnotatedParameter(Parameter[] parameters, Object[] args) { + for (int i = 0; i < parameters.length; i++) { + if (parameters[i].isAnnotationPresent(org.eclipse.microprofile.ai.llm.UserMessage.class)) { + return Optional.of(toString(args[i])); + } + } + return Optional.empty(); + } + + private static Optional findUserMessageTemplateFromTheOnlyArgument(Parameter[] parameters, Object[] args) { + if (parameters != null && parameters.length == 1 && parameters[0].getAnnotations().length == 0) { + return Optional.of(toString(args[0])); + } + return Optional.empty(); + } + + private static Optional findUserName(Parameter[] parameters, Object[] args) { + for (int i = 0; i < parameters.length; i++) { + if (parameters[i].isAnnotationPresent(UserName.class)) { + return Optional.of(args[i].toString()); + } + } + return Optional.empty(); + } + + private static String getTemplate(Method method, String type, String resource, String[] value, String delimiter) { + String messageTemplate; + if (!resource.trim().isEmpty()) { + messageTemplate = getResourceText(method.getDeclaringClass(), resource); + if (messageTemplate == null) { + throw illegalConfiguration("@%sMessage's resource '%s' not found", type, resource); + } + } else { + messageTemplate = String.join(delimiter, value); + } + if (messageTemplate.trim().isEmpty()) { + throw illegalConfiguration("@%sMessage's template cannot be empty", type); + } + return messageTemplate; + } + + private static String getResourceText(Class clazz, String resource) { + InputStream inputStream = clazz.getResourceAsStream(resource); + if (inputStream == null) { + inputStream = clazz.getResourceAsStream("/" + resource); + } + return getText(inputStream); + } + + private static String getText(InputStream inputStream) { + if (inputStream == null) { + return null; + } + try (Scanner scanner = new Scanner(inputStream); + Scanner s = scanner.useDelimiter("\\A")) { + return s.hasNext() ? s.next() : ""; + } + } + + private static String toString(Object arg) { + if (arg.getClass().isArray()) { + return arrayToString(arg); + } else if (arg.getClass().isAnnotationPresent(StructuredPrompt.class)) { + return StructuredPromptProcessor.toPrompt(arg).text(); + } else { + return arg.toString(); + } + } + + private static String arrayToString(Object arg) { + StringBuilder sb = new StringBuilder("["); + int length = Array.getLength(arg); + for (int i = 0; i < length; i++) { + sb.append(toString(Array.get(arg, i))); + if (i < length - 1) { + sb.append(", "); + } + } + sb.append("]"); + return sb.toString(); + } +} diff --git a/smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/core/langchain4j/services/SmallRyeLang4JchainAiServicesFactory.java b/smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/core/langchain4j/services/SmallRyeLang4JchainAiServicesFactory.java new file mode 100644 index 0000000..f74fe16 --- /dev/null +++ b/smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/core/langchain4j/services/SmallRyeLang4JchainAiServicesFactory.java @@ -0,0 +1,23 @@ +package io.smallrye.llm.core.langchain4j.services; + +import dev.langchain4j.service.AiServiceContext; +import dev.langchain4j.service.AiServices; +import dev.langchain4j.spi.services.AiServicesFactory; + +/** + * @author Buhake Sindi + * @since 10 October 2024 + */ +public class SmallRyeLang4JchainAiServicesFactory implements AiServicesFactory { + + /* + * (non-Javadoc) + * + * @see dev.langchain4j.spi.services.AiServicesFactory#create(dev.langchain4j.service.AiServiceContext) + */ + @Override + public AiServices create(AiServiceContext context) { + // TODO Auto-generated method stub + return new SmallRyeLang4JAiServices(context); + } +} diff --git a/smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/core/langchain4j/services/SmallRyeStructuredPromptFactory.java b/smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/core/langchain4j/services/SmallRyeStructuredPromptFactory.java new file mode 100644 index 0000000..3b47182 --- /dev/null +++ b/smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/core/langchain4j/services/SmallRyeStructuredPromptFactory.java @@ -0,0 +1,82 @@ +package io.smallrye.llm.core.langchain4j.services; + +import java.util.Map; + +import org.eclipse.microprofile.ai.llm.StructuredPrompt; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.ToNumberPolicy; +import com.google.gson.reflect.TypeToken; + +import dev.langchain4j.internal.ValidationUtils; +import dev.langchain4j.model.input.Prompt; +import dev.langchain4j.model.input.PromptTemplate; +import dev.langchain4j.spi.prompt.structured.StructuredPromptFactory; + +/** + * @author Buhake Sindi + * @since 10 October 2024 + */ +public class SmallRyeStructuredPromptFactory implements StructuredPromptFactory { + private static final Gson GSON = new GsonBuilder().setObjectToNumberStrategy(ToNumberPolicy.LONG_OR_DOUBLE).create(); + + /* + * (non-Javadoc) + * + * @see dev.langchain4j.spi.prompt.structured.StructuredPromptFactory#toPrompt(java.lang.Object) + */ + + @Override + public Prompt toPrompt(Object structuredPrompt) { + // TODO Auto-generated method stub + StructuredPrompt annotation = validateStructuredPrompt(structuredPrompt); + + String promptTemplateString = join(annotation); + PromptTemplate promptTemplate = PromptTemplate.from(promptTemplateString); + + Map variables = extractVariables(structuredPrompt); + + return promptTemplate.apply(variables); + } + + /** + * Extracts the variables from the structured prompt. + * + * @param structuredPrompt The structured prompt. + * @return The variables map. + */ + private static Map extractVariables(Object structuredPrompt) { + String json = GSON.toJson(structuredPrompt); + TypeToken> mapType = new TypeToken>() { + }; + return GSON.fromJson(json, mapType); + } + + /** + * Validates that the given object is annotated with {@link StructuredPrompt}. + * + * @param structuredPrompt the object to validate. + * @return the annotation. + */ + private static StructuredPrompt validateStructuredPrompt(Object structuredPrompt) { + ValidationUtils.ensureNotNull(structuredPrompt, "structuredPrompt"); + + Class cls = structuredPrompt.getClass(); + + return ValidationUtils.ensureNotNull( + cls.getAnnotation(StructuredPrompt.class), + "%s should be annotated with @StructuredPrompt to be used as a structured prompt", + cls.getName()); + } + + /** + * Joins the lines of the prompt template. + * + * @param structuredPrompt the structured prompt. + * @return the joined prompt template. + */ + private static String join(StructuredPrompt structuredPrompt) { + return String.join(structuredPrompt.delimiter(), structuredPrompt.value()); + } +} diff --git a/smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/core/langchain4j/services/ToolSpecifications.java b/smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/core/langchain4j/services/ToolSpecifications.java new file mode 100644 index 0000000..ec3d2a9 --- /dev/null +++ b/smallrye-llm-langchain4j-core/src/main/java/io/smallrye/llm/core/langchain4j/services/ToolSpecifications.java @@ -0,0 +1,279 @@ +package io.smallrye.llm.core.langchain4j.services; + +import static dev.langchain4j.agent.tool.JsonSchemaProperty.ARRAY; +import static dev.langchain4j.agent.tool.JsonSchemaProperty.BOOLEAN; +import static dev.langchain4j.agent.tool.JsonSchemaProperty.INTEGER; +import static dev.langchain4j.agent.tool.JsonSchemaProperty.NUMBER; +import static dev.langchain4j.agent.tool.JsonSchemaProperty.OBJECT; +import static dev.langchain4j.agent.tool.JsonSchemaProperty.STRING; +import static dev.langchain4j.agent.tool.JsonSchemaProperty.description; +import static dev.langchain4j.agent.tool.JsonSchemaProperty.enums; +import static dev.langchain4j.agent.tool.JsonSchemaProperty.from; +import static dev.langchain4j.agent.tool.JsonSchemaProperty.items; +import static dev.langchain4j.agent.tool.JsonSchemaProperty.objectItems; +import static dev.langchain4j.internal.TypeUtils.isJsonBoolean; +import static dev.langchain4j.internal.TypeUtils.isJsonInteger; +import static dev.langchain4j.internal.TypeUtils.isJsonNumber; +import static dev.langchain4j.internal.Utils.isNullOrBlank; +import static java.lang.String.format; +import static java.util.Arrays.stream; +import static java.util.stream.Collectors.toList; + +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; + +import org.eclipse.microprofile.ai.llm.P; +import org.eclipse.microprofile.ai.llm.Tool; +import org.eclipse.microprofile.ai.llm.ToolMemoryId; + +import dev.langchain4j.agent.tool.JsonSchemaProperty; +import dev.langchain4j.agent.tool.ToolSpecification; +import dev.langchain4j.model.output.structured.Description; + +/** + * @author Buhake Sindi + * @since 10 October 2024 + */ +public class ToolSpecifications { + + private ToolSpecifications() { + } + + /** + * Returns {@link ToolSpecification}s for all methods annotated with @{@link Tool} within the specified class. + * + * @param classWithTools the class. + * @return the {@link ToolSpecification}s. + */ + public static List toolSpecificationsFrom(Class classWithTools) { + List toolSpecifications = stream(classWithTools.getDeclaredMethods()) + .filter(method -> method.isAnnotationPresent(Tool.class)) + .map(ToolSpecifications::toolSpecificationFrom) + .collect(toList()); + validateSpecifications(toolSpecifications); + return toolSpecifications; + } + + /** + * Returns {@link ToolSpecification}s for all methods annotated with @{@link Tool} + * within the class of the specified object. + * + * @param objectWithTools the object. + * @return the {@link ToolSpecification}s. + */ + public static List toolSpecificationsFrom(Object objectWithTools) { + return toolSpecificationsFrom(objectWithTools.getClass()); + } + + /** + * Validates all the {@link ToolSpecification}s. The validation checks for duplicate method names. + * Throws {@link IllegalArgumentException} if validation fails + * + * @param toolSpecifications list of ToolSpecification to be validated. + */ + public static void validateSpecifications(List toolSpecifications) throws IllegalArgumentException { + + // Checks for duplicates methods + Set names = new HashSet<>(); + for (ToolSpecification toolSpecification : toolSpecifications) { + if (!names.add(toolSpecification.name())) { + throw new IllegalArgumentException( + format("Tool names must be unique. The tool '%s' appears several times", toolSpecification.name())); + } + } + } + + /** + * Returns the {@link ToolSpecification} for the given method annotated with @{@link Tool}. + * + * @param method the method. + * @return the {@link ToolSpecification}. + */ + public static ToolSpecification toolSpecificationFrom(Method method) { + Tool annotation = method.getAnnotation(Tool.class); + + String name = isNullOrBlank(annotation.name()) ? method.getName() : annotation.name(); + String description = String.join("\n", annotation.value()); // TODO provide null instead of "" ? + + ToolSpecification.Builder builder = ToolSpecification.builder() + .name(name) + .description(description); + + for (Parameter parameter : method.getParameters()) { + if (parameter.isAnnotationPresent(ToolMemoryId.class)) { + continue; + } + + boolean required = Optional.ofNullable(parameter.getAnnotation(P.class)) + .map(P::required) + .orElse(true); + + if (required) { + builder.addParameter(parameter.getName(), toJsonSchemaProperties(parameter)); + } else { + builder.addOptionalParameter(parameter.getName(), toJsonSchemaProperties(parameter)); + } + } + + return builder.build(); + } + + /** + * Convert a {@link Parameter} to a {@link JsonSchemaProperty}. + * + * @param parameter the parameter. + * @return the {@link JsonSchemaProperty}. + */ + static Iterable toJsonSchemaProperties(Parameter parameter) { + + Class type = parameter.getType(); + + P annotation = parameter.getAnnotation(P.class); + JsonSchemaProperty description = annotation == null ? null : description(annotation.value()); + + Iterable simpleType = toJsonSchemaProperties(type, description); + + if (simpleType != null) { + return simpleType; + } + + if (Collection.class.isAssignableFrom(type)) { + return removeNulls(ARRAY, arrayTypeFrom(parameter.getParameterizedType()), description); + } + + return removeNulls(OBJECT, schema(type), description); + } + + static JsonSchemaProperty schema(Class structured) { + return schema(structured, new HashMap<>()); + } + + private static JsonSchemaProperty schema(Class structured, HashMap, JsonSchemaProperty> visited) { + if (visited.containsKey(structured)) { + return visited.get(structured); + } + + // Mark the class as visited by inserting it in the visited map with a null value initially. + visited.put(structured, null); + Map properties = new HashMap<>(); + for (Field field : structured.getDeclaredFields()) { + String name = field.getName(); + if (name.equals("this$0") || java.lang.reflect.Modifier.isStatic(field.getModifiers())) { + // Skip inner class reference. + continue; + } + Iterable schemaProperties = toJsonSchemaProperties(field, visited); + Map objectMap = new HashMap<>(); + for (JsonSchemaProperty jsonSchemaProperty : schemaProperties) { + objectMap.put(jsonSchemaProperty.key(), jsonSchemaProperty.value()); + } + properties.put(name, objectMap); + } + JsonSchemaProperty jsonSchemaProperty = from("properties", properties); + // Update the visited map with the final JsonSchemaProperty for the current class + visited.put(structured, jsonSchemaProperty); + return jsonSchemaProperty; + } + + private static Iterable toJsonSchemaProperties(Field field, + HashMap, JsonSchemaProperty> visited) { + + Class type = field.getType(); + + Description annotation = field.getAnnotation(Description.class); + JsonSchemaProperty description = annotation == null ? null : description(String.join(" ", annotation.value())); + + Iterable simpleType = toJsonSchemaProperties(type, description); + + if (simpleType != null) { + return simpleType; + } + + if (Collection.class.isAssignableFrom(type)) { + return removeNulls(ARRAY, + arrayTypeFrom((Class) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]), + description); + } + + return removeNulls(OBJECT, schema(type, visited), description); + } + + private static Iterable toJsonSchemaProperties(Class type, JsonSchemaProperty description) { + + if (type == String.class) { + return removeNulls(STRING, description); + } + + if (isJsonBoolean(type)) { + return removeNulls(BOOLEAN, description); + } + + if (isJsonInteger(type)) { + return removeNulls(INTEGER, description); + } + + if (isJsonNumber(type)) { + return removeNulls(NUMBER, description); + } + + if (type.isArray()) { + return removeNulls(ARRAY, arrayTypeFrom(type.getComponentType()), description); + } + + if (type.isEnum()) { + return removeNulls(STRING, enums((Class) type), description); + } + + return null; + } + + private static JsonSchemaProperty arrayTypeFrom(Type type) { + if (type instanceof ParameterizedType) { + ParameterizedType parameterizedType = (ParameterizedType) type; + Type[] actualTypeArguments = parameterizedType.getActualTypeArguments(); + if (actualTypeArguments.length == 1) { + return arrayTypeFrom((Class) actualTypeArguments[0]); + } + } + return items(JsonSchemaProperty.OBJECT); + } + + private static JsonSchemaProperty arrayTypeFrom(Class clazz) { + if (clazz == String.class) { + return items(JsonSchemaProperty.STRING); + } + if (isJsonBoolean(clazz)) { + return items(JsonSchemaProperty.BOOLEAN); + } + if (isJsonInteger(clazz)) { + return items(JsonSchemaProperty.INTEGER); + } + if (isJsonNumber(clazz)) { + return items(JsonSchemaProperty.NUMBER); + } + return objectItems(schema(clazz)); + } + + /** + * Remove nulls from the given array. + * + * @param items the array + * @return an iterable of the non-null items. + */ + static Iterable removeNulls(JsonSchemaProperty... items) { + return stream(items) + .filter(Objects::nonNull) + .collect(toList()); + } +} diff --git a/smallrye-llm-langchain4j-core/src/main/resources/META-INF/services/dev.langchain4j.spi.prompt.structured.StructuredPromptFactory b/smallrye-llm-langchain4j-core/src/main/resources/META-INF/services/dev.langchain4j.spi.prompt.structured.StructuredPromptFactory new file mode 100644 index 0000000..c5471d4 --- /dev/null +++ b/smallrye-llm-langchain4j-core/src/main/resources/META-INF/services/dev.langchain4j.spi.prompt.structured.StructuredPromptFactory @@ -0,0 +1 @@ +io.smallrye.llm.core.langchain4j.services.SmallRyeStructuredPromptFactory \ No newline at end of file diff --git a/smallrye-llm-langchain4j-core/src/main/resources/META-INF/services/dev.langchain4j.spi.services.AiServicesFactory b/smallrye-llm-langchain4j-core/src/main/resources/META-INF/services/dev.langchain4j.spi.services.AiServicesFactory new file mode 100644 index 0000000..d5e51a5 --- /dev/null +++ b/smallrye-llm-langchain4j-core/src/main/resources/META-INF/services/dev.langchain4j.spi.services.AiServicesFactory @@ -0,0 +1 @@ +io.smallrye.llm.core.langchain4j.services.SmallRyeLang4JchainAiServicesFactory \ No newline at end of file diff --git a/smallrye-llm-langchain4j-portable-extension/pom.xml b/smallrye-llm-langchain4j-portable-extension/pom.xml index 0bbf84d..17c206b 100644 --- a/smallrye-llm-langchain4j-portable-extension/pom.xml +++ b/smallrye-llm-langchain4j-portable-extension/pom.xml @@ -42,7 +42,6 @@ io.smallrye.config smallrye-config - 3.8.1 test diff --git a/smallrye-llm-langchain4j-portable-extension/src/main/java/io/smallrye/llm/core/langchain4j/portableextension/LangChain4JAIServicePortableExtension.java b/smallrye-llm-langchain4j-portable-extension/src/main/java/io/smallrye/llm/core/langchain4j/portableextension/LangChain4JAIServicePortableExtension.java index 304de6c..44c8800 100644 --- a/smallrye-llm-langchain4j-portable-extension/src/main/java/io/smallrye/llm/core/langchain4j/portableextension/LangChain4JAIServicePortableExtension.java +++ b/smallrye-llm-langchain4j-portable-extension/src/main/java/io/smallrye/llm/core/langchain4j/portableextension/LangChain4JAIServicePortableExtension.java @@ -17,10 +17,10 @@ import jakarta.enterprise.inject.spi.ProcessInjectionPoint; import jakarta.enterprise.inject.spi.WithAnnotations; +import org.eclipse.microprofile.ai.llm.RegisterAIService; import org.jboss.logging.Logger; import io.smallrye.llm.aiservice.CommonAIServiceCreator; -import io.smallrye.llm.spi.RegisterAIService; public class LangChain4JAIServicePortableExtension implements Extension { private static final Logger LOGGER = Logger.getLogger(LangChain4JAIServicePortableExtension.class); diff --git a/smallrye-llm-langchain4j-portable-extension/src/test/java/io/smallrye/llm/core/MyDummyAIService.java b/smallrye-llm-langchain4j-portable-extension/src/test/java/io/smallrye/llm/core/MyDummyAIService.java index fb87b1f..5d656c1 100644 --- a/smallrye-llm-langchain4j-portable-extension/src/test/java/io/smallrye/llm/core/MyDummyAIService.java +++ b/smallrye-llm-langchain4j-portable-extension/src/test/java/io/smallrye/llm/core/MyDummyAIService.java @@ -1,9 +1,9 @@ package io.smallrye.llm.core; -import dev.langchain4j.service.SystemMessage; -import dev.langchain4j.service.UserMessage; -import dev.langchain4j.service.V; -import io.smallrye.llm.spi.RegisterAIService; +import org.eclipse.microprofile.ai.llm.RegisterAIService; +import org.eclipse.microprofile.ai.llm.SystemMessage; +import org.eclipse.microprofile.ai.llm.UserMessage; +import org.eclipse.microprofile.ai.llm.V; @SuppressWarnings("CdiManagedBeanInconsistencyInspection") @RegisterAIService diff --git a/smallrye-llm-langchain4j-portable-extension/src/test/java/io/smallrye/llm/core/MyDummyApplicationScopedAIService.java b/smallrye-llm-langchain4j-portable-extension/src/test/java/io/smallrye/llm/core/MyDummyApplicationScopedAIService.java index 81deb7e..c86cf42 100644 --- a/smallrye-llm-langchain4j-portable-extension/src/test/java/io/smallrye/llm/core/MyDummyApplicationScopedAIService.java +++ b/smallrye-llm-langchain4j-portable-extension/src/test/java/io/smallrye/llm/core/MyDummyApplicationScopedAIService.java @@ -2,7 +2,7 @@ import jakarta.enterprise.context.ApplicationScoped; -import io.smallrye.llm.spi.RegisterAIService; +import org.eclipse.microprofile.ai.llm.RegisterAIService; @SuppressWarnings("CdiManagedBeanInconsistencyInspection") @RegisterAIService(scope = ApplicationScoped.class)