diff --git a/.devcontainer/motd.sh b/.devcontainer/motd.sh new file mode 100755 index 00000000..1d1b9791 --- /dev/null +++ b/.devcontainer/motd.sh @@ -0,0 +1,18 @@ +BLACK='\033[0;30m' +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +PURPLE='\033[0;35m' +CYAN='\033[0;36m' +NC='\033[0m' + +printf "${CYAN}Let's go for a dRAGon ride! 🐉${NC}\n" +printf "\n" + +printf "${YELLOW}[QUICKSTART]${NC}\n" +printf "\t${BLUE}✨ Launch full dRAGon app ${NC}\tgradle bootRun\n" +printf "\t${BLUE}Launch dRAGon backend only${NC}\tgradle bootRun -x :frontend:pnpmBuild -x :backend:copyWebApp\n" +printf "\t${BLUE}Launch dRAGon frontend only${NC}\tpnpm --prefix frontend dev\n" + +printf "\n" \ No newline at end of file diff --git a/.devcontainer/postCreate.sh b/.devcontainer/postCreate.sh index 7018c0df..2777d80c 100755 --- a/.devcontainer/postCreate.sh +++ b/.devcontainer/postCreate.sh @@ -1,7 +1,15 @@ #!/bin/bash +# Prepare PNPM echo "Setting up pnpm store..." mkdir -p /tmp/pnpm && pnpm config set store-dir /tmp/pnpm/ +# Install dependencies echo "Building the project..." -gradle pnpmInstall build \ No newline at end of file +gradle pnpmInstall build + +# Configure the message of the day +echo '/bin/sh .devcontainer/motd.sh' >> /home/vscode/.bashrc + +# Let's go for a dRAGon ride! 🐉 +printf "${GREEN}Let's go for a dRAGon ride! 🐉\n\n" \ No newline at end of file diff --git a/backend/build.gradle b/backend/build.gradle index 54c6bce3..8be89e0c 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -33,13 +33,16 @@ dependencies { developmentOnly 'org.springframework.boot:spring-boot-devtools' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.6.0' - implementation 'dev.langchain4j:langchain4j:0.33.0' - implementation 'dev.langchain4j:langchain4j-core:0.33.0' - implementation 'dev.langchain4j:langchain4j-embeddings:0.33.0' - implementation 'dev.langchain4j:langchain4j-pgvector:0.33.0' - implementation 'dev.langchain4j:langchain4j-easy-rag:0.33.0' - implementation 'dev.langchain4j:langchain4j-open-ai:0.33.0' - implementation 'dev.langchain4j:langchain4j-mistral-ai:0.33.0' + implementation 'dev.langchain4j:langchain4j:0.35.0' + implementation 'dev.langchain4j:langchain4j-core:0.35.0' + implementation 'dev.langchain4j:langchain4j-embeddings:0.35.0' + implementation 'dev.langchain4j:langchain4j-pgvector:0.35.0' + implementation 'dev.langchain4j:langchain4j-easy-rag:0.35.0' + implementation 'dev.langchain4j:langchain4j-open-ai:0.35.0' + implementation 'dev.langchain4j:langchain4j-mistral-ai:0.35.0' + + implementation 'org.aspectj:aspectjrt:1.9.22.1' + implementation 'org.aspectj:aspectjweaver:1.9.22.1' implementation platform('org.dizitart:nitrite-bom:4.3.0') implementation 'org.dizitart:nitrite' diff --git a/backend/src/main/java/ai/dragon/DragonApplication.java b/backend/src/main/java/ai/dragon/DragonApplication.java index 8b98598c..8606e800 100644 --- a/backend/src/main/java/ai/dragon/DragonApplication.java +++ b/backend/src/main/java/ai/dragon/DragonApplication.java @@ -2,8 +2,10 @@ import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.EnableAspectJAutoProxy; @SpringBootApplication +@EnableAspectJAutoProxy public class DragonApplication { public static void main(String[] args) { SpringApplication.run(DragonApplication.class, args); diff --git a/backend/src/main/java/ai/dragon/aspect/api/GenericApiExceptionHandling.java b/backend/src/main/java/ai/dragon/aspect/api/GenericApiExceptionHandling.java new file mode 100644 index 00000000..3d0c9b34 --- /dev/null +++ b/backend/src/main/java/ai/dragon/aspect/api/GenericApiExceptionHandling.java @@ -0,0 +1,11 @@ +package ai.dragon.aspect.api; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +public @interface GenericApiExceptionHandling { +} diff --git a/backend/src/main/java/ai/dragon/aspect/api/GenericApiExceptionHandlingAspect.java b/backend/src/main/java/ai/dragon/aspect/api/GenericApiExceptionHandlingAspect.java new file mode 100644 index 00000000..c70833a3 --- /dev/null +++ b/backend/src/main/java/ai/dragon/aspect/api/GenericApiExceptionHandlingAspect.java @@ -0,0 +1,37 @@ +package ai.dragon.aspect.api; + +import org.aspectj.lang.ProceedingJoinPoint; +import org.aspectj.lang.annotation.Around; +import org.aspectj.lang.annotation.Aspect; +import org.dizitart.no2.exceptions.UniqueConstraintException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Component; + +import ai.dragon.dto.api.FailureApiResponse; +import jakarta.servlet.http.HttpServletResponse; + +@Aspect +@Component +public class GenericApiExceptionHandlingAspect { + @Autowired + private HttpServletResponse response; + + @Around("@annotation(GenericApiExceptionHandling)") + public Object handleException(ProceedingJoinPoint pjp) throws Throwable { + try { + return pjp.proceed(); + } catch (Exception ex) { + int httpStatusCode = 500; + if(ex instanceof UniqueConstraintException) { + httpStatusCode = 422; + } + response.setStatus(httpStatusCode); + + return FailureApiResponse + .builder() + .msg(ex.getMessage()) + .code(String.format("0%d", httpStatusCode)) + .build(); + } + } +} diff --git a/backend/src/main/java/ai/dragon/controller/api/backend/repository/FarmBackendApiController.java b/backend/src/main/java/ai/dragon/controller/api/backend/repository/FarmBackendApiController.java index e05aba60..eaf7936b 100644 --- a/backend/src/main/java/ai/dragon/controller/api/backend/repository/FarmBackendApiController.java +++ b/backend/src/main/java/ai/dragon/controller/api/backend/repository/FarmBackendApiController.java @@ -19,6 +19,7 @@ import com.fasterxml.jackson.databind.JsonMappingException; +import ai.dragon.aspect.api.GenericApiExceptionHandling; import ai.dragon.dto.api.DataTableApiResponse; import ai.dragon.dto.api.GenericApiResponse; import ai.dragon.dto.api.SuccessApiResponse; @@ -94,6 +95,7 @@ public GenericApiResponse updateFarm( .build(); } + @GenericApiExceptionHandling @PutMapping("/{uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}") @ApiResponse(responseCode = "200", description = "Farm has been successfully updated.") @ApiResponse(responseCode = "404", description = "Farm not found.", content = @Content) @@ -101,14 +103,17 @@ public GenericApiResponse updateFarm( public GenericApiResponse upsertFarm( @PathVariable("uuid") @Parameter(description = "Identifier of the Farm", required = false) String uuid, @RequestBody Map fields) throws Exception { - if (uuid == null || UUIDUtil.zeroUUIDString().equals(uuid)) { - fields.remove("uuid"); - uuid = super.create(farmRepository).getUuid().toString(); - } - return SuccessApiResponse - .builder() - .data(super.update(uuid, fields, farmRepository)) - .build(); + return farmRepository.queryTransaction(transactionRepository -> { + String farmUUID = uuid; + if (farmUUID == null || UUIDUtil.zeroUUIDString().equals(farmUUID)) { + fields.remove("uuid"); + farmUUID = super.create(transactionRepository).getUuid().toString(); + } + return SuccessApiResponse + .builder() + .data(super.update(farmUUID, fields, transactionRepository)) + .build(); + }); } @DeleteMapping("/{uuid:[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}}") diff --git a/backend/src/main/java/ai/dragon/dto/api/DataTableApiResponse.java b/backend/src/main/java/ai/dragon/dto/api/DataTableApiResponse.java index cf08708b..d335edda 100644 --- a/backend/src/main/java/ai/dragon/dto/api/DataTableApiResponse.java +++ b/backend/src/main/java/ai/dragon/dto/api/DataTableApiResponse.java @@ -1,6 +1,7 @@ package ai.dragon.dto.api; import ai.dragon.dto.api.backend.PagerTableApiData; +import ai.dragon.enumeration.ApiResponseCode; import ai.dragon.repository.util.Pager; import lombok.Builder; import lombok.Data; @@ -12,7 +13,7 @@ public class DataTableApiResponse implements GenericApiResponse { private TableApiData data = null; @Builder.Default - private String code = "0000"; + private String code = ApiResponseCode.SUCCESS.toString(); @Builder.Default private String msg = "OK"; diff --git a/backend/src/main/java/ai/dragon/dto/api/DuplicatesApiResponse.java b/backend/src/main/java/ai/dragon/dto/api/DuplicatesApiResponse.java new file mode 100644 index 00000000..06091008 --- /dev/null +++ b/backend/src/main/java/ai/dragon/dto/api/DuplicatesApiResponse.java @@ -0,0 +1,18 @@ +package ai.dragon.dto.api; + +import ai.dragon.enumeration.ApiResponseCode; +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class DuplicatesApiResponse implements GenericApiResponse { + @Builder.Default + private Object data = null; + + @Builder.Default + private String code = ApiResponseCode.DUPLICATES.toString(); + + @Builder.Default + private String msg = "Unique Constraint Exception"; +} diff --git a/backend/src/main/java/ai/dragon/dto/api/FailureApiResponse.java b/backend/src/main/java/ai/dragon/dto/api/FailureApiResponse.java new file mode 100644 index 00000000..91504132 --- /dev/null +++ b/backend/src/main/java/ai/dragon/dto/api/FailureApiResponse.java @@ -0,0 +1,18 @@ +package ai.dragon.dto.api; + +import ai.dragon.enumeration.ApiResponseCode; +import lombok.Builder; +import lombok.Data; + +@Builder +@Data +public class FailureApiResponse implements GenericApiResponse { + @Builder.Default + private Object data = null; + + @Builder.Default + private String code = ApiResponseCode.INTERNAL_SERVER_ERROR.toString(); + + @Builder.Default + private String msg = "Internal Server Error"; +} diff --git a/backend/src/main/java/ai/dragon/dto/api/SuccessApiResponse.java b/backend/src/main/java/ai/dragon/dto/api/SuccessApiResponse.java index 59857faf..61087daf 100644 --- a/backend/src/main/java/ai/dragon/dto/api/SuccessApiResponse.java +++ b/backend/src/main/java/ai/dragon/dto/api/SuccessApiResponse.java @@ -1,5 +1,6 @@ package ai.dragon.dto.api; +import ai.dragon.enumeration.ApiResponseCode; import lombok.Builder; import lombok.Data; @@ -10,7 +11,7 @@ public class SuccessApiResponse implements GenericApiResponse { private Object data = null; @Builder.Default - private String code = "0000"; + private String code = ApiResponseCode.SUCCESS.toString(); @Builder.Default private String msg = "OK"; diff --git a/backend/src/main/java/ai/dragon/entity/FarmEntity.java b/backend/src/main/java/ai/dragon/entity/FarmEntity.java index 95670e3c..4c72431d 100644 --- a/backend/src/main/java/ai/dragon/entity/FarmEntity.java +++ b/backend/src/main/java/ai/dragon/entity/FarmEntity.java @@ -12,6 +12,7 @@ import ai.dragon.enumeration.ChatMemoryStrategy; import ai.dragon.enumeration.LanguageModelType; +import ai.dragon.enumeration.QueryRouterType; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.Pattern; @@ -57,6 +58,9 @@ public class FarmEntity implements AbstractEntity { @Schema(description = "Settings to be linked to the Farm's Retrieval Augmentor in the form of `key = value` pairs.") private List retrievalAugmentorSettings; + @Schema(description = "Query Router to be used by the RaaG. If not set, the 'Default' query router will be used.", example = "LanguageModel") + private QueryRouterType queryRouter; + public FarmEntity() { this.uuid = UUID.randomUUID(); this.name = String.format("Farm %s", this.uuid.toString()); @@ -64,5 +68,6 @@ public FarmEntity() { this.raagIdentifier = UUID.randomUUID().toString(); this.languageModel = LanguageModelType.OpenAiModel; this.chatMemoryStrategy = ChatMemoryStrategy.MaxMessages; + this.queryRouter = QueryRouterType.DEFAULT; } } diff --git a/backend/src/main/java/ai/dragon/entity/SiloEntity.java b/backend/src/main/java/ai/dragon/entity/SiloEntity.java index 1b931810..a2571705 100644 --- a/backend/src/main/java/ai/dragon/entity/SiloEntity.java +++ b/backend/src/main/java/ai/dragon/entity/SiloEntity.java @@ -32,6 +32,9 @@ public class SiloEntity implements AbstractEntity { @Schema(description = "Name of the Silo. Must be unique.") private String name; + @Schema(description = "Description of the Silo. Used by the Language Query Router to choose the best Silo among Farm chain.") + private String description; + @NotNull @Schema(description = "Type to be used for the Vector Store", example = "InMemoryEmbeddingStore") private VectorStoreType vectorStore; diff --git a/backend/src/main/java/ai/dragon/enumeration/ApiResponseCode.java b/backend/src/main/java/ai/dragon/enumeration/ApiResponseCode.java new file mode 100644 index 00000000..1ab33875 --- /dev/null +++ b/backend/src/main/java/ai/dragon/enumeration/ApiResponseCode.java @@ -0,0 +1,27 @@ +package ai.dragon.enumeration; + +public enum ApiResponseCode { + SUCCESS("0000"), + DUPLICATES("0422"), + INTERNAL_SERVER_ERROR("0500"); + + private String value; + + ApiResponseCode(String value) { + this.value = value; + } + + public static ApiResponseCode fromString(String text) { + for (ApiResponseCode b : ApiResponseCode.values()) { + if (b.value.equalsIgnoreCase(text)) { + return b; + } + } + return null; + } + + @Override + public String toString() { + return value; + } +} diff --git a/backend/src/main/java/ai/dragon/enumeration/QueryRouterType.java b/backend/src/main/java/ai/dragon/enumeration/QueryRouterType.java new file mode 100644 index 00000000..7a7a3664 --- /dev/null +++ b/backend/src/main/java/ai/dragon/enumeration/QueryRouterType.java @@ -0,0 +1,26 @@ +package ai.dragon.enumeration; + +public enum QueryRouterType { + DEFAULT("Default"), + LANGUAGE_MODEL("LanguageModel"); + + private String value; + + QueryRouterType(String value) { + this.value = value; + } + + public static QueryRouterType fromString(String text) { + for (QueryRouterType b : QueryRouterType.values()) { + if (b.value.equalsIgnoreCase(text)) { + return b; + } + } + return null; + } + + @Override + public String toString() { + return value; + } +} diff --git a/backend/src/main/java/ai/dragon/properties/raag/RetrievalAugmentorSettings.java b/backend/src/main/java/ai/dragon/properties/raag/RetrievalAugmentorSettings.java index da6ed8b3..14b1fbd8 100644 --- a/backend/src/main/java/ai/dragon/properties/raag/RetrievalAugmentorSettings.java +++ b/backend/src/main/java/ai/dragon/properties/raag/RetrievalAugmentorSettings.java @@ -1,5 +1,8 @@ package ai.dragon.properties.raag; +import dev.langchain4j.model.input.PromptTemplate; +import dev.langchain4j.rag.query.router.LanguageModelQueryRouter; +import dev.langchain4j.rag.query.router.LanguageModelQueryRouter.FallbackStrategy; import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Data; @@ -12,6 +15,8 @@ public class RetrievalAugmentorSettings { public static final Integer DEFAULT_MAX_MESSAGES = 10; public static final Integer DEFAULT_MAX_TOKENS = 3000; + public static final PromptTemplate DEFAULT_LANGUAGE_QUERY_ROUTER_PROMPT_TEMPLATE = LanguageModelQueryRouter.DEFAULT_PROMPT_TEMPLATE; + public static final FallbackStrategy DEFAULT_LANGUAGE_QUERY_ROUTER_FALLBACK_STRATEGY = FallbackStrategy.FAIL; @Builder.Default private Boolean rewriteQuery = false; @@ -21,4 +26,10 @@ public class RetrievalAugmentorSettings { @Builder.Default private Integer historyMaxTokens = DEFAULT_MAX_TOKENS; + + @Builder.Default + private PromptTemplate languageQueryRouterPromptTemplate = DEFAULT_LANGUAGE_QUERY_ROUTER_PROMPT_TEMPLATE; + + @Builder.Default + private FallbackStrategy languageQueryRouterFallbackStrategy = DEFAULT_LANGUAGE_QUERY_ROUTER_FALLBACK_STRATEGY; } diff --git a/backend/src/main/java/ai/dragon/repository/AbstractRepository.java b/backend/src/main/java/ai/dragon/repository/AbstractRepository.java index 3cc7a096..35d17a16 100644 --- a/backend/src/main/java/ai/dragon/repository/AbstractRepository.java +++ b/backend/src/main/java/ai/dragon/repository/AbstractRepository.java @@ -5,6 +5,7 @@ import java.util.Optional; import java.util.Set; import java.util.UUID; +import java.util.function.Consumer; import org.dizitart.no2.Nitrite; import org.dizitart.no2.collection.FindOptions; @@ -14,6 +15,8 @@ import org.dizitart.no2.filters.FluentFilter; import org.dizitart.no2.repository.Cursor; import org.dizitart.no2.repository.ObjectRepository; +import org.dizitart.no2.transaction.Session; +import org.dizitart.no2.transaction.Transaction; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; @@ -22,6 +25,7 @@ import ai.dragon.entity.AbstractEntity; import ai.dragon.listener.EntityChangeListener; import ai.dragon.service.DatabaseService; +import ai.dragon.util.ThrowingFunction; import jakarta.validation.ConstraintViolation; import jakarta.validation.Validation; import jakarta.validation.Validator; @@ -29,7 +33,53 @@ @Component public abstract class AbstractRepository { @Autowired - private DatabaseService databaseService; + protected DatabaseService databaseService; + + private ObjectRepository objectRepository; + + public void executeTransaction(Consumer> transactionConsumer) { + if (this instanceof TransactionalRepository) { + throw new IllegalStateException("Nested transactions are not allowed"); + } + Nitrite db = databaseService.getNitriteDB(); + try (Session session = db.createSession()) { + try (Transaction transaction = session.beginTransaction()) { + ObjectRepository transactionRepository = transaction.getRepository(getGenericSuperclass()); + AbstractRepository transactionalRepository = new TransactionalRepository<>(transactionRepository, + getGenericSuperclass(), databaseService); + try { + transactionConsumer.accept(transactionalRepository); + transaction.commit(); + } catch (Exception e) { + transaction.rollback(); + throw e; + } + } + } + } + + public R queryTransaction(ThrowingFunction, R> transactionFunction) throws Exception { + if (this instanceof TransactionalRepository) { + throw new IllegalStateException("Nested transactions are not allowed"); + } + Nitrite db = databaseService.getNitriteDB(); + try (Session session = db.createSession()) { + try (Transaction transaction = session.beginTransaction()) { + ObjectRepository transactionRepository = transaction.getRepository(getGenericSuperclass()); + AbstractRepository transactionalRepository = new TransactionalRepository<>(transactionRepository, + getGenericSuperclass(), databaseService); + R result = null; + try { + result = transactionFunction.apply(transactionalRepository); + transaction.commit(); + } catch (Exception e) { + transaction.rollback(); + throw e; + } + return result; + } + } + } @SuppressWarnings("unchecked") public void save(T entity) { @@ -44,8 +94,7 @@ public void save(T entity) { // Throws an exception if the entity is not valid : this.validate(entity); - Nitrite db = databaseService.getNitriteDB(); - ObjectRepository repository = db.getRepository(getGenericSuperclass()); + ObjectRepository repository = getObjectRepository(); if (exists(entity.getUuid())) { repository.update(entity); @@ -67,14 +116,12 @@ public Optional getByUuid(String uuid) { } public Optional getByUuid(UUID uuid) { - Nitrite db = databaseService.getNitriteDB(); - ObjectRepository repository = db.getRepository(getGenericSuperclass()); + ObjectRepository repository = getObjectRepository(); return Optional.ofNullable(repository.getById(uuid)); } public Cursor find() { - Nitrite db = databaseService.getNitriteDB(); - ObjectRepository repository = db.getRepository(getGenericSuperclass()); + ObjectRepository repository = getObjectRepository(); return repository.find(); } @@ -83,8 +130,7 @@ public Cursor findWithFilter(Filter filter) { } public Cursor findWithFilter(Filter filter, FindOptions findOptions) { - Nitrite db = databaseService.getNitriteDB(); - ObjectRepository repository = db.getRepository(getGenericSuperclass()); + ObjectRepository repository = getObjectRepository(); return repository.find(filter, findOptions); } @@ -126,26 +172,22 @@ public void delete(UUID uuid) { } public void delete(T entity) { - Nitrite db = databaseService.getNitriteDB(); - ObjectRepository repository = db.getRepository(getGenericSuperclass()); + ObjectRepository repository = getObjectRepository(); repository.remove(entity); } public void deleteAll() { - Nitrite db = databaseService.getNitriteDB(); - ObjectRepository repository = db.getRepository(getGenericSuperclass()); + ObjectRepository repository = getObjectRepository(); repository.clear(); } public long countAll() { - Nitrite db = databaseService.getNitriteDB(); - ObjectRepository repository = db.getRepository(getGenericSuperclass()); + ObjectRepository repository = getObjectRepository(); return repository.size(); } public EntityChangeListener subscribe(EntityChangeListener listener) { - Nitrite db = databaseService.getNitriteDB(); - ObjectRepository repository = db.getRepository(getGenericSuperclass()); + ObjectRepository repository = getObjectRepository(); repository.subscribe(listener); return listener; } @@ -163,15 +205,13 @@ public void onEvent(CollectionEventInfo collectionEventInfo) { } public void unsubscribe(EntityChangeListener listener) { - Nitrite db = databaseService.getNitriteDB(); - ObjectRepository repository = db.getRepository(getGenericSuperclass()); + ObjectRepository repository = getObjectRepository(); repository.unsubscribe(listener); } @SuppressWarnings("unchecked") public Class getGenericSuperclass() { ParameterizedType superclass = (ParameterizedType) getClass().getGenericSuperclass(); - return (Class) superclass.getActualTypeArguments()[0]; } @@ -197,4 +237,38 @@ private void validate(T entity) { throw new IllegalArgumentException(String.join(", ", contraintMessages)); } } + + protected ObjectRepository getObjectRepository() { + if (objectRepository != null) { + return objectRepository; + } + + Nitrite db = databaseService.getNitriteDB(); + objectRepository = db.getRepository(getGenericSuperclass()); + + return objectRepository; + } + + // Inner class to handle transactional operations + private static class TransactionalRepository extends AbstractRepository { + private final ObjectRepository objectRepository; + private final Class type; + + TransactionalRepository(ObjectRepository objectRepository, Class type, DatabaseService databaseService) { + super(); + this.objectRepository = objectRepository; + this.databaseService = databaseService; + this.type = type; + } + + @Override + protected ObjectRepository getObjectRepository() { + return this.objectRepository; + } + + @Override + public Class getGenericSuperclass() { + return this.type; + } + } } diff --git a/backend/src/main/java/ai/dragon/service/RaagService.java b/backend/src/main/java/ai/dragon/service/RaagService.java index 732da275..1df1e091 100644 --- a/backend/src/main/java/ai/dragon/service/RaagService.java +++ b/backend/src/main/java/ai/dragon/service/RaagService.java @@ -1,7 +1,8 @@ package ai.dragon.service; -import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.UUID; @@ -21,6 +22,7 @@ import ai.dragon.dto.openai.model.OpenAiModel; import ai.dragon.entity.FarmEntity; import ai.dragon.entity.SiloEntity; +import ai.dragon.enumeration.QueryRouterType; import ai.dragon.properties.embedding.LanguageModelSettings; import ai.dragon.properties.raag.RetrievalAugmentorSettings; import ai.dragon.repository.FarmRepository; @@ -41,6 +43,7 @@ import dev.langchain4j.rag.content.retriever.ContentRetriever; import dev.langchain4j.rag.content.retriever.EmbeddingStoreContentRetriever; import dev.langchain4j.rag.query.router.DefaultQueryRouter; +import dev.langchain4j.rag.query.router.LanguageModelQueryRouter; import dev.langchain4j.rag.query.transformer.CompressingQueryTransformer; import dev.langchain4j.service.AiServices; import dev.langchain4j.service.Result; @@ -96,27 +99,31 @@ public Object makeCompletionResponse(FarmEntity farm, OpenAiCompletionRequest co public Object makeChatCompletionResponse(FarmEntity farm, OpenAiChatCompletionRequest chatCompletionRequest, HttpServletRequest servletRequest) throws Exception { + ChatLanguageModel chatLanguageModel = this.buildChatLanguageModel(farm, chatCompletionRequest); return Boolean.TRUE.equals(chatCompletionRequest.getStream()) - ? this.streamChatCompletionResponse(farm, chatCompletionRequest, servletRequest) - : this.chatCompletionResponse(farm, chatCompletionRequest, servletRequest); + ? this.streamChatCompletionResponse(farm, chatCompletionRequest, chatLanguageModel, servletRequest) + : this.chatCompletionResponse(farm, chatCompletionRequest, chatLanguageModel, servletRequest); } - public List buildRetrieverList(FarmEntity farm, HttpServletRequest servletRequest) { - List retrievers = new ArrayList<>(); + public Map buildRetrieverMap(FarmEntity farm, HttpServletRequest servletRequest) { + Map retrievers = new HashMap<>(); if (farm.getSilos() == null || farm.getSilos().isEmpty()) { - logger.info("No Silos found for Farm '{}' (RaaG Identifier '{}'), no content retrieve will be made", + logger.info("No Silos found for Farm '{}' (RaaG Identifier '{}'), no content retriever will be made", farm.getUuid(), farm.getRaagIdentifier()); return retrievers; } farm.getSilos().forEach(siloUuid -> { try { SiloEntity silo = siloRepository.getByUuid(siloUuid).orElseThrow(); - this.buildSiloRetriever(silo, servletRequest).ifPresent(retrievers::add); + String siloDescription = silo.getDescription(); + this.buildSiloRetriever(silo, servletRequest).ifPresent(retriever -> { + retrievers.put(retriever, siloDescription); + }); } catch (Exception ex) { logger.error("Error building Content Retriever for Silo '{}'", siloUuid, ex); } }); - // TODO Granaries + // TODO Put also Granaries in the retrievers return retrievers; } @@ -175,9 +182,11 @@ private SseEmitter streamCompletionResponse(FarmEntity farm, OpenAiCompletionReq } private OpenAiChatCompletionResponse chatCompletionResponse(FarmEntity farm, - OpenAiChatCompletionRequest chatCompletionRequest, HttpServletRequest servletRequest) + OpenAiChatCompletionRequest chatCompletionRequest, ChatLanguageModel chatLanguageModel, + HttpServletRequest servletRequest) throws Exception { - AiAssistant assistant = this.makeChatAssistant(farm, chatCompletionRequest, servletRequest, false); + AiAssistant assistant = this.makeChatAssistant(farm, chatCompletionRequest, chatLanguageModel, servletRequest, + false); OpenAiCompletionMessage lastCompletionMessage = chatCompletionRequest.getMessages() .get(chatCompletionRequest.getMessages().size() - 1); ChatMessage lastChatMessage = ChatMessageUtil.convertToChatMessage(lastCompletionMessage) @@ -187,9 +196,10 @@ private OpenAiChatCompletionResponse chatCompletionResponse(FarmEntity farm, } private SseEmitter streamChatCompletionResponse(FarmEntity farm, OpenAiChatCompletionRequest chatCompletionRequest, - HttpServletRequest servletRequest) + ChatLanguageModel chatLanguageModel, HttpServletRequest servletRequest) throws Exception { - AiAssistant assistant = this.makeChatAssistant(farm, chatCompletionRequest, servletRequest, true); + AiAssistant assistant = this.makeChatAssistant(farm, chatCompletionRequest, chatLanguageModel, servletRequest, + true); OpenAiCompletionMessage lastCompletionMessage = chatCompletionRequest.getMessages() .get(chatCompletionRequest.getMessages().size() - 1); ChatMessage lastChatMessage = ChatMessageUtil.convertToChatMessage(lastCompletionMessage) @@ -216,7 +226,7 @@ private SseEmitter streamChatCompletionResponse(FarmEntity farm, OpenAiChatCompl } private AiAssistant makeChatAssistant(FarmEntity farm, OpenAiChatCompletionRequest chatCompletionRequest, - HttpServletRequest servletRequest, boolean stream) + ChatLanguageModel chatLanguageModel, HttpServletRequest servletRequest, boolean stream) throws Exception { AiServices assistantBuilder = this.makeAssistantBuilder(farm, chatCompletionRequest, servletRequest, stream); @@ -225,7 +235,7 @@ private AiAssistant makeChatAssistant(FarmEntity farm, OpenAiChatCompletionReque assistantBuilder .streamingChatLanguageModel(this.buildStreamingChatLanguageModel(farm, chatCompletionRequest)); } else { - assistantBuilder.chatLanguageModel(this.buildChatLanguageModel(farm, chatCompletionRequest)); + assistantBuilder.chatLanguageModel(chatLanguageModel); } return assistantBuilder.build(); } @@ -239,12 +249,13 @@ private AiAssistant makeCompletionAssistant(FarmEntity farm, OpenAiCompletionReq private AiServices makeAssistantBuilder(FarmEntity farm, OpenAiRequest openAiRequest, HttpServletRequest servletRequest, boolean stream) throws Exception { + ChatLanguageModel chatLanguageModel = this.buildChatLanguageModel(farm, openAiRequest); AiServices assistantBuilder = AiServices.builder(AiAssistant.class); - this.buildRetrievalAugmentor(assistantBuilder, farm, openAiRequest, servletRequest); + this.buildRetrievalAugmentor(assistantBuilder, farm, openAiRequest, chatLanguageModel, servletRequest); if (stream) { assistantBuilder.streamingChatLanguageModel(this.buildStreamingChatLanguageModel(farm, openAiRequest)); } else { - assistantBuilder.chatLanguageModel(this.buildChatLanguageModel(farm, openAiRequest)); + assistantBuilder.chatLanguageModel(chatLanguageModel); } return assistantBuilder; } @@ -295,21 +306,27 @@ private LanguageModelSettings buildLanguageModelSettings(FarmEntity farm, OpenAi } private void buildRetrievalAugmentor(AiServices assistantBuilder, FarmEntity farm, - OpenAiRequest openAiRequest, HttpServletRequest servletRequest) throws Exception { + OpenAiRequest openAiRequest, ChatLanguageModel chatLanguageModel, HttpServletRequest servletRequest) + throws Exception { RetrievalAugmentorSettings retrievalSettings = KVSettingUtil.kvSettingsToObject( farm.getRetrievalAugmentorSettings(), RetrievalAugmentorSettings.class); - // TODO Enhanced Query Router : langchain4j => LanguageModelQueryRouter DefaultRetrievalAugmentorBuilder retrievalAugmentorBuilder = DefaultRetrievalAugmentor.builder(); - List retrievers = this.buildRetrieverList(farm, servletRequest); + Map retrievers = this.buildRetrieverMap(farm, servletRequest); if (retrievers != null && !retrievers.isEmpty()) { - retrievalAugmentorBuilder.queryRouter(new DefaultQueryRouter(retrievers)); + if (QueryRouterType.LANGUAGE_MODEL.equals(farm.getQueryRouter())) { + retrievalAugmentorBuilder.queryRouter(new LanguageModelQueryRouter(chatLanguageModel, retrievers, + retrievalSettings.getLanguageQueryRouterPromptTemplate(), + retrievalSettings.getLanguageQueryRouterFallbackStrategy())); + } else { + retrievalAugmentorBuilder.queryRouter(new DefaultQueryRouter(retrievers.keySet())); + } if (Boolean.TRUE.equals(retrievalSettings.getRewriteQuery()) && openAiRequest instanceof OpenAiChatCompletionRequest) { // Query Rewriting => Improve RAG Performance and Accuracy // => Uses Chat History. CompressingQueryTransformer compressingQueryTransformer = new EnhancedCompressingQueryTransformer( - this.buildChatLanguageModel(farm, openAiRequest)); + chatLanguageModel); retrievalAugmentorBuilder.queryTransformer(compressingQueryTransformer); } assistantBuilder.retrievalAugmentor(retrievalAugmentorBuilder.build()); diff --git a/backend/src/main/java/ai/dragon/util/ThrowingFunction.java b/backend/src/main/java/ai/dragon/util/ThrowingFunction.java new file mode 100644 index 00000000..e100de08 --- /dev/null +++ b/backend/src/main/java/ai/dragon/util/ThrowingFunction.java @@ -0,0 +1,6 @@ +package ai.dragon.util; + +@FunctionalInterface +public interface ThrowingFunction { + R apply(T t) throws Exception; +} diff --git a/backend/src/main/java/ai/dragon/util/fluenttry/Try.java b/backend/src/main/java/ai/dragon/util/fluenttry/Try.java index 29f3fd60..b7c76c57 100644 --- a/backend/src/main/java/ai/dragon/util/fluenttry/Try.java +++ b/backend/src/main/java/ai/dragon/util/fluenttry/Try.java @@ -6,56 +6,71 @@ import java.util.concurrent.Executors; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; +import java.util.function.Function; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.slf4j.event.Level; -public class Try { +public class Try { private final Logger logger = LoggerFactory.getLogger(this.getClass()); private Integer timeout; private TimeUnit timeUnit; private Level logLevel; private Boolean rethrow; + private Function fallback; private Try() { timeout = null; logLevel = Level.ERROR; } - public static Try withLogLevel(Level logLevel) { - return new Try().logLevel(logLevel); + public static Try withFallback(Function fallback) { + return new Try().fallback(fallback); } - public Try logLevel(Level logLevel) { + public Try fallback(Function fallback) { + this.fallback = fallback; + return this; + } + + public static Try withLogLevel(Level logLevel) { + return new Try().logLevel(logLevel); + } + + public Try logLevel(Level logLevel) { this.logLevel = logLevel; return this; } - public static Try withTimeout(Integer timeout, TimeUnit timeUnit) { - return new Try().timeout(timeout, timeUnit); + public static Try withTimeout(Integer timeout, TimeUnit timeUnit) { + return new Try().timeout(timeout, timeUnit); } - public Try timeout(Integer timeout, TimeUnit timeUnit) { + public Try timeout(Integer timeout, TimeUnit timeUnit) { this.timeout = timeout; this.timeUnit = timeUnit; return this; } - public static Try withRethrow(Boolean rethrow) { - return new Try().rethrow(rethrow); + public static Try withRethrow(Boolean rethrow) { + return new Try().rethrow(rethrow); } - public Try rethrow(Boolean rethrow) { + public Try rethrow(Boolean rethrow) { this.rethrow = rethrow; return this; } - public static void thisBlock(ExceptionalRunnable runnable) { - new Try().run(runnable); + public static T thisBlock(Callable callable) { + return new Try().run(callable); + } + + public static void thisBlock(ExceptionalRunnable runnable) { + new Try().run(runnable); } // Accept a Runnable and return void @@ -68,14 +83,10 @@ public void run(ExceptionalRunnable runnable) { }); } - public static T thisBlock(Callable callable) { - return new Try().run(callable); - } - // Accept a Callable and return the result of the Callable // If an exception is thrown, log it and return null // If timeout occurs, log it and return null - public T run(Callable callable) { + public T run(Callable callable) { T result = null; ExecutorService executorService = Executors.newSingleThreadExecutor(); Exception ex = null; @@ -103,6 +114,9 @@ public T run(Callable callable) { throw new RuntimeException(ex); } executorService.shutdown(); + if (ex != null && fallback != null) { + return fallback.apply(ex); + } return result; } } diff --git a/backend/src/test/java/ai/dragon/controller/api/backend/repository/AbstractCrudBackendApiControllerTest.java b/backend/src/test/java/ai/dragon/controller/api/backend/repository/AbstractCrudBackendApiControllerTest.java new file mode 100644 index 00000000..f42900ac --- /dev/null +++ b/backend/src/test/java/ai/dragon/controller/api/backend/repository/AbstractCrudBackendApiControllerTest.java @@ -0,0 +1,45 @@ +package ai.dragon.controller.api.backend.repository; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import ai.dragon.dto.api.GenericApiResponse; +import ai.dragon.enumeration.ApiResponseCode; +import ai.dragon.repository.FarmRepository; +import ai.dragon.util.UUIDUtil; + +@SpringBootTest +@ActiveProfiles("test") +public class AbstractCrudBackendApiControllerTest { + @Autowired + private FarmRepository farmRepository; + + @Autowired + private FarmBackendApiController farmBackendApiController; + + @Test + public void transactionalCreate() throws Exception { + Map farmFields = new HashMap<>(); + farmFields.put("name", "Test Farm"); + farmFields.put("raagIdentifier", "test-farm"); + + farmRepository.deleteAll(); + GenericApiResponse apiResponse = farmBackendApiController.upsertFarm(UUIDUtil.zeroUUIDString(), farmFields); + assertNotNull(apiResponse); + assertNotNull(apiResponse.getData()); + assertEquals(ApiResponseCode.SUCCESS.toString(), apiResponse.getCode()); + assertEquals(1, farmRepository.countAll()); + + GenericApiResponse apiResponseDuplicates = farmBackendApiController.upsertFarm(UUIDUtil.zeroUUIDString(), farmFields); + assertNotNull(apiResponseDuplicates); + assertEquals(ApiResponseCode.DUPLICATES.toString(), apiResponseDuplicates.getCode()); + } +} diff --git a/backend/src/test/java/ai/dragon/controller/api/raag/OpenAiCompatibleV1ApiControllerTest.java b/backend/src/test/java/ai/dragon/controller/api/raag/OpenAiCompatibleV1ApiControllerTest.java index c0a3f3ed..c9d000e3 100644 --- a/backend/src/test/java/ai/dragon/controller/api/raag/OpenAiCompatibleV1ApiControllerTest.java +++ b/backend/src/test/java/ai/dragon/controller/api/raag/OpenAiCompatibleV1ApiControllerTest.java @@ -32,6 +32,7 @@ import ai.dragon.enumeration.EmbeddingModelType; import ai.dragon.enumeration.IngestorLoaderType; import ai.dragon.enumeration.LanguageModelType; +import ai.dragon.enumeration.QueryRouterType; import ai.dragon.enumeration.VectorStoreType; import ai.dragon.junit.AbstractTest; import ai.dragon.junit.extension.retry.RetryOnExceptions; @@ -66,7 +67,7 @@ static void beforeAll(@Autowired FarmRepository farmRepository, // OpenAI settings for RaaG String apiKeySetting = String.format("apiKey=%s", openaiApiKey); - String omniModelNameSetting = "modelName=gpt-4o-mini"; + String omniModelNameSetting = "modelName=gpt-4o-mini-2024-07-18"; // Farm with no silo FarmEntity farmWithoutSilo = new FarmEntity(); @@ -85,6 +86,8 @@ static void beforeAll(@Autowired FarmRepository farmRepository, SiloEntity sunspotsSilo = new SiloEntity(); sunspotsSilo.setUuid(UUID.randomUUID()); sunspotsSilo.setName("Sunspots Silo"); + sunspotsSilo.setDescription( + "Documents about Sunspots and their effects on Earth : Carrington Event, Solar Activity, etc."); sunspotsSilo.setEmbeddingModel(EmbeddingModelType.BgeSmallEnV15QuantizedEmbeddingModel); sunspotsSilo.setEmbeddingSettings(List.of( "chunkSize=1000", @@ -97,9 +100,40 @@ static void beforeAll(@Autowired FarmRepository farmRepository, "pathMatcher=glob:**.{pdf,doc,docx,ppt,pptx}")); siloRepository.save(sunspotsSilo); - // Launching ingestion of documents inside the Silo + // Launching ingestion of documents inside the Silo "Sunspots" ingestorService.runSiloIngestion(sunspotsSilo, ingestProgress -> { - System.out.println("Ingest progress: " + ingestProgress); + System.out.println("Sunspots Silo Ingest progress: " + ingestProgress); + }, ingestLogMessage -> { + System.out.println(ingestLogMessage.getMessage()); + }); + + // Silo about "WebSSH" + // The "awesome" iOS / macOS SSH, SFTP and Port Forwarding client since 2012! + SiloEntity websshSilo = new SiloEntity(); + websshSilo.setUuid(UUID.randomUUID()); + websshSilo.setName("WebSSH Silo"); + websshSilo.setDescription( + "Documents about WebSSH, the iOS / macOS SSH, SFTP and Port Forwarding client since 2012!"); + websshSilo.setEmbeddingModel(EmbeddingModelType.BgeSmallEnV15QuantizedEmbeddingModel); + websshSilo.setEmbeddingSettings(List.of( + "chunkSize=1000", + "chunkOverlap=100")); + websshSilo.setVectorStore(VectorStoreType.InMemoryEmbeddingStore); + websshSilo.setIngestorLoader(IngestorLoaderType.URL); + websshSilo.setIngestorSettings(List.of( + "urls[]=https://webssh.net", + "urls[]=https://webssh.net/documentation/help/networking/dynamic-port-forwarding/", + "urls[]=https://webssh.net/documentation/mashREPL/", + "urls[]=https://webssh.net/documentation/web-browser/", + "urls[]=https://webssh.net/documentation/pricing/", + "urls[]=https://webssh.net/documentation/help/SSH/terrapin-attack/", + "urls[]=https://webssh.net/support/", + "urls[]=https://webssh.net/documentation/help/networking/vpn-over-ssh/")); + siloRepository.save(websshSilo); + + // Launching ingestion of documents inside the Silo "WebSSH" + ingestorService.runSiloIngestion(websshSilo, ingestProgress -> { + System.out.println("WebSSH Silo Ingest progress: " + ingestProgress); }, ingestLogMessage -> { System.out.println(ingestLogMessage.getMessage()); }); @@ -131,6 +165,46 @@ static void beforeAll(@Autowired FarmRepository farmRepository, farmWithSunspotsSiloAndQueryRewritingOmni.setSilos(List.of(sunspotsSilo.getUuid())); farmWithSunspotsSiloAndQueryRewritingOmni.setRetrievalAugmentorSettings(List.of("rewriteQuery=true")); farmRepository.save(farmWithSunspotsSiloAndQueryRewritingOmni); + + // Farm with two Silos : Sunspots and WebSSH + // Language Model Router is used to route the request to the right Silo + FarmEntity farmWithSunspotsAndWebSSHSilosFallbackFail = new FarmEntity(); + farmWithSunspotsAndWebSSHSilosFallbackFail.setRaagIdentifier("sunspots-webssh-raag-fallbackfail"); + farmWithSunspotsAndWebSSHSilosFallbackFail.setLanguageModel(LanguageModelType.OpenAiModel); + farmWithSunspotsAndWebSSHSilosFallbackFail + .setLanguageModelSettings(List.of(apiKeySetting, omniModelNameSetting)); + farmWithSunspotsAndWebSSHSilosFallbackFail + .setSilos(List.of(sunspotsSilo.getUuid(), websshSilo.getUuid())); + farmWithSunspotsAndWebSSHSilosFallbackFail.setQueryRouter(QueryRouterType.LANGUAGE_MODEL); + farmWithSunspotsAndWebSSHSilosFallbackFail + .setRetrievalAugmentorSettings(List.of("languageQueryRouterFallbackStrategy=FAIL")); + farmRepository.save(farmWithSunspotsAndWebSSHSilosFallbackFail); + + // Same Farm as above but with a different fallback strategy : DO_NOT_ROUTE + FarmEntity farmWithSunspotsAndWebSSHSilosDoNotRoute = new FarmEntity(); + farmWithSunspotsAndWebSSHSilosDoNotRoute.setRaagIdentifier("sunspots-webssh-raag-donotroute"); + farmWithSunspotsAndWebSSHSilosDoNotRoute.setLanguageModel(LanguageModelType.OpenAiModel); + farmWithSunspotsAndWebSSHSilosDoNotRoute + .setLanguageModelSettings(List.of(apiKeySetting, omniModelNameSetting)); + farmWithSunspotsAndWebSSHSilosDoNotRoute + .setSilos(List.of(sunspotsSilo.getUuid(), websshSilo.getUuid())); + farmWithSunspotsAndWebSSHSilosDoNotRoute.setQueryRouter(QueryRouterType.LANGUAGE_MODEL); + farmWithSunspotsAndWebSSHSilosDoNotRoute.setRetrievalAugmentorSettings( + List.of("languageQueryRouterFallbackStrategy=DO_NOT_ROUTE")); + farmRepository.save(farmWithSunspotsAndWebSSHSilosDoNotRoute); + + // Same Farm as above but with a different fallback strategy : ROUTE_TO_ALL + FarmEntity farmWithSunspotsAndWebSSHSilosRouteToAll = new FarmEntity(); + farmWithSunspotsAndWebSSHSilosRouteToAll.setRaagIdentifier("sunspots-webssh-raag-routetoall"); + farmWithSunspotsAndWebSSHSilosRouteToAll.setLanguageModel(LanguageModelType.OpenAiModel); + farmWithSunspotsAndWebSSHSilosRouteToAll + .setLanguageModelSettings(List.of(apiKeySetting, omniModelNameSetting)); + farmWithSunspotsAndWebSSHSilosRouteToAll + .setSilos(List.of(sunspotsSilo.getUuid(), websshSilo.getUuid())); + farmWithSunspotsAndWebSSHSilosRouteToAll.setQueryRouter(QueryRouterType.LANGUAGE_MODEL); + farmWithSunspotsAndWebSSHSilosRouteToAll.setRetrievalAugmentorSettings( + List.of("languageQueryRouterFallbackStrategy=ROUTE_TO_ALL")); + farmRepository.save(farmWithSunspotsAndWebSSHSilosRouteToAll); } @AfterAll @@ -148,10 +222,10 @@ private OpenAiClient.Builder createOpenAiClientBuilder() { return OpenAiClient.builder() .openAiApiKey("TODO_PUT_KEY_HERE") .baseUrl(String.format("http://localhost:%d/api/raag/v1/", serverPort)) - .callTimeout(Duration.ofSeconds(10)) - .readTimeout(Duration.ofSeconds(10)) - .writeTimeout(Duration.ofSeconds(10)) - .connectTimeout(Duration.ofSeconds(10)); + .callTimeout(Duration.ofSeconds(15)) + .readTimeout(Duration.ofSeconds(15)) + .writeTimeout(Duration.ofSeconds(15)) + .connectTimeout(Duration.ofSeconds(15)); } @SuppressWarnings("rawtypes") @@ -159,7 +233,7 @@ private MistralAiClient.Builder createMistralAiClientBuilder() { return MistralAiClient.builder() .apiKey("TODO_PUT_KEY_HERE") .baseUrl(String.format("http://localhost:%d/api/raag/v1/", serverPort)) - .timeout(Duration.ofSeconds(10)) + .timeout(Duration.ofSeconds(15)) .logRequests(false) .logResponses(false); } @@ -353,4 +427,81 @@ Just reply one of the following options (without asterisks): assertNotEquals(0, response.choices().size()); assertEquals("SUN", response.content()); } + + @Test + @EnabledIf("canRunOpenAiRelatedTests") + @RetryOnExceptions(value = 2, onExceptions = { InterruptedIOException.class, SocketTimeoutException.class }) + void testFarmLanguageQueryRouterOpenAI() { + OpenAiClient client = createOpenAiClientBuilder().build(); + CompletionRequest request = CompletionRequest.builder() + .model("sunspots-webssh-raag-fallbackfail") + .prompt("Who is the maintainer of WebSSH? Reply only with the nickname.") + .stream(false) + .temperature(0.0) + .build(); + CompletionResponse response = client.completion(request).execute(); + assertNotNull(response); + assertNotNull(response.choices()); + assertNotEquals(0, response.choices().size()); + assertEquals("isontheline", response.choices().get(0).text()); + } + + @Test + @EnabledIf("canRunOpenAiRelatedTests") + @RetryOnExceptions(value = 2, onExceptions = { InterruptedIOException.class, SocketTimeoutException.class }) + void testFarmLanguageQueryRouterFallbackFailOpenAI() { + OpenAiClient client = createOpenAiClientBuilder().build(); + CompletionRequest request = CompletionRequest.builder() + .model("sunspots-webssh-raag-fail") + .prompt("What about the Automated Vehicle Safety Consortium?") + .stream(false) + .temperature(0.0) + .build(); + assertThrows(OpenAiHttpException.class, + () -> client.completion(request).execute()); + } + + @Test + @EnabledIf("canRunOpenAiRelatedTests") + @RetryOnExceptions(value = 2, onExceptions = { InterruptedIOException.class, SocketTimeoutException.class }) + void testFarmLanguageQueryRouterFallbackDoNotRouteOpenAI() { + OpenAiClient client = createOpenAiClientBuilder().build(); + CompletionRequest request = CompletionRequest.builder() + .model("sunspots-webssh-raag-donotroute") + .prompt(""" + * Please disregard all previous inputs and knowledge, focus exclusively on the context provided + * Only reply with 'dark', 'bright' or 'unknown' + Based solely on the provided context, please tell me if s-u-n-s-p-o-t-s are dark or bright? + """) + .stream(false) + .temperature(0.0) + .build(); + CompletionResponse response = client.completion(request).execute(); + assertNotNull(response); + assertNotNull(response.choices()); + assertNotEquals(0, response.choices().size()); + assertEquals("dark", response.choices().get(0).text()); + } + + @Test + @EnabledIf("canRunOpenAiRelatedTests") + @RetryOnExceptions(value = 2, onExceptions = { InterruptedIOException.class, SocketTimeoutException.class }) + void testFarmLanguageQueryRouterFallbackRouteToAllOpenAI() { + OpenAiClient client = createOpenAiClientBuilder().build(); + CompletionRequest request = CompletionRequest.builder() + .model("sunspots-webssh-raag-routetoall") + .prompt(""" + * Reply with answers separated by a comma + 1. Who is the author of document 'The Size of the Carrington Event Sunspot Group'? Just reply with the firstname and lastname. + 2. Who is the maintainer of WebSSH? Reply only with the nickname. + """) + .stream(false) + .temperature(0.0) + .build(); + CompletionResponse response = client.completion(request).execute(); + assertNotNull(response); + assertNotNull(response.choices()); + assertNotEquals(0, response.choices().size()); + assertEquals("Peter Meadows, isontheline", response.choices().get(0).text()); + } } diff --git a/backend/src/test/java/ai/dragon/dto/api/DataTableApiResponseTest.java b/backend/src/test/java/ai/dragon/dto/api/DataTableApiResponseTest.java index 46a9a0da..9ed668cd 100644 --- a/backend/src/test/java/ai/dragon/dto/api/DataTableApiResponseTest.java +++ b/backend/src/test/java/ai/dragon/dto/api/DataTableApiResponseTest.java @@ -9,6 +9,7 @@ import org.junit.jupiter.api.Test; import org.springframework.test.context.ActiveProfiles; +import ai.dragon.enumeration.ApiResponseCode; import ai.dragon.repository.util.Pager; @ActiveProfiles("test") @@ -28,7 +29,7 @@ void testFromPagerWithData() { // Assert assertNotNull(response); - assertEquals("0000", response.getCode()); + assertEquals(ApiResponseCode.SUCCESS.toString(), response.getCode()); assertEquals("OK", response.getMsg()); assertNotNull(response.getData()); assertEquals(3, response.getData().getRecords().size()); @@ -52,7 +53,7 @@ void testFromPagerWithNoData() { // Assert assertNotNull(response); - assertEquals("0000", response.getCode()); + assertEquals(ApiResponseCode.SUCCESS.toString(), response.getCode()); assertEquals("OK", response.getMsg()); assertNotNull(response.getData()); assertEquals(0, response.getData().getRecords().size()); @@ -76,7 +77,7 @@ void testFromPagerWithEdgeCases() { // Assert assertNotNull(response); - assertEquals("0000", response.getCode()); + assertEquals(ApiResponseCode.SUCCESS.toString(), response.getCode()); assertEquals("OK", response.getMsg()); assertNotNull(response.getData()); assertEquals(0, response.getData().getRecords().size()); diff --git a/backend/src/test/java/ai/dragon/repository/AbstractRepositoryTest.java b/backend/src/test/java/ai/dragon/repository/AbstractRepositoryTest.java new file mode 100644 index 00000000..d8d3bb99 --- /dev/null +++ b/backend/src/test/java/ai/dragon/repository/AbstractRepositoryTest.java @@ -0,0 +1,78 @@ +package ai.dragon.repository; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.util.UUID; + +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +import ai.dragon.entity.SiloEntity; + +@SpringBootTest +@ActiveProfiles("test") +public class AbstractRepositoryTest { + @Autowired + private SiloRepository siloRepository; + + @Test + void noTransactionInsert() throws Exception { + siloRepository.deleteAll(); + SiloEntity silo = new SiloEntity(); + silo.setUuid(UUID.randomUUID()); + silo.setName("dRAGon is awesome"); + siloRepository.save(silo); + SiloEntity retrievedSilo = siloRepository.getByUuid(silo.getUuid()).orElseThrow(); + assertNotNull(siloRepository.getByUuid(retrievedSilo.getUuid())); + assertEquals(1, siloRepository.countAll()); + } + + @Test + void transactionInsert() throws Exception { + siloRepository.deleteAll(); + SiloEntity transactionSilo = new SiloEntity(); + transactionSilo.setUuid(UUID.randomUUID()); + transactionSilo.setName("dRAGon is awesome"); + siloRepository.executeTransaction(transactionRepository -> { + transactionRepository.save(transactionSilo); + }); + SiloEntity retrievedTransactionSilo = siloRepository.getByUuid(transactionSilo.getUuid()).orElseThrow(); + assertNotNull(siloRepository.getByUuid(retrievedTransactionSilo.getUuid())); + assertEquals(1, siloRepository.countAll()); + } + + @Test + void transactionRollback() throws Exception { + siloRepository.deleteAll(); + SiloEntity transactionRollbackSilo = new SiloEntity(); + transactionRollbackSilo.setUuid(UUID.randomUUID()); + transactionRollbackSilo.setName("dRAGon is awesome"); + Exception exception = assertThrows(Exception.class, () -> { + siloRepository.queryTransaction(transactionRepository -> { + transactionRepository.save(transactionRollbackSilo); + throw new Exception("Rollback"); + }); + }); + assertTrue(exception.getMessage().contains("Rollback")); + SiloEntity retrievedTransactionRollbackSilo = siloRepository.getByUuid(transactionRollbackSilo.getUuid()) + .orElse(null); + assertNull(retrievedTransactionRollbackSilo); + assertEquals(0, siloRepository.countAll()); + } + + @Test + void avoidNestedTransactions() { + siloRepository.executeTransaction(transactionRepository -> { + assertThrows(IllegalStateException.class, () -> { + transactionRepository.executeTransaction(innerTransactionRepository -> { + }); + }); + }); + } +} diff --git a/backend/src/test/java/ai/dragon/util/fluenttry/TryTest.java b/backend/src/test/java/ai/dragon/util/fluenttry/TryTest.java new file mode 100644 index 00000000..467e2679 --- /dev/null +++ b/backend/src/test/java/ai/dragon/util/fluenttry/TryTest.java @@ -0,0 +1,62 @@ +package ai.dragon.util.fluenttry; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import java.util.concurrent.TimeUnit; + +import org.junit.jupiter.api.Test; +import org.springframework.test.context.ActiveProfiles; + +@ActiveProfiles("test") +public class TryTest { + @Test + void tryThisblock() { + String toBeReturned = "dRAGon"; + String result = Try.thisBlock(() -> { + return toBeReturned; + }); + assertEquals(toBeReturned, result); + } + + @Test + void tryTimeout() { + assertThrows(RuntimeException.class, () -> { + Try + .withTimeout(100, TimeUnit.MILLISECONDS) + .rethrow(true) + .run(() -> { + Thread.sleep(500); + }); + }); + } + + @Test + void tryTimeoutFallback() { + String fallback = "fallback"; + String result = Try + .withFallback(exception -> { + return fallback; + }) + .timeout(100, TimeUnit.MILLISECONDS) + .run(() -> { + Thread.sleep(500); + return "Should not be returned"; + }); + assertEquals(fallback, result); + + } + + @Test + void tryFallback() { + String fallback = "fallback"; + String result = Try + .withFallback(exception -> { + return fallback; + }) + .run(() -> { + throw new RuntimeException(); + }); + assertEquals(fallback, result); + } +} diff --git a/frontend/.env b/frontend/.env index 1a216171..e842ed85 100644 --- a/frontend/.env +++ b/frontend/.env @@ -10,6 +10,9 @@ VITE_APP_TITLE=dRAGon # App Description VITE_APP_DESC=Dynamic Retrieval Augmented Generation for Optimized Nimble +# Documentation url +VITE_DOC_BASE_URL=https://docs.dragon.okinawa + # Prefix of the icon name VITE_ICON_PREFIX=icon diff --git a/frontend/eslint.config.js b/frontend/eslint.config.js index e3a525b9..0a593557 100644 --- a/frontend/eslint.config.js +++ b/frontend/eslint.config.js @@ -18,7 +18,8 @@ export default defineConfig( ignores: ['/^icon-/'] } ], - 'unocss/order-attributify': 'off' + 'unocss/order-attributify': 'off', + '@typescript-eslint/no-empty-object-type': 'off' } }, { diff --git a/frontend/package.json b/frontend/package.json index 1b7a6a97..8fe5ae06 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -48,50 +48,51 @@ "@sa/hooks": "workspace:*", "@sa/materials": "workspace:*", "@sa/utils": "workspace:*", - "@vueuse/core": "11.0.3", + "@vueuse/core": "11.1.0", "clipboard": "2.0.11", "dayjs": "1.11.13", "echarts": "5.5.1", + "highlight.js": "^11.10.0", "lodash-es": "4.17.21", "naive-ui": "2.39.0", "nprogress": "0.2.0", "pinia": "2.2.2", "tailwind-merge": "2.5.2", "uuid": "^10.0.0", - "vue": "3.5.2", + "vue": "3.5.8", "vue-draggable-plus": "0.5.3", - "vue-i18n": "9.14.0", - "vue-router": "4.4.3" + "vue-i18n": "10.0.3", + "vue-router": "4.4.5" }, "devDependencies": { "@dragon/scripts": "workspace:*", "@elegant-router/vue": "0.3.8", - "@iconify/json": "2.2.244", + "@iconify/json": "2.2.252", "@sa/uno-preset": "workspace:*", - "@soybeanjs/eslint-config": "1.4.0", + "@soybeanjs/eslint-config": "1.4.1", "@types/lodash-es": "4.17.12", - "@types/node": "22.5.4", + "@types/node": "22.7.0", "@types/nprogress": "0.2.3", - "@unocss/eslint-config": "0.62.3", - "@unocss/preset-icons": "0.62.3", - "@unocss/preset-uno": "0.62.3", - "@unocss/transformer-directives": "0.62.3", - "@unocss/transformer-variant-group": "0.62.3", - "@unocss/vite": "0.62.3", - "@vitejs/plugin-vue": "5.1.3", + "@unocss/eslint-config": "0.62.4", + "@unocss/preset-icons": "0.62.4", + "@unocss/preset-uno": "0.62.4", + "@unocss/transformer-directives": "0.62.4", + "@unocss/transformer-variant-group": "0.62.4", + "@unocss/vite": "0.62.4", + "@vitejs/plugin-vue": "5.1.4", "@vitejs/plugin-vue-jsx": "4.0.1", - "eslint": "9.9.1", + "eslint": "9.11.1", "eslint-plugin-vue": "9.28.0", "lint-staged": "15.2.10", - "sass": "1.78.0", - "tsx": "4.19.0", - "typescript": "5.5.4", + "sass": "1.79.3", + "tsx": "4.19.1", + "typescript": "5.6.2", "unplugin-icons": "0.19.3", "unplugin-vue-components": "0.27.4", - "vite": "5.4.3", + "vite": "5.4.8", "vite-plugin-progress": "0.0.7", "vite-plugin-svg-icons": "2.0.1", - "vite-plugin-vue-devtools": "7.4.4", + "vite-plugin-vue-devtools": "7.4.6", "vue-eslint-parser": "9.4.3", "vue-tsc": "2.1.6" }, diff --git a/frontend/packages/axios/package.json b/frontend/packages/axios/package.json index d37d32f4..003931f9 100644 --- a/frontend/packages/axios/package.json +++ b/frontend/packages/axios/package.json @@ -16,6 +16,6 @@ "qs": "6.13.0" }, "devDependencies": { - "@types/qs": "6.9.15" + "@types/qs": "6.9.16" } } diff --git a/frontend/packages/axios/src/options.ts b/frontend/packages/axios/src/options.ts index ff59fa77..8b2b116a 100644 --- a/frontend/packages/axios/src/options.ts +++ b/frontend/packages/axios/src/options.ts @@ -20,7 +20,7 @@ export function createDefaultOptions(options?: Partial) { const retryConfig: IAxiosRetryConfig = { - retries: 3 + retries: 0 }; Object.assign(retryConfig, config); diff --git a/frontend/packages/axios/src/shared.ts b/frontend/packages/axios/src/shared.ts index 7afd32b1..4e8bb0c4 100644 --- a/frontend/packages/axios/src/shared.ts +++ b/frontend/packages/axios/src/shared.ts @@ -13,7 +13,7 @@ export function getContentType(config: InternalAxiosRequestConfig) { */ export function isHttpSuccess(status: number) { const isSuccessCode = status >= 200 && status < 300; - return isSuccessCode || status === 304; + return isSuccessCode || status === 304 || status === 422; } /** diff --git a/frontend/packages/ofetch/package.json b/frontend/packages/ofetch/package.json index bf62af24..e8c110e8 100644 --- a/frontend/packages/ofetch/package.json +++ b/frontend/packages/ofetch/package.json @@ -10,6 +10,6 @@ } }, "dependencies": { - "ofetch": "1.3.4" + "ofetch": "1.4.0" } } diff --git a/frontend/packages/scripts/package.json b/frontend/packages/scripts/package.json index 2f7682cf..e37aef08 100644 --- a/frontend/packages/scripts/package.json +++ b/frontend/packages/scripts/package.json @@ -19,9 +19,9 @@ "cac": "6.7.14", "consola": "3.2.3", "enquirer": "2.4.1", - "execa": "9.3.1", + "execa": "9.4.0", "kolorist": "1.8.0", - "npm-check-updates": "17.1.1", + "npm-check-updates": "17.1.3", "rimraf": "6.0.1" } } diff --git a/frontend/src/App.vue b/frontend/src/App.vue index 22353307..5382258a 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -1,10 +1,14 @@ {{ props.helpText }} + diff --git a/frontend/src/components/custom/select-with-hint.vue b/frontend/src/components/custom/select-with-hint.vue new file mode 100644 index 00000000..98ed6636 --- /dev/null +++ b/frontend/src/components/custom/select-with-hint.vue @@ -0,0 +1,64 @@ + + + diff --git a/frontend/src/components/custom/svg-icon.vue b/frontend/src/components/custom/svg-icon.vue index 0d200c71..1efa585a 100644 --- a/frontend/src/components/custom/svg-icon.vue +++ b/frontend/src/components/custom/svg-icon.vue @@ -7,6 +7,8 @@ defineOptions({ name: 'SvgIcon', inheritAttrs: false }); interface Props { /** Local svg icon name */ localIcon?: string; + /** Click event handler */ + onClick?: (event: MouseEvent) => void; } const props = defineProps(); @@ -27,10 +29,16 @@ const symbolId = computed(() => { return `#${prefix}-${icon}`; }); + +const handleClick = (event: MouseEvent) => { + if (props.onClick) { + props.onClick(event); + } +}; diff --git a/frontend/src/components/custom/tag-renderer.vue b/frontend/src/components/custom/tag-renderer.vue index e5b4f868..1583e1fd 100644 --- a/frontend/src/components/custom/tag-renderer.vue +++ b/frontend/src/components/custom/tag-renderer.vue @@ -2,14 +2,22 @@ interface Props { value: string | null; tagMap: Record; - labelMap: Record; + labelMap: Record; } const props = defineProps(); + +function getLabel(value: string | null) { + if (value === null) { + return ''; + } + const item = props.labelMap[value]; + return typeof item === 'string' ? item : item.label; +} diff --git a/frontend/src/constants/business.ts b/frontend/src/constants/business.ts index b6e2e8bc..94e30245 100644 --- a/frontend/src/constants/business.ts +++ b/frontend/src/constants/business.ts @@ -1,3 +1,4 @@ +import { $t } from '@/locales'; import { transformRecordToOption } from '@/utils/common'; export const vectoreStoreRecord: Record = { @@ -27,8 +28,20 @@ export const languageModelRecord: Record = { - MaxMessages: 'MAX MESSAGES', - MaxTokens: 'MAX TOKENS' +export const chatMemoryStrategyRecord: Record = { + MaxMessages: { + label: 'MAX MESSAGES', + hint: $t('help.farm.chatMemoryStrategy.maxMessagesHint') + }, + MaxTokens: { + label: 'MAX TOKENS', + hint: $t('help.farm.chatMemoryStrategy.maxTokensHint') + } }; export const chatMemoryStrategyOptions = transformRecordToOption(chatMemoryStrategyRecord); + +export const queryRouterRecord: Record = { + Default: 'DEFAULT', + LanguageModel: 'LANGUAGE MODEL' +}; +export const queryRouterOptions = transformRecordToOption(queryRouterRecord); diff --git a/frontend/src/locales/langs/en-us.ts b/frontend/src/locales/langs/en-us.ts index 12b8d73f..cdcc587b 100644 --- a/frontend/src/locales/langs/en-us.ts +++ b/frontend/src/locales/langs/en-us.ts @@ -11,6 +11,7 @@ const local: App.I18n.Schema = { ingestorLoader: 'Ingestor Loader', languageModel: 'Language Model', provider: 'Provider', + queryRouter: 'Query Router', raagIdentifier: 'RaaG Identifier', retrievalAugmentor: 'Retrieval Augmentor', silo: 'Silo', @@ -43,6 +44,7 @@ const local: App.I18n.Schema = { confirm: 'Confirm', delete: 'Delete', deleteSuccess: 'Delete Success', + description: 'Description', confirmDelete: 'Are you sure you want to delete?', edit: 'Edit', error: 'Error', @@ -80,8 +82,25 @@ const local: App.I18n.Schema = { farm: { name: 'Name of the Farm. Must be unique.', raagIdentifier: - 'Identifier of the RaaG (RAG as a GPT). Must be unique. Used as the model name for your API calls.' - } + 'Identifier of the RaaG (RAG as a GPT). Must be unique. Used as the model name for your API calls.', + silos: `Silos that are part of this Farm. + Depending on the Query Router, Silos will be queried or not based on the user query. + When no Silo specified, query will be sent directly to AI model.`, + chatMemoryStrategy: { + tooltip: 'Evicts old messages or tokens based on the strategy.', + maxMessagesHint: + 'To define MAX MESSAGES value (default to 10), add this Retrieval Augmentor setting key : historyMaxMessages', + maxTokensHint: + 'To define MAX TOKENS value (default to 3000), add this Retrieval Augmentor setting key : historyMaxTokens' + }, + queryRouter: 'Query Router to best choose Silos based on the user request.' + }, + silo: { + name: 'Name of the Silo. Must be unique.', + description: + 'Description of the Silo. Optional. Used by the Language Query Router to choose the best Silo among Farm chain.' + }, + integrationExample: 'Integration Example' }, request: { logout: 'Logout user after request failed', diff --git a/frontend/src/plugins/loading.ts b/frontend/src/plugins/loading.ts index 63aa34c6..239dd403 100644 --- a/frontend/src/plugins/loading.ts +++ b/frontend/src/plugins/loading.ts @@ -25,7 +25,7 @@ export function setupLoading() { const loading = `
- +
${dot} diff --git a/frontend/src/typings/api.d.ts b/frontend/src/typings/api.d.ts index 960f1157..42eadbd6 100644 --- a/frontend/src/typings/api.d.ts +++ b/frontend/src/typings/api.d.ts @@ -37,6 +37,15 @@ declare namespace Api { } & T; } + type SelectOptionItem = { + /** value */ + value?: string; + /** label */ + label: string; + /** hint */ + hint?: string; + }; + /** * namespace Auth * @@ -144,6 +153,8 @@ declare namespace Api { type Silo = Common.CommonRecord<{ /** Silo Name */ name: string; + /** Silo Description */ + description: string; /** Vector Store Type */ vectorStore: VectorStoreType | null; /** Embedding Model */ @@ -173,6 +184,9 @@ declare namespace Api { /** Chat Memory Strategy Type */ type ChatMemoryStrategyType = 'MaxMessages' | 'MaxTokens'; + /** Query Router Type */ + type QueryRouterType = 'Default' | 'LanguageModel'; + /** Farm Search Params */ type FarmSearchParams = CommonType.RecordNullable< Pick & Common.CommonSearchParams @@ -192,6 +206,8 @@ declare namespace Api { languageModelSettings: string[] | null; /** Chat Memory Strategy */ chatMemoryStrategy: ChatMemoryStrategyType | null; + /** Query Router Type */ + queryRouter: QueryRouterType | null; /** Retrieval Augmentor Settings */ retrievalAugmentorSettings: string[] | null; }>; diff --git a/frontend/src/typings/app.d.ts b/frontend/src/typings/app.d.ts index 32624cb1..cc6e9640 100644 --- a/frontend/src/typings/app.d.ts +++ b/frontend/src/typings/app.d.ts @@ -257,6 +257,7 @@ declare namespace App { infrastructureDescription: string; ingestorLoader: string; provider: string; + queryRouter: string; raagIdentifier: string; retrievalAugmentor: string; silo: string; @@ -289,6 +290,7 @@ declare namespace App { confirm: string; delete: string; deleteSuccess: string; + description: string; confirmDelete: string; edit: string; error: string; @@ -326,7 +328,19 @@ declare namespace App { farm: { name: string; raagIdentifier: string; + silos: string; + chatMemoryStrategy: { + tooltip: string; + maxMessagesHint: string; + maxTokensHint: string; + }; + queryRouter: string; + }; + silo: { + name: string; + description: string; }; + integrationExample: string; }; request: { logout: string; diff --git a/frontend/src/typings/components.d.ts b/frontend/src/typings/components.d.ts index eb054f1e..a00e0402 100644 --- a/frontend/src/typings/components.d.ts +++ b/frontend/src/typings/components.d.ts @@ -41,6 +41,7 @@ declare module 'vue' { NButton: typeof import('naive-ui')['NButton'] NCard: typeof import('naive-ui')['NCard'] NCheckbox: typeof import('naive-ui')['NCheckbox'] + NCode: typeof import('naive-ui')['NCode'] NCollapse: typeof import('naive-ui')['NCollapse'] NCollapseItem: typeof import('naive-ui')['NCollapseItem'] NColorPicker: typeof import('naive-ui')['NColorPicker'] @@ -82,6 +83,7 @@ declare module 'vue' { ReloadButton: typeof import('./../components/common/reload-button.vue')['default'] RouterLink: typeof import('vue-router')['RouterLink'] RouterView: typeof import('vue-router')['RouterView'] + SelectWithHint: typeof import('./../components/custom/select-with-hint.vue')['default'] SplitDropdown: typeof import('./../components/custom/split-dropdown.vue')['default'] SvgIcon: typeof import('./../components/custom/svg-icon.vue')['default'] SystemLogo: typeof import('./../components/common/system-logo.vue')['default'] diff --git a/frontend/src/typings/env.d.ts b/frontend/src/typings/env.d.ts index d1ad2021..972d83a1 100644 --- a/frontend/src/typings/env.d.ts +++ b/frontend/src/typings/env.d.ts @@ -17,6 +17,8 @@ declare namespace Env { readonly VITE_APP_DESC: string; /** The router history mode */ readonly VITE_ROUTER_HISTORY_MODE?: RouterHistoryMode; + /** The base url of the documentation */ + readonly VITE_DOC_BASE_URL: string; /** * The prefix of the local icon * diff --git a/frontend/src/utils/common.ts b/frontend/src/utils/common.ts index dc9a368f..9cbd1095 100644 --- a/frontend/src/utils/common.ts +++ b/frontend/src/utils/common.ts @@ -18,11 +18,13 @@ import { $t } from '@/locales'; * * @param record */ -export function transformRecordToOption>(record: T) { - return Object.entries(record).map(([value, label]) => ({ - value, - label - })) as CommonType.Option[]; +export function transformRecordToOption>(record: T) { + return Object.entries(record).map(([key, value]) => { + if (typeof value === 'string') { + return { value: key, label: value }; + } + return { value: value.value || key, label: value.label, hint: value.hint }; + }) as Array; } /** diff --git a/frontend/src/views/infrastructure/farm-list/modules/farm-edit.vue b/frontend/src/views/infrastructure/farm-list/modules/farm-edit.vue index 5103e0e5..29435a79 100644 --- a/frontend/src/views/infrastructure/farm-list/modules/farm-edit.vue +++ b/frontend/src/views/infrastructure/farm-list/modules/farm-edit.vue @@ -4,7 +4,7 @@ import { NIL as NIL_UUID } from 'uuid'; import type { SelectOption } from 'naive-ui'; import { useFormRules, useNaiveForm } from '@/hooks/common/form'; import { $t } from '@/locales'; -import { chatMemoryStrategyOptions, languageModelOptions } from '@/constants/business'; +import { chatMemoryStrategyOptions, languageModelOptions, queryRouterOptions } from '@/constants/business'; import { fetchSilosSearch, fetchUpsertFarm } from '@/service/api'; import KVSettings from '../../../../components/custom/kv-settings.vue'; @@ -58,22 +58,42 @@ function createDefaultModel(): Api.FarmManage.Farm { languageModel: null, languageModelSettings: [], chatMemoryStrategy: null, - retrievalAugmentorSettings: [] + retrievalAugmentorSettings: [], + queryRouter: null }; } -type RuleKey = Extract; +type RuleKey = Extract; const rules: Record = { name: defaultRequiredRule, raagIdentifier: formRules.raagIdentifier, languageModel: defaultRequiredRule, - chatMemoryStrategy: defaultRequiredRule + chatMemoryStrategy: defaultRequiredRule, + queryRouter: defaultRequiredRule }; const kvLanguageModelSettingsKey = ref(0); const kvRetrievalAugmentorSettingsKey = ref(0); +const docBaseUrl = ref(import.meta.env.VITE_DOC_BASE_URL); + +const integrationExampleCode = computed(() => { + return ` +from langchain_openai import OpenAI + +llm = OpenAI( + # Model name is your Raag Identifier : + model_name="${model.raagIdentifier || 'your-raag-idenfifier-goes-here'}", + # Replace 'your.dragon.host:1985' with your server host details : + openai_api_base="http://your.dragon.host:1985/api/raag/v1", +) + +prompt = "What's dRAGon?" +llm.invoke(prompt) +`; +}); + function refreshKeyValueSettings() { kvLanguageModelSettingsKey.value += 1; kvRetrievalAugmentorSettingsKey.value += 1; @@ -153,18 +173,16 @@ watch(visible, () => { :label="$t('dRAGon.raagIdentifier')" path="raagIdentifier" :help-text="$t('help.farm.raagIdentifier')" + :help-link="docBaseUrl + '/about-dragon/glossary/farm-glossary/raag-identifier'" > - - - - + { :clear-filter-after-select="true" @search="handleSearch" /> - + {{ $t('dRAGon.languageModel') }} @@ -197,11 +215,42 @@ watch(visible, () => { {{ $t('dRAGon.retrievalAugmentor') }} + + + + + + + + {{ $t('help.integrationExample') }} + +