diff --git a/.github/workflows/github-actions.yml b/.github/workflows/github-actions.yml index a48d6949006..8a6d2a3d4de 100644 --- a/.github/workflows/github-actions.yml +++ b/.github/workflows/github-actions.yml @@ -255,6 +255,7 @@ jobs: - customjhlite - typescriptapp - thymeleafapp + - langchain4japp include: - java-build-tool: maven - spring-config-format: yaml diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index abf1d781578..5f9a6563f6d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -370,6 +370,7 @@ from the main (upstream) repository: - reactiveapp - customjhlite - typescriptapp + - langchain4japp - Below is the list of build tools that can be used for testing (supported input params for the generate.sh script): - gradle - maven diff --git a/src/main/java/tech/jhipster/lite/generator/server/springboot/langchain4j/domain/LangChain4JModuleFactory.java b/src/main/java/tech/jhipster/lite/generator/server/springboot/langchain4j/domain/LangChain4JModuleFactory.java index 483d6dd54ac..9465f047371 100644 --- a/src/main/java/tech/jhipster/lite/generator/server/springboot/langchain4j/domain/LangChain4JModuleFactory.java +++ b/src/main/java/tech/jhipster/lite/generator/server/springboot/langchain4j/domain/LangChain4JModuleFactory.java @@ -7,6 +7,7 @@ import tech.jhipster.lite.module.domain.javabuild.ArtifactId; import tech.jhipster.lite.module.domain.javabuild.GroupId; import tech.jhipster.lite.module.domain.javabuild.VersionSlug; +import tech.jhipster.lite.module.domain.javaproperties.PropertyKey; import tech.jhipster.lite.module.domain.properties.JHipsterModuleProperties; import tech.jhipster.lite.shared.error.domain.Assert; @@ -22,6 +23,7 @@ public class LangChain4JModuleFactory { private static final VersionSlug VERSION_SLUG = versionSlug("langchain4j"); private static final String PROPERTIES = "properties"; + private static final PropertyKey LANGCHAIN4J_PROPERTY_API_KEY = propertyKey("langchain4j.open-ai.chat-model.api-key"); public JHipsterModule buildModule(JHipsterModuleProperties properties) { Assert.notNull(PROPERTIES, properties); @@ -34,12 +36,15 @@ public JHipsterModule buildModule(JHipsterModuleProperties properties) { .addDependency(GROUP_ID, OPEN_AI_ARTIFACT_ID, VERSION_SLUG) .and() .springMainProperties() - .set(propertyKey("langchain4j.open-ai.chat-model.api-key"), propertyValue("${OPENAI_API_KEY}")) - .comment(propertyKey("langchain4j.open-ai.chat-model.api-key"), comment(API_KEY_DEMO_COMMENT)) + .set(LANGCHAIN4J_PROPERTY_API_KEY, propertyValue("${OPENAI_API_KEY}")) + .comment(LANGCHAIN4J_PROPERTY_API_KEY, comment(API_KEY_DEMO_COMMENT)) .set(propertyKey("langchain4j.open-ai.chat-model.model-name"), propertyValue("gpt-4o-mini")) .set(propertyKey("langchain4j.open-ai.chat-model.log-requests"), propertyValue("true")) .set(propertyKey("langchain4j.open-ai.chat-model.log-responses"), propertyValue("true")) .and() + .springTestProperties() + .set(LANGCHAIN4J_PROPERTY_API_KEY, propertyValue("jhipster")) + .and() .build(); //@formatter:on } diff --git a/src/main/java/tech/jhipster/lite/generator/server/springboot/mvc/sample/langchain4j/application/SampleLangChain4jApplicationService.java b/src/main/java/tech/jhipster/lite/generator/server/springboot/mvc/sample/langchain4j/application/SampleLangChain4jApplicationService.java new file mode 100644 index 00000000000..e8fe52f63fc --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/server/springboot/mvc/sample/langchain4j/application/SampleLangChain4jApplicationService.java @@ -0,0 +1,20 @@ +package tech.jhipster.lite.generator.server.springboot.mvc.sample.langchain4j.application; + +import org.springframework.stereotype.Service; +import tech.jhipster.lite.generator.server.springboot.mvc.sample.langchain4j.domain.SampleLangChain4jModuleFactory; +import tech.jhipster.lite.module.domain.JHipsterModule; +import tech.jhipster.lite.module.domain.properties.JHipsterModuleProperties; + +@Service +public class SampleLangChain4jApplicationService { + + private final SampleLangChain4jModuleFactory factory; + + public SampleLangChain4jApplicationService() { + factory = new SampleLangChain4jModuleFactory(); + } + + public JHipsterModule buildModule(JHipsterModuleProperties properties) { + return factory.buildModule(properties); + } +} diff --git a/src/main/java/tech/jhipster/lite/generator/server/springboot/mvc/sample/langchain4j/domain/SampleLangChain4jModuleFactory.java b/src/main/java/tech/jhipster/lite/generator/server/springboot/mvc/sample/langchain4j/domain/SampleLangChain4jModuleFactory.java new file mode 100644 index 00000000000..8edc458dbe6 --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/server/springboot/mvc/sample/langchain4j/domain/SampleLangChain4jModuleFactory.java @@ -0,0 +1,39 @@ +package tech.jhipster.lite.generator.server.springboot.mvc.sample.langchain4j.domain; + +import static tech.jhipster.lite.module.domain.JHipsterModule.*; + +import tech.jhipster.lite.module.domain.JHipsterModule; +import tech.jhipster.lite.module.domain.file.JHipsterDestination; +import tech.jhipster.lite.module.domain.file.JHipsterSource; +import tech.jhipster.lite.module.domain.properties.JHipsterModuleProperties; +import tech.jhipster.lite.shared.error.domain.Assert; + +public class SampleLangChain4jModuleFactory { + + private static final String SAMPLE = "sample"; + + private static final JHipsterSource SOURCE = from("server/springboot/mvc/sample/langchain4j"); + + private static final String PRIMARY = "infrastructure/primary"; + + public JHipsterModule buildModule(JHipsterModuleProperties properties) { + Assert.notNull("properties", properties); + + String packagePath = properties.packagePath(); + JHipsterDestination mainDestination = toSrcMainJava().append(packagePath).append(SAMPLE); + JHipsterDestination testDestination = toSrcTestJava().append(packagePath).append(SAMPLE); + + //@formatter:off + return moduleBuilder(properties) + .files() + .batch(SOURCE.append("main").append(PRIMARY), mainDestination.append(PRIMARY)) + .addTemplate("ChatResource.java") + .and() + .batch(SOURCE.append("test").append(SAMPLE).append(PRIMARY), testDestination.append(PRIMARY)) + .addTemplate("ChatResourceTest.java") + .and() + .and() + .build(); + //@formatter:on + } +} diff --git a/src/main/java/tech/jhipster/lite/generator/server/springboot/mvc/sample/langchain4j/infrastructure/primary/SampleLangChain4jModuleConfiguration.java b/src/main/java/tech/jhipster/lite/generator/server/springboot/mvc/sample/langchain4j/infrastructure/primary/SampleLangChain4jModuleConfiguration.java new file mode 100644 index 00000000000..92e0f632273 --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/server/springboot/mvc/sample/langchain4j/infrastructure/primary/SampleLangChain4jModuleConfiguration.java @@ -0,0 +1,28 @@ +package tech.jhipster.lite.generator.server.springboot.mvc.sample.langchain4j.infrastructure.primary; + +import static tech.jhipster.lite.shared.slug.domain.JHLiteFeatureSlug.SPRING_MVC_SERVER; +import static tech.jhipster.lite.shared.slug.domain.JHLiteModuleSlug.*; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import tech.jhipster.lite.generator.server.springboot.mvc.sample.langchain4j.application.SampleLangChain4jApplicationService; +import tech.jhipster.lite.module.domain.resource.JHipsterModuleOrganization; +import tech.jhipster.lite.module.domain.resource.JHipsterModulePropertiesDefinition; +import tech.jhipster.lite.module.domain.resource.JHipsterModuleResource; + +@Configuration +class SampleLangChain4jModuleConfiguration { + + @Bean + JHipsterModuleResource langChain4jResourceInit(SampleLangChain4jApplicationService applicationService) { + return JHipsterModuleResource.builder() + .slug(SPRING_BOOT_LANGCHAIN4J_SAMPLE) + .propertiesDefinition( + JHipsterModulePropertiesDefinition.builder().addBasePackage().addIndentation().addSpringConfigurationFormat().build() + ) + .apiDoc("Spring Boot - LangChain4j", "Add LangChain4j sample") + .organization(JHipsterModuleOrganization.builder().addDependency(SPRING_MVC_SERVER).addDependency(LANGCHAIN4J).build()) + .tags("spring-boot", "spring", "server", "langchain4j") + .factory(applicationService::buildModule); + } +} diff --git a/src/main/java/tech/jhipster/lite/generator/server/springboot/mvc/sample/langchain4j/package-info.java b/src/main/java/tech/jhipster/lite/generator/server/springboot/mvc/sample/langchain4j/package-info.java new file mode 100644 index 00000000000..a0e75e2f4d7 --- /dev/null +++ b/src/main/java/tech/jhipster/lite/generator/server/springboot/mvc/sample/langchain4j/package-info.java @@ -0,0 +1,2 @@ +@tech.jhipster.lite.BusinessContext +package tech.jhipster.lite.generator.server.springboot.mvc.sample.langchain4j; diff --git a/src/main/java/tech/jhipster/lite/shared/slug/domain/JHLiteModuleSlug.java b/src/main/java/tech/jhipster/lite/shared/slug/domain/JHLiteModuleSlug.java index 7a10ba75582..66dd0ef692b 100644 --- a/src/main/java/tech/jhipster/lite/shared/slug/domain/JHLiteModuleSlug.java +++ b/src/main/java/tech/jhipster/lite/shared/slug/domain/JHLiteModuleSlug.java @@ -157,7 +157,8 @@ public enum JHLiteModuleSlug implements JHipsterModuleSlugFactory { VUE_PINIA("vue-pinia"), TS_PAGINATION_DOMAIN("ts-pagination-domain"), TS_REST_PAGINATION("ts-rest-pagination"), - LANGCHAIN4J("langchain4j"); + LANGCHAIN4J("langchain4j"), + SPRING_BOOT_LANGCHAIN4J_SAMPLE("spring-boot-langchain4j-sample"); private static final Map moduleSlugMap = Stream.of(values()).collect( Collectors.toMap(JHLiteModuleSlug::get, Function.identity()) diff --git a/src/main/resources/generator/server/springboot/mvc/sample/langchain4j/main/infrastructure/primary/ChatResource.java.mustache b/src/main/resources/generator/server/springboot/mvc/sample/langchain4j/main/infrastructure/primary/ChatResource.java.mustache new file mode 100644 index 00000000000..5098457c422 --- /dev/null +++ b/src/main/resources/generator/server/springboot/mvc/sample/langchain4j/main/infrastructure/primary/ChatResource.java.mustache @@ -0,0 +1,23 @@ +package {{packageName}}.sample.infrastructure.primary; + +import dev.langchain4j.model.chat.ChatLanguageModel; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequestMapping("/api") +class ChatResource { + + private final ChatLanguageModel chatLanguageModel; + + public ChatResource(ChatLanguageModel chatLanguageModel) { + this.chatLanguageModel = chatLanguageModel; + } + + @GetMapping("/chat") + public String send(@RequestParam(value = "message", defaultValue = "Hello") String message) { + return chatLanguageModel.generate(message); + } +} diff --git a/src/main/resources/generator/server/springboot/mvc/sample/langchain4j/test/sample/infrastructure/primary/ChatResourceTest.java.mustache b/src/main/resources/generator/server/springboot/mvc/sample/langchain4j/test/sample/infrastructure/primary/ChatResourceTest.java.mustache new file mode 100644 index 00000000000..8ebb8a53d95 --- /dev/null +++ b/src/main/resources/generator/server/springboot/mvc/sample/langchain4j/test/sample/infrastructure/primary/ChatResourceTest.java.mustache @@ -0,0 +1,32 @@ +package {{packageName}}.sample.infrastructure.primary; + +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +import {{packageName}}.UnitTest; +import dev.langchain4j.model.chat.ChatLanguageModel; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@UnitTest +@ExtendWith(MockitoExtension.class) +class ChatResourceTest { + + private static final String ANSWER = "Hello! How can I assist you today?"; + + @Mock + private ChatLanguageModel chatLanguageModel; + + @InjectMocks + private ChatResource chat; + + @Test + void shouldSendMessage() { + when(chatLanguageModel.generate("Hello")).thenReturn(ANSWER); + + assertThat(chat.send("Hello")).isEqualTo(ANSWER); + } +} diff --git a/src/test/features/server/springboot/langchain4j.feature b/src/test/features/server/springboot/langchain4j.feature index 38975e08172..7347f894f80 100644 --- a/src/test/features/server/springboot/langchain4j.feature +++ b/src/test/features/server/springboot/langchain4j.feature @@ -8,3 +8,10 @@ Feature: LangChain4j module Then I should have entries in "src/main/resources/config/application.yml" | open-ai | | langchain4j | + + Scenario: Should add Spring Boot LangChain4j Sample + When I apply "spring-boot-langchain4j-sample" module to default project with maven file + | packageName | tech.jhipster.chips | + | baseName | jhipster | + Then I should have files in "src/main/java/tech/jhipster/chips/sample/infrastructure/primary" + | ChatResource.java | diff --git a/src/test/java/tech/jhipster/lite/generator/server/springboot/langchain4j/domain/LangChain4JModuleFactoryTest.java b/src/test/java/tech/jhipster/lite/generator/server/springboot/langchain4j/domain/LangChain4JModuleFactoryTest.java index fb51b511606..15e4afb3c67 100644 --- a/src/test/java/tech/jhipster/lite/generator/server/springboot/langchain4j/domain/LangChain4JModuleFactoryTest.java +++ b/src/test/java/tech/jhipster/lite/generator/server/springboot/langchain4j/domain/LangChain4JModuleFactoryTest.java @@ -62,6 +62,16 @@ void shouldCreateModule() { log-responses: 'true' model-name: gpt-4o-mini """ + ) + .and() + .hasFile("src/test/resources/config/application-test.yml") + .containing( + """ + langchain4j: + open-ai: + chat-model: + api-key: jhipster + """ ); } } diff --git a/tests-ci/generate.sh b/tests-ci/generate.sh index bf3e521f706..fecefbb2435 100755 --- a/tests-ci/generate.sh +++ b/tests-ci/generate.sh @@ -508,8 +508,17 @@ elif [[ $application == 'thymeleafapp' ]]; then "htmx-webjars" \ "thymeleaf-template-htmx-webjars" +elif [[ $application == 'langchain4japp' ]]; then + init_server + spring_boot_mvc + sonar_back + + applyModules \ + "langchain4j" \ + "spring-boot-langchain4j-sample" + else - echo "*** Unknown configuration..." + echo "*** Unknown configuration..." "$application" exit 1 fi