Save progress

This commit is contained in:
Tyler 2025-08-19 21:12:10 -04:00
parent db13632687
commit 678bb04f08
Signed by: tyler
GPG Key ID: 03B27509E17EFDC8
31 changed files with 367 additions and 65 deletions

10
.idea/dataSources.xml generated
View File

@ -18,7 +18,15 @@
<jdbc-url>jdbc:sqlite:./data/agent.db</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="jdbc:sqlite:./data/agent.db [DEBUG]" group="AgentApplication" uuid="0b41b15e-1a4f-4cf2-8b38-4cbc2765a473">
<data-source source="LOCAL" name="jdbc:sqlite:./data/agent.db [DEBUG]" group="AgentApplication" uuid="299c235a-a428-45f1-bcb5-84f2759cfcaa">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<jdbc-driver>org.sqlite.JDBC</jdbc-driver>
<jdbc-url>jdbc:sqlite:./data/agent.db</jdbc-url>
<working-dir>$ProjectFileDir$</working-dir>
</data-source>
<data-source source="LOCAL" name="jdbc:sqlite:./data/agent.db [DEBUG]" group="AgentApplication" uuid="864d54cf-e8c1-4eab-9cb4-bcd8507cd10f">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>

View File

@ -54,11 +54,23 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp</artifactId>
</dependency>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-spring-webflux</artifactId>
</dependency>
<!-- OpenAPI/Swagger UI via springdoc -->
<dependency>
<groupId>org.springdoc</groupId>
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.6.0</version>
<version>2.8.9</version>
</dependency>
<dependency>
@ -74,6 +86,17 @@
<version>10.16.0</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<version>1.21.3</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>1.21.3</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
@ -149,6 +172,13 @@
<version>1.6.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>io.modelcontextprotocol.sdk</groupId>
<artifactId>mcp-bom</artifactId>
<version>0.10.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

View File

@ -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;
}
}

View File

@ -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"
)
)
));
}
}

View File

@ -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;
}

View File

@ -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<ChatMessage> messages,
Boolean stream,
Map<String, Object> options,
String format,
String keep_alive
) {}

View File

@ -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
) {}

View File

@ -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);
}
}

View File

@ -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<String> families;
private String parameter_size;
private String quantization_level;
}

View File

@ -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;
}

View File

@ -0,0 +1,6 @@
package com.clortox.agent.agent.dto.ollama;
import java.util.List;
public record TagsResponse(List<ModelTag> models) {
}

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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

View File

@ -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<AgentExecutor.State> agent;
@ -63,7 +63,4 @@ public class SmartAgent {
AgentExecutor.State state = finalState.orElseThrow();
return state.finalResponse().orElse("Agent failed");
}
}

View File

@ -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<ErrorResponseDTO> generalError(Throwable t, HttpServletRequest request, HttpServletResponse response) {

View File

@ -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 {

View File

@ -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;
}
}

View File

@ -27,7 +27,6 @@ springdoc:
swagger-ui:
path: /swagger-ui
agent:
prompt:
smartAgent: "${SMART_AGENT_PROMPT_PATH:./data/smartAgent.md}"

View File

@ -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;

View File

@ -0,0 +1,6 @@
package com.clortox.agent;
public abstract class BaseTest {
}

View File

@ -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<Object> 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");
}
}

View File

@ -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;

View File

@ -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: