diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml index 24aecec..c2cfa53 100644 --- a/.idea/dataSources.xml +++ b/.idea/dataSources.xml @@ -18,7 +18,15 @@ jdbc:sqlite:./data/agent.db $ProjectFileDir$ - + + sqlite.xerial + true + true + org.sqlite.JDBC + jdbc:sqlite:./data/agent.db + $ProjectFileDir$ + + sqlite.xerial true true diff --git a/agent/pom.xml b/agent/pom.xml index 079ec96..47675d7 100644 --- a/agent/pom.xml +++ b/agent/pom.xml @@ -54,11 +54,23 @@ org.springframework.boot spring-boot-starter-web + + + + io.modelcontextprotocol.sdk + mcp + + + io.modelcontextprotocol.sdk + mcp-spring-webflux + + + org.springdoc springdoc-openapi-starter-webmvc-ui - 2.6.0 + 2.8.9 @@ -74,6 +86,17 @@ 10.16.0 + + org.testcontainers + junit-jupiter + 1.21.3 + + + org.testcontainers + testcontainers + 1.21.3 + + org.projectlombok @@ -149,6 +172,13 @@ 1.6.0 pom import + + + io.modelcontextprotocol.sdk + mcp-bom + 0.10.0 + pom + import diff --git a/agent/src/main/java/com/clortox/agent/agent/controllers/AgentController.java b/agent/src/main/java/com/clortox/agent/agent/controllers/AgentController.java deleted file mode 100644 index cdf5910..0000000 --- a/agent/src/main/java/com/clortox/agent/agent/controllers/AgentController.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.clortox.agent.agent.controllers; - -import com.clortox.agent.agent.state.SmartAgent; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RestController; - -@RestController("Agent") -public class AgentController { - - @Autowired - private SmartAgent agent; - - - @GetMapping(path = "/assistant") - public ResponseEntity get(String message) { - return ResponseEntity.status(HttpStatus.OK).body(agent.invoke(message)); - -// return null; - } -} diff --git a/agent/src/main/java/com/clortox/agent/agent/controllers/OllamaCompatibleApi.java b/agent/src/main/java/com/clortox/agent/agent/controllers/OllamaCompatibleApi.java new file mode 100644 index 0000000..7a0f38d --- /dev/null +++ b/agent/src/main/java/com/clortox/agent/agent/controllers/OllamaCompatibleApi.java @@ -0,0 +1,39 @@ +package com.clortox.agent.agent.controllers; + +import com.clortox.agent.agent.dto.ollama.ModelDetails; +import com.clortox.agent.agent.dto.ollama.ModelTag; +import com.clortox.agent.agent.dto.ollama.TagsResponse; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.time.OffsetDateTime; +import java.util.List; + +@RestController +@RequestMapping("/ollama/api") +public class OllamaCompatibleApi { + + @GetMapping("/tags") + public TagsResponse listModels() { + return new TagsResponse(List.of( + new ModelTag( + "SmartAgent:latest", + OffsetDateTime.now().toString(), + 1024, + "sha256:deadbeef1", + new ModelDetails( + "gguf", + "llama", + List.of("llama"), + "8B", + "Q4_0" + ) + ) + )); + } + + + + +} diff --git a/agent/src/main/java/com/clortox/agent/agent/dto/ollama/ChatMessage.java b/agent/src/main/java/com/clortox/agent/agent/dto/ollama/ChatMessage.java new file mode 100644 index 0000000..02578bc --- /dev/null +++ b/agent/src/main/java/com/clortox/agent/agent/dto/ollama/ChatMessage.java @@ -0,0 +1,13 @@ +package com.clortox.agent.agent.dto.ollama; + +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@AllArgsConstructor +@NoArgsConstructor +public class ChatMessage { + private ChatRole role; + private String content; +} diff --git a/agent/src/main/java/com/clortox/agent/agent/dto/ollama/ChatRequest.java b/agent/src/main/java/com/clortox/agent/agent/dto/ollama/ChatRequest.java new file mode 100644 index 0000000..5bfa405 --- /dev/null +++ b/agent/src/main/java/com/clortox/agent/agent/dto/ollama/ChatRequest.java @@ -0,0 +1,16 @@ +package com.clortox.agent.agent.dto.ollama; + +import lombok.Builder; + +import java.util.List; +import java.util.Map; + +@Builder +public record ChatRequest( + String model, + List messages, + Boolean stream, + Map options, + String format, + String keep_alive +) {} diff --git a/agent/src/main/java/com/clortox/agent/agent/dto/ollama/ChatResponse.java b/agent/src/main/java/com/clortox/agent/agent/dto/ollama/ChatResponse.java new file mode 100644 index 0000000..d5c233c --- /dev/null +++ b/agent/src/main/java/com/clortox/agent/agent/dto/ollama/ChatResponse.java @@ -0,0 +1,11 @@ +package com.clortox.agent.agent.dto.ollama; + +import lombok.Builder; + +@Builder +public record ChatResponse( + String model, + String created_at, + ChatMessage message, + Boolean done +) {} diff --git a/agent/src/main/java/com/clortox/agent/agent/dto/ollama/ChatRole.java b/agent/src/main/java/com/clortox/agent/agent/dto/ollama/ChatRole.java new file mode 100644 index 0000000..794acd2 --- /dev/null +++ b/agent/src/main/java/com/clortox/agent/agent/dto/ollama/ChatRole.java @@ -0,0 +1,31 @@ +package com.clortox.agent.agent.dto.ollama; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +public enum ChatRole { + SYSTEM("system"), + USER("user"), + ASSISTANT("assistant"), + TOOL("tool"); + + private String friendlyName; + + ChatRole(String friendlyString) { + this.friendlyName = friendlyString; + } + + @JsonValue + public String getFriendlyName() { + return friendlyName; + } + + @JsonCreator + public static ChatRole fromFriendlyName(String name) { + for(ChatRole role : ChatRole.values()) { + if(role.friendlyName.equals(name)) return role; + } + throw new IllegalStateException("Unknown ChatRole " + name); + } + +} diff --git a/agent/src/main/java/com/clortox/agent/agent/dto/ollama/ModelDetails.java b/agent/src/main/java/com/clortox/agent/agent/dto/ollama/ModelDetails.java new file mode 100644 index 0000000..104dd61 --- /dev/null +++ b/agent/src/main/java/com/clortox/agent/agent/dto/ollama/ModelDetails.java @@ -0,0 +1,16 @@ +package com.clortox.agent.agent.dto.ollama; + +import lombok.AllArgsConstructor; +import lombok.Data; + +import java.util.List; + +@Data +@AllArgsConstructor +public class ModelDetails { + private String format; + private String family; + private List families; + private String parameter_size; + private String quantization_level; +} diff --git a/agent/src/main/java/com/clortox/agent/agent/dto/ollama/ModelTag.java b/agent/src/main/java/com/clortox/agent/agent/dto/ollama/ModelTag.java new file mode 100644 index 0000000..f6ece9c --- /dev/null +++ b/agent/src/main/java/com/clortox/agent/agent/dto/ollama/ModelTag.java @@ -0,0 +1,17 @@ +package com.clortox.agent.agent.dto.ollama; + +import lombok.AllArgsConstructor; +import lombok.Data; + +@Data +@AllArgsConstructor +public class ModelTag { + private String name; + /** + * ISO format + */ + private String modified_at; + private long size; + private String digest; + private ModelDetails details; +} diff --git a/agent/src/main/java/com/clortox/agent/agent/dto/ollama/TagsResponse.java b/agent/src/main/java/com/clortox/agent/agent/dto/ollama/TagsResponse.java new file mode 100644 index 0000000..d50b15e --- /dev/null +++ b/agent/src/main/java/com/clortox/agent/agent/dto/ollama/TagsResponse.java @@ -0,0 +1,6 @@ +package com.clortox.agent.agent.dto.ollama; + +import java.util.List; + +public record TagsResponse(List models) { +} diff --git a/agent/src/main/java/com/clortox/agent/agent/dto/ChatCompletionChunk.java b/agent/src/main/java/com/clortox/agent/agent/dto/openai/ChatCompletionChunk.java similarity index 71% rename from agent/src/main/java/com/clortox/agent/agent/dto/ChatCompletionChunk.java rename to agent/src/main/java/com/clortox/agent/agent/dto/openai/ChatCompletionChunk.java index 961a179..781dd8a 100644 --- a/agent/src/main/java/com/clortox/agent/agent/dto/ChatCompletionChunk.java +++ b/agent/src/main/java/com/clortox/agent/agent/dto/openai/ChatCompletionChunk.java @@ -1,7 +1,10 @@ -package com.clortox.agent.agent.dto; +package com.clortox.agent.agent.dto.openai; import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; import java.util.List; diff --git a/agent/src/main/java/com/clortox/agent/agent/dto/ChatCompletionRequest.java b/agent/src/main/java/com/clortox/agent/agent/dto/openai/ChatCompletionRequest.java similarity index 75% rename from agent/src/main/java/com/clortox/agent/agent/dto/ChatCompletionRequest.java rename to agent/src/main/java/com/clortox/agent/agent/dto/openai/ChatCompletionRequest.java index f64a829..f1bba5a 100644 --- a/agent/src/main/java/com/clortox/agent/agent/dto/ChatCompletionRequest.java +++ b/agent/src/main/java/com/clortox/agent/agent/dto/openai/ChatCompletionRequest.java @@ -1,9 +1,11 @@ -package com.clortox.agent.agent.dto; +package com.clortox.agent.agent.dto.openai; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonValue; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; import java.util.List; diff --git a/agent/src/main/java/com/clortox/agent/agent/dto/ChatCompletionResponse.java b/agent/src/main/java/com/clortox/agent/agent/dto/openai/ChatCompletionResponse.java similarity index 62% rename from agent/src/main/java/com/clortox/agent/agent/dto/ChatCompletionResponse.java rename to agent/src/main/java/com/clortox/agent/agent/dto/openai/ChatCompletionResponse.java index a696247..8e45046 100644 --- a/agent/src/main/java/com/clortox/agent/agent/dto/ChatCompletionResponse.java +++ b/agent/src/main/java/com/clortox/agent/agent/dto/openai/ChatCompletionResponse.java @@ -1,16 +1,13 @@ -package com.clortox.agent.agent.dto; - -package com.example.openai.dto; +package com.clortox.agent.agent.dto.openai; import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonValue; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; import java.util.List; -// ========= NON-STREAMING RESPONSE DTO ========= - @Data @NoArgsConstructor @AllArgsConstructor diff --git a/agent/src/main/java/com/clortox/agent/agent/dto/Choice.java b/agent/src/main/java/com/clortox/agent/agent/dto/openai/Choice.java similarity index 69% rename from agent/src/main/java/com/clortox/agent/agent/dto/Choice.java rename to agent/src/main/java/com/clortox/agent/agent/dto/openai/Choice.java index c760e42..9452064 100644 --- a/agent/src/main/java/com/clortox/agent/agent/dto/Choice.java +++ b/agent/src/main/java/com/clortox/agent/agent/dto/openai/Choice.java @@ -1,8 +1,11 @@ -package com.clortox.agent.agent.dto; +package com.clortox.agent.agent.dto.openai; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; @Data @NoArgsConstructor diff --git a/agent/src/main/java/com/clortox/agent/agent/dto/ChunkChoice.java b/agent/src/main/java/com/clortox/agent/agent/dto/openai/ChunkChoice.java similarity index 69% rename from agent/src/main/java/com/clortox/agent/agent/dto/ChunkChoice.java rename to agent/src/main/java/com/clortox/agent/agent/dto/openai/ChunkChoice.java index 846c734..b8653f2 100644 --- a/agent/src/main/java/com/clortox/agent/agent/dto/ChunkChoice.java +++ b/agent/src/main/java/com/clortox/agent/agent/dto/openai/ChunkChoice.java @@ -1,8 +1,11 @@ -package com.clortox.agent.agent.dto; +package com.clortox.agent.agent.dto.openai; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; @Data @NoArgsConstructor diff --git a/agent/src/main/java/com/clortox/agent/agent/dto/Delta.java b/agent/src/main/java/com/clortox/agent/agent/dto/openai/Delta.java similarity index 62% rename from agent/src/main/java/com/clortox/agent/agent/dto/Delta.java rename to agent/src/main/java/com/clortox/agent/agent/dto/openai/Delta.java index 49c45e0..6acb58c 100644 --- a/agent/src/main/java/com/clortox/agent/agent/dto/Delta.java +++ b/agent/src/main/java/com/clortox/agent/agent/dto/openai/Delta.java @@ -1,7 +1,10 @@ -package com.clortox.agent.agent.dto; +package com.clortox.agent.agent.dto.openai; import com.fasterxml.jackson.annotation.JsonInclude; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; @Data @NoArgsConstructor diff --git a/agent/src/main/java/com/clortox/agent/agent/dto/FinishReason.java b/agent/src/main/java/com/clortox/agent/agent/dto/openai/FinishReason.java similarity index 87% rename from agent/src/main/java/com/clortox/agent/agent/dto/FinishReason.java rename to agent/src/main/java/com/clortox/agent/agent/dto/openai/FinishReason.java index f7b7ba6..37fdb49 100644 --- a/agent/src/main/java/com/clortox/agent/agent/dto/FinishReason.java +++ b/agent/src/main/java/com/clortox/agent/agent/dto/openai/FinishReason.java @@ -1,4 +1,4 @@ -package com.clortox.agent.agent.dto; +package com.clortox.agent.agent.dto.openai; import com.fasterxml.jackson.annotation.JsonValue; import lombok.AllArgsConstructor; diff --git a/agent/src/main/java/com/clortox/agent/agent/dto/Message.java b/agent/src/main/java/com/clortox/agent/agent/dto/openai/Message.java similarity index 88% rename from agent/src/main/java/com/clortox/agent/agent/dto/Message.java rename to agent/src/main/java/com/clortox/agent/agent/dto/openai/Message.java index 95b0911..abd8d9a 100644 --- a/agent/src/main/java/com/clortox/agent/agent/dto/Message.java +++ b/agent/src/main/java/com/clortox/agent/agent/dto/openai/Message.java @@ -1,4 +1,4 @@ -package com.clortox.agent.agent.dto; +package com.clortox.agent.agent.dto.openai; import com.fasterxml.jackson.annotation.JsonInclude; import lombok.AllArgsConstructor; diff --git a/agent/src/main/java/com/clortox/agent/agent/dto/Role.java b/agent/src/main/java/com/clortox/agent/agent/dto/openai/Role.java similarity index 86% rename from agent/src/main/java/com/clortox/agent/agent/dto/Role.java rename to agent/src/main/java/com/clortox/agent/agent/dto/openai/Role.java index 2f78889..a195210 100644 --- a/agent/src/main/java/com/clortox/agent/agent/dto/Role.java +++ b/agent/src/main/java/com/clortox/agent/agent/dto/openai/Role.java @@ -1,4 +1,4 @@ -package com.clortox.agent.agent.dto; +package com.clortox.agent.agent.dto.openai; import com.fasterxml.jackson.annotation.JsonValue; import lombok.AllArgsConstructor; diff --git a/agent/src/main/java/com/clortox/agent/agent/dto/Usage.java b/agent/src/main/java/com/clortox/agent/agent/dto/openai/Usage.java similarity index 73% rename from agent/src/main/java/com/clortox/agent/agent/dto/Usage.java rename to agent/src/main/java/com/clortox/agent/agent/dto/openai/Usage.java index a0842cc..5b553b0 100644 --- a/agent/src/main/java/com/clortox/agent/agent/dto/Usage.java +++ b/agent/src/main/java/com/clortox/agent/agent/dto/openai/Usage.java @@ -1,8 +1,11 @@ -package com.clortox.agent.agent.dto; +package com.clortox.agent.agent.dto.openai; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; @Data @NoArgsConstructor diff --git a/agent/src/main/java/com/clortox/agent/agent/state/SmartAgent.java b/agent/src/main/java/com/clortox/agent/agent/implementations/SmartAgent.java similarity index 94% rename from agent/src/main/java/com/clortox/agent/agent/state/SmartAgent.java rename to agent/src/main/java/com/clortox/agent/agent/implementations/SmartAgent.java index 5a1eb7e..6296dbf 100644 --- a/agent/src/main/java/com/clortox/agent/agent/state/SmartAgent.java +++ b/agent/src/main/java/com/clortox/agent/agent/implementations/SmartAgent.java @@ -1,4 +1,4 @@ -package com.clortox.agent.agent.state; +package com.clortox.agent.agent.implementations; import com.clortox.agent.tools.ISmartAgentTool; import dev.langchain4j.data.message.SystemMessage; @@ -27,7 +27,7 @@ public class SmartAgent { # Personality - You are a very smart cute anime girl. You talk like an anime girl. + You are a kind helpful assistant """; private final CompiledGraph agent; @@ -63,7 +63,4 @@ public class SmartAgent { AgentExecutor.State state = finalState.orElseThrow(); return state.finalResponse().orElse("Agent failed"); } - - - } diff --git a/agent/src/main/java/com/clortox/agent/common/config/GenericControllerAdvice.java b/agent/src/main/java/com/clortox/agent/common/config/GenericControllerAdvice.java index 024a6cd..3dd9149 100644 --- a/agent/src/main/java/com/clortox/agent/common/config/GenericControllerAdvice.java +++ b/agent/src/main/java/com/clortox/agent/common/config/GenericControllerAdvice.java @@ -9,11 +9,13 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; -import java.net.http.HttpResponse; @ControllerAdvice public class GenericControllerAdvice { + public GenericControllerAdvice() { + + } @ExceptionHandler(Throwable.class) public ResponseEntity generalError(Throwable t, HttpServletRequest request, HttpServletResponse response) { diff --git a/agent/src/main/java/com/clortox/agent/agent/llm/LLMConfiguration.java b/agent/src/main/java/com/clortox/agent/llm/LLMConfiguration.java similarity index 83% rename from agent/src/main/java/com/clortox/agent/agent/llm/LLMConfiguration.java rename to agent/src/main/java/com/clortox/agent/llm/LLMConfiguration.java index 29feefa..b330534 100644 --- a/agent/src/main/java/com/clortox/agent/agent/llm/LLMConfiguration.java +++ b/agent/src/main/java/com/clortox/agent/llm/LLMConfiguration.java @@ -1,22 +1,16 @@ -package com.clortox.agent.agent.llm; +package com.clortox.agent.llm; -import dev.langchain4j.http.client.HttpClientBuilder; import dev.langchain4j.http.client.spring.restclient.SpringRestClient; import dev.langchain4j.http.client.spring.restclient.SpringRestClientBuilder; -import dev.langchain4j.model.chat.Capability; import dev.langchain4j.model.chat.ChatModel; import dev.langchain4j.model.ollama.OllamaChatModel; import org.springframework.beans.factory.annotation.Value; -import org.springframework.boot.http.client.HttpClientSettings; -import org.springframework.boot.http.client.JdkHttpClientBuilder; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.task.VirtualThreadTaskExecutor; import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; import org.springframework.web.client.RestClient; -import java.net.http.HttpClient; -import java.util.Set; @Configuration public class LLMConfiguration { diff --git a/agent/src/main/java/com/clortox/agent/mcp/McpConfig.java b/agent/src/main/java/com/clortox/agent/mcp/McpConfig.java new file mode 100644 index 0000000..9fa8a27 --- /dev/null +++ b/agent/src/main/java/com/clortox/agent/mcp/McpConfig.java @@ -0,0 +1,59 @@ +package com.clortox.agent.mcp; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.modelcontextprotocol.server.McpAsyncServer; +import io.modelcontextprotocol.server.McpServer; +import io.modelcontextprotocol.server.McpServerFeatures; +import io.modelcontextprotocol.server.McpSyncServer; +import io.modelcontextprotocol.server.transport.WebFluxSseServerTransportProvider; +import io.modelcontextprotocol.spec.McpSchema; +import lombok.extern.slf4j.Slf4j; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.reactive.function.server.RouterFunction; +import reactor.core.publisher.Mono; + +import java.util.List; + +@Slf4j +@Configuration +public class McpConfig { + + @Bean + WebFluxSseServerTransportProvider webFluxSseServerTransportProvider(ObjectMapper mapper) { + return new WebFluxSseServerTransportProvider(mapper, "/mpc/message"); + } + + @Bean + RouterFunction mcpRouterFunction(WebFluxSseServerTransportProvider transportProvider) { + return transportProvider.getRouterFunction(); + } + + @Bean + McpAsyncServer mcpSyncServer(WebFluxSseServerTransportProvider transportProvider) { + var server = McpServer.async(transportProvider) + .serverInfo("Clortox's MCP Server", "1.0.0") + .capabilities(McpSchema.ServerCapabilities.builder() + .logging() + .prompts(false) + .tools(true) + .resources(false, true) + .build() + ).build(); + + var resourceSpec = new McpServerFeatures.AsyncResourceSpecification( + new McpSchema.Resource("clortox://vault", "valut", "Contents of obisidian Valut", "plain/text", null), + (exchange, request) -> { + var response = new McpSchema.ReadResourceResult(List.of( + )); + return Mono.justOrEmpty(response); + } + ); + + server.addResource(resourceSpec) + .doOnSubscribe(v -> log.info("Registered resource")) + .subscribe(); + + return server; + } +} diff --git a/agent/src/main/resources/application.yml b/agent/src/main/resources/application.yml index 8a33bf4..1ebfd47 100644 --- a/agent/src/main/resources/application.yml +++ b/agent/src/main/resources/application.yml @@ -27,7 +27,6 @@ springdoc: swagger-ui: path: /swagger-ui - agent: prompt: smartAgent: "${SMART_AGENT_PROMPT_PATH:./data/smartAgent.md}" diff --git a/agent/src/test/java/com/clortox/agent/AgentApplicationTests.java b/agent/src/test/java/com/clortox/agent/AgentApplicationTests.java index 40b041a..2025258 100644 --- a/agent/src/test/java/com/clortox/agent/AgentApplicationTests.java +++ b/agent/src/test/java/com/clortox/agent/AgentApplicationTests.java @@ -1,5 +1,6 @@ package com.clortox.agent; +import com.clortox.agent.testconfig.FlywayTestConfiguration; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; diff --git a/agent/src/test/java/com/clortox/agent/BaseTest.java b/agent/src/test/java/com/clortox/agent/BaseTest.java new file mode 100644 index 0000000..22a0608 --- /dev/null +++ b/agent/src/test/java/com/clortox/agent/BaseTest.java @@ -0,0 +1,6 @@ +package com.clortox.agent; + +public abstract class BaseTest { + + +} diff --git a/agent/src/test/java/com/clortox/agent/llm/LLMConfigTest.java b/agent/src/test/java/com/clortox/agent/llm/LLMConfigTest.java new file mode 100644 index 0000000..bfcfc38 --- /dev/null +++ b/agent/src/test/java/com/clortox/agent/llm/LLMConfigTest.java @@ -0,0 +1,66 @@ +package com.clortox.agent.llm; + +import com.clortox.agent.BaseTest; +import dev.langchain4j.model.chat.ChatModel; +import net.bytebuddy.description.type.TypeList; +import org.junit.jupiter.api.BeforeAll; +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.DynamicPropertyRegistrar; +import org.springframework.test.context.DynamicPropertyRegistry; +import org.springframework.test.context.DynamicPropertySource; +import org.testcontainers.containers.BindMode; +import org.testcontainers.containers.GenericContainer; +import org.testcontainers.containers.wait.strategy.Wait; +import org.testcontainers.junit.jupiter.Container; +import org.testcontainers.junit.jupiter.Testcontainers; +import org.testcontainers.utility.DockerImageName; + +import java.util.function.Supplier; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@Testcontainers +@SpringBootTest +public class LLMConfigTest extends BaseTest { + + private static final int OLLAMA_PORT = 11434; + private static final String MODEL = System.getProperty("agent.ollama.model", "qwen3:0.6b"); + + @Autowired + private ChatModel chatModel; + + @Container + static GenericContainer OLLAMA = new GenericContainer<>(DockerImageName.parse("ollama/ollama:latest")) + .withExposedPorts(OLLAMA_PORT) + .withEnv("OLLAMA_KEEP_ALIVE", "5m") + .withEnv("OLLAMA_MODELS", "/models") + .withFileSystemBind("~/.ollama", "/models", BindMode.READ_WRITE) + .waitingFor(Wait.forHttp("/").forStatusCode(200)); + + @DynamicPropertySource + static void props(DynamicPropertyRegistry r) { + Supplier baseUrl = () -> "http://" + OLLAMA.getHost() + ":" + OLLAMA.getMappedPort(OLLAMA_PORT); + r.add("agent.ollama.baseUrl", baseUrl); + r.add("agent.ollama.model", () -> MODEL); + } + + @BeforeAll + static void pullModel() throws Exception { + var res = OLLAMA.execInContainer("sh", "-lc", "ollama pull " + MODEL); + if(res.getExitCode() != 0) { + throw new IllegalStateException("Failed to pull model!"); + } + } + + @Test + void chatModel_callsOllama() { + String reply = chatModel.chat("Say 'ok.'"); + assertThat(reply).isNotBlank(); + assertThat(reply.toLowerCase()).contains("ok"); + } + + + +} diff --git a/agent/src/test/java/com/clortox/agent/FlywayTestConfiguration.java b/agent/src/test/java/com/clortox/agent/testconfig/FlywayTestConfiguration.java similarity index 91% rename from agent/src/test/java/com/clortox/agent/FlywayTestConfiguration.java rename to agent/src/test/java/com/clortox/agent/testconfig/FlywayTestConfiguration.java index 493f4d2..8ceeb3c 100644 --- a/agent/src/test/java/com/clortox/agent/FlywayTestConfiguration.java +++ b/agent/src/test/java/com/clortox/agent/testconfig/FlywayTestConfiguration.java @@ -1,4 +1,4 @@ -package com.clortox.agent; +package com.clortox.agent.testconfig; import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy; import org.springframework.boot.test.context.TestConfiguration; diff --git a/agent/src/test/resources/application-test.yml b/agent/src/test/resources/application-test.yml index f6cee4c..5842167 100644 --- a/agent/src/test/resources/application-test.yml +++ b/agent/src/test/resources/application-test.yml @@ -20,7 +20,7 @@ agent: smartAgent: "${SMART_AGENT_PROMPT_PATH:./data/smartAgent.md}" ollama: baseUrl: "${OLLAMA_BASEURL:http://10.0.3.2:8080}" - model: "${OLLAMA_MODEL:mistral-small3.2}" + model: "qwen3:0.6b" # Tiny model for testing temp: "${OLLAMA_TEMP:0.2}" tools: tenor: