Save progress
This commit is contained in:
parent
aa593433bc
commit
db13632687
11
.idea/dataSources.xml
generated
11
.idea/dataSources.xml
generated
@ -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
6
.idea/data_source_mapping.xml
generated
Normal 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
2
.idea/sqldialects.xml
generated
@ -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
5
agent/.env.example
Normal 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
3
agent/.gitignore
vendored
@ -31,3 +31,6 @@ build/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
||||
.env
|
||||
data/
|
||||
|
@ -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>
|
||||
|
||||
|
@ -1,4 +0,0 @@
|
||||
package com.clortox.agent.agent;
|
||||
|
||||
public interface IAgent {
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
@ -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;
|
||||
|
||||
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
18
agent/src/main/java/com/clortox/agent/agent/dto/Choice.java
Normal file
18
agent/src/main/java/com/clortox/agent/agent/dto/Choice.java
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
14
agent/src/main/java/com/clortox/agent/agent/dto/Delta.java
Normal file
14
agent/src/main/java/com/clortox/agent/agent/dto/Delta.java
Normal 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;
|
||||
}
|
@ -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;
|
||||
}
|
17
agent/src/main/java/com/clortox/agent/agent/dto/Message.java
Normal file
17
agent/src/main/java/com/clortox/agent/agent/dto/Message.java
Normal 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;
|
||||
}
|
17
agent/src/main/java/com/clortox/agent/agent/dto/Role.java
Normal file
17
agent/src/main/java/com/clortox/agent/agent/dto/Role.java
Normal 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;
|
||||
}
|
21
agent/src/main/java/com/clortox/agent/agent/dto/Usage.java
Normal file
21
agent/src/main/java/com/clortox/agent/agent/dto/Usage.java
Normal 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;
|
||||
}
|
@ -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
|
||||
|
@ -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;
|
||||
|
||||
|
||||
|
@ -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> {
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
@ -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()
|
||||
);
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
@ -0,0 +1,7 @@
|
||||
package com.clortox.agent.tools;
|
||||
|
||||
/**
|
||||
* Interface to categorize all tools for use with the smart agent
|
||||
*/
|
||||
public interface ISmartAgentTool {
|
||||
}
|
@ -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;
|
||||
}
|
@ -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
|
||||
}
|
||||
}
|
@ -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:
|
||||
|
@ -1,5 +0,0 @@
|
||||
CREATE TABLE message (
|
||||
ID INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
conversationId VARCHAR(32) NOT NULL,
|
||||
content VARCHAR(4096) NOT NULL
|
||||
)
|
19
agent/src/main/resources/db/migration/V1.0.0.1__init.sql
Normal file
19
agent/src/main/resources/db/migration/V1.0.0.1__init.sql
Normal 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);
|
@ -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 {
|
||||
|
||||
|
@ -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();
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
28
agent/src/test/resources/application-test.yml
Normal file
28
agent/src/test/resources/application-test.yml
Normal 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"
|
||||
|
BIN
data/agent.db
BIN
data/agent.db
Binary file not shown.
Loading…
x
Reference in New Issue
Block a user