diff --git a/.gitignore b/.gitignore index 5516aa3..572cf9f 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,7 @@ replay_pid* frontend/node_modules/ backend/local-db + +# Gradle may extract Spring Boot 4.x spring.factories into the project root +# during dependency resolution; this file is a build artifact, not project source +META-INF/ diff --git a/backend/build.gradle b/backend/build.gradle index cb2eb2c..6aeb463 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -1,6 +1,6 @@ plugins { id 'java' - id 'org.springframework.boot' version '3.5.15' + id 'org.springframework.boot' version '4.1.0' id 'io.spring.dependency-management' version '1.1.7' id 'com.diffplug.spotless' } @@ -26,7 +26,7 @@ repositories { } ext { - set('springAiVersion', '1.1.6') + set('springAiVersion', '2.0.0') } dependencies { @@ -38,12 +38,22 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' implementation 'org.postgresql:postgresql' implementation 'com.h2database:h2' + // Spring Boot 4 extracted FlywayAutoConfiguration out of spring-boot-autoconfigure into the + // dedicated spring-boot-flyway module, which flyway-core does not pull transitively. Without it + // migrations never run (neither at app startup nor in @DataJdbcTest slices). + implementation 'org.springframework.boot:spring-boot-flyway' implementation 'org.flywaydb:flyway-core' implementation 'org.flywaydb:flyway-database-postgresql' implementation 'org.springframework.ai:spring-ai-starter-model-openai' implementation 'org.springframework.ai:spring-ai-starter-model-chat-memory-repository-jdbc' + // Spring Boot 4 makes Jackson 3 (tools.jackson) the auto-configured JSON mapper and demotes + // the retained Jackson 2 modules to runtime-only. Our internal serialization (ChatMemoryService, + // Spring Data JDBC converters, tool-call persistence) still targets Jackson 2, so pull its + // Java-time module back onto the compile classpath. Version is managed by the Jackson 2 BOM. + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' + implementation 'com.google.guava:guava:33.6.0-jre' implementation 'io.github.bonede:tree-sitter:0.26.6' @@ -61,6 +71,10 @@ dependencies { annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' + // Spring Boot 4 extracted the per-slice test support out of spring-boot-test-autoconfigure + // into dedicated spring-boot--test artifacts (@DataJdbcTest, @AutoConfigureTestDatabase). + testImplementation 'org.springframework.boot:spring-boot-data-jdbc-test' + testImplementation 'org.springframework.boot:spring-boot-jdbc-test' testImplementation 'org.springframework.security:spring-security-test' testImplementation 'org.testcontainers:postgresql' testImplementation 'org.testcontainers:junit-jupiter' @@ -70,6 +84,8 @@ dependencies { dependencyManagement { imports { mavenBom "org.springframework.ai:spring-ai-bom:${springAiVersion}" + // Spring Boot 4 no longer manages Testcontainers versions in its BOM, so import it directly. + mavenBom "org.testcontainers:testcontainers-bom:1.21.4" } } diff --git a/backend/gradle.lockfile b/backend/gradle.lockfile index f6a6c4e..479d23e 100644 --- a/backend/gradle.lockfile +++ b/backend/gradle.lockfile @@ -1,96 +1,129 @@ # This is a Gradle generated file for dependency locking. # Manual edits can break the build and are not advised. # This file is expected to be part of source control. +biz.aQute.bnd:biz.aQute.bnd.annotation:7.1.0=compileClasspath,testCompileClasspath ch.qos.logback:logback-classic:1.5.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath ch.qos.logback:logback-core:1.5.34=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.ethlo.time:itu:1.14.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson.core:jackson-annotations:2.21=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson.core:jackson-core:2.21.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml.jackson.core:jackson-databind:2.21.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.fasterxml.jackson.dataformat:jackson-dataformat-toml:2.21.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.21.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.21.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.21.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.21.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.fasterxml.jackson.module:jackson-module-jsonSchema:2.21.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.fasterxml.jackson.module:jackson-module-parameter-names:2.21.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.fasterxml.jackson.module:jackson-module-kotlin:2.21.4=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath com.fasterxml.jackson:jackson-bom:2.21.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.fasterxml:classmate:1.7.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-api:3.4.2=testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-transport-zerodep:3.4.2=testCompileClasspath,testRuntimeClasspath com.github.docker-java:docker-java-transport:3.4.2=testCompileClasspath,testRuntimeClasspath -com.github.victools:jsonschema-generator:4.38.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.github.victools:jsonschema-module-jackson:4.38.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.github.victools:jsonschema-module-swagger-2:4.38.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.victools:jsonschema-generator:5.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.victools:jsonschema-module-jackson:5.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.github.victools:jsonschema-module-swagger-2:5.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.errorprone:error_prone_annotations:2.47.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.guava:failureaccess:1.0.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.guava:guava:33.6.0-jre=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.google.j2objc:j2objc-annotations:3.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.h2database:h2:2.3.232=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.jayway.jsonpath:json-path:2.9.0=testCompileClasspath,testRuntimeClasspath +com.h2database:h2:2.4.240=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.jayway.jsonpath:json-path:2.10.0=testCompileClasspath,testRuntimeClasspath com.knuddels:jtokkit:1.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -com.networknt:json-schema-validator:2.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.networknt:json-schema-validator:3.0.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.openai:openai-java-core:4.39.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.squareup.okhttp3:okhttp:4.12.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio-jvm:3.6.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.squareup.okio:okio:3.6.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testRuntimeClasspath -com.zaxxer:HikariCP:6.3.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +com.zaxxer:HikariCP:7.0.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +commons-logging:commons-logging:1.3.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.github.bonede:tree-sitter-java:0.23.5=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.github.bonede:tree-sitter-javascript:0.25.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.github.bonede:tree-sitter-python:0.25.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.github.bonede:tree-sitter-typescript:0.23.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.github.bonede:tree-sitter:0.26.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.micrometer:context-propagation:1.1.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.micrometer:micrometer-commons:1.15.12=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.micrometer:micrometer-core:1.15.12=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.micrometer:micrometer-jakarta9:1.15.12=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.micrometer:micrometer-observation:1.15.12=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.modelcontextprotocol.sdk:mcp-core:0.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.modelcontextprotocol.sdk:mcp-json-jackson2:0.18.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -io.projectreactor:reactor-core:3.7.19=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.micrometer:context-propagation:1.2.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.micrometer:micrometer-commons:1.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.micrometer:micrometer-core:1.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.micrometer:micrometer-jakarta9:1.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.micrometer:micrometer-observation:1.17.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-buffer:4.2.15.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-codec-base:4.2.15.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-codec-classes-quic:4.2.15.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-codec-compression:4.2.15.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-codec-dns:4.2.15.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-codec-http2:4.2.15.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-codec-http3:4.2.15.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-codec-http:4.2.15.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-codec-native-quic:4.2.15.Final=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +io.netty:netty-codec-socks:4.2.15.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-common:4.2.15.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-handler-proxy:4.2.15.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-handler:4.2.15.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-resolver-dns-classes-macos:4.2.15.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-resolver-dns-native-macos:4.2.15.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-resolver-dns:4.2.15.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-resolver:4.2.15.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-transport-classes-epoll:4.2.15.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-transport-native-epoll:4.2.15.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-transport-native-unix-common:4.2.15.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.netty:netty-transport:4.2.15.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.projectreactor.netty:reactor-netty-core:1.3.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.projectreactor.netty:reactor-netty-http:1.3.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.projectreactor:reactor-core:3.8.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath io.swagger.core.v3:swagger-annotations-jakarta:2.2.38=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +io.swagger.core.v3:swagger-annotations:2.2.31=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.activation:jakarta.activation-api:2.1.4=testCompileClasspath,testRuntimeClasspath -jakarta.annotation:jakarta.annotation-api:2.1.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +jakarta.annotation:jakarta.annotation-api:3.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath jakarta.xml.bind:jakarta.xml.bind-api:4.0.5=testCompileClasspath,testRuntimeClasspath -javax.validation:validation-api:1.1.0.Final=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath junit:junit:4.13.2=testCompileClasspath,testRuntimeClasspath -net.bytebuddy:byte-buddy-agent:1.17.8=testCompileClasspath,testRuntimeClasspath -net.bytebuddy:byte-buddy:1.17.8=testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.18.10=testCompileClasspath,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.18.10=testCompileClasspath,testRuntimeClasspath net.java.dev.jna:jna:5.13.0=testCompileClasspath,testRuntimeClasspath -net.minidev:accessors-smart:2.5.2=testCompileClasspath,testRuntimeClasspath -net.minidev:json-smart:2.5.2=testCompileClasspath,testRuntimeClasspath +net.minidev:accessors-smart:2.6.0=testCompileClasspath,testRuntimeClasspath +net.minidev:json-smart:2.6.0=testCompileClasspath,testRuntimeClasspath org.antlr:ST4:4.3.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.antlr:antlr-runtime:3.5.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.antlr:antlr4-runtime:4.13.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apache.commons:commons-compress:1.24.0=testCompileClasspath,testRuntimeClasspath -org.apache.httpcomponents.client5:httpclient5:5.5.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.httpcomponents.core5:httpcore5-h2:5.3.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.httpcomponents.core5:httpcore5:5.3.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.logging.log4j:log4j-api:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.logging.log4j:log4j-to-slf4j:2.24.3=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.tomcat.embed:tomcat-embed-core:10.1.55=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.tomcat.embed:tomcat-embed-el:10.1.55=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.apache.tomcat.embed:tomcat-embed-websocket:10.1.55=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.httpcomponents.client5:httpclient5:5.6.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.httpcomponents.core5:httpcore5-h2:5.4.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.httpcomponents.core5:httpcore5:5.4.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.logging.log4j:log4j-api:2.25.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.logging.log4j:log4j-to-slf4j:2.25.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.tomcat.embed:tomcat-embed-core:11.0.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.tomcat.embed:tomcat-embed-el:11.0.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.apache.tomcat.embed:tomcat-embed-websocket:11.0.22=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath org.assertj:assertj-core:3.27.7=testCompileClasspath,testRuntimeClasspath -org.awaitility:awaitility:4.2.2=testCompileClasspath,testRuntimeClasspath -org.flywaydb:flyway-core:11.7.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.flywaydb:flyway-database-postgresql:11.7.2=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.awaitility:awaitility:4.3.0=testCompileClasspath,testRuntimeClasspath +org.flywaydb:flyway-core:12.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.flywaydb:flyway-database-postgresql:12.4.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.hamcrest:hamcrest-core:3.0=testCompileClasspath,testRuntimeClasspath org.hamcrest:hamcrest:3.0=testCompileClasspath,testRuntimeClasspath org.hdrhistogram:HdrHistogram:2.2.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-reflect:2.3.21=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-common:2.3.21=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:2.3.21=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:2.3.21=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:2.3.21=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jetbrains:annotations:13.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath org.jetbrains:annotations:17.0.0=testCompileClasspath,testRuntimeClasspath org.jspecify:jspecify:1.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-api:5.12.2=testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter-engine:5.12.2=testRuntimeClasspath -org.junit.jupiter:junit-jupiter-params:5.12.2=testCompileClasspath,testRuntimeClasspath -org.junit.jupiter:junit-jupiter:5.12.2=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-commons:1.12.2=testCompileClasspath,testRuntimeClasspath -org.junit.platform:junit-platform-engine:1.12.2=testRuntimeClasspath -org.junit.platform:junit-platform-launcher:1.12.2=testRuntimeClasspath -org.junit:junit-bom:5.12.2=testCompileClasspath,testRuntimeClasspath -org.latencyutils:LatencyUtils:2.0.3=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath -org.mockito:mockito-core:5.17.0=testCompileClasspath,testRuntimeClasspath -org.mockito:mockito-junit-jupiter:5.17.0=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:6.0.3=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:6.0.3=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:6.0.3=testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter:6.0.3=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-commons:6.0.3=testCompileClasspath,testRuntimeClasspath +org.junit.platform:junit-platform-engine:6.0.3=testRuntimeClasspath +org.junit.platform:junit-platform-launcher:6.0.3=testRuntimeClasspath +org.junit:junit-bom:6.0.3=testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-core:5.23.0=testCompileClasspath,testRuntimeClasspath +org.mockito:mockito-junit-jupiter:5.23.0=testCompileClasspath,testRuntimeClasspath org.objenesis:objenesis:3.3=testRuntimeClasspath org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testRuntimeClasspath +org.osgi:org.osgi.annotation.bundle:2.0.0=compileClasspath,testCompileClasspath +org.osgi:org.osgi.annotation.versioning:1.1.2=compileClasspath,testCompileClasspath +org.osgi:org.osgi.resource:1.0.0=compileClasspath,testCompileClasspath +org.osgi:org.osgi.service.serviceloader:1.0.0=compileClasspath,testCompileClasspath org.ow2.asm:asm:9.7.1=testCompileClasspath,testRuntimeClasspath org.postgresql:postgresql:42.7.11=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.projectlombok:lombok:1.18.46=annotationProcessor,compileClasspath @@ -99,69 +132,97 @@ org.rnorth.duct-tape:duct-tape:1.0.8=testCompileClasspath,testRuntimeClasspath org.skyscreamer:jsonassert:1.5.3=testCompileClasspath,testRuntimeClasspath org.slf4j:jul-to-slf4j:2.0.18=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.slf4j:slf4j-api:2.0.18=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.ai:spring-ai-autoconfigure-model-chat-client:1.1.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.ai:spring-ai-autoconfigure-model-chat-memory-repository-jdbc:1.1.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.ai:spring-ai-autoconfigure-model-chat-memory:1.1.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.ai:spring-ai-autoconfigure-model-chat-observation:1.1.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.ai:spring-ai-autoconfigure-model-embedding-observation:1.1.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.ai:spring-ai-autoconfigure-model-image-observation:1.1.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.ai:spring-ai-autoconfigure-model-openai:1.1.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.ai:spring-ai-autoconfigure-model-tool:1.1.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.ai:spring-ai-autoconfigure-retry:1.1.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.ai:spring-ai-client-chat:1.1.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.ai:spring-ai-commons:1.1.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.ai:spring-ai-model-chat-memory-repository-jdbc:1.1.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.ai:spring-ai-model:1.1.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.ai:spring-ai-openai:1.1.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.ai:spring-ai-retry:1.1.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.ai:spring-ai-starter-model-chat-memory-repository-jdbc:1.1.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.ai:spring-ai-starter-model-openai:1.1.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.ai:spring-ai-template-st:1.1.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-actuator-autoconfigure:3.5.15=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-actuator:3.5.15=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-autoconfigure:3.5.15=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-configuration-processor:3.5.15=annotationProcessor,compileClasspath -org.springframework.boot:spring-boot-starter-actuator:3.5.15=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-starter-data-jdbc:3.5.15=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-starter-jdbc:3.5.15=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-starter-json:3.5.15=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-starter-logging:3.5.15=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-starter-security:3.5.15=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-starter-test:3.5.15=testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-starter-tomcat:3.5.15=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-starter-web:3.5.15=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-starter:3.5.15=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-test-autoconfigure:3.5.15=testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot-test:3.5.15=testCompileClasspath,testRuntimeClasspath -org.springframework.boot:spring-boot:3.5.15=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.data:spring-data-commons:3.5.12=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.data:spring-data-jdbc:3.5.12=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.data:spring-data-relational:3.5.12=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.retry:spring-retry:2.0.13=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.security:spring-security-config:6.5.11=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.security:spring-security-core:6.5.11=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.security:spring-security-crypto:6.5.11=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework.security:spring-security-test:6.5.11=testCompileClasspath,testRuntimeClasspath -org.springframework.security:spring-security-web:6.5.11=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-aop:6.2.19=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-beans:6.2.19=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-context-support:6.2.19=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-context:6.2.19=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-core:6.2.19=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-expression:6.2.19=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-jcl:6.2.19=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-jdbc:6.2.19=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-messaging:6.2.19=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-test:6.2.19=testCompileClasspath,testRuntimeClasspath -org.springframework:spring-tx:6.2.19=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-web:6.2.19=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-webflux:6.2.19=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath -org.springframework:spring-webmvc:6.2.19=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.ai:spring-ai-autoconfigure-model-chat-client:2.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.ai:spring-ai-autoconfigure-model-chat-memory-repository-jdbc:2.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.ai:spring-ai-autoconfigure-model-chat-memory:2.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.ai:spring-ai-autoconfigure-model-chat-observation:2.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.ai:spring-ai-autoconfigure-model-embedding-observation:2.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.ai:spring-ai-autoconfigure-model-image-observation:2.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.ai:spring-ai-autoconfigure-model-openai:2.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.ai:spring-ai-autoconfigure-model-tool:2.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.ai:spring-ai-autoconfigure-retry:2.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.ai:spring-ai-client-chat:2.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.ai:spring-ai-commons:2.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.ai:spring-ai-model-chat-memory-repository-jdbc:2.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.ai:spring-ai-model:2.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.ai:spring-ai-openai:2.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.ai:spring-ai-starter-model-chat-memory-repository-jdbc:2.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.ai:spring-ai-starter-model-openai:2.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.ai:spring-ai-template-st:2.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-actuator-autoconfigure:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-actuator:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-autoconfigure:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-configuration-processor:4.1.0=annotationProcessor,compileClasspath +org.springframework.boot:spring-boot-data-commons:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-data-jdbc-test:4.1.0=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-data-jdbc:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-flyway:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-health:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-http-client:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-http-codec:4.1.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-http-converter:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-jackson:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-jdbc-test:4.1.0=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-jdbc:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-micrometer-metrics:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-micrometer-observation:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-persistence:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-reactor:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-restclient:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-security:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-servlet:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-sql:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-actuator:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-data-jdbc:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-jackson:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-jdbc:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-logging:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-micrometer-metrics:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-restclient:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-security:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-test:4.1.0=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-tomcat-runtime:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-tomcat:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-web:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-webclient:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-starter:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-test-autoconfigure:4.1.0=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-test:4.1.0=testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-tomcat:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-transaction:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-web-server:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-webclient:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot-webmvc:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.boot:spring-boot:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.data:spring-data-commons:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.data:spring-data-jdbc:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.data:spring-data-relational:4.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.security:spring-security-config:7.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.security:spring-security-core:7.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.security:spring-security-crypto:7.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework.security:spring-security-test:7.1.0=testCompileClasspath,testRuntimeClasspath +org.springframework.security:spring-security-web:7.1.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework:spring-aop:7.0.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework:spring-beans:7.0.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework:spring-context-support:7.0.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework:spring-context:7.0.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework:spring-core:7.0.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework:spring-expression:7.0.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework:spring-jdbc:7.0.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework:spring-messaging:7.0.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework:spring-test:7.0.8=testCompileClasspath,testRuntimeClasspath +org.springframework:spring-tx:7.0.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework:spring-web:7.0.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework:spring-webflux:7.0.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.springframework:spring-webmvc:7.0.8=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath org.testcontainers:database-commons:1.21.4=testCompileClasspath,testRuntimeClasspath org.testcontainers:jdbc:1.21.4=testCompileClasspath,testRuntimeClasspath org.testcontainers:junit-jupiter:1.21.4=testCompileClasspath,testRuntimeClasspath org.testcontainers:postgresql:1.21.4=testCompileClasspath,testRuntimeClasspath org.testcontainers:testcontainers:1.21.4=testCompileClasspath,testRuntimeClasspath -org.xmlunit:xmlunit-core:2.10.4=testCompileClasspath,testRuntimeClasspath -org.yaml:snakeyaml:2.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.xmlunit:xmlunit-core:2.11.0=testCompileClasspath,testRuntimeClasspath +org.yaml:snakeyaml:2.6=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +tools.jackson.core:jackson-core:3.1.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +tools.jackson.core:jackson-databind:3.1.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +tools.jackson:jackson-bom:3.1.4=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath empty=developmentOnly,testAndDevelopmentOnly,testAnnotationProcessor diff --git a/backend/src/main/java/io/github/trialiya/kb/advisor/ToolPreparingAdvisor.java b/backend/src/main/java/io/github/trialiya/kb/advisor/ToolPreparingAdvisor.java new file mode 100644 index 0000000..2540169 --- /dev/null +++ b/backend/src/main/java/io/github/trialiya/kb/advisor/ToolPreparingAdvisor.java @@ -0,0 +1,89 @@ +package io.github.trialiya.kb.advisor; + +import static io.github.trialiya.kb.model.chat.dto.ChatEventType.TOOL_PREPARING; + +import io.github.trialiya.kb.service.ChatEventService; +import java.util.Optional; +import java.util.concurrent.atomic.AtomicBoolean; +import org.springframework.ai.chat.client.ChatClientRequest; +import org.springframework.ai.chat.client.ChatClientResponse; +import org.springframework.ai.chat.client.advisor.api.StreamAdvisor; +import org.springframework.ai.chat.client.advisor.api.StreamAdvisorChain; +import org.springframework.ai.chat.memory.ChatMemory; +import org.springframework.ai.chat.metadata.ChatGenerationMetadata; +import org.springframework.ai.chat.model.ChatResponse; +import org.springframework.ai.chat.model.Generation; +import org.springframework.core.Ordered; +import reactor.core.publisher.Flux; + +/** + * Самый внутренний advisor стримингового пути ({@link Ordered#LOWEST_PRECEDENCE} — ближе всего к + * модели). Находится внутри цикла {@link + * org.springframework.ai.chat.client.advisor.ToolCallingAdvisor}: вызывается на каждой итерации и + * видит сырой поток модели до того, как инструмент будет запущен. + * + *

Когда модель заканчивает формировать вызов инструмента (последний чанк несёт {@code + * finishReason=TOOL_CALLS} или {@code hasToolCalls()=true}), публикует в {@link ChatEventService} + * событие {@link io.github.trialiya.kb.model.chat.dto.ChatEventType#TOOL_PREPARING}. Фронт + * показывает «готовлю данные…» с задержкой — быстрые вызовы проходят незаметно. + */ +public class ToolPreparingAdvisor implements StreamAdvisor { + + /** Ключ для передачи runId через advisor-параметры запроса. */ + public static final String RUN_ID_PARAM = "RUN_ID"; + + private final ChatEventService events; + + public ToolPreparingAdvisor(ChatEventService events) { + this.events = events; + } + + @Override + public String getName() { + return "toolPreparingAdvisor"; + } + + @Override + public int getOrder() { + return Ordered.LOWEST_PRECEDENCE; + } + + @Override + public Flux adviseStream( + ChatClientRequest request, StreamAdvisorChain chain) { + final String conversationId = + String.valueOf(request.context().getOrDefault(ChatMemory.CONVERSATION_ID, "?")); + final String runId = String.valueOf(request.context().getOrDefault(RUN_ID_PARAM, "?")); + // Одна публикация на итерацию: модель отдаёт несколько tool-call дельт — + // нам достаточно одного сигнала TOOL_PREPARING. + final AtomicBoolean preparingSent = new AtomicBoolean(false); + + return chain.nextStream(request) + .doOnNext( + response -> { + if (preparingSent.get()) { + return; + } + final ChatResponse cr = response.chatResponse(); + if (cr == null) { + return; + } + final boolean hasToolCalls = cr.hasToolCalls(); + final String finishReason = finishReasonOf(cr); + final boolean toolCallFinish = + "TOOL_CALLS".equalsIgnoreCase(finishReason); + if ((hasToolCalls || toolCallFinish) + && preparingSent.compareAndSet(false, true)) { + events.publish(conversationId, TOOL_PREPARING, runId, null, null); + } + }); + } + + private static String finishReasonOf(ChatResponse response) { + return Optional.ofNullable(response) + .map(ChatResponse::getResult) + .map(Generation::getMetadata) + .map(ChatGenerationMetadata::getFinishReason) + .orElse(null); + } +} diff --git a/backend/src/main/java/io/github/trialiya/kb/config/ChatConfig.java b/backend/src/main/java/io/github/trialiya/kb/config/ChatConfig.java index 521af90..9918ed9 100644 --- a/backend/src/main/java/io/github/trialiya/kb/config/ChatConfig.java +++ b/backend/src/main/java/io/github/trialiya/kb/config/ChatConfig.java @@ -1,5 +1,6 @@ package io.github.trialiya.kb.config; +import io.github.trialiya.kb.advisor.ToolPreparingAdvisor; import io.github.trialiya.kb.config.model.SubAgentConfig; import io.github.trialiya.kb.functions.AttachmentFunction; import io.github.trialiya.kb.functions.DocumentFunction; @@ -10,6 +11,7 @@ import io.github.trialiya.kb.repository.ChatMessageRepository; import io.github.trialiya.kb.repository.ChatTopicRepository; import io.github.trialiya.kb.service.AttachmentService; +import io.github.trialiya.kb.service.ChatEventService; import io.github.trialiya.kb.service.ChatMemoryService; import io.github.trialiya.kb.service.DocumentService; import io.github.trialiya.kb.service.GitService; @@ -23,6 +25,8 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.ai.chat.client.ChatClient; import org.springframework.ai.chat.client.advisor.MessageChatMemoryAdvisor; +import org.springframework.ai.chat.client.advisor.ToolCallingAdvisor; +import org.springframework.ai.chat.client.advisor.api.Advisor; import org.springframework.ai.chat.memory.ChatMemory; import org.springframework.ai.chat.memory.MessageWindowChatMemory; import org.springframework.ai.chat.model.ChatModel; @@ -117,7 +121,8 @@ public ChatClient chatClientBuilder( GitFunction gitFunction, DocumentFunction documentFunction, AttachmentService attachmentService, - ObjectProvider searchAgentService) { + ObjectProvider searchAgentService, + ChatEventService chatEventService) { log.info("Model: {}", chatModel.getDefaultOptions()); List functions = @@ -135,8 +140,29 @@ public ChatClient chatClientBuilder( Stream.of(ToolCallbacks.from(functions.toArray())) .map(RecordingToolCallback::new) .toArray(ToolCallback[]::new); + + // Advisor chain — outermost to innermost (ascending getOrder()): + // + // MessageChatMemoryAdvisor (HIGHEST_PRECEDENCE+200 = MIN+200) — OUTSIDE the loop: + // loads conversation history once before the loop starts and saves only the user + // message + final assistant reply. Tool request/response messages are NOT written to + // the store. This is intentional — our JDBC ChatMemoryRepository does not support + // ToolResponseMessage / tool-call serialization. Matches Spring AI 1.x behaviour. + // + // ToolCallingAdvisor (DEFAULT_ORDER = MIN+300) — drives the tool loop. + // Because MessageChatMemoryAdvisor is OUTSIDE the loop (order < DEFAULT_ORDER), + // ToolCallingAdvisor manages its own internal conversation accumulation across + // iterations and no call to .disableInternalConversationHistory() is needed. + // + // ToolPreparingAdvisor (LOWEST_PRECEDENCE = MAX) — INSIDE the loop: + // called on every iteration; emits TOOL_PREPARING before each tool execution round. + List advisors = new ArrayList<>(); + advisors.add(MessageChatMemoryAdvisor.builder(chatMemory).build()); + advisors.add(ToolCallingAdvisor.builder().toolCallingManager(toolCallingManager).build()); + advisors.add(new ToolPreparingAdvisor(chatEventService)); + return ChatClient.builder(chatModel) - .defaultAdvisors(MessageChatMemoryAdvisor.builder(chatMemory).build()) + .defaultAdvisors(advisors) .defaultSystem(sysPrompt) .defaultToolCallbacks(callbacks) .build(); diff --git a/backend/src/main/java/io/github/trialiya/kb/controller/ChatController.java b/backend/src/main/java/io/github/trialiya/kb/controller/ChatController.java index 5518d67..73f1a18 100644 --- a/backend/src/main/java/io/github/trialiya/kb/controller/ChatController.java +++ b/backend/src/main/java/io/github/trialiya/kb/controller/ChatController.java @@ -249,7 +249,7 @@ public List createMessage( .toolContext(buildContext(conversationId, toolCollector)) .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)); if (resolvedModel != null) { - spec = spec.options(OpenAiChatOptions.builder().model(resolvedModel).build()); + spec = spec.options(OpenAiChatOptions.builder().model(resolvedModel)); } final ChatResponse chatResponse = spec.call().chatResponse(); diff --git a/backend/src/main/java/io/github/trialiya/kb/model/chat/dto/ChatEventType.java b/backend/src/main/java/io/github/trialiya/kb/model/chat/dto/ChatEventType.java index 11db44f..26753df 100644 --- a/backend/src/main/java/io/github/trialiya/kb/model/chat/dto/ChatEventType.java +++ b/backend/src/main/java/io/github/trialiya/kb/model/chat/dto/ChatEventType.java @@ -17,7 +17,8 @@ public enum ChatEventType { /** * Ранний сигнал: модель начала формировать вызов инструмента (генерирует аргументы), но сам * инструмент ещё не запущен и его имя пока недоступно. Без payload — фронт показывает «готовлю - * данные…», если ожидание затягивается. + * данные…», если ожидание затягивается. В данный момент работает не корректно, см. TOOL_PREPARING. */ TOOL_PREPARING, /** Обновление одного вызова инструмента ({@code payload}: {@link ToolCallMessage}). */ diff --git a/backend/src/main/java/io/github/trialiya/kb/service/ChatRunService.java b/backend/src/main/java/io/github/trialiya/kb/service/ChatRunService.java index 26c01a4..6fc27c4 100644 --- a/backend/src/main/java/io/github/trialiya/kb/service/ChatRunService.java +++ b/backend/src/main/java/io/github/trialiya/kb/service/ChatRunService.java @@ -1,5 +1,6 @@ package io.github.trialiya.kb.service; +import static io.github.trialiya.kb.advisor.ToolPreparingAdvisor.RUN_ID_PARAM; import static io.github.trialiya.kb.model.chat.dto.ChatEventType.RUN_DONE; import static io.github.trialiya.kb.model.chat.dto.ChatEventType.RUN_ERROR; import static io.github.trialiya.kb.model.chat.dto.ChatEventType.RUN_STARTED; @@ -7,7 +8,6 @@ import static io.github.trialiya.kb.model.chat.dto.ChatEventType.STREAM; import static io.github.trialiya.kb.model.chat.dto.ChatEventType.TOOL_CALL; import static io.github.trialiya.kb.model.chat.dto.ChatEventType.TOOL_CALLS; -import static io.github.trialiya.kb.model.chat.dto.ChatEventType.TOOL_PREPARING; import static io.github.trialiya.kb.model.chat.dto.ChatEventType.USER_MESSAGE; import io.github.trialiya.kb.model.chat.dto.ChatEventType; @@ -161,14 +161,9 @@ private void run( final String conversationId = handle.conversationId(); final String runId = handle.runId(); final AtomicInteger callIndex = new AtomicInteger(0); - // Защёлка «ранний сигнал отправлен»: пока модель формирует вызов инструмента (генерирует - // аргументы), видимого текста нет. Шлём TOOL_PREPARING один раз на такую паузу; сбрасываем, - // когда инструмент реально стартовал, чтобы следующая пауза снова дала сигнал. - final AtomicBoolean preparing = new AtomicBoolean(false); final Consumer liveSink = payload -> { if (payload instanceof ToolCallMessage tcm) { - preparing.set(false); if (tcm.toolCall().status() != ToolInvocationStatus.STARTED) { chatMemoryService.saveToolCallIncremental( conversationId, @@ -198,9 +193,12 @@ private void run( .toolContext( ChatUtils.buildContext( conversationId, toolCollector, handle.user())) - .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)); + .advisors( + a -> + a.param(ChatMemory.CONVERSATION_ID, conversationId) + .param(RUN_ID_PARAM, runId)); if (resolvedModel != null) { - spec = spec.options(OpenAiChatOptions.builder().model(resolvedModel).build()); + spec = spec.options(OpenAiChatOptions.builder().model(resolvedModel)); } final Disposable disposable = @@ -214,7 +212,6 @@ private void run( runId, buffer, liveSink, - preparing, response), error -> log.error("Stream error {}", conversationId, error), () -> onComplete(handle, toolCollector, liveSink)); @@ -232,7 +229,6 @@ private void onNext( String runId, StringBuffer buffer, Consumer liveSink, - AtomicBoolean preparing, ChatResponse response) { final String chunk = Optional.ofNullable(response) @@ -247,28 +243,9 @@ private void onNext( .map(ChatGenerationMetadata::getFinishReason) .orElse(null); - // Ранний сигнал: модель формирует вызов инструмента — в чанке появились tool-call дельты - // (или пришёл finishReason=TOOL_CALLS), но видимого текста нет. Шлём TOOL_PREPARING один - // раз; как только снова идёт текст — снимаем «подготовку», чтобы не залипал индикатор. - final boolean toolDelta = hasToolCallDelta(response) || "TOOL_CALLS".equals(finishReason); - if (chunk != null && !chunk.isEmpty()) { - preparing.set(false); - } else if (toolDelta && preparing.compareAndSet(false, true)) { - events.publish(conversationId, TOOL_PREPARING, runId, null, null); - } - // Копим ВЕСЬ текст ответа — он понадобится для частичного сохранения при stop/ошибке. - // На нормальном завершении ответ сохраняет advisor (по doOnComplete), а на отмене/ошибке - // (doOnComplete не срабатывает) сохраняем только мы. Поэтому на границе сегмента (модель - // пошла звать инструменты, finishReason=TOOL_CALLS) буфер НЕ сбрасываем — иначе при - // остановке после tool-call потеряются ранние сегменты; вместо сброса ставим разделитель. if (chunk != null && !chunk.isEmpty()) { buffer.append(chunk); } - if ("TOOL_CALLS".equals(finishReason) - && buffer.length() > 0 - && buffer.charAt(buffer.length() - 1) != '\n') { - buffer.append("\n\n"); - } liveSink.accept(new StreamMessage(chunk, finishReason)); printUsageStatistics(conversationId, response, finishReason); } @@ -367,20 +344,6 @@ private void printUsageStatistics( }); } - /** - * Несёт ли чанк стрима данные вызова инструмента: в стриминге OpenAI имя/аргументы инструмента - * приходят отдельными дельтами с пустым текстом — это и есть самый ранний момент, когда видно, - * что модель готовит вызов (ещё до finishReason=TOOL_CALLS и до запуска самого инструмента). - */ - private static boolean hasToolCallDelta(ChatResponse response) { - return Optional.ofNullable(response) - .map(ChatResponse::getResult) - .map(Generation::getOutput) - .map(AssistantMessage::getToolCalls) - .map(calls -> !calls.isEmpty()) - .orElse(false); - } - private static ChatEventType eventType(Object payload) { return switch (payload) { case ToolCallMessage _ -> TOOL_CALL; diff --git a/backend/src/main/java/io/github/trialiya/kb/service/SearchAgentService.java b/backend/src/main/java/io/github/trialiya/kb/service/SearchAgentService.java index 14b19c1..051c5d2 100644 --- a/backend/src/main/java/io/github/trialiya/kb/service/SearchAgentService.java +++ b/backend/src/main/java/io/github/trialiya/kb/service/SearchAgentService.java @@ -111,7 +111,6 @@ public SearchAgentResult run( .model(config.modelId()) .maxTokens(config.maxTokens()) .temperature(0.0) - .internalToolExecutionEnabled(false) // we drive the loop ourselves .toolCallbacks(toolCallbacks) .toolContext(buildContext(conversationId)) .build(); diff --git a/backend/src/main/resources/application.yaml b/backend/src/main/resources/application.yaml index 109e985..d2045ad 100644 --- a/backend/src/main/resources/application.yaml +++ b/backend/src/main/resources/application.yaml @@ -140,8 +140,9 @@ kb: base-url: ${CONFLUENCE_BASE_URL:} api-token: ${CONFLUENCE_TOKEN:} -#logging: -# level: +logging: + level: + root: INFO # org: # springframework: # jdbc: diff --git a/backend/src/test/java/io/github/trialiya/kb/ChatModelClientIT.java b/backend/src/test/java/io/github/trialiya/kb/ChatModelClientIT.java index ffa4d05..c5ff35c 100644 --- a/backend/src/test/java/io/github/trialiya/kb/ChatModelClientIT.java +++ b/backend/src/test/java/io/github/trialiya/kb/ChatModelClientIT.java @@ -31,8 +31,8 @@ import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.data.jdbc.test.autoconfigure.DataJdbcTest; +import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase; import org.springframework.context.annotation.Import; /** @@ -79,8 +79,10 @@ void selectedModelReachesModelLayerAndReplyIsPersisted() { // ── модель-заглушка ──────────────────────────────────────────────── ChatModel chatModel = mock(ChatModel.class); - // ChatClient.builder копирует defaultOptions модели — не отдаём null + // ChatClient.builder копирует опции модели — не отдаём null. Spring AI 2.0.0 читает + // getOptions() в DefaultChatClientUtils, поэтому мокаем оба геттера опций. when(chatModel.getDefaultOptions()).thenReturn(OpenAiChatOptions.builder().build()); + when(chatModel.getOptions()).thenReturn(OpenAiChatOptions.builder().build()); when(chatModel.call(any(Prompt.class))) .thenReturn( new ChatResponse( @@ -96,7 +98,7 @@ void selectedModelReachesModelLayerAndReplyIsPersisted() { chatClient .prompt() .user("Привет, модель") - .options(OpenAiChatOptions.builder().model("gpt-test").build()) + .options(OpenAiChatOptions.builder().model("gpt-test")) .advisors(a -> a.param(ChatMemory.CONVERSATION_ID, conversationId)) .call() .content(); diff --git a/backend/src/test/java/io/github/trialiya/kb/PostgresChatMemoryIT.java b/backend/src/test/java/io/github/trialiya/kb/PostgresChatMemoryIT.java index b397505..c522a21 100644 --- a/backend/src/test/java/io/github/trialiya/kb/PostgresChatMemoryIT.java +++ b/backend/src/test/java/io/github/trialiya/kb/PostgresChatMemoryIT.java @@ -22,8 +22,8 @@ import org.springframework.ai.chat.messages.MessageType; import org.springframework.ai.chat.messages.UserMessage; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.data.jdbc.test.autoconfigure.DataJdbcTest; +import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase; import org.springframework.context.annotation.Import; /** diff --git a/backend/src/test/java/io/github/trialiya/kb/PostgresDocumentIT.java b/backend/src/test/java/io/github/trialiya/kb/PostgresDocumentIT.java index 98d5824..1a7810a 100644 --- a/backend/src/test/java/io/github/trialiya/kb/PostgresDocumentIT.java +++ b/backend/src/test/java/io/github/trialiya/kb/PostgresDocumentIT.java @@ -23,8 +23,8 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.data.jdbc.test.autoconfigure.DataJdbcTest; +import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase; import org.springframework.context.annotation.Import; /** diff --git a/backend/src/test/java/io/github/trialiya/kb/ToolCallRepositoryIT.java b/backend/src/test/java/io/github/trialiya/kb/ToolCallRepositoryIT.java index d7c4204..a7ffa4f 100644 --- a/backend/src/test/java/io/github/trialiya/kb/ToolCallRepositoryIT.java +++ b/backend/src/test/java/io/github/trialiya/kb/ToolCallRepositoryIT.java @@ -19,8 +19,8 @@ import java.util.UUID; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.data.jdbc.test.autoconfigure.DataJdbcTest; +import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase; import org.springframework.context.annotation.Import; /** diff --git a/backend/src/test/java/io/github/trialiya/kb/service/DocumentServiceUnitTest.java b/backend/src/test/java/io/github/trialiya/kb/service/DocumentServiceUnitTest.java index 86dfd85..fd603fb 100644 --- a/backend/src/test/java/io/github/trialiya/kb/service/DocumentServiceUnitTest.java +++ b/backend/src/test/java/io/github/trialiya/kb/service/DocumentServiceUnitTest.java @@ -18,8 +18,10 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.boot.test.autoconfigure.data.jdbc.DataJdbcTest; -import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.data.jdbc.test.autoconfigure.DataJdbcTest; +import org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration; +import org.springframework.boot.jdbc.test.autoconfigure.AutoConfigureTestDatabase; import org.springframework.context.annotation.Import; import org.springframework.http.HttpStatus; import org.springframework.test.context.ActiveProfiles; @@ -46,6 +48,9 @@ "spring.data.jdbc.dialect=postgresql", }) @AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +// Spring Boot 4 dropped Flyway from the @DataJdbcTest slice imports, so pull it in explicitly — +// these tests run the real db/migration-h2 schema. +@ImportAutoConfiguration(FlywayAutoConfiguration.class) @Import({CommonConfig.class}) class DocumentServiceUnitTest { @@ -345,7 +350,7 @@ void rejectsAfterIdFromAnotherLevel() { .satisfies( t -> assertThat(statusOf(t)) - .isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY)); + .isEqualTo(HttpStatus.UNPROCESSABLE_CONTENT)); } @Test @@ -368,7 +373,7 @@ void rejectsNonFolderTarget() { .satisfies( t -> assertThat(statusOf(t)) - .isEqualTo(HttpStatus.UNPROCESSABLE_ENTITY)); + .isEqualTo(HttpStatus.UNPROCESSABLE_CONTENT)); } @Test diff --git a/backend/src/test/java/io/github/trialiya/kb/service/SearchAgentServiceIT.java b/backend/src/test/java/io/github/trialiya/kb/service/SearchAgentServiceIT.java index da2fec0..f9e7078 100644 --- a/backend/src/test/java/io/github/trialiya/kb/service/SearchAgentServiceIT.java +++ b/backend/src/test/java/io/github/trialiya/kb/service/SearchAgentServiceIT.java @@ -140,8 +140,9 @@ void iterationCap_forcesToolLessSummaryCall() { Prompt finalCall = prompts.getAllValues().get(3); // The summarization call offers no tools (model cannot ask for more) and carries the - // budget-reached instruction as its last user message. - assertThat(((OpenAiChatOptions) finalCall.getOptions()).getToolCallbacks()).isEmpty(); + // budget-reached instruction as its last user message. Spring AI 2.0.0 leaves an unset + // tool-callback list as null (1.x returned an empty list); both mean "no tools". + assertThat(((OpenAiChatOptions) finalCall.getOptions()).getToolCallbacks()).isNullOrEmpty(); assertThat(lastUserText(finalCall)).contains("Лимит шагов поиска исчерпан"); } diff --git a/backend/src/test/java/io/github/trialiya/kb/support/AbstractPostgresIntegrationTest.java b/backend/src/test/java/io/github/trialiya/kb/support/AbstractPostgresIntegrationTest.java index b9081bd..fdf9c72 100644 --- a/backend/src/test/java/io/github/trialiya/kb/support/AbstractPostgresIntegrationTest.java +++ b/backend/src/test/java/io/github/trialiya/kb/support/AbstractPostgresIntegrationTest.java @@ -1,5 +1,7 @@ package io.github.trialiya.kb.support; +import org.springframework.boot.autoconfigure.ImportAutoConfiguration; +import org.springframework.boot.flyway.autoconfigure.FlywayAutoConfiguration; import org.springframework.test.context.DynamicPropertyRegistry; import org.springframework.test.context.DynamicPropertySource; import org.testcontainers.containers.PostgreSQLContainer; @@ -21,6 +23,10 @@ *

Логин/пароль/имя БД совпадают с продовыми ({@code knowledgebase}), чтобы отрабатывал, в * частности, {@code ALTER TABLE attachments OWNER TO knowledgebase} из V1. */ +// Spring Boot 4 dropped Flyway from the @DataJdbcTest slice imports (and @OverrideAutoConfiguration +// disables everything not explicitly imported), so pull FlywayAutoConfiguration in for subclasses — +// otherwise migrations never run and the schema is empty. +@ImportAutoConfiguration(FlywayAutoConfiguration.class) public abstract class AbstractPostgresIntegrationTest { protected static final PostgreSQLContainer POSTGRES = diff --git a/docs/todo/tool-preparing.md b/docs/todo/tool-preparing.md new file mode 100644 index 0000000..d2046cc --- /dev/null +++ b/docs/todo/tool-preparing.md @@ -0,0 +1,109 @@ +# TOOL_PREPARING — ранний сигнал о вызове инструмента + +## Что это + +Событие SSE-потока, которое бэкенд должен отправлять **до** `TOOL_CALL`, чтобы фронтенд мог +показать индикатор «готовлю данные…» во время тихой паузы между последним текстовым токеном и +стартом инструмента. + +## Текущее состояние + +**Отключено.** Событие приходит вплотную к `TOOL_CALL` — зазора нет, индикатор никогда не успевает +появиться. Обработчик в `chatEventReducer.js` закомментирован. + +## Почему не работает + +### Архитектура стрима в Spring AI 2.0 + openai-java 4.x + +`OpenAiChatModel.internalStream` получает от `OpenAIClientAsync` сырой +`AsyncStreamResponse` — поток дельт, где каждая дельта содержит либо кусок +текста, либо фрагмент аргументов tool call. Внутри `internalStream` стоит буфер: + +```java +AtomicBoolean isInsideTool = new AtomicBoolean(false); +chunks + .doOnNext(chunk -> { if (ChunkMerger.hasToolCall(chunk)) isInsideTool.set(true); }) + .bufferUntil(chunk -> { + if (isInsideTool.get() && ChunkMerger.toolCallsDone(chunk)) { + isInsideTool.set(false); + return true; // буфер сбрасывается одним элементом + } + return !isInsideTool.get(); + }) + .map(ChunkMerger::mergeChunks) + .map(ChunkMerger::chunkToChatCompletion); +``` + +Как только приходит первый tool-дельта, `bufferUntil` начинает копить все последующие чанки и не +выдаёт ничего наружу, пока не придёт `toolCallsDone`. После этого `ChunkMerger` склеивает их в +**один** `ChatCompletion` с полными аргументами. + +Всё, что находится выше (advisor-chain, `ChatRunService`, observation) видит только этот +агрегированный чанк. К моменту его появления `ToolCallingAdvisor` немедленно запускает +инструмент — `TOOL_PREPARING` и `TOOL_CALL` уходят в SSE практически одновременно. + +### Проверенные точки расширения + +| Точка | Проблема | +|---|---| +| `StreamAdvisor` (`ToolPreparingAdvisor`, `LOWEST_PRECEDENCE`) | Видит чанки уже после `bufferUntil` — первый чанк с `hasToolCalls()=true` уже содержит полные аргументы | +| `ChatModelObservationConvention` / `ObservationHandler` | Spring AI вызывает `setResponse` один раз по завершении, а не на каждом чанке; `requestTools` содержит **все** зарегистрированные инструменты, а не выбранный | +| `OpenAIClientAsync.withOptions(...)` | Фасад клиентских сервисов, не поток — подписаться не на что | + +### Единственный реальный хук + +`AsyncStreamResponse` из `openai-java`: + +```java +// com.openai.core.http.AsyncStreamResponse +interface Handler { + void onNext(T chunk); + default void onComplete(Optional error) {} +} +``` + +Первый tool-дельта (`choice.delta().toolCalls()` непуст, `function.name()` уже есть) приходит +сюда **до** `bufferUntil`. `OpenAiChatModel.Builder.openAiClientAsync(...)` позволяет передать +собственный декоратор клиента, который оборачивает `AsyncStreamResponse`. + +**Почему не реализовано:** корреляция перехваченного события с `conversationId`/`runId` сложна +(callback-поток SDK не несёт Reactor-контекст), единственный чистый путь — прокидывать `runId` +через поле `user` или `metadata` в `ChatCompletionCreateParams`, но это утечка внутреннего id в +OpenAI и дополнительная проводка в `ChatConfig`. Стоимость нетривиальна ради косметического +индикатора. + +## Варианты реализации + +### A. Детекция тишины на фронте (рекомендуется) + +Клиент сам отслеживает паузу между событиями SSE: таймер запускается после каждого `STREAM`, +сбрасывается при следующем событии. Если тишина длится ≥ 600–800 мс — показываем индикатор. + +- Плюсы: ловит **реальную** паузу, не зависит от бэкенда, минимум кода +- Минусы: порог подбирается эмпирически; не знает имени инструмента + +Вся обвязка (`preparing`-флаг, `ToolPreparingIndicator`, `clearPreparing`) уже есть — меняется +только триггер: вместо бэкенд-события запускается клиентский таймер тишины. + +### B. Декоратор `OpenAIClientAsync` + +Оборачиваем реальный клиент, перехватываем первый tool-дельта в `Handler.onNext`, публикуем +`TOOL_PREPARING` с именем инструмента. Декоратор передаётся через +`OpenAiChatModel.Builder.openAiClientAsync(...)`. + +- Плюсы: реальный ранний сигнал + имя инструмента до начала аргументов +- Минусы: OpenAI-специфично; корреляция с `runId` требует хака (поле `user`/`metadata`); + декоратор покрывает большой интерфейс + +### C. Детекция первого дельта через `internalStream` (не рекомендуется) + +Патч или переопределение `OpenAiChatModel`, чтобы добавить `doOnNext` до `bufferUntil`. +Теряется вся framework-обвязка при обновлении Spring AI. + +## Связанные файлы + +- `backend/src/main/java/io/github/trialiya/kb/advisor/ToolPreparingAdvisor.java` — существующий advisor, публикует событие когда видит `hasToolCalls()`; остаётся рабочим, просто сигнал поздний +- `backend/src/main/java/io/github/trialiya/kb/config/ChatConfig.java` — регистрирует `ToolPreparingAdvisor` как самый внутренний advisor (LOWEST_PRECEDENCE) +- `frontend/src/components/chatPanel/chatEventReducer.js` — обработчик `TOOL_PREPARING` отключён (no-op) +- `frontend/src/components/chatPanel/Message.jsx` — `ToolPreparingIndicator` + `showPreparing`-логика готовы к использованию +- `docs/проект/диагностика-tool-preparing-стриминг.md` — история диагностики (Spring AI 1.1.x) diff --git "a/docs/todo/\320\264\320\270\320\260\320\263\320\275\320\276\321\201\321\202\320\270\320\272\320\260-tool-preparing-\321\201\321\202\321\200\320\270\320\274\320\270\320\275\320\263.md" "b/docs/todo/\320\264\320\270\320\260\320\263\320\275\320\276\321\201\321\202\320\270\320\272\320\260-tool-preparing-\321\201\321\202\321\200\320\270\320\274\320\270\320\275\320\263.md" new file mode 100644 index 0000000..be65cf0 --- /dev/null +++ "b/docs/todo/\320\264\320\270\320\260\320\263\320\275\320\276\321\201\321\202\320\270\320\272\320\260-tool-preparing-\321\201\321\202\321\200\320\270\320\274\320\270\320\275\320\263.md" @@ -0,0 +1,88 @@ +# Диагностика: ранний сигнал TOOL_PREPARING при стриминге + +> Временная ветка `claude/clever-ramanujan-oucoiw`. Цель — эмпирически понять, почему ранний +> сигнал `TOOL_PREPARING` (коммит `944e1ac`) не срабатывает, и какой путь решения выбрать. + +## Симптом + +Индикатор «готовлю данные для вызова инструмента…» на фронте не появляется никогда. В потоке +SSE событий `TOOL_PREPARING` отсутствует — есть только `STREAM`, затем сразу `TOOL_CALL`. + +## Где сейчас детектируется (и почему ломается) + +`ChatRunService.onNext` пытается поймать формирование вызова так: + +```java +boolean toolDelta = hasToolCallDelta(response) || "TOOL_CALLS".equals(finishReason); +// hasToolCallDelta = response.getResult().getOutput().getToolCalls() не пуст +``` + +Две причины, по которым это почти наверняка не работает в Spring AI **1.1.x** при стриминге: + +1. **`MessageAggregator` вырезает `toolCalls`** из стримового `AssistantMessage` + (spring-projects/spring-ai#3366, #5167). То есть `getOutput().getToolCalls()` на подписчике + пуст ⇒ `hasToolCallDelta` всегда `false`. +2. **Внутреннее исполнение инструментов** (`internalToolExecutionEnabled=true`, дефолт основного + чата) гоняет цикл tool-calls *внутри* `ChatModel`. Чанк-граница с `finishReason=tool_calls` + может не доходить до подписчика, либо приходить без полезной для нас формы. + +Итог: единственная наблюдаемая точка — реальный старт инструмента (`RecordingToolCallback.call`), +который уже соответствует событию `TOOL_CALL`/`STARTED`. Отдельного «раньше» сигнала нет. + +## Что измеряет эта диагностика + +Включается свойством `kb.diag.stream-tool-calls=true` (env `KB_DIAG_STREAM_TOOL_CALLS`, по +умолчанию `true` на этой ветке). Логирует форму потока в трёх точках: + +| Метка в логе | Точка наблюдения | Отвечает на вопрос | +|---|---|---| +| `subscriber` | `ChatRunService.onNext` | Что реально доходит до приложения (где работает текущий детектор) | +| `advisor` | самый внутренний `StreamAdvisor` (ближе всего к модели) | Можно ли поймать вызов **изменением конфигурации ChatConfig** (через advisor) | +| `>>> TOOL STARTED` | `liveSink`, статус `STARTED` | Опорная точка для замера «тихой паузы» перед вызовом | + +Каждая строка чанка: `textLen`, `empty`, `finishReason`, `hasToolCalls`, `outToolCalls` +(имя + id + длина аргументов по каждому tool call, если они вообще видны). + +## Как запустить + +```bash +# основной чат, фоновая генерация — диагностика включается автоматически (default true) +./gradlew :backend:bootRun +# чтобы видеть КАЖДЫЙ чанк (не только «интересные»): logging.level.io.github.trialiya.kb.diag=DEBUG +``` + +Задайте в чате вопрос, провоцирующий вызов инструмента (например «последние изменения в гит»), +и смотрите лог. + +## Как читать результат + +- **Если `outToolCalls` непуст на `subscriber`** до `>>> TOOL STARTED` — баг #3366 нас не задел, + достаточно починить `hasToolCallDelta` (но это маловероятно на 1.1.x). +- **Если `outToolCalls` пуст на `subscriber`, но непуст на `advisor`** — сигнал можно вытащить + advisor'ом, т.е. **изменением ChatConfig** (вариант с кастомным `StreamAdvisor`). +- **Если `outToolCalls` пуст в обеих точках** — на 1.1.x момент формирования вызова через + публичный API не виден. Тогда варианты ниже. +- Разница таймстампов между последним `subscriber`-чанком с текстом и `>>> TOOL STARTED` + показывает, насколько вообще полезен ранний индикатор (если пауза < ~1 c — фича почти не нужна). + +## Варианты решения + +1. **Spring AI 1.1.x, ручной цикл** — `internalToolExecutionEnabled(false)` + + `ToolCallingManager.executeToolCalls` (как в `SearchAgentService`). Первая точка наблюдения — + `hasToolCalls()` на агрегированном ответе сегмента: аргументы **полные**, не частичные, но это + уже даёт надёжный сигнал «модель собирается звать инструмент» с именем инструмента. Минус: на + стриминге всё ещё мешает #3366 (придётся читать tool calls в обход агрегатора либо делать + tool-решающий ход через `chatModel.call`). +2. **Апгрейд до Spring AI 2.0.0** (релиз 13.06.2026) — `internalToolExecutionEnabled` и + «сломанный» `.streamToolCallResponses(...)` удалены; цикл вынесен в `ToolCallingAdvisor`. + Наблюдать обмен инструментами при стриминге нужно advisor'ом **внутри** цикла + (`order > ToolCallingAdvisor.DEFAULT_ORDER`) — он вызывается на каждой итерации с полной + историей запроса/ответа. Это «правильный» путь: надёжный сигнал на итерации + доступно имя + инструмента. Рекомендуется, если апгрейд по остальным зависимостям приемлем. +3. **Частичные аргументы в реальном времени** — только через низкоуровневый `OpenAiApi` + стрим (delta.tool_calls с кусками аргументов). Большой рефактор, теряются advisors/memory/ + retries. Не оправдано ради косметического индикатора. + +**Рекомендация:** сначала прогнать эту диагностику и зафиксировать факты; затем — вариант 2 +(апгрейд до 2.0 + observer-advisor внутри `ToolCallingAdvisor`) как целевое решение, либо вариант +1, если остаёмся на 1.1.x. Вариант 3 — не делать. diff --git a/frontend/src/components/chatPanel/chatEventReducer.js b/frontend/src/components/chatPanel/chatEventReducer.js index 7479eda..cd4a885 100644 --- a/frontend/src/components/chatPanel/chatEventReducer.js +++ b/frontend/src/components/chatPanel/chatEventReducer.js @@ -108,13 +108,18 @@ export function applyChatEvent(chat, ev, ctx) { return { ...chat, messages: msgs, runId }; } + // TOOL_PREPARING отключён: сигнал приходит вплотную к TOOL_CALL и не даёт раннего + // предупреждения. Причина — OpenAiChatModel.internalStream буферизует все дельты + // tool-call через bufferUntil/ChunkMerger и выдаёт один агрегированный чанк уже + // с полными аргументами; к этому моменту ToolCallingAdvisor тут же запускает + // инструмент. Раннего сигнала ни через advisor, ни через observation получить нельзя — + // единственный доступный хук до буферизации — это AsyncStreamResponse.Handler внутри + // самого клиента openai-java, но корреляция с conversationId там нетривиальна. + // Альтернатива: детекция тишины на фронте (таймер после последнего STREAM-события). + // Подробнее: docs/проект/диагностика-tool-preparing-стриминг.md + // и docs/features/tool-preparing.md case 'TOOL_PREPARING': { - // Ранний сигнал: модель формирует вызов инструмента. Помечаем текущий пузырь — - // компонент сам по таймеру покажет «готовлю данные…», если пауза затянется. - let idx = lastAiIndexForRun(msgs, runId); - if (idx < 0) idx = pushAi(msgs, runId); - msgs[idx] = { ...msgs[idx], preparing: true }; - return { ...chat, messages: msgs, runId }; + return { ...chat, runId }; } case 'STREAM': {