diff --git a/.gitignore b/.gitignore index 264b524..49852a7 100644 --- a/.gitignore +++ b/.gitignore @@ -64,3 +64,4 @@ buildNumber.properties # JDT-specific (Eclipse Java Development Tools) .classpath +/agent/data/agent.db diff --git a/.idea/Agent-Java.iml b/.idea/Agent-Java.iml deleted file mode 100644 index d6ebd48..0000000 --- a/.idea/Agent-Java.iml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml new file mode 100644 index 0000000..688d071 --- /dev/null +++ b/.idea/compiler.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/dataSources.xml b/.idea/dataSources.xml new file mode 100644 index 0000000..494566a --- /dev/null +++ b/.idea/dataSources.xml @@ -0,0 +1,39 @@ + + + + + sqlite.xerial + true + true + $PROJECT_DIR$/agent/src/main/resources/application.yml + org.sqlite.JDBC + jdbc:sqlite:./data/agent.db + $ProjectFileDir$ + + + sqlite.xerial + true + true + $PROJECT_DIR$/agent/src/main/resources/application.yml + org.sqlite.JDBC + jdbc:sqlite:./data/agent.db + $ProjectFileDir$ + + + sqlite.xerial + true + true + org.sqlite.JDBC + jdbc:sqlite:./data/agent.db + $ProjectFileDir$ + + + sqlite.xerial + true + true + org.sqlite.JDBC + jdbc:sqlite:./data/agent.db + $ProjectFileDir$ + + + \ No newline at end of file diff --git a/.idea/encodings.xml b/.idea/encodings.xml new file mode 100644 index 0000000..608ba93 --- /dev/null +++ b/.idea/encodings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/jarRepositories.xml b/.idea/jarRepositories.xml new file mode 100644 index 0000000..712ab9d --- /dev/null +++ b/.idea/jarRepositories.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 6f29fee..76a772f 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,14 @@ - + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index 0230b78..417e50d 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -3,6 +3,7 @@ + \ No newline at end of file diff --git a/.idea/runConfigurations/AgentApplication.xml b/.idea/runConfigurations/AgentApplication.xml new file mode 100644 index 0000000..369831b --- /dev/null +++ b/.idea/runConfigurations/AgentApplication.xml @@ -0,0 +1,17 @@ + + + + \ No newline at end of file diff --git a/.idea/sqldialects.xml b/.idea/sqldialects.xml new file mode 100644 index 0000000..39b8559 --- /dev/null +++ b/.idea/sqldialects.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..8ecd757 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,38 @@ +# Repository Guidelines + +## Project Structure & Modules +- `src/main/java/com/clortox/agent`: Spring Boot app code (`AgentApplication`, support classes). +- `src/main/resources`: Config (e.g., `application.yml`). SQLite DB path: `./data/agent.db`. +- `src/test/java/com/clortox/agent`: JUnit 5 tests (Spring test support). +- `target/`: Build outputs. Not checked in. +- `data/`: Created at startup for local DB. Do not commit. + +## Build, Test, and Run +- Build: `./mvnw clean verify` — compile, run tests, package the app. +- Package (skip tests): `./mvnw -DskipTests package` — produces JAR in `target/`. +- Run locally: `./mvnw spring-boot:run` — starts the API with SQLite. +- Run JAR: `java -jar target/agent-0.0.1-SNAPSHOT.jar` — same as above. +- Java version: JDK 21 is enforced by the build (enforcer plugin). + +## Coding Style & Naming +- Language: Java 21. Indentation: 4 spaces; 120-col soft wrap. +- Packages: `com.clortox.agent.*`. Class names are PascalCase; methods/fields camelCase. +- Spring components: annotate appropriately (`@RestController`, `@Service`, `@Entity`). Favor constructor injection. +- Lombok: allowed where present (e.g., `@Getter`, `@RequiredArgsConstructor`). Avoid overuse that obscures intent. + +## Testing Guidelines +- Frameworks: JUnit 5 with Spring Boot test starter; Modulith test starter is available. +- Naming: `*Tests.java` per class or feature (e.g., `AgentApplicationTests`). +- Run tests: `./mvnw test`. Prefer slice tests where possible; use `@SpringBootTest` only when needed. +- Aim for meaningful coverage on domain logic; avoid brittle tests against logs or timing. + +## Commit & Pull Requests +- Commits: imperative, concise subject (≤72 chars). Example: `Add SQLite dialect and datasource config`. +- Include context in body: rationale, notable trade-offs, follow-up TODOs. +- PRs: clear description, linked issues (`Fixes #123`), test evidence (logs or screenshots), and manual run steps. +- Ensure: `./mvnw clean verify` passes locally and no files added to `data/` or `target/`. + +## Configuration Tips +- DB location: `application.yml` points to `jdbc:sqlite:./data/agent.db`. Override via CLI: + `./mvnw spring-boot:run -Dspring-boot.run.arguments="--spring.datasource.url=jdbc:sqlite:/tmp/agent.db"`. +- The app creates `./data` at startup; avoid hardcoding absolute paths. diff --git a/README.md b/README.md index df95abc..c7c05bb 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,10 @@ # Agent-Java +- Build and run the app from `agent/`. +- Swagger UI is available once running. + +Quick start: + +- Run: `cd agent && ./mvnw spring-boot:run` +- Swagger UI: `http://localhost:8080/swagger-ui` +- OpenAPI JSON: `http://localhost:8080/v3/api-docs` diff --git a/agent/.sdkmanrc b/agent/.sdkmanrc new file mode 100644 index 0000000..2b8a854 --- /dev/null +++ b/agent/.sdkmanrc @@ -0,0 +1 @@ +java=21.0.4-tem diff --git a/agent/pom.xml b/agent/pom.xml index a864481..485cccb 100644 --- a/agent/pom.xml +++ b/agent/pom.xml @@ -35,10 +35,32 @@ org.springframework.boot spring-boot-starter-data-jpa + + + org.springframework + spring-context + + + + org.xerial + sqlite-jdbc + 3.46.0.0 + + + + org.hibernate.orm + hibernate-community-dialects + org.springframework.boot spring-boot-starter-web + + + org.springdoc + springdoc-openapi-starter-webmvc-ui + 2.6.0 + org.springframework.modulith spring-modulith-starter-core @@ -63,6 +85,48 @@ spring-modulith-starter-test test + + org.apache.httpcomponents.client5 + httpclient5 + + + + + io.projectreactor + reactor-core + 3.7.9 + + + + + dev.langchain4j + langchain4j-ollama-spring-boot-starter + 1.3.0-beta9 + + + dev.langchain4j + langchain4j-spring-boot-starter + 1.3.0-beta9 + + + dev.langchain4j + langchain4j-reactor + 1.3.0-beta9 + + + dev.langchain4j + langchain4j + 1.3.0 + + + + + + + dev.langchain4j + langchain4j-ollama + 1.3.0 + @@ -78,6 +142,27 @@ + + + org.apache.maven.plugins + maven-enforcer-plugin + 3.5.0 + + + enforce-java + + enforce + + + + + [21,) + + + + + + org.apache.maven.plugins maven-compiler-plugin diff --git a/agent/src/main/java/com/clortox/agent/AgentApplication.java b/agent/src/main/java/com/clortox/agent/AgentApplication.java index 2924406..b69ef6e 100644 --- a/agent/src/main/java/com/clortox/agent/AgentApplication.java +++ b/agent/src/main/java/com/clortox/agent/AgentApplication.java @@ -7,6 +7,7 @@ import org.springframework.boot.autoconfigure.SpringBootApplication; public class AgentApplication { public static void main(String[] args) { + DataDirectoryInitializer.createDataDirectory(); SpringApplication.run(AgentApplication.class, args); } diff --git a/agent/src/main/java/com/clortox/agent/DataDirectoryInitializer.java b/agent/src/main/java/com/clortox/agent/DataDirectoryInitializer.java new file mode 100644 index 0000000..0a8a390 --- /dev/null +++ b/agent/src/main/java/com/clortox/agent/DataDirectoryInitializer.java @@ -0,0 +1,29 @@ +package com.clortox.agent; + +import java.io.UncheckedIOException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; + +/** + * Ensures the application's data directory exists before Spring Boot starts. + */ +public final class DataDirectoryInitializer { + + private static final String DATA_DIR = "data"; + + private DataDirectoryInitializer() {} + + public static void createDataDirectory() { + Path path = Paths.get(DATA_DIR); + try { + if (Files.notExists(path)) { + Files.createDirectories(path); + } + } catch (IOException e) { + throw new UncheckedIOException("Failed to create data directory: " + path.toAbsolutePath(), e); + } + } +} + diff --git a/agent/src/main/java/com/clortox/agent/agent/IAgent.java b/agent/src/main/java/com/clortox/agent/agent/IAgent.java new file mode 100644 index 0000000..6f560ea --- /dev/null +++ b/agent/src/main/java/com/clortox/agent/agent/IAgent.java @@ -0,0 +1,4 @@ +package com.clortox.agent.agent; + +public interface IAgent { +} 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 new file mode 100644 index 0000000..dc40643 --- /dev/null +++ b/agent/src/main/java/com/clortox/agent/agent/controllers/AgentController.java @@ -0,0 +1,17 @@ +package com.clortox.agent.agent.controllers; + +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 { + + + @GetMapping(path = "/assistant") + public ResponseEntity get(String message) { + return ResponseEntity.status(HttpStatus.OK).body("lol"); + } +} diff --git a/agent/src/main/java/com/clortox/agent/agent/llm/LLMConfiguration.java b/agent/src/main/java/com/clortox/agent/agent/llm/LLMConfiguration.java new file mode 100644 index 0000000..29feefa --- /dev/null +++ b/agent/src/main/java/com/clortox/agent/agent/llm/LLMConfiguration.java @@ -0,0 +1,49 @@ +package com.clortox.agent.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 { + + @Bean + public ChatModel getOllama( + @Value("${agent.ollama.baseUrl}") String baseUrl, + @Value("${agent.ollama.model}") String model, + @Value("${agent.ollama.temp}") Double temp + ) { + + RestClient.Builder restClientBuilder = RestClient.builder() + .requestFactory(new HttpComponentsClientHttpRequestFactory()); + + SpringRestClientBuilder springRestClientBuilder = SpringRestClient.builder() + .restClientBuilder(restClientBuilder) + .streamingRequestExecutor(new VirtualThreadTaskExecutor()); + + return OllamaChatModel.builder() + .baseUrl(baseUrl) + .httpClientBuilder(springRestClientBuilder) + .modelName(model) + .temperature(temp) + .maxRetries(3) + .returnThinking(false) + .logResponses(true) + .logResponses(true) + .build(); + } +} diff --git a/agent/src/main/java/com/clortox/agent/agent/memory/PersistentChatMemoryStore.java b/agent/src/main/java/com/clortox/agent/agent/memory/PersistentChatMemoryStore.java new file mode 100644 index 0000000..43eb967 --- /dev/null +++ b/agent/src/main/java/com/clortox/agent/agent/memory/PersistentChatMemoryStore.java @@ -0,0 +1,24 @@ +package com.clortox.agent.agent.memory; + +import dev.langchain4j.data.message.ChatMessage; +import dev.langchain4j.store.memory.chat.ChatMemoryStore; + +import java.util.List; + +public class PersistentChatMemoryStore implements ChatMemoryStore { + + @Override + public List getMessages(Object memoryId) { + return List.of(); + } + + @Override + public void updateMessages(Object memoryId, List messages) { + + } + + @Override + public void deleteMessages(Object memoryId) { + + } +} diff --git a/agent/src/main/java/com/clortox/agent/agent/memory/persistence/Message.java b/agent/src/main/java/com/clortox/agent/agent/memory/persistence/Message.java new file mode 100644 index 0000000..489bf2d --- /dev/null +++ b/agent/src/main/java/com/clortox/agent/agent/memory/persistence/Message.java @@ -0,0 +1,30 @@ +package com.clortox.agent.agent.memory.persistence; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import lombok.*; + +@Entity +@Table(name = "message") +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class Message { + + @Id + @Getter + @Setter + @Column(name = "ID", nullable = false, updatable = false) + private Long id; + + @Column(name = "conversationId", nullable = false, updatable = false) + private String conversationId; + + @Column(name = "content", nullable = false, updatable = false) + private String content; + + + +} diff --git a/agent/src/main/java/com/clortox/agent/agent/service/Agent.java b/agent/src/main/java/com/clortox/agent/agent/service/Agent.java new file mode 100644 index 0000000..6a1ab0d --- /dev/null +++ b/agent/src/main/java/com/clortox/agent/agent/service/Agent.java @@ -0,0 +1,19 @@ +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; + } +} diff --git a/agent/src/main/java/com/clortox/agent/starters/StarterListener.java b/agent/src/main/java/com/clortox/agent/starters/StarterListener.java new file mode 100644 index 0000000..2ef14a7 --- /dev/null +++ b/agent/src/main/java/com/clortox/agent/starters/StarterListener.java @@ -0,0 +1,32 @@ +package com.clortox.agent.starters; + +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +@Slf4j +public class StarterListener { + + @EventListener + public void onApplicationReady(ApplicationReadyEvent event) { + + printBanner(); + + } + + private void printBanner() { + log.info(" █████╗ ██████╗ ███████╗███╗ ██╗████████╗"); + log.info("██╔══██╗██╔════╝ ██╔════╝████╗ ██║╚══██╔══╝"); + log.info("███████║██║ ███╗█████╗ ██╔██╗ ██║ ██║"); + log.info("██╔══██║██║ ██║██╔══╝ ██║╚██╗██║ ██║"); + log.info("██║ ██║╚██████╔╝███████╗██║ ╚████║ ██║"); + log.info("╚═╝ ╚═╝ ╚═════╝ ╚══════╝╚═╝ ╚═══╝ ╚═╝"); + + log.info("Welcome to the local agent!"); + + log.info("View documentation at http://localhost:{}/swagger-ui/index.html", 8080); + } +} diff --git a/agent/src/main/resources/application.properties b/agent/src/main/resources/application.properties deleted file mode 100644 index 0b1a488..0000000 --- a/agent/src/main/resources/application.properties +++ /dev/null @@ -1 +0,0 @@ -spring.application.name=agent diff --git a/agent/src/main/resources/application.yml b/agent/src/main/resources/application.yml new file mode 100644 index 0000000..3198954 --- /dev/null +++ b/agent/src/main/resources/application.yml @@ -0,0 +1,37 @@ +spring: + datasource: + url: jdbc:sqlite:${SQLITE_DB_LOCATION:./data/agent.db} + driver-class-name: org.sqlite.JDBC + jpa: + hibernate: + ddl-auto: none + properties: + hibernate: + dialect: org.hibernate.community.dialect.SQLiteDialect + format_sql: true + open-in-view: false + +logging: + level: + org.hibernate.SQL: warn + org.hibernate.type.descriptor.sql.BasicBinder: warn + +springdoc: + swagger-ui: + path: /swagger-ui + + +agent: + ollama: + baseUrl: "${OLLAMA_BASEURL:http://10.0.3.2:8080}" + model: "${OLLAMA_MODEL:llama3.2}" + temp: "${OLLAMA_TEMP:0.2}" + +#langchain4j: +# ollama: +# chat-model: +# base-url: "${OLLAMA_BASEURL:http://10.0.3.2:8080}" +# model-name: "${OLLAMA_MODEL:llama3.2}" +# log-requests: true +# log-responses: true +# return-thinking: false diff --git a/agent/src/main/resources/db/migration/V0__placeholder.sql b/agent/src/main/resources/db/migration/V0__placeholder.sql new file mode 100644 index 0000000..e7bd515 --- /dev/null +++ b/agent/src/main/resources/db/migration/V0__placeholder.sql @@ -0,0 +1,5 @@ +CREATE TABLE message ( + ID INTEGER PRIMARY KEY AUTOINCREMENT, + conversationId VARCHAR(32) NOT NULL, + content VARCHAR(4096) NOT NULL +) \ No newline at end of file diff --git a/agent/src/test/java/com/clortox/agent/ModularityTest.java b/agent/src/test/java/com/clortox/agent/ModularityTest.java new file mode 100644 index 0000000..9743b12 --- /dev/null +++ b/agent/src/test/java/com/clortox/agent/ModularityTest.java @@ -0,0 +1,12 @@ +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(); + } +} diff --git a/data/agent.db b/data/agent.db new file mode 100644 index 0000000..b7c0e37 Binary files /dev/null and b/data/agent.db differ