Save progress

This commit is contained in:
Tyler 2025-08-18 21:02:22 -04:00
parent aa593433bc
commit db13632687
Signed by: tyler
GPG Key ID: 03B27509E17EFDC8
36 changed files with 724 additions and 80 deletions

11
.idea/dataSources.xml generated
View File

@ -1,15 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourceManagerImpl" format="xml" multifile-model="true">
<data-source source="LOCAL" name="agent.db" uuid="e748f0a0-6da5-4df7-ad15-d313f949230c">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>
<remarks>$PROJECT_DIR$/agent/src/main/resources/application.yml</remarks>
<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="agent.db" uuid="2d94ac6a-09f6-41f0-be54-1a576bdb0bfd">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
@ -27,7 +18,7 @@
<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 (1)" uuid="a1194a9f-8452-4ed5-99db-9b8813a9b174">
<data-source source="LOCAL" name="jdbc:sqlite:./data/agent.db [DEBUG]" group="AgentApplication" uuid="0b41b15e-1a4f-4cf2-8b38-4cbc2765a473">
<driver-ref>sqlite.xerial</driver-ref>
<synchronize>true</synchronize>
<imported>true</imported>

6
.idea/data_source_mapping.xml generated Normal file
View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="DataSourcePerFileMappings">
<file url="file://$PROJECT_DIR$/agent/src/main/resources/db/migration/V1.0.0.1__init.sql" value="2d94ac6a-09f6-41f0-be54-1a576bdb0bfd" />
</component>
</project>

2
.idea/sqldialects.xml generated
View File

@ -1,7 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="SqlDialectMappings">
<file url="file://$PROJECT_DIR$/agent/src/main/resources/db/migration/V0__placeholder.sql" dialect="GenericSQL" />
<file url="file://$PROJECT_DIR$/agent/src/main/resources/db/migration/V1.0.0.1__init.sql" dialect="SQLite" />
<file url="PROJECT" dialect="SQLite" />
</component>
</project>

5
agent/.env.example Normal file
View File

@ -0,0 +1,5 @@
OLLAMA_BASEURL=https://10.0.3.2:8080
OLLAMA_MODEL=mistral-small3.2
OLLAMA_TEMP=0.2
TOOLS_TENOR_API_KEY=mytenorkey

3
agent/.gitignore vendored
View File

@ -31,3 +31,6 @@ build/
### VS Code ###
.vscode/
.env
data/

View File

@ -28,7 +28,6 @@
</scm>
<properties>
<java.version>21</java.version>
<spring-modulith.version>1.4.1</spring-modulith.version>
</properties>
<dependencies>
<dependency>
@ -61,14 +60,20 @@
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-core</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.xerial</groupId>
<artifactId>sqlite-jdbc</artifactId>
<version>3.46.0.0</version>
</dependency>
<!-- Flyway core -->
<dependency>
<groupId>org.flywaydb</groupId>
<artifactId>flyway-core</artifactId>
<version>10.16.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
@ -80,11 +85,6 @@
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
@ -118,25 +118,38 @@
<artifactId>langchain4j</artifactId>
<version>1.3.0</version>
</dependency>
<!-- <dependency>-->
<!-- <groupId>org.bsc.langgraph4j</groupId>-->
<!-- <artifactId>langgraph4j-core</artifactId>-->
<!-- </dependency>-->
<dependency>
<groupId>org.bsc.langgraph4j</groupId>
<artifactId>langgraph4j-langchain4j</artifactId>
</dependency>
<dependency>
<groupId>org.bsc.langgraph4j</groupId>
<artifactId>langgraph4j-agent-executor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.bsc.langgraph4j</groupId>
<artifactId>langgraph4j-core</artifactId>
</dependency>
<dependency>
<groupId>dev.langchain4j</groupId>
<artifactId>langchain4j-ollama</artifactId>
<version>1.3.0</version>
</dependency>
</dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.modulith</groupId>
<artifactId>spring-modulith-bom</artifactId>
<version>${spring-modulith.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.bsc.langgraph4j</groupId>
<artifactId>langgraph4j-bom</artifactId>
<version>1.6.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

View File

@ -1,4 +0,0 @@
package com.clortox.agent.agent;
public interface IAgent {
}

View File

@ -1,5 +1,6 @@
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;
@ -9,9 +10,14 @@ 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("lol");
return ResponseEntity.status(HttpStatus.OK).body(agent.invoke(message));
// return null;
}
}

View File

@ -0,0 +1,19 @@
package com.clortox.agent.agent.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.*;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ChatCompletionChunk {
private String id;
private String object; // "chat.completion.chunk"
private long created;
private String model;
private List<ChunkChoice> choices;
}

View File

@ -0,0 +1,30 @@
package com.clortox.agent.agent.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.*;
import java.util.List;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ChatCompletionRequest {
private String model;
private List<Message> messages;
private Double temperature;
@JsonProperty("max_tokens")
private Integer maxTokens;
private Boolean stream;
}

View File

@ -0,0 +1,26 @@
package com.clortox.agent.agent.dto;
package com.example.openai.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.*;
import java.util.List;
// ========= NON-STREAMING RESPONSE DTO =========
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ChatCompletionResponse {
private String id;
private String object; // "chat.completion"
private long created;
private String model;
private List<Choice> choices;
private Usage usage;
}

View File

@ -0,0 +1,18 @@
package com.clortox.agent.agent.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Choice {
private int index;
private Message message;
@JsonProperty("finish_reason")
private FinishReason finishReason;
}

View File

@ -0,0 +1,18 @@
package com.clortox.agent.agent.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class ChunkChoice {
private int index;
private Delta delta;
@JsonProperty("finish_reason")
private FinishReason finishReason;
}

View File

@ -0,0 +1,14 @@
package com.clortox.agent.agent.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Delta {
private Role role; // only set in first chunk
private String content;
}

View File

@ -0,0 +1,17 @@
package com.clortox.agent.agent.dto;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum FinishReason {
STOP("stop"),
LENGTH("length"),
TOOL_CALLS("tool_calls"),
CONTENT_FILTER("content_filter");
@JsonValue
private final String value;
}

View File

@ -0,0 +1,17 @@
package com.clortox.agent.agent.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Message {
private Role role;
private String content;
}

View File

@ -0,0 +1,17 @@
package com.clortox.agent.agent.dto;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;
import lombok.Getter;
@Getter
@AllArgsConstructor
public enum Role {
SYSTEM("system"),
USER("user"),
ASSISTANT("assistant"),
TOOL("tool");
@JsonValue
private final String value;
}

View File

@ -0,0 +1,21 @@
package com.clortox.agent.agent.dto;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.*;
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@JsonInclude(JsonInclude.Include.NON_NULL)
public class Usage {
@JsonProperty("prompt_tokens")
private int promptTokens;
@JsonProperty("completion_tokens")
private int completionTokens;
@JsonProperty("total_tokens")
private int totalTokens;
}

View File

@ -1,15 +1,36 @@
package com.clortox.agent.agent.memory;
import com.clortox.agent.agent.memory.persistence.Message;
import com.clortox.agent.agent.memory.persistence.MessageRepository;
import dev.langchain4j.data.message.ChatMessage;
import dev.langchain4j.store.memory.chat.ChatMemoryStore;
import org.springframework.data.domain.Example;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.List;
@Component
public class PersistentChatMemoryStore implements ChatMemoryStore {
private MessageRepository repository;
public PersistentChatMemoryStore(MessageRepository repository) {
this.repository = repository;
}
@Override
public List<ChatMessage> getMessages(Object memoryId) {
return List.of();
Message exampleMessage = new Message();
exampleMessage.setConversationId(memoryId.toString());
Collection<Message> messages = repository.findAll(Example.of(exampleMessage));
for (Message message : messages) {
}
return null;
}
@Override

View File

@ -20,9 +20,18 @@ public class Message {
private Long id;
@Column(name = "conversationId", nullable = false, updatable = false)
@Getter
@Setter
private String conversationId;
@Column(name = "messsageType", nullable = false, updatable = false)
@Getter
@Setter
private String messageType;
@Column(name = "content", nullable = false, updatable = false)
@Getter
@Setter
private String content;

View File

@ -0,0 +1,6 @@
package com.clortox.agent.agent.memory.persistence;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MessageRepository extends JpaRepository<Message, Long> {
}

View File

@ -1,19 +0,0 @@
package com.clortox.agent.agent.service;
import dev.langchain4j.model.chat.ChatModel;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class Agent {
private final ChatModel model;
@Autowired
public Agent(
ChatModel model
) {
this.model = model;
}
}

View File

@ -0,0 +1,69 @@
package com.clortox.agent.agent.state;
import com.clortox.agent.tools.ISmartAgentTool;
import dev.langchain4j.data.message.SystemMessage;
import dev.langchain4j.data.message.UserMessage;
import dev.langchain4j.model.chat.ChatModel;
import lombok.extern.slf4j.Slf4j;
import org.bsc.langgraph4j.CompiledGraph;
import org.bsc.langgraph4j.GraphStateException;
import org.bsc.langgraph4j.agentexecutor.AgentExecutor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
import java.util.Map;
import java.util.Optional;
@Slf4j
@Service
public class SmartAgent {
private static final String SYSTEM_PROMPT = """
# Instructions
You have access to tools. When you think you should use one
use it and send back the result of the tool call, if applicable.
# Personality
You are a very smart cute anime girl. You talk like an anime girl.
""";
private final CompiledGraph<AgentExecutor.State> agent;
@Autowired
public SmartAgent(ChatModel model, List<ISmartAgentTool> tools) throws GraphStateException {
log.info("Building SmartAgent");
var agentBuilder = AgentExecutor.builder()
.systemMessage(SystemMessage.from(SYSTEM_PROMPT))
.chatModel(model);
for(ISmartAgentTool tool : tools) {
log.info("Adding tool {}", tool.getClass().getSimpleName());
agentBuilder.toolsFromObject(tool);
}
agent = agentBuilder.build().compile();
log.info("SmartAgent built");
}
/**
* Invoke the agent with a seed message
* @param message
* @return
*/
public String invoke(String message) {
Optional<AgentExecutor.State> finalState = agent.invoke(Map.of(
"messages", UserMessage.from(message)
));
AgentExecutor.State state = finalState.orElseThrow();
return state.finalResponse().orElse("Agent failed");
}
}

View File

@ -0,0 +1,31 @@
package com.clortox.agent.common.config;
import com.clortox.agent.common.dto.ErrorResponseDTO;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.http.HttpRequest;
import org.springframework.http.HttpStatus;
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 {
@ExceptionHandler(Throwable.class)
public ResponseEntity<ErrorResponseDTO> generalError(Throwable t, HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = HttpStatus.INTERNAL_SERVER_ERROR;
return ResponseEntity.status(status)
.body(ErrorResponseDTO.builder()
.path(request.getRequestURI())
.status(status.value())
.message(t.getMessage())
.error(status.getReasonPhrase())
.build()
);
}
}

View File

@ -0,0 +1,17 @@
package com.clortox.agent.common.dto;
import lombok.Builder;
import lombok.Data;
import java.time.Instant;
@Data
@Builder
public class ErrorResponseDTO {
Instant timestamp = Instant.now();
Integer status;
String error;
String message;
String path;
}

View File

@ -0,0 +1,7 @@
package com.clortox.agent.tools;
/**
* Interface to categorize all tools for use with the smart agent
*/
public interface ISmartAgentTool {
}

View File

@ -0,0 +1,19 @@
package com.clortox.agent.tools.dto;
import lombok.Builder;
import lombok.Data;
import java.util.Collection;
@Builder
@Data
public class TenorResult {
public String url;
public Collection<String> tags;
public String description;
public String title;
}

View File

@ -0,0 +1,202 @@
package com.clortox.agent.tools.tenor;
import com.clortox.agent.tools.ISmartAgentTool;
import com.clortox.agent.tools.dto.TenorResult;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestClientException;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponentsBuilder;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.*;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors;
@Service
@Slf4j
public class TenorSearch implements ISmartAgentTool {
@Value("${agent.tools.tenor.api_key}")
private String tenorApiKey;
private static final String TENOR_SEARCH = "https://tenor.googleapis.com/v2/search";
private static final String TENOR_RELATED = "https://tenor.googleapis.com/v2/related";
// constant params required by your ask:
private static final String MEDIA_FILTER = "minimal";
private static final String CONTENTFILTER = "off";
private final RestTemplate http = new RestTemplate();
/**
* Search tenor for a gif
* @param query
* @param results
* @return
*/
@Tool("Search Tenor for GIFs with a text query")
public Collection<TenorResult> search(
@P("Search query") String query,
@P(value = "Number of results to return", required = false) Integer results) {
if(results == null) results = 5;
int limit = normalizeLimit(results);
if (!StringUtils.hasText(tenorApiKey) || !StringUtils.hasText(query)) return List.of();
String url = UriComponentsBuilder.fromHttpUrl(TENOR_SEARCH)
.queryParam("key", tenorApiKey)
.queryParam("q", query)
.queryParam("limit", limit)
.queryParam("media_filter", MEDIA_FILTER)
.queryParam("contentfilter", CONTENTFILTER)
.build(true)
.toUriString();
return executeAndMap(url);
}
/**
* Serach tenor for gifs similar or related to a given tenor
* @param urlOrId
* @param results
* @return
*/
@Tool("Find GIFs related to a given Tenor GIF URL or ID")
public Collection<TenorResult> related(
@P("A Tenor GIF URL or Tenor GIF ID") String urlOrId,
@P(value = "Number of results to return", required = false) Integer results) {
if(results == null) results = 5;
int limit = normalizeLimit(results);
if (!StringUtils.hasText(tenorApiKey) || !StringUtils.hasText(urlOrId)) return List.of();
String id = extractTenorId(urlOrId);
UriComponentsBuilder ucb;
if (id != null) {
ucb = UriComponentsBuilder.fromHttpUrl(TENOR_RELATED)
.queryParam("key", tenorApiKey)
.queryParam("id", id)
.queryParam("limit", limit)
.queryParam("media_filter", MEDIA_FILTER)
.queryParam("contentfilter", CONTENTFILTER);
} else {
// If we can't parse an ID, fall back to a normal search using the string (best-effort)
ucb = UriComponentsBuilder.fromHttpUrl(TENOR_SEARCH)
.queryParam("key", tenorApiKey)
.queryParam("q", urlOrId)
.queryParam("limit", limit)
.queryParam("media_filter", MEDIA_FILTER)
.queryParam("contentfilter", CONTENTFILTER);
}
return executeAndMap(ucb.build(true).toUriString());
}
// ------------------------
// Helpers
// ------------------------
private int normalizeLimit(Integer results) {
return (results == null || results <= 0) ? 10 : Math.min(results, 50);
}
private Collection<TenorResult> executeAndMap(String url) {
try {
TenorApiResponse resp = http.getForObject(url, TenorApiResponse.class);
if (resp == null || resp.results == null) return List.of();
return resp.results.stream()
.map(TenorSearch::mapToDto)
.filter(Objects::nonNull)
.collect(Collectors.toList());
} catch (RestClientException e) {
log.error("Tenor API request failed: {}", e.getMessage(), e);
return List.of();
}
}
private static TenorResult mapToDto(TenorApiResult r) {
if (r == null) return null;
String shareUrl = firstNonBlank(r.itemurl, r.url);
String title = firstNonBlank(r.title, r.content_description);
String desc = firstNonBlank(r.content_description, r.title);
Collection<String> tags = (r.tags == null) ? List.of() : r.tags;
return TenorResult.builder()
.url(shareUrl)
.title(title)
.description(desc)
.tags(tags)
.build();
}
private static String firstNonBlank(String... vals) {
for (String v : vals) if (v != null && !v.isBlank()) return v;
return null;
}
private static String extractTenorId(String input) {
// Direct ID
if (input.matches("\\d+")) return input;
// Try URL forms
try {
URI uri = new URI(input);
String host = Optional.ofNullable(uri.getHost()).orElse("");
if (!host.contains("tenor.com")) return null;
// itemid query param
String query = uri.getQuery();
if (query != null) {
for (String part : query.split("&")) {
int idx = part.indexOf('=');
if (idx > 0) {
String key = part.substring(0, idx);
String val = part.substring(idx + 1);
if ("itemid".equalsIgnoreCase(key) && val.matches("\\d+")) return val;
}
}
}
// trailing -digits in last path segment
String path = Optional.ofNullable(uri.getPath()).orElse("");
String lastSeg = path.substring(path.lastIndexOf('/') + 1);
Matcher m = TRAILING_ID.matcher(lastSeg);
if (m.find()) return m.group(1);
return null;
} catch (URISyntaxException ignored) {
return null;
}
}
private static final Pattern TRAILING_ID = Pattern.compile(".*-(\\d+)$");
/* ===== Minimal Tenor API models ===== */
@Data
static class TenorApiResponse {
List<TenorApiResult> results;
}
@Data
static class TenorApiResult {
String id;
String itemurl; // share page
String url; // sometimes present
String title;
String content_description;
List<String> tags;
// media_formats omitted; you asked for media_filter=minimal + DTO doesn't need direct media links
}
}

View File

@ -2,6 +2,12 @@ spring:
datasource:
url: jdbc:sqlite:${SQLITE_DB_LOCATION:./data/agent.db}
driver-class-name: org.sqlite.JDBC
flyway:
enabled: true
locations: classpath:db/migration
create-schemas: false
baseline-on-migrate: true
baseline-version: 1.0.0.0
jpa:
hibernate:
ddl-auto: none
@ -13,6 +19,7 @@ spring:
logging:
level:
com.clortox.agent: debug
org.hibernate.SQL: warn
org.hibernate.type.descriptor.sql.BasicBinder: warn
@ -22,10 +29,15 @@ springdoc:
agent:
prompt:
smartAgent: "${SMART_AGENT_PROMPT_PATH:./data/smartAgent.md}"
ollama:
baseUrl: "${OLLAMA_BASEURL:http://10.0.3.2:8080}"
model: "${OLLAMA_MODEL:llama3.2}"
model: "${OLLAMA_MODEL:mistral-small3.2}"
temp: "${OLLAMA_TEMP:0.2}"
tools:
tenor:
api_key: "${TOOLS_TENOR_API_KEY:}"
#langchain4j:
# ollama:

View File

@ -1,5 +0,0 @@
CREATE TABLE message (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
conversationId VARCHAR(32) NOT NULL,
content VARCHAR(4096) NOT NULL
)

View File

@ -0,0 +1,19 @@
CREATE TABLE IF NOT EXISTS message (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
conversationId VARCHAR(32) NOT NULL,
messageType VARCHAR(64) NOT NULL CHECK ( messageType IN ( 'SYSTEM', 'AI', 'USER', 'TOOL_RESULT', 'CUSTOM' ) ),
content VARCHAR(4096) NOT NULL
);
CREATE TABLE IF NOT EXISTS EVENT_PUBLICATION (
ID TEXT NOT NULL,
LISTENER_ID TEXT NOT NULL,
EVENT_TYPE TEXT NOT NULL,
SERIALIZED_EVENT TEXT NOT NULL,
PUBLICATION_DATE DATETIME NOT NULL,
COMPLETION_DATE DATETIME DEFAULT NULL,
PRIMARY KEY (ID)
);
CREATE INDEX IF NOT EXISTS EVENT_PUBLICATION_BY_COMPLETION_DATE_IDX
ON EVENT_PUBLICATION (COMPLETION_DATE);

View File

@ -2,7 +2,11 @@ package com.clortox.agent;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Import;
import org.springframework.test.context.ActiveProfiles;
@ActiveProfiles("test")
@Import(FlywayTestConfiguration.class)
@SpringBootTest
class AgentApplicationTests {

View File

@ -0,0 +1,19 @@
package com.clortox.agent;
import org.springframework.boot.autoconfigure.flyway.FlywayMigrationStrategy;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.context.annotation.Bean;
@TestConfiguration
public class FlywayTestConfiguration {
@Bean
FlywayMigrationStrategy cleanMigrate() {
return flyway -> {
flyway.clean();
flyway.migrate();
};
}
}

View File

@ -1,12 +0,0 @@
package com.clortox.agent;
import org.junit.jupiter.api.Test;
import org.springframework.modulith.core.ApplicationModules;
public class ModularityTest {
@Test
public void verify() {
ApplicationModules.of(AgentApplication.class).verify();
}
}

View File

@ -0,0 +1,28 @@
spring:
datasource:
url: jdbc:sqlite:./data/agent-test.db
driver-class-name: org.sqlite.JDBC
hikari:
maximum-pool-size: 1 # SQLite + tests = keep it simple
flyway:
enabled: true
locations: classpath:db/migration
clean-disabled: false # allow clean() in tests if you want
baseline-on-migrate: true
baseline-version: 1.0.0.0
spring.jpa:
hibernate.ddl-auto: none
database-platform: org.hibernate.dialect.SQLiteDialect
agent:
prompt:
smartAgent: "${SMART_AGENT_PROMPT_PATH:./data/smartAgent.md}"
ollama:
baseUrl: "${OLLAMA_BASEURL:http://10.0.3.2:8080}"
model: "${OLLAMA_MODEL:mistral-small3.2}"
temp: "${OLLAMA_TEMP:0.2}"
tools:
tenor:
api_key: "test-key"

Binary file not shown.