diff --git a/.github/workflows/dev-build.yml b/.github/workflows/dev-build.yml index e6031c8..8454308 100644 --- a/.github/workflows/dev-build.yml +++ b/.github/workflows/dev-build.yml @@ -21,11 +21,11 @@ jobs: with: fetch-depth: 0 ref: Development - - name: Set up JDK 21 - uses: actions/setup-java@v4 + - name: Set up JDK 25 + uses: actions/setup-java@v5 with: - distribution: 'adopt' - java-version: '21' + distribution: 'temurin' + java-version: '25' - name: Cache Maven repository uses: actions/cache@v4 with: @@ -38,3 +38,5 @@ jobs: value: "mongodb://localhost:27017" - name: Build with Maven run: mvn -B package --file pom.xml + env: + QUARKUS_LANGCHAIN4J_MISTRALAI_API_KEY: ${{ secrets.QUARKUS_LANGCHAIN4J_MISTRALAI_API_KEY }} \ No newline at end of file diff --git a/.github/workflows/main-build.yml b/.github/workflows/main-build.yml index feeb2cf..ccc3bd5 100644 --- a/.github/workflows/main-build.yml +++ b/.github/workflows/main-build.yml @@ -20,11 +20,11 @@ jobs: with: fetch-depth: 0 ref: main - - name: Set up JDK 21 - uses: actions/setup-java@v4 + - name: Set up JDK 25 + uses: actions/setup-java@v5 with: - distribution: 'adopt' - java-version: '21' + distribution: 'temurin' + java-version: '25' - name: Cache Maven repository uses: actions/cache@v4 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ec03a38..5c75e65 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -28,11 +28,11 @@ jobs: with: fetch-depth: 0 ref: main - - name: Set up JDK 21 - uses: actions/setup-java@v4 + - name: Set up JDK 25 + uses: actions/setup-java@v5 with: - distribution: 'adopt' - java-version: '21' + distribution: 'temurin' + java-version: '25' - name: Cache Maven repository uses: actions/cache@v4 with: diff --git a/pom.xml b/pom.xml index 3ade41e..8e9280d 100644 --- a/pom.xml +++ b/pom.xml @@ -6,16 +6,16 @@ project-services 1.0.0-SNAPSHOT - 3.12.1 - 17 + 3.14.1 + 25 UTF-8 UTF-8 quarkus-bom io.quarkus.platform - 3.9.1 + 3.31.4 true - 3.2.5 - 0.3.1 + 3.5.4 + 0.4.1-SNAPSHOT @@ -26,12 +26,24 @@ pom import + + ${quarkus.platform.group-id} + quarkus-langchain4j-bom + ${quarkus.platform.version} + pom + import + io.quarkus - quarkus-junit5 + quarkus-junit + test + + + io.quarkus + quarkus-junit-mockito test @@ -47,6 +59,10 @@ io.quarkus quarkus-mutiny + + io.quarkus + quarkus-websockets-next + io.quarkus quarkus-mongodb-client @@ -59,6 +75,21 @@ io.quarkus quarkus-rest-jackson + + io.quarkiverse.langchain4j + quarkus-langchain4j-mistral-ai + + + net.dontcode.common + quarkus-mongo + ${dontcode-common.version} + + + net.dontcode.common + quarkus-mongo-test + ${dontcode-common.version} + test + ${project.artifactId} @@ -67,6 +98,7 @@ io.quarkus quarkus-maven-plugin ${quarkus.platform.version} + true @@ -121,8 +153,21 @@ - native + native + + + Central Portal Snapshots + central-portal-snapshots + https://central.sonatype.com/repository/maven-snapshots/ + + false + + + true + + + diff --git a/src/main/java/net/dontcode/prj/GenerateProjectResource.java b/src/main/java/net/dontcode/prj/GenerateProjectResource.java new file mode 100644 index 0000000..f9fb5ce --- /dev/null +++ b/src/main/java/net/dontcode/prj/GenerateProjectResource.java @@ -0,0 +1,45 @@ +package net.dontcode.prj; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.quarkus.websockets.next.OnOpen; +import io.quarkus.websockets.next.OnTextMessage; +import io.quarkus.websockets.next.WebSocket; +import jakarta.websocket.EncodeException; +import net.dontcode.common.websocket.MessageEncoderDecoder; +import net.dontcode.core.project.DontCodeProjectModel; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@WebSocket(path = "/generate") +public class GenerateProjectResource { + private static Logger log = LoggerFactory.getLogger(GenerateProjectResource.class); + + private final GenerateProjectService service; + + public GenerateProjectResource(GenerateProjectService service) { + this.service = service; + } + + + @OnOpen + public String onOpen() { + return "Hello, please describe the application you want to generate."; + } + + @OnTextMessage() + public String onMessage(String message) { + return projectToString(service.generateProjectJson(message)); + } + + protected String projectToString (DontCodeProjectModel prj) { + ObjectMapper mapper = new ObjectMapper(); + String json = ""; + try { + json = mapper.writeValueAsString(prj); + } catch (JsonProcessingException e) { + throw new RuntimeException("Error decoding project", e); + } + return json; + } +} diff --git a/src/main/java/net/dontcode/prj/GenerateProjectService.java b/src/main/java/net/dontcode/prj/GenerateProjectService.java new file mode 100644 index 0000000..090044a --- /dev/null +++ b/src/main/java/net/dontcode/prj/GenerateProjectService.java @@ -0,0 +1,22 @@ +package net.dontcode.prj; + +import dev.langchain4j.service.SystemMessage; +import io.quarkiverse.langchain4j.RegisterAiService; +import jakarta.enterprise.context.SessionScoped; +import net.dontcode.core.project.DontCodeProjectModel; + +@RegisterAiService +@SystemMessage(""" + Tu es un createur d'application utilisant le framework dont-code. Ce framework génère une application à partir d'un fichier json. + Basé sur la demande d'un utilisateur, tu fournis le fichier json permettant de générer l'application voulue. + Quand tu reçois une demande, trouve les objets qui devront être manipulés. Ces objets doivent être définis dans la liste entities du json.Ensuite, pour chaque objet, cherche les champs nécessaire, et leur type. + Ces champs sont renseignés dans la liste fields de chaque entity.Un champ peut-être d'un des types prédéfinis suivant: + "number","string","date","time","date-time","currency","country","money-amount","eur-amount","usd-amount","image","link","rating","recurring-task","task-complete" + ou du type d'une autre entité. + Optionnellement, un champ peut être une référence vers une autre entité, en ajoutant "reference" a la description avec les informations nécessaire pour faire le lien entre les deux entités. + """) +@SessionScoped +public interface GenerateProjectService { + + DontCodeProjectModel generateProjectJson (String msg); +} diff --git a/src/main/java/net/dontcode/prj/PrjTestResource.java b/src/main/java/net/dontcode/prj/PrjTestResource.java index 67445ef..75111f2 100644 --- a/src/main/java/net/dontcode/prj/PrjTestResource.java +++ b/src/main/java/net/dontcode/prj/PrjTestResource.java @@ -1,6 +1,11 @@ package net.dontcode.prj; +import io.quarkus.arc.Arc; +import io.quarkus.arc.ManagedContext; import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.SessionScoped; +import jakarta.inject.Inject; +import net.dontcode.core.project.DontCodeProjectModel; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -15,6 +20,9 @@ public class PrjTestResource { private static Logger log = LoggerFactory.getLogger(PrjTestResource.class); +// @Inject +// GenerateProjectService generateProjectService; + @POST @Consumes(MediaType.APPLICATION_JSON) public Response testAsIde(String update) { @@ -46,4 +54,25 @@ public Uni cookie(@CookieParam("Test") String testCookie) { new NewCookie(new Cookie("Test", Integer.toString(nextValue), "/", "localhost"), null, 60*60, false)).build()); } + +/* @GET + @Path("/generator") + @Produces(MediaType.APPLICATION_JSON) + public Uni generator(@QueryParam("message") String message) { + // We create a session context just to call the session scoped service + ManagedContext sessionContext = null; + try { + sessionContext = Arc.requireContainer().sessionContext(); + if (!sessionContext.isActive()) { + sessionContext.activate(); + } + DontCodeProjectModel response=generateProjectService.generateProjectJson(message); + return Uni.createFrom().item(Response.ok().entity(response).build()); + + } finally { + if (sessionContext != null) { + sessionContext.terminate(); + } + } + }*/ } diff --git a/src/main/java/net/dontcode/prj/ProjectResource.java b/src/main/java/net/dontcode/prj/ProjectResource.java index 414079e..6c7c612 100644 --- a/src/main/java/net/dontcode/prj/ProjectResource.java +++ b/src/main/java/net/dontcode/prj/ProjectResource.java @@ -1,27 +1,18 @@ package net.dontcode.prj; -import com.mongodb.client.model.FindOneAndReplaceOptions; -import com.mongodb.client.model.ReturnDocument; -import io.quarkus.mongodb.MongoClientName; -import io.quarkus.mongodb.reactive.ReactiveMongoClient; -import io.quarkus.mongodb.reactive.ReactiveMongoCollection; -import io.quarkus.mongodb.reactive.ReactiveMongoDatabase; import io.smallrye.common.annotation.Blocking; import io.smallrye.mutiny.Multi; import io.smallrye.mutiny.Uni; -import org.bson.Document; -import org.bson.types.ObjectId; -import org.eclipse.microprofile.config.inject.ConfigProperty; -import org.jboss.resteasy.reactive.RestHeader; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import jakarta.ws.rs.*; import jakarta.ws.rs.core.MediaType; import jakarta.ws.rs.core.Response; import jakarta.ws.rs.core.UriInfo; +import org.bson.Document; +import org.jboss.resteasy.reactive.RestHeader; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; @Path("/project") @ApplicationScoped @@ -29,36 +20,14 @@ public class ProjectResource { private static Logger log = LoggerFactory.getLogger(ProjectResource.class); @Inject - @MongoClientName("projects") - ReactiveMongoClient mongoClient; - - @ConfigProperty(name = "projects-database-name") - String projectDbName; + ProjectService projectService; @GET @Path("/") @Produces(MediaType.APPLICATION_JSON) public Multi listProjects (UriInfo info, @RestHeader("DbName") String dbName) { log.debug("Hostname = {}, DbName Header = {}", info.getAbsolutePath(), dbName); -/* Multi ret = Multi.createFrom().emitter(multiEmitter -> { - for (int i=0;i<100000;i++) { - multiEmitter.emit(Document.parse(""" - { - "projectId":"EGFERGG", - "status":"PIZZADAZRFERFERF", - "data":{ - "value":"name" - } - } - """)); - } - multiEmitter.complete(); - });*/ - Multi ret = getProjects(dbName).find().map(document -> { - changeIdToString(document); - return document; - }); - return ret; + return projectService.listProjects(info, dbName); } @GET @@ -66,15 +35,7 @@ public Multi listProjects (UriInfo info, @RestHeader("DbName") String @Produces(MediaType.APPLICATION_JSON) @Blocking public Uni getProject (String projectName, @HeaderParam("DbName") String dbName) { - Uni ret = getProjects(dbName).find(new Document().append("name", projectName)).toUni().map(document -> { - if( document != null) { - changeIdToString(document); - return Response.ok(document).build(); - } else { - return Response.status(Response.Status.NOT_FOUND).build(); - } - }); - return ret; + return projectService.getProject(projectName, dbName); } @PUT @@ -82,25 +43,7 @@ public Uni getProject (String projectName, @HeaderParam("DbName") Stri @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Uni updateProject (String projectName, @HeaderParam("DbName") String dbName, Document body) { - changeIdToObjectId(body); - Uni ret = getProjects(dbName).findOneAndReplace(new Document().append("_id", body.get("_id")), body, - new FindOneAndReplaceOptions().upsert(false).returnDocument(ReturnDocument.AFTER)).map(document -> { - if( document != null) { - changeIdToString(document); - return Response.ok(document).build(); - } else { - return Response.status(Response.Status.NOT_FOUND).build(); - } - }); - return ret; - } - - protected void changeIdToObjectId(Document body) { - body.put("_id", new ObjectId(body.getString("_id"))); - } - - protected void changeIdToString(Document body) { - body.put("_id", body.getObjectId("_id").toHexString()); + return projectService.updateProject(projectName, dbName, body); } @DELETE @@ -108,15 +51,7 @@ protected void changeIdToString(Document body) { @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Uni deleteProject (String projectName, @HeaderParam("DbName") String dbName) { - Uni ret = getProjects(dbName).findOneAndDelete(new Document().append("name", projectName)).map(document -> { - if( document != null) { - changeIdToString(document); - return Response.ok(document).build(); - } else { - return Response.status(Response.Status.NOT_FOUND).build(); - } - }); - return ret; + return projectService.deleteProject(projectName, dbName); } @POST @@ -124,31 +59,7 @@ public Uni deleteProject (String projectName, @HeaderParam("DbName") S @Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON) public Uni insertProject(Document body, @HeaderParam("DbName") String dbName) { - //System.out.println("Received"+ body); - return getProjects(dbName).insertOne(body).map(result -> { - changeIdToString(body); - return Response.ok(body).build(); - }); + return projectService.insertProject(body, dbName); } - protected ReactiveMongoCollection getProjects(String dbName) { - return getDatabase(dbName).getCollection("projects", Document.class); - } - - /*protected ReactiveMongoCollection getProjects() { - return getDatabase().getCollection("projects", Document.class); - } - protected ReactiveMongoCollection getProjects(Class clazz) { - return getDatabase().getCollection("projects", clazz); - } - - protected ReactiveMongoDatabase getDatabase () { - return mongoClient.getDatabase(projectDbName); - }*/ - - - protected ReactiveMongoDatabase getDatabase (String dbName) { - if( dbName==null) dbName = projectDbName; - return mongoClient.getDatabase(dbName); - } } diff --git a/src/main/java/net/dontcode/prj/ProjectService.java b/src/main/java/net/dontcode/prj/ProjectService.java new file mode 100644 index 0000000..9262c3c --- /dev/null +++ b/src/main/java/net/dontcode/prj/ProjectService.java @@ -0,0 +1,118 @@ +package net.dontcode.prj; + +import com.mongodb.client.model.FindOneAndReplaceOptions; +import com.mongodb.client.model.ReturnDocument; +import io.quarkus.mongodb.MongoClientName; +import io.quarkus.mongodb.reactive.ReactiveMongoClient; +import io.quarkus.mongodb.reactive.ReactiveMongoCollection; +import io.quarkus.mongodb.reactive.ReactiveMongoDatabase; +import io.smallrye.mutiny.Multi; +import io.smallrye.mutiny.Uni; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import jakarta.ws.rs.core.Response; +import jakarta.ws.rs.core.UriInfo; +import org.bson.Document; +import org.bson.types.ObjectId; +import org.eclipse.microprofile.config.inject.ConfigProperty; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ApplicationScoped +public class ProjectService { + private static Logger log = LoggerFactory.getLogger(ProjectService.class); + + @Inject + @MongoClientName("projects") + ReactiveMongoClient mongoClient; + + @ConfigProperty(name = "projects-database-name") + String projectDbName; + + public Multi listProjects (UriInfo info, String dbName) { + log.debug("Hostname = {}, DbName Header = {}", info.getAbsolutePath(), dbName); +/* Multi ret = Multi.createFrom().emitter(multiEmitter -> { + for (int i=0;i<100000;i++) { + multiEmitter.emit(Document.parse(""" + { + "projectId":"EGFERGG", + "status":"PIZZADAZRFERFERF", + "data":{ + "value":"name" + } + } + """)); + } + multiEmitter.complete(); + });*/ + Multi ret = getProjects(dbName).find().map(document -> { + changeIdToString(document); + return document; + }); + return ret; + } + + public Uni getProject (String projectName, String dbName) { + Uni ret = getProjects(dbName).find(new Document().append("name", projectName)).toUni().map(document -> { + if( document != null) { + changeIdToString(document); + return Response.ok(document).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + }); + return ret; + } + + public Uni updateProject (String projectName, String dbName, Document body) { + changeIdToObjectId(body); + Uni ret = getProjects(dbName).findOneAndReplace(new Document().append("_id", body.get("_id")), body, + new FindOneAndReplaceOptions().upsert(false).returnDocument(ReturnDocument.AFTER)).map(document -> { + if( document != null) { + changeIdToString(document); + return Response.ok(document).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + }); + return ret; + } + + protected void changeIdToObjectId(Document body) { + body.put("_id", new ObjectId(body.getString("_id"))); + } + + protected void changeIdToString(Document body) { + body.put("_id", body.getObjectId("_id").toHexString()); + } + + public Uni deleteProject (String projectName, String dbName) { + Uni ret = getProjects(dbName).findOneAndDelete(new Document().append("name", projectName)).map(document -> { + if( document != null) { + changeIdToString(document); + return Response.ok(document).build(); + } else { + return Response.status(Response.Status.NOT_FOUND).build(); + } + }); + return ret; + } + + public Uni insertProject(Document body,String dbName) { + //System.out.println("Received"+ body); + return getProjects(dbName).insertOne(body).map(result -> { + changeIdToString(body); + return Response.ok(body).build(); + }); + } + + protected ReactiveMongoCollection getProjects(String dbName) { + return getDatabase(dbName).getCollection("projects", Document.class); + } + + protected ReactiveMongoDatabase getDatabase (String dbName) { + if( dbName==null) dbName = projectDbName; + return mongoClient.getDatabase(dbName); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index b08a5df..6a75c91 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -15,10 +15,13 @@ data-database-name=dontCodeDevData %scalewaytest.projects-database-name=dontCodeTestProjects %scalewaytest.data-database-name=dontCodeTestData -quarkus.http.cors=true +quarkus.http.cors.enabled=true quarkus.http.cors.origins=/.*/ -quarkus.package.type=uber-jar +quarkus.package.jar.type=uber-jar + + +quarkus.langchain4j.mistralai.chat-model.model-name=codestral-latest quarkus.log.level=INFO quarkus.log.category."net.dontcode.prj".level=DEBUG diff --git a/src/test/java/net/dontcode/prj/GenerateProjectResourceTest.java b/src/test/java/net/dontcode/prj/GenerateProjectResourceTest.java new file mode 100644 index 0000000..0f7c883 --- /dev/null +++ b/src/test/java/net/dontcode/prj/GenerateProjectResourceTest.java @@ -0,0 +1,105 @@ +package net.dontcode.prj; + +import io.quarkus.test.InjectMock; +import io.quarkus.test.common.http.TestHTTPResource; +import io.quarkus.test.junit.QuarkusTest; +import jakarta.websocket.*; +import net.dontcode.core.Message; +import net.dontcode.core.project.*; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.io.IOException; +import java.net.URI; +import java.util.concurrent.ExecutionException; + +import static org.mockito.ArgumentMatchers.anyString; + +@QuarkusTest +public class GenerateProjectResourceTest { + + @TestHTTPResource("/generate") + URI generateUri; + + @InjectMock + GenerateProjectService serviceMock; + + @Test + public void testGeneration() throws DeploymentException, IOException, InterruptedException { + DontCodeProjectEntities[] entities = new DontCodeProjectEntities[]{}; + Mockito.when(serviceMock.generateProjectJson(anyString())).thenReturn( + new DontCodeProjectModel("Test", + new DontCodeProjectContent( + new DontCodeProjectCreation("Test App", DontCodeProjectCreationType.application, entities)))); + ClientTestSession.opened=false; + try (Session session = ContainerProvider.getWebSocketContainer().connectToServer(ClientTestSession.class, generateUri)) { + // Wait the data to be saved in the database +/** for (int i = 0; i < 10; i++) { + Thread.sleep(50); + if( ClientTestSession.opened) { + break; + } + } + Assertions.assertTrue(ClientTestSession.opened, "Session was not opened");**/ + ClientTestSession.response=null; + + session.getAsyncRemote().sendText("Generate a new Test Application").get(); + + // Wait for the answer + for (int i = 0; i < 10; i++) { + Thread.sleep(50); + if( ClientTestSession.response!=null) { + break; + } + } + + Assertions.assertNotNull(ClientTestSession.response); + ClientTestSession.response=null; + session.getAsyncRemote().sendText("The application should be named Super Test").get(); + + // Wait for the answer + for (int i = 0; i < 10; i++) { + Thread.sleep(50); + if( ClientTestSession.response!=null) { + break; + } + } + + Assertions.assertNotNull(ClientTestSession.response); + Mockito.verify(serviceMock, Mockito.times(2)).generateProjectJson(anyString()); + + } catch (ExecutionException e) { + System.err.println(e.getCause().getMessage()); + throw new RuntimeException(e); + } + } + + @ClientEndpoint + public static class ClientTestSession { + + public static boolean opened=false; + public static String response=null; + + public ClientTestSession () { + } + + @OnOpen + public void open(Session session) { + opened=true; + } + + @OnMessage + void message(String msg) throws DecodeException { + response=msg; + } + + @OnError + void error (Throwable error) { + System.err.println("Error "+ error.getMessage()); + } + + } + + +} diff --git a/src/test/java/net/dontcode/prj/GenerateProjectServiceTest.java b/src/test/java/net/dontcode/prj/GenerateProjectServiceTest.java new file mode 100644 index 0000000..7dc661e --- /dev/null +++ b/src/test/java/net/dontcode/prj/GenerateProjectServiceTest.java @@ -0,0 +1,23 @@ +package net.dontcode.prj; + +import io.quarkus.test.junit.QuarkusTest; +import jakarta.inject.Inject; +import net.dontcode.core.project.DontCodeProjectModel; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +@QuarkusTest +public class GenerateProjectServiceTest { + + @Inject + GenerateProjectService service; + + @Test + public void testSimpleApplication () { + + //DontCodeProjectModel response=service.generateProjectJson("Please create a cooking recipe application"); + //Assertions.assertNotNull(response); + //Assertions.assertTrue(response.content().creation().entities().length > 0); + } + +} diff --git a/src/test/resources/application.properties b/src/test/resources/application.properties new file mode 100644 index 0000000..4db837a --- /dev/null +++ b/src/test/resources/application.properties @@ -0,0 +1 @@ +quarkus.langchain4j.mistralai.api-key=DUMMY_KEY \ No newline at end of file