From 1be7a2dbf5f83969c6083fde3109db7f79c84eb7 Mon Sep 17 00:00:00 2001 From: "patrick.whelan@telekom.de" Date: Sat, 17 Jan 2026 17:50:31 +0100 Subject: [PATCH 01/40] feat: update TestCreatorAgent and add TestCaseQuery for fetching test cases --- adl-server/build.gradle.kts | 58 +++++++++++++++++++ adl-server/src/main/kotlin/AdlServer.kt | 14 +++++ .../main/kotlin/agents/TestCreatorAgent.kt | 2 +- .../main/kotlin/inbound/AdlExampleQuery.kt | 5 +- .../kotlin/inbound/AdlTestCreatorMutation.kt | 1 + .../src/main/kotlin/inbound/TestCaseQuery.kt | 30 ++++++++++ adl-server/src/main/resources/assistant.md | 3 +- arc-api/src/main/kotlin/AgentRequest.kt | 6 +- 8 files changed, 113 insertions(+), 6 deletions(-) create mode 100644 adl-server/src/main/kotlin/inbound/TestCaseQuery.kt diff --git a/adl-server/build.gradle.kts b/adl-server/build.gradle.kts index 996e5eb6..14de7d6a 100644 --- a/adl-server/build.gradle.kts +++ b/adl-server/build.gradle.kts @@ -5,12 +5,69 @@ plugins { alias(libs.plugins.ktor) id("sh.ondr.koja") version "0.4.6" + id("org.graalvm.buildtools.native") version "0.11.3" } application { mainClass = "org.eclipse.lmos.adl.server.AdlServerKt" } +graalvmNative { + binaries { + named("main") { + fallback.set(false) + verbose.set(true) + + buildArgs.add("--initialize-at-build-time=ch.qos.logback") + buildArgs.add("--initialize-at-build-time=io.ktor,kotlin") + buildArgs.add("--initialize-at-build-time=org.slf4j.LoggerFactory") + + buildArgs.add("--initialize-at-build-time=org.slf4j.helpers.Reporter") + buildArgs.add("--initialize-at-build-time=kotlinx.io.bytestring.ByteString") + buildArgs.add("--initialize-at-build-time=kotlinx.io.SegmentPool") + + buildArgs.add("--initialize-at-build-time=kotlinx.serialization.json.Json") + buildArgs.add("--initialize-at-build-time=kotlinx.serialization.json.JsonImpl") + buildArgs.add("--initialize-at-build-time=kotlinx.serialization.json.ClassDiscriminatorMode") + buildArgs.add("--initialize-at-build-time=kotlinx.serialization.modules.SerializersModuleKt") + + buildArgs.add("-H:+InstallExitHandlers") + buildArgs.add("-H:+ReportUnsupportedElementsAtRuntime") + buildArgs.add("-H:+ReportExceptionStackTraces") + + imageName.set("graalvm-server") + } + + named("test"){ + fallback.set(false) + verbose.set(true) + + buildArgs.add("--initialize-at-build-time=ch.qos.logback") + buildArgs.add("--initialize-at-build-time=io.ktor,kotlin") + buildArgs.add("--initialize-at-build-time=org.slf4j.LoggerFactory") + + buildArgs.add("--initialize-at-build-time=org.slf4j.helpers.Reporter") + buildArgs.add("--initialize-at-build-time=kotlinx.io.bytestring.ByteString") + buildArgs.add("--initialize-at-build-time=kotlinx.io.SegmentPool") + + buildArgs.add("--initialize-at-build-time=kotlinx.serialization.json.Json") + buildArgs.add("--initialize-at-build-time=kotlinx.serialization.json.JsonImpl") + buildArgs.add("--initialize-at-build-time=kotlinx.serialization.json.ClassDiscriminatorMode") + buildArgs.add("--initialize-at-build-time=kotlinx.serialization.modules.SerializersModuleKt") + + buildArgs.add("-H:+InstallExitHandlers") + buildArgs.add("-H:+ReportUnsupportedElementsAtRuntime") + buildArgs.add("-H:+ReportExceptionStackTraces") + + val path = "${projectDir}/src/test/resources/META-INF/native-image/" + buildArgs.add("-H:ReflectionConfigurationFiles=${path}reflect-config.json") + buildArgs.add("-H:ResourceConfigurationFiles=${path}resource-config.json") + + imageName.set("adl-server") + } + } +} + dependencies { implementation(project(":arc-assistants")) implementation(project(":arc-api")) @@ -20,6 +77,7 @@ dependencies { implementation(libs.ktor.server.cio) implementation(libs.ktor.server.websockets) implementation(libs.ktor.server.static) + implementation(libs.ktor.server.cors) implementation(libs.graphql.kotlin.ktor) implementation("org.jetbrains.kotlinx:kotlinx-coroutines-guava:1.10.2") diff --git a/adl-server/src/main/kotlin/AdlServer.kt b/adl-server/src/main/kotlin/AdlServer.kt index 6aceaf95..fcb94a9d 100644 --- a/adl-server/src/main/kotlin/AdlServer.kt +++ b/adl-server/src/main/kotlin/AdlServer.kt @@ -12,8 +12,11 @@ import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2Embedding import io.ktor.server.application.* import io.ktor.server.cio.* import io.ktor.server.engine.* +import io.ktor.server.plugins.cors.routing.CORS import io.ktor.server.plugins.statuspages.* import io.ktor.server.routing.routing +import io.ktor.http.HttpHeaders +import io.ktor.http.HttpMethod import kotlinx.coroutines.runBlocking import org.eclipse.lmos.adl.server.agents.createAssistantAgent import org.eclipse.lmos.adl.server.agents.createEvalAgent @@ -32,6 +35,7 @@ import org.eclipse.lmos.adl.server.inbound.AdlValidationMutation import org.eclipse.lmos.adl.server.inbound.GlobalExceptionHandler import org.eclipse.lmos.adl.server.inbound.SystemPromptMutation import org.eclipse.lmos.adl.server.repositories.InMemoryTestCaseRepository +import org.eclipse.lmos.adl.server.inbound.TestCaseQuery import org.eclipse.lmos.adl.server.services.ConversationEvaluator import org.eclipse.lmos.adl.server.sessions.InMemorySessions import org.eclipse.lmos.adl.server.templates.TemplateLoader @@ -67,6 +71,16 @@ fun startServer( useCaseStore.close() } + install(CORS) { + allowMethod(HttpMethod.Options) + allowMethod(HttpMethod.Put) + allowMethod(HttpMethod.Delete) + allowMethod(HttpMethod.Patch) + allowHeader(HttpHeaders.Authorization) + allowHeader(HttpHeaders.ContentType) + anyHost() + } + install(GraphQL) { schema { packages = listOf( diff --git a/adl-server/src/main/kotlin/agents/TestCreatorAgent.kt b/adl-server/src/main/kotlin/agents/TestCreatorAgent.kt index 976c660a..95345eed 100644 --- a/adl-server/src/main/kotlin/agents/TestCreatorAgent.kt +++ b/adl-server/src/main/kotlin/agents/TestCreatorAgent.kt @@ -44,7 +44,7 @@ fun createTestCreatorAgent(): ConversationAgent = agents { [ { - "title": "String", + "name": "String", "description": "String", "expected_conversation": [ {"role": "user", "content": [Action] }, diff --git a/adl-server/src/main/kotlin/inbound/AdlExampleQuery.kt b/adl-server/src/main/kotlin/inbound/AdlExampleQuery.kt index dfaa20bd..b8ec2cd7 100644 --- a/adl-server/src/main/kotlin/inbound/AdlExampleQuery.kt +++ b/adl-server/src/main/kotlin/inbound/AdlExampleQuery.kt @@ -33,7 +33,10 @@ class AdlExampleQuery( return UseCaseExample( useCase = useCase, - examples = content.lines().filter { it.isNotBlank() }, + examples = content.lines() + .map { it.trim() } + .filter { it.isNotBlank() && it.startsWith("- ") } + .map { it.substringAfter("-") }, ) } } diff --git a/adl-server/src/main/kotlin/inbound/AdlTestCreatorMutation.kt b/adl-server/src/main/kotlin/inbound/AdlTestCreatorMutation.kt index e5154ff9..2caf4efb 100644 --- a/adl-server/src/main/kotlin/inbound/AdlTestCreatorMutation.kt +++ b/adl-server/src/main/kotlin/inbound/AdlTestCreatorMutation.kt @@ -13,6 +13,7 @@ import org.eclipse.lmos.arc.agents.agent.process import org.eclipse.lmos.arc.assistants.support.usecases.toUseCases import org.eclipse.lmos.arc.core.getOrThrow import org.eclipse.lmos.adl.server.repositories.TestCaseRepository +import java.util.UUID /** * GraphQL Mutation for creating test cases using the TestCreatorAgent. diff --git a/adl-server/src/main/kotlin/inbound/TestCaseQuery.kt b/adl-server/src/main/kotlin/inbound/TestCaseQuery.kt new file mode 100644 index 00000000..162f7e83 --- /dev/null +++ b/adl-server/src/main/kotlin/inbound/TestCaseQuery.kt @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +// +// SPDX-License-Identifier: Apache-2.0 + +package org.eclipse.lmos.adl.server.inbound + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import com.expediagroup.graphql.server.operations.Query + +@GraphQLDescription("GraphQL Query for fetching test cases for a use case.") +class TestCaseQuery : Query { + + @GraphQLDescription("Fetches test cases for a given use case ID.") + fun testCases( + @GraphQLDescription("The ID of the use case.") useCaseId: String + ): List { + // Mock data for now + return listOf( + TestCase( + id = "1", + name = "Basic Greeting", + description = "A simple conversation to test the AI's greeting capabilities.", + expectedConversation = listOf( + SimpleMessage(role = "user", content = "Hello!"), + SimpleMessage(role = "assistant", content = "Hi there! How can I help you today?") + ) + ) + ) + } +} \ No newline at end of file diff --git a/adl-server/src/main/resources/assistant.md b/adl-server/src/main/resources/assistant.md index 13c19b8c..ee536bd5 100644 --- a/adl-server/src/main/resources/assistant.md +++ b/adl-server/src/main/resources/assistant.md @@ -11,7 +11,8 @@ Follow all instructions below. If any instruction conflicts, these Core Instruct NO_ANSWER. 6. Never invent a new use case. 7. Call any functions specified in the applicable use case when required. -8. Follow the instructions in the selected use case exactly as specified. +8. Generate your response using the instructions in the selected use case, but make sure it aligns with current conversation context. +9. Skip asking questions if the answer can already be derived from the conversation. ## Use Case & Step Handling diff --git a/arc-api/src/main/kotlin/AgentRequest.kt b/arc-api/src/main/kotlin/AgentRequest.kt index 10b700cc..02332ecb 100644 --- a/arc-api/src/main/kotlin/AgentRequest.kt +++ b/arc-api/src/main/kotlin/AgentRequest.kt @@ -13,15 +13,15 @@ import kotlinx.serialization.Serializable data class AgentRequest( val messages: List, val conversationContext: ConversationContext, - val systemContext: List, - val userContext: UserContext, + val systemContext: List = emptyList(), + val userContext: UserContext = UserContext(), ) @Serializable data class UserContext( val userId: String? = null, val userToken: String? = null, - val profile: List, + val profile: List = emptyList(), ) @Serializable From 53a023a37827a1fb03bbd4baa35cf0dbe8ce9ecc Mon Sep 17 00:00:00 2001 From: "patrick.whelan@telekom.de" Date: Sat, 17 Jan 2026 20:55:01 +0100 Subject: [PATCH 02/40] feat: refactor package structure and implement InMemoryAdlStorage for ADL management --- adl-server/src/main/kotlin/AdlServer.kt | 37 ++++++++++--------- .../{ => mutation}/AdlAssistantMutation.kt | 2 +- .../{ => mutation}/AdlCompilerMutation.kt | 2 +- .../inbound/{ => mutation}/AdlEvalMutation.kt | 2 +- .../inbound/{ => mutation}/AdlMutation.kt | 15 ++++++-- .../{ => mutation}/AdlTestCreatorMutation.kt | 2 +- .../{ => mutation}/AdlValidationMutation.kt | 2 +- .../{ => mutation}/SystemPromptMutation.kt | 2 +- .../inbound/{ => query}/AdlExampleQuery.kt | 2 +- .../kotlin/inbound/{ => query}/AdlQuery.kt | 11 +++++- .../inbound/{ => query}/TestCaseQuery.kt | 4 +- adl-server/src/main/kotlin/model/Adl.kt | 22 +++++++++++ .../src/main/kotlin/storage/AdlStorage.kt | 10 +++++ .../storage/memory/InMemoryAdlStorage.kt | 26 +++++++++++++ .../AdlValidationMutationTest.kt | 0 .../SystemPromptMutationTest.kt | 0 16 files changed, 110 insertions(+), 29 deletions(-) rename adl-server/src/main/kotlin/inbound/{ => mutation}/AdlAssistantMutation.kt (98%) rename adl-server/src/main/kotlin/inbound/{ => mutation}/AdlCompilerMutation.kt (96%) rename adl-server/src/main/kotlin/inbound/{ => mutation}/AdlEvalMutation.kt (97%) rename adl-server/src/main/kotlin/inbound/{ => mutation}/AdlMutation.kt (77%) rename adl-server/src/main/kotlin/inbound/{ => mutation}/AdlTestCreatorMutation.kt (98%) rename adl-server/src/main/kotlin/inbound/{ => mutation}/AdlValidationMutation.kt (99%) rename adl-server/src/main/kotlin/inbound/{ => mutation}/SystemPromptMutation.kt (97%) rename adl-server/src/main/kotlin/inbound/{ => query}/AdlExampleQuery.kt (96%) rename adl-server/src/main/kotlin/inbound/{ => query}/AdlQuery.kt (90%) rename adl-server/src/main/kotlin/inbound/{ => query}/TestCaseQuery.kt (86%) create mode 100644 adl-server/src/main/kotlin/model/Adl.kt create mode 100644 adl-server/src/main/kotlin/storage/AdlStorage.kt create mode 100644 adl-server/src/main/kotlin/storage/memory/InMemoryAdlStorage.kt rename adl-server/src/test/kotlin/inbound/{ => mutattion}/AdlValidationMutationTest.kt (100%) rename adl-server/src/test/kotlin/inbound/{ => mutattion}/SystemPromptMutationTest.kt (100%) diff --git a/adl-server/src/main/kotlin/AdlServer.kt b/adl-server/src/main/kotlin/AdlServer.kt index fcb94a9d..31c2b937 100644 --- a/adl-server/src/main/kotlin/AdlServer.kt +++ b/adl-server/src/main/kotlin/AdlServer.kt @@ -23,21 +23,20 @@ import org.eclipse.lmos.adl.server.agents.createEvalAgent import org.eclipse.lmos.adl.server.agents.createExampleAgent import org.eclipse.lmos.adl.server.agents.createTestCreatorAgent import org.eclipse.lmos.adl.server.embeddings.QdrantUseCaseEmbeddingsStore -import org.eclipse.lmos.adl.server.inbound.AdlAssistantMutation -import org.eclipse.lmos.adl.server.inbound.AdlCompilerMutation -import org.eclipse.lmos.adl.server.inbound.AdlEvalMutation -import org.eclipse.lmos.adl.server.inbound.AdlExampleQuery -import org.eclipse.lmos.adl.server.inbound.AdlMutation -import org.eclipse.lmos.adl.server.inbound.AdlQuery -import org.eclipse.lmos.adl.server.inbound.AdlTestCreatorMutation -import org.eclipse.lmos.adl.server.inbound.AdlTestQuery -import org.eclipse.lmos.adl.server.inbound.AdlValidationMutation +import org.eclipse.lmos.adl.server.inbound.mutation.AdlAssistantMutation +import org.eclipse.lmos.adl.server.inbound.mutation.AdlCompilerMutation +import org.eclipse.lmos.adl.server.inbound.mutation.AdlEvalMutation +import org.eclipse.lmos.adl.server.inbound.query.AdlExampleQuery +import org.eclipse.lmos.adl.server.inbound.mutation.AdlMutation +import org.eclipse.lmos.adl.server.inbound.query.AdlQuery +import org.eclipse.lmos.adl.server.inbound.mutation.AdlTestCreatorMutation +import org.eclipse.lmos.adl.server.inbound.mutation.AdlValidationMutation import org.eclipse.lmos.adl.server.inbound.GlobalExceptionHandler -import org.eclipse.lmos.adl.server.inbound.SystemPromptMutation -import org.eclipse.lmos.adl.server.repositories.InMemoryTestCaseRepository -import org.eclipse.lmos.adl.server.inbound.TestCaseQuery +import org.eclipse.lmos.adl.server.inbound.mutation.SystemPromptMutation +import org.eclipse.lmos.adl.server.inbound.query.TestCaseQuery import org.eclipse.lmos.adl.server.services.ConversationEvaluator import org.eclipse.lmos.adl.server.sessions.InMemorySessions +import org.eclipse.lmos.adl.server.storage.memory.InMemoryAdlStorage import org.eclipse.lmos.adl.server.templates.TemplateLoader fun startServer( @@ -51,7 +50,7 @@ fun startServer( val sessions = InMemorySessions() val embeddingModel = AllMiniLmL6V2EmbeddingModel() val useCaseStore = QdrantUseCaseEmbeddingsStore(embeddingModel, qdrantConfig) - val testCaseRepository = InMemoryTestCaseRepository() + val adlStorage = InMemoryAdlStorage() // Agents val exampleAgent = createExampleAgent() @@ -87,21 +86,25 @@ fun startServer( "org.eclipse.lmos.adl.server.inbound", "org.eclipse.lmos.adl.server.agents", "org.eclipse.lmos.arc.api", + "org.eclipse.lmos.adl.server.model", ) queries = listOf( - AdlQuery(useCaseStore), + AdlQuery(useCaseStore, adlStorage), AdlExampleQuery(exampleAgent), - AdlTestQuery(testCaseRepository), + TestCaseQuery(), ) mutations = listOf( AdlCompilerMutation(), - AdlMutation(useCaseStore), + AdlMutation(useCaseStore, adlStorage), SystemPromptMutation(sessions, templateLoader), AdlEvalMutation(evalAgent, conversationEvaluator), AdlAssistantMutation(assistantAgent), AdlValidationMutation(), - AdlTestCreatorMutation(testCreatorAgent, testCaseRepository), + AdlTestCreatorMutation(testCreatorAgent), ) + } + server { + } engine { exceptionHandler = GlobalExceptionHandler() diff --git a/adl-server/src/main/kotlin/inbound/AdlAssistantMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/AdlAssistantMutation.kt similarity index 98% rename from adl-server/src/main/kotlin/inbound/AdlAssistantMutation.kt rename to adl-server/src/main/kotlin/inbound/mutation/AdlAssistantMutation.kt index 2a156967..0794d7ab 100644 --- a/adl-server/src/main/kotlin/inbound/AdlAssistantMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/AdlAssistantMutation.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.eclipse.lmos.adl.server.inbound +package org.eclipse.lmos.adl.server.inbound.mutation import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Mutation diff --git a/adl-server/src/main/kotlin/inbound/AdlCompilerMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/AdlCompilerMutation.kt similarity index 96% rename from adl-server/src/main/kotlin/inbound/AdlCompilerMutation.kt rename to adl-server/src/main/kotlin/inbound/mutation/AdlCompilerMutation.kt index 3a03521e..5ad47e04 100644 --- a/adl-server/src/main/kotlin/inbound/AdlCompilerMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/AdlCompilerMutation.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.eclipse.lmos.adl.server.inbound +package org.eclipse.lmos.adl.server.inbound.mutation import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Mutation diff --git a/adl-server/src/main/kotlin/inbound/AdlEvalMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/AdlEvalMutation.kt similarity index 97% rename from adl-server/src/main/kotlin/inbound/AdlEvalMutation.kt rename to adl-server/src/main/kotlin/inbound/mutation/AdlEvalMutation.kt index 2ef554c8..587f9faf 100644 --- a/adl-server/src/main/kotlin/inbound/AdlEvalMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/AdlEvalMutation.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.eclipse.lmos.adl.server.inbound +package org.eclipse.lmos.adl.server.inbound.mutation import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Mutation diff --git a/adl-server/src/main/kotlin/inbound/AdlMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/AdlMutation.kt similarity index 77% rename from adl-server/src/main/kotlin/inbound/AdlMutation.kt rename to adl-server/src/main/kotlin/inbound/mutation/AdlMutation.kt index 33ee049b..b0872360 100644 --- a/adl-server/src/main/kotlin/inbound/AdlMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/AdlMutation.kt @@ -2,22 +2,31 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.eclipse.lmos.adl.server.inbound +package org.eclipse.lmos.adl.server.inbound.mutation import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Mutation import org.eclipse.lmos.adl.server.embeddings.UseCaseEmbeddingsStore +import org.eclipse.lmos.adl.server.model.Adl +import org.eclipse.lmos.adl.server.storage.AdlStorage /** * GraphQL Mutation for storing UseCases in the Embeddings store. */ class AdlMutation( private val useCaseStore: UseCaseEmbeddingsStore, + private val adlStorage: AdlStorage, ) : Mutation { @GraphQLDescription("Stores a UseCase in the embeddings store. Embeddings are generated from the provided examples.") - suspend fun store(adl: String): StorageResult { - val storedCount = useCaseStore.storeUseCase(adl) + suspend fun store( + @GraphQLDescription("Unique identifier for the ADL") id: String, + @GraphQLDescription("The content of the ADL") content: String, + @GraphQLDescription("Tags associated with the ADL") tags: List, + @GraphQLDescription("Timestamp when the ADL was created") createdAt: String + ): StorageResult { + adlStorage.store(Adl(id, content, tags, createdAt)) + val storedCount = useCaseStore.storeUseCase(content) return StorageResult( storedExamplesCount = storedCount, message = "UseCase successfully stored with $storedCount embeddings", diff --git a/adl-server/src/main/kotlin/inbound/AdlTestCreatorMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/AdlTestCreatorMutation.kt similarity index 98% rename from adl-server/src/main/kotlin/inbound/AdlTestCreatorMutation.kt rename to adl-server/src/main/kotlin/inbound/mutation/AdlTestCreatorMutation.kt index 2caf4efb..d8900928 100644 --- a/adl-server/src/main/kotlin/inbound/AdlTestCreatorMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/AdlTestCreatorMutation.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.eclipse.lmos.adl.server.inbound +package org.eclipse.lmos.adl.server.inbound.mutation import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Mutation diff --git a/adl-server/src/main/kotlin/inbound/AdlValidationMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/AdlValidationMutation.kt similarity index 99% rename from adl-server/src/main/kotlin/inbound/AdlValidationMutation.kt rename to adl-server/src/main/kotlin/inbound/mutation/AdlValidationMutation.kt index e57e4b39..8d567e44 100644 --- a/adl-server/src/main/kotlin/inbound/AdlValidationMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/AdlValidationMutation.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.eclipse.lmos.adl.server.inbound +package org.eclipse.lmos.adl.server.inbound.mutation import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Mutation diff --git a/adl-server/src/main/kotlin/inbound/SystemPromptMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/SystemPromptMutation.kt similarity index 97% rename from adl-server/src/main/kotlin/inbound/SystemPromptMutation.kt rename to adl-server/src/main/kotlin/inbound/mutation/SystemPromptMutation.kt index 29a69132..a599739f 100644 --- a/adl-server/src/main/kotlin/inbound/SystemPromptMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/SystemPromptMutation.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.eclipse.lmos.adl.server.inbound +package org.eclipse.lmos.adl.server.inbound.mutation import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Mutation diff --git a/adl-server/src/main/kotlin/inbound/AdlExampleQuery.kt b/adl-server/src/main/kotlin/inbound/query/AdlExampleQuery.kt similarity index 96% rename from adl-server/src/main/kotlin/inbound/AdlExampleQuery.kt rename to adl-server/src/main/kotlin/inbound/query/AdlExampleQuery.kt index b8ec2cd7..4f37a3ef 100644 --- a/adl-server/src/main/kotlin/inbound/AdlExampleQuery.kt +++ b/adl-server/src/main/kotlin/inbound/query/AdlExampleQuery.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.eclipse.lmos.adl.server.inbound +package org.eclipse.lmos.adl.server.inbound.query import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Query diff --git a/adl-server/src/main/kotlin/inbound/AdlQuery.kt b/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt similarity index 90% rename from adl-server/src/main/kotlin/inbound/AdlQuery.kt rename to adl-server/src/main/kotlin/inbound/query/AdlQuery.kt index 7d5c3287..d8af1751 100644 --- a/adl-server/src/main/kotlin/inbound/AdlQuery.kt +++ b/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt @@ -2,18 +2,22 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.eclipse.lmos.adl.server.inbound +package org.eclipse.lmos.adl.server.inbound.query import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Query import org.eclipse.lmos.adl.server.embeddings.QdrantUseCaseEmbeddingsStore import org.eclipse.lmos.adl.server.embeddings.UseCaseSearchResult +import org.eclipse.lmos.adl.server.inbound.SimpleMessage +import org.eclipse.lmos.adl.server.model.Adl +import org.eclipse.lmos.adl.server.storage.AdlStorage /** * GraphQL Query for searching UseCases based on conversation embeddings. */ class AdlQuery( private val useCaseStore: QdrantUseCaseEmbeddingsStore, + private val adlStorage: AdlStorage, ) : Query { @GraphQLDescription("Returns the supported version of the ALD.") @@ -34,6 +38,11 @@ class AdlQuery( return results.toMatches() } + @GraphQLDescription("Returns a list of all stored ADLs.") + suspend fun adls(): List { + return adlStorage.list() + } + @GraphQLDescription("Searches for UseCases using a text query.") suspend fun searchByText( query: String, diff --git a/adl-server/src/main/kotlin/inbound/TestCaseQuery.kt b/adl-server/src/main/kotlin/inbound/query/TestCaseQuery.kt similarity index 86% rename from adl-server/src/main/kotlin/inbound/TestCaseQuery.kt rename to adl-server/src/main/kotlin/inbound/query/TestCaseQuery.kt index 162f7e83..b2b37e99 100644 --- a/adl-server/src/main/kotlin/inbound/TestCaseQuery.kt +++ b/adl-server/src/main/kotlin/inbound/query/TestCaseQuery.kt @@ -2,10 +2,12 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.eclipse.lmos.adl.server.inbound +package org.eclipse.lmos.adl.server.inbound.query import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Query +import org.eclipse.lmos.adl.server.inbound.SimpleMessage +import org.eclipse.lmos.adl.server.inbound.mutation.TestCase @GraphQLDescription("GraphQL Query for fetching test cases for a use case.") class TestCaseQuery : Query { diff --git a/adl-server/src/main/kotlin/model/Adl.kt b/adl-server/src/main/kotlin/model/Adl.kt new file mode 100644 index 00000000..95322236 --- /dev/null +++ b/adl-server/src/main/kotlin/model/Adl.kt @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +// +// SPDX-License-Identifier: Apache-2.0 + +package org.eclipse.lmos.adl.server.model + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription + +@GraphQLDescription("A representation of an Agent Definition Language (ADL) file.") +data class Adl( + @GraphQLDescription("Unique identifier for the ADL") + val id: String, + + @GraphQLDescription("The content of the ADL") + val content: String, + + @GraphQLDescription("Tags associated with the ADL") + val tags: List, + + @GraphQLDescription("Timestamp when the ADL was created") + val createdAt: String +) diff --git a/adl-server/src/main/kotlin/storage/AdlStorage.kt b/adl-server/src/main/kotlin/storage/AdlStorage.kt new file mode 100644 index 00000000..48097e8a --- /dev/null +++ b/adl-server/src/main/kotlin/storage/AdlStorage.kt @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +// +// SPDX-License-Identifier: Apache-2.0 +package org.eclipse.lmos.adl.server.storage +import org.eclipse.lmos.adl.server.model.Adl +interface AdlStorage { + suspend fun store(adl: Adl): Adl + suspend fun get(id: String): Adl? + suspend fun list(): List +} diff --git a/adl-server/src/main/kotlin/storage/memory/InMemoryAdlStorage.kt b/adl-server/src/main/kotlin/storage/memory/InMemoryAdlStorage.kt new file mode 100644 index 00000000..5e5de488 --- /dev/null +++ b/adl-server/src/main/kotlin/storage/memory/InMemoryAdlStorage.kt @@ -0,0 +1,26 @@ +// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +// +// SPDX-License-Identifier: Apache-2.0 + +package org.eclipse.lmos.adl.server.storage.memory + +import org.eclipse.lmos.adl.server.model.Adl +import org.eclipse.lmos.adl.server.storage.AdlStorage +import java.util.concurrent.ConcurrentHashMap + +class InMemoryAdlStorage : AdlStorage { + private val storage = ConcurrentHashMap() + + override suspend fun store(adl: Adl): Adl { + storage[adl.id] = adl + return adl + } + + override suspend fun get(id: String): Adl? { + return storage[id] + } + + override suspend fun list(): List { + return storage.values.toList() + } +} diff --git a/adl-server/src/test/kotlin/inbound/AdlValidationMutationTest.kt b/adl-server/src/test/kotlin/inbound/mutattion/AdlValidationMutationTest.kt similarity index 100% rename from adl-server/src/test/kotlin/inbound/AdlValidationMutationTest.kt rename to adl-server/src/test/kotlin/inbound/mutattion/AdlValidationMutationTest.kt diff --git a/adl-server/src/test/kotlin/inbound/SystemPromptMutationTest.kt b/adl-server/src/test/kotlin/inbound/mutattion/SystemPromptMutationTest.kt similarity index 100% rename from adl-server/src/test/kotlin/inbound/SystemPromptMutationTest.kt rename to adl-server/src/test/kotlin/inbound/mutattion/SystemPromptMutationTest.kt From 93dce5a5e5382bb0d5b979efbc9b3db2457d0ef9 Mon Sep 17 00:00:00 2001 From: "patrick.whelan@telekom.de" Date: Sat, 17 Jan 2026 22:41:26 +0100 Subject: [PATCH 03/40] feat: refactor SimpleMessage import and update AdlStorageMutation to include examples --- adl-server/src/main/kotlin/AdlServer.kt | 4 ++-- .../kotlin/embeddings/ConversationEmbedder.kt | 2 +- .../embeddings/ConversationToTextStrategy.kt | 3 ++- .../embeddings/QdrantUseCaseEmbeddingsStore.kt | 7 ++++--- .../embeddings/UseCaseEmbeddingsStore.kt | 2 +- .../kotlin/inbound/mutation/AdlEvalMutation.kt | 1 + .../{AdlMutation.kt => AdlStorageMutation.kt} | 18 ++++++++++++++---- .../inbound/mutation/AdlTestCreatorMutation.kt | 1 + .../src/main/kotlin/inbound/query/AdlQuery.kt | 4 ++-- .../main/kotlin/inbound/query/TestCaseQuery.kt | 2 +- adl-server/src/main/kotlin/model/Adl.kt | 5 ++++- .../kotlin/{inbound => model}/SimpleMessage.kt | 8 ++------ .../kotlin/services/ConversationEvaluator.kt | 2 +- ...kt => AdlStorageMutationIntegrationTest.kt} | 2 +- 14 files changed, 37 insertions(+), 24 deletions(-) rename adl-server/src/main/kotlin/inbound/mutation/{AdlMutation.kt => AdlStorageMutation.kt} (80%) rename adl-server/src/main/kotlin/{inbound => model}/SimpleMessage.kt (78%) rename adl-server/src/test/kotlin/{AdlMutationIntegrationTest.kt => AdlStorageMutationIntegrationTest.kt} (99%) diff --git a/adl-server/src/main/kotlin/AdlServer.kt b/adl-server/src/main/kotlin/AdlServer.kt index 31c2b937..39077d4f 100644 --- a/adl-server/src/main/kotlin/AdlServer.kt +++ b/adl-server/src/main/kotlin/AdlServer.kt @@ -27,7 +27,7 @@ import org.eclipse.lmos.adl.server.inbound.mutation.AdlAssistantMutation import org.eclipse.lmos.adl.server.inbound.mutation.AdlCompilerMutation import org.eclipse.lmos.adl.server.inbound.mutation.AdlEvalMutation import org.eclipse.lmos.adl.server.inbound.query.AdlExampleQuery -import org.eclipse.lmos.adl.server.inbound.mutation.AdlMutation +import org.eclipse.lmos.adl.server.inbound.mutation.AdlStorageMutation import org.eclipse.lmos.adl.server.inbound.query.AdlQuery import org.eclipse.lmos.adl.server.inbound.mutation.AdlTestCreatorMutation import org.eclipse.lmos.adl.server.inbound.mutation.AdlValidationMutation @@ -95,7 +95,7 @@ fun startServer( ) mutations = listOf( AdlCompilerMutation(), - AdlMutation(useCaseStore, adlStorage), + AdlStorageMutation(useCaseStore, adlStorage), SystemPromptMutation(sessions, templateLoader), AdlEvalMutation(evalAgent, conversationEvaluator), AdlAssistantMutation(assistantAgent), diff --git a/adl-server/src/main/kotlin/embeddings/ConversationEmbedder.kt b/adl-server/src/main/kotlin/embeddings/ConversationEmbedder.kt index ef3efafe..3123f3d9 100644 --- a/adl-server/src/main/kotlin/embeddings/ConversationEmbedder.kt +++ b/adl-server/src/main/kotlin/embeddings/ConversationEmbedder.kt @@ -5,7 +5,7 @@ package org.eclipse.lmos.adl.server.embeddings import dev.langchain4j.model.embedding.EmbeddingModel -import org.eclipse.lmos.adl.server.inbound.SimpleMessage +import org.eclipse.lmos.adl.server.model.SimpleMessage /** * Creates a single embedding for an entire conversation. diff --git a/adl-server/src/main/kotlin/embeddings/ConversationToTextStrategy.kt b/adl-server/src/main/kotlin/embeddings/ConversationToTextStrategy.kt index 79a0d789..c690771d 100644 --- a/adl-server/src/main/kotlin/embeddings/ConversationToTextStrategy.kt +++ b/adl-server/src/main/kotlin/embeddings/ConversationToTextStrategy.kt @@ -4,7 +4,8 @@ package org.eclipse.lmos.adl.server.embeddings -import org.eclipse.lmos.adl.server.inbound.SimpleMessage +import org.eclipse.lmos.adl.server.model.SimpleMessage + /** * Strategy for converting a list of messages into text for embedding. diff --git a/adl-server/src/main/kotlin/embeddings/QdrantUseCaseEmbeddingsStore.kt b/adl-server/src/main/kotlin/embeddings/QdrantUseCaseEmbeddingsStore.kt index ac749f2e..52e33931 100644 --- a/adl-server/src/main/kotlin/embeddings/QdrantUseCaseEmbeddingsStore.kt +++ b/adl-server/src/main/kotlin/embeddings/QdrantUseCaseEmbeddingsStore.kt @@ -16,6 +16,7 @@ import io.qdrant.client.grpc.Points.PointStruct import io.qdrant.client.grpc.Points.ScoredPoint import kotlinx.coroutines.guava.await import org.eclipse.lmos.adl.server.QdrantConfig +import org.eclipse.lmos.adl.server.model.SimpleMessage import org.eclipse.lmos.arc.assistants.support.usecases.toUseCases import java.util.UUID import java.util.concurrent.ExecutionException @@ -64,7 +65,7 @@ class QdrantUseCaseEmbeddingsStore( * @param adl The UseCases to create embeddings from. * @return The number of embeddings stored. */ - override suspend fun storeUseCase(adl: String): Int { + override suspend fun storeUseCase(adl: String, examples: List): Int { val points = mutableListOf() val parsedUseCases = adl.toUseCases() @@ -72,7 +73,7 @@ class QdrantUseCaseEmbeddingsStore( parsedUseCases.forEach { useCase -> deleteByUseCaseId(useCase.id) } parsedUseCases.forEach { useCase -> - val examples = parseExamples(useCase.examples) + val examples = (parseExamples(useCase.examples) + examples).distinct() examples.forEach { example -> val embedding = embeddingModel.embed(example).content().vector() val point = PointStruct.newBuilder() @@ -145,7 +146,7 @@ class QdrantUseCaseEmbeddingsStore( * @return List of matching UseCase embeddings with their scores. */ suspend fun searchByConversation( - messages: List, + messages: List, limit: Int = 5, scoreThreshold: Float = 0.0f, ): List { diff --git a/adl-server/src/main/kotlin/embeddings/UseCaseEmbeddingsStore.kt b/adl-server/src/main/kotlin/embeddings/UseCaseEmbeddingsStore.kt index a734338a..e2865657 100644 --- a/adl-server/src/main/kotlin/embeddings/UseCaseEmbeddingsStore.kt +++ b/adl-server/src/main/kotlin/embeddings/UseCaseEmbeddingsStore.kt @@ -7,7 +7,7 @@ package org.eclipse.lmos.adl.server.embeddings * Interface for storing UseCase embeddings. */ interface UseCaseEmbeddingsStore { - suspend fun storeUseCase(adl: String): Int + suspend fun storeUseCase(adl: String, examples: List = emptyList()): Int suspend fun deleteByUseCaseId(useCaseId: String) suspend fun clear() } diff --git a/adl-server/src/main/kotlin/inbound/mutation/AdlEvalMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/AdlEvalMutation.kt index 587f9faf..d7479016 100644 --- a/adl-server/src/main/kotlin/inbound/mutation/AdlEvalMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/AdlEvalMutation.kt @@ -8,6 +8,7 @@ import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Mutation import kotlinx.serialization.Serializable import org.eclipse.lmos.adl.server.agents.EvalOutput +import org.eclipse.lmos.adl.server.model.SimpleMessage import org.eclipse.lmos.adl.server.services.ConversationEvaluator import org.eclipse.lmos.arc.agents.ConversationAgent import org.eclipse.lmos.arc.agents.agent.process diff --git a/adl-server/src/main/kotlin/inbound/mutation/AdlMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt similarity index 80% rename from adl-server/src/main/kotlin/inbound/mutation/AdlMutation.kt rename to adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt index b0872360..54785455 100644 --- a/adl-server/src/main/kotlin/inbound/mutation/AdlMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt @@ -9,24 +9,32 @@ import com.expediagroup.graphql.server.operations.Mutation import org.eclipse.lmos.adl.server.embeddings.UseCaseEmbeddingsStore import org.eclipse.lmos.adl.server.model.Adl import org.eclipse.lmos.adl.server.storage.AdlStorage +import org.slf4j.LoggerFactory +import java.time.Instant +import java.time.Instant.now /** * GraphQL Mutation for storing UseCases in the Embeddings store. */ -class AdlMutation( +class AdlStorageMutation( private val useCaseStore: UseCaseEmbeddingsStore, private val adlStorage: AdlStorage, ) : Mutation { + private val log = LoggerFactory.getLogger(this::class.java) + @GraphQLDescription("Stores a UseCase in the embeddings store. Embeddings are generated from the provided examples.") suspend fun store( @GraphQLDescription("Unique identifier for the ADL") id: String, @GraphQLDescription("The content of the ADL") content: String, @GraphQLDescription("Tags associated with the ADL") tags: List, - @GraphQLDescription("Timestamp when the ADL was created") createdAt: String + @GraphQLDescription("Timestamp when the ADL was created") createdAt: String? = null, + @GraphQLDescription("Examples") examples: List, ): StorageResult { - adlStorage.store(Adl(id, content, tags, createdAt)) - val storedCount = useCaseStore.storeUseCase(content) + log.info("Storing ADL with id: {}", id) + adlStorage.store(Adl(id, content, tags, createdAt ?: now().toString(), examples)) + val storedCount = useCaseStore.storeUseCase(content, examples) + log.debug("Successfully stored ADL with id: {}. Generated {} embeddings.", id, storedCount) return StorageResult( storedExamplesCount = storedCount, message = "UseCase successfully stored with $storedCount embeddings", @@ -37,6 +45,7 @@ class AdlMutation( suspend fun delete( @GraphQLDescription("The unique ID of the UseCase to delete") useCaseId: String, ): DeletionResult { + log.info("Deleting ADL with id: {}", useCaseId) useCaseStore.deleteByUseCaseId(useCaseId) return DeletionResult( useCaseId = useCaseId, @@ -46,6 +55,7 @@ class AdlMutation( @GraphQLDescription("Clears all UseCases from the embeddings store.") suspend fun clearAll(): ClearResult { + log.info("Clearing all ADLs from store") useCaseStore.clear() return ClearResult( message = "All UseCases successfully cleared", diff --git a/adl-server/src/main/kotlin/inbound/mutation/AdlTestCreatorMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/AdlTestCreatorMutation.kt index d8900928..90785af2 100644 --- a/adl-server/src/main/kotlin/inbound/mutation/AdlTestCreatorMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/AdlTestCreatorMutation.kt @@ -8,6 +8,7 @@ import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Mutation import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import org.eclipse.lmos.adl.server.model.SimpleMessage import org.eclipse.lmos.arc.agents.ConversationAgent import org.eclipse.lmos.arc.agents.agent.process import org.eclipse.lmos.arc.assistants.support.usecases.toUseCases diff --git a/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt b/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt index d8af1751..64f94202 100644 --- a/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt +++ b/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt @@ -8,8 +8,8 @@ import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Query import org.eclipse.lmos.adl.server.embeddings.QdrantUseCaseEmbeddingsStore import org.eclipse.lmos.adl.server.embeddings.UseCaseSearchResult -import org.eclipse.lmos.adl.server.inbound.SimpleMessage import org.eclipse.lmos.adl.server.model.Adl +import org.eclipse.lmos.adl.server.model.SimpleMessage import org.eclipse.lmos.adl.server.storage.AdlStorage /** @@ -39,7 +39,7 @@ class AdlQuery( } @GraphQLDescription("Returns a list of all stored ADLs.") - suspend fun adls(): List { + suspend fun list(): List { return adlStorage.list() } diff --git a/adl-server/src/main/kotlin/inbound/query/TestCaseQuery.kt b/adl-server/src/main/kotlin/inbound/query/TestCaseQuery.kt index b2b37e99..8aec05a5 100644 --- a/adl-server/src/main/kotlin/inbound/query/TestCaseQuery.kt +++ b/adl-server/src/main/kotlin/inbound/query/TestCaseQuery.kt @@ -6,8 +6,8 @@ package org.eclipse.lmos.adl.server.inbound.query import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Query -import org.eclipse.lmos.adl.server.inbound.SimpleMessage import org.eclipse.lmos.adl.server.inbound.mutation.TestCase +import org.eclipse.lmos.adl.server.model.SimpleMessage @GraphQLDescription("GraphQL Query for fetching test cases for a use case.") class TestCaseQuery : Query { diff --git a/adl-server/src/main/kotlin/model/Adl.kt b/adl-server/src/main/kotlin/model/Adl.kt index 95322236..84a5504b 100644 --- a/adl-server/src/main/kotlin/model/Adl.kt +++ b/adl-server/src/main/kotlin/model/Adl.kt @@ -18,5 +18,8 @@ data class Adl( val tags: List, @GraphQLDescription("Timestamp when the ADL was created") - val createdAt: String + val createdAt: String, + + @GraphQLDescription("Examples included in the ADL") + val examples: List = emptyList() ) diff --git a/adl-server/src/main/kotlin/inbound/SimpleMessage.kt b/adl-server/src/main/kotlin/model/SimpleMessage.kt similarity index 78% rename from adl-server/src/main/kotlin/inbound/SimpleMessage.kt rename to adl-server/src/main/kotlin/model/SimpleMessage.kt index 1678ce74..3fed2643 100644 --- a/adl-server/src/main/kotlin/inbound/SimpleMessage.kt +++ b/adl-server/src/main/kotlin/model/SimpleMessage.kt @@ -1,8 +1,4 @@ -// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others -// -// SPDX-License-Identifier: Apache-2.0 - -package org.eclipse.lmos.adl.server.inbound +package org.eclipse.lmos.adl.server.model import com.expediagroup.graphql.generator.annotations.GraphQLDescription import kotlinx.serialization.Serializable @@ -18,4 +14,4 @@ data class SimpleMessage( val role: String, @param:GraphQLDescription("The content of the message") val content: String, -) +) \ No newline at end of file diff --git a/adl-server/src/main/kotlin/services/ConversationEvaluator.kt b/adl-server/src/main/kotlin/services/ConversationEvaluator.kt index be8f9c77..1afa164f 100644 --- a/adl-server/src/main/kotlin/services/ConversationEvaluator.kt +++ b/adl-server/src/main/kotlin/services/ConversationEvaluator.kt @@ -7,7 +7,7 @@ import dev.langchain4j.model.embedding.EmbeddingModel import dev.langchain4j.store.embedding.CosineSimilarity import org.eclipse.lmos.adl.server.agents.EvalEvidence import org.eclipse.lmos.adl.server.agents.EvalOutput -import org.eclipse.lmos.adl.server.inbound.SimpleMessage +import org.eclipse.lmos.adl.server.model.SimpleMessage import kotlin.math.roundToInt class ConversationEvaluator( diff --git a/adl-server/src/test/kotlin/AdlMutationIntegrationTest.kt b/adl-server/src/test/kotlin/AdlStorageMutationIntegrationTest.kt similarity index 99% rename from adl-server/src/test/kotlin/AdlMutationIntegrationTest.kt rename to adl-server/src/test/kotlin/AdlStorageMutationIntegrationTest.kt index decabc18..d5ee2a92 100644 --- a/adl-server/src/test/kotlin/AdlMutationIntegrationTest.kt +++ b/adl-server/src/test/kotlin/AdlStorageMutationIntegrationTest.kt @@ -26,7 +26,7 @@ import org.testcontainers.qdrant.QdrantContainer import io.ktor.client.engine.cio.CIO as ClientCIO @Testcontainers -class AdlMutationIntegrationTest { +class AdlStorageMutationIntegrationTest { companion object { @Container From 6901c3db9439e56dbc63298226b8eb6961ef4262 Mon Sep 17 00:00:00 2001 From: "patrick.whelan@telekom.de" Date: Wed, 21 Jan 2026 16:52:32 +0100 Subject: [PATCH 04/40] feat: enhance ADL storage with delete functionality and add search by ID method --- adl-server/build.gradle.kts | 2 +- .../kotlin/inbound/mutation/AdlStorageMutation.kt | 11 ++++++----- adl-server/src/main/kotlin/inbound/query/AdlQuery.kt | 5 +++++ adl-server/src/main/kotlin/storage/AdlStorage.kt | 3 +++ .../main/kotlin/storage/memory/InMemoryAdlStorage.kt | 4 ++++ 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/adl-server/build.gradle.kts b/adl-server/build.gradle.kts index 14de7d6a..78ac22d0 100644 --- a/adl-server/build.gradle.kts +++ b/adl-server/build.gradle.kts @@ -63,7 +63,7 @@ graalvmNative { buildArgs.add("-H:ReflectionConfigurationFiles=${path}reflect-config.json") buildArgs.add("-H:ResourceConfigurationFiles=${path}resource-config.json") - imageName.set("adl-server") + imageName.set("adl-server-test") } } } diff --git a/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt index 54785455..3c82c925 100644 --- a/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt @@ -31,7 +31,7 @@ class AdlStorageMutation( @GraphQLDescription("Timestamp when the ADL was created") createdAt: String? = null, @GraphQLDescription("Examples") examples: List, ): StorageResult { - log.info("Storing ADL with id: {}", id) + log.info("Storing ADL with id: {} with {} examples", id, examples.size) adlStorage.store(Adl(id, content, tags, createdAt ?: now().toString(), examples)) val storedCount = useCaseStore.storeUseCase(content, examples) log.debug("Successfully stored ADL with id: {}. Generated {} embeddings.", id, storedCount) @@ -43,12 +43,13 @@ class AdlStorageMutation( @GraphQLDescription("Deletes a UseCase from the embeddings store.") suspend fun delete( - @GraphQLDescription("The unique ID of the UseCase to delete") useCaseId: String, + @GraphQLDescription("The unique ID of the UseCase to delete") id: String, ): DeletionResult { - log.info("Deleting ADL with id: {}", useCaseId) - useCaseStore.deleteByUseCaseId(useCaseId) + log.info("Deleting ADL with id: {}", id) + adlStorage.deleteById(id) + useCaseStore.deleteByUseCaseId(id) return DeletionResult( - useCaseId = useCaseId, + useCaseId = id, message = "UseCase successfully deleted", ) } diff --git a/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt b/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt index 64f94202..38154948 100644 --- a/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt +++ b/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt @@ -43,6 +43,11 @@ class AdlQuery( return adlStorage.list() } + @GraphQLDescription("Returns a single ADL by ID.") + suspend fun searchById(@GraphQLDescription("The ID of the ADL") id: String): Adl? { + return adlStorage.get(id) + } + @GraphQLDescription("Searches for UseCases using a text query.") suspend fun searchByText( query: String, diff --git a/adl-server/src/main/kotlin/storage/AdlStorage.kt b/adl-server/src/main/kotlin/storage/AdlStorage.kt index 48097e8a..332aa9cc 100644 --- a/adl-server/src/main/kotlin/storage/AdlStorage.kt +++ b/adl-server/src/main/kotlin/storage/AdlStorage.kt @@ -2,9 +2,12 @@ // // SPDX-License-Identifier: Apache-2.0 package org.eclipse.lmos.adl.server.storage + import org.eclipse.lmos.adl.server.model.Adl + interface AdlStorage { suspend fun store(adl: Adl): Adl suspend fun get(id: String): Adl? suspend fun list(): List + suspend fun deleteById(id: String) } diff --git a/adl-server/src/main/kotlin/storage/memory/InMemoryAdlStorage.kt b/adl-server/src/main/kotlin/storage/memory/InMemoryAdlStorage.kt index 5e5de488..a8a3fe0b 100644 --- a/adl-server/src/main/kotlin/storage/memory/InMemoryAdlStorage.kt +++ b/adl-server/src/main/kotlin/storage/memory/InMemoryAdlStorage.kt @@ -23,4 +23,8 @@ class InMemoryAdlStorage : AdlStorage { override suspend fun list(): List { return storage.values.toList() } + + override suspend fun deleteById(id: String) { + storage.remove(id) + } } From dee208e64eb10b70fc7386c99d417fb035068cf3 Mon Sep 17 00:00:00 2001 From: "patrick.whelan@telekom.de" Date: Wed, 21 Jan 2026 19:19:22 +0100 Subject: [PATCH 05/40] feat: add storeUtterances method to UseCaseEmbeddingsStore for embedding storage --- .../QdrantUseCaseEmbeddingsStore.kt | 33 +++++++++++++++++++ .../embeddings/UseCaseEmbeddingsStore.kt | 1 + .../inbound/mutation/AdlStorageMutation.kt | 2 +- 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/adl-server/src/main/kotlin/embeddings/QdrantUseCaseEmbeddingsStore.kt b/adl-server/src/main/kotlin/embeddings/QdrantUseCaseEmbeddingsStore.kt index 52e33931..88deff97 100644 --- a/adl-server/src/main/kotlin/embeddings/QdrantUseCaseEmbeddingsStore.kt +++ b/adl-server/src/main/kotlin/embeddings/QdrantUseCaseEmbeddingsStore.kt @@ -59,6 +59,39 @@ class QdrantUseCaseEmbeddingsStore( } } + /** + * Stores embeddings the given ADL utterances. + * Overwrites any existing embeddings for UseCases with the same ID. + * @param id The ADL identifier. + * @return The number of embeddings stored. + */ + override suspend fun storeUtterances(id: String, examples: List): Int { + val points = mutableListOf() + + // Delete existing embeddings for all ADL IDs that will be stored + deleteByUseCaseId(id) + + examples.forEach { example -> + val embedding = embeddingModel.embed(example).content().vector() + val point = PointStruct.newBuilder() + .setId(id(UUID.randomUUID())) + .setVectors(vectors(embedding.toList())) + .putAllPayload(buildPayload(id, "", example)) + .build() + points.add(point) + } + + if (points.isNotEmpty()) { + try { + client.upsertAsync(config.collectionName, points).await() + } catch (e: ExecutionException) { + throw RuntimeException("Failed to store embeddings in Qdrant", e.cause) + } + } + + return points.size + } + /** * Stores embeddings the given UseCases. * Overwrites any existing embeddings for UseCases with the same ID. diff --git a/adl-server/src/main/kotlin/embeddings/UseCaseEmbeddingsStore.kt b/adl-server/src/main/kotlin/embeddings/UseCaseEmbeddingsStore.kt index e2865657..6a7013a1 100644 --- a/adl-server/src/main/kotlin/embeddings/UseCaseEmbeddingsStore.kt +++ b/adl-server/src/main/kotlin/embeddings/UseCaseEmbeddingsStore.kt @@ -7,6 +7,7 @@ package org.eclipse.lmos.adl.server.embeddings * Interface for storing UseCase embeddings. */ interface UseCaseEmbeddingsStore { + suspend fun storeUtterances(id: String, examples: List): Int suspend fun storeUseCase(adl: String, examples: List = emptyList()): Int suspend fun deleteByUseCaseId(useCaseId: String) suspend fun clear() diff --git a/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt index 3c82c925..fe83f22d 100644 --- a/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt @@ -33,7 +33,7 @@ class AdlStorageMutation( ): StorageResult { log.info("Storing ADL with id: {} with {} examples", id, examples.size) adlStorage.store(Adl(id, content, tags, createdAt ?: now().toString(), examples)) - val storedCount = useCaseStore.storeUseCase(content, examples) + val storedCount = useCaseStore.storeUtterances(content, examples) log.debug("Successfully stored ADL with id: {}. Generated {} embeddings.", id, storedCount) return StorageResult( storedExamplesCount = storedCount, From 9a80d4dbc5103409a3bb919319b0afb297e5aa77 Mon Sep 17 00:00:00 2001 From: "patrick.whelan@telekom.de" Date: Thu, 22 Jan 2026 21:40:49 +0100 Subject: [PATCH 06/40] feat: add UseCaseImprovementMutation for analyzing and suggesting improvements to use cases --- adl-server/src/main/kotlin/AdlServer.kt | 5 ++ .../kotlin/agents/UseCaseImprovementAgent.kt | 52 +++++++++++++++++++ .../inbound/mutation/AdlStorageMutation.kt | 2 +- .../mutation/UseCaseImprovementMutation.kt | 41 +++++++++++++++ .../src/main/kotlin/inbound/query/AdlQuery.kt | 25 ++++++++- adl-server/src/main/kotlin/model/Adl.kt | 8 ++- .../src/main/kotlin/agent/Extensions.kt | 2 +- 7 files changed, 130 insertions(+), 5 deletions(-) create mode 100644 adl-server/src/main/kotlin/agents/UseCaseImprovementAgent.kt create mode 100644 adl-server/src/main/kotlin/inbound/mutation/UseCaseImprovementMutation.kt diff --git a/adl-server/src/main/kotlin/AdlServer.kt b/adl-server/src/main/kotlin/AdlServer.kt index 39077d4f..f0d1c3b8 100644 --- a/adl-server/src/main/kotlin/AdlServer.kt +++ b/adl-server/src/main/kotlin/AdlServer.kt @@ -30,6 +30,7 @@ import org.eclipse.lmos.adl.server.inbound.query.AdlExampleQuery import org.eclipse.lmos.adl.server.inbound.mutation.AdlStorageMutation import org.eclipse.lmos.adl.server.inbound.query.AdlQuery import org.eclipse.lmos.adl.server.inbound.mutation.AdlTestCreatorMutation +import org.eclipse.lmos.adl.server.inbound.mutation.UseCaseImprovementMutation import org.eclipse.lmos.adl.server.inbound.mutation.AdlValidationMutation import org.eclipse.lmos.adl.server.inbound.GlobalExceptionHandler import org.eclipse.lmos.adl.server.inbound.mutation.SystemPromptMutation @@ -38,6 +39,8 @@ import org.eclipse.lmos.adl.server.services.ConversationEvaluator import org.eclipse.lmos.adl.server.sessions.InMemorySessions import org.eclipse.lmos.adl.server.storage.memory.InMemoryAdlStorage import org.eclipse.lmos.adl.server.templates.TemplateLoader +import org.eclipse.lmos.adl.server.agents.createImprovementAgent +import java.security.Security fun startServer( wait: Boolean = true, @@ -58,6 +61,7 @@ fun startServer( val assistantAgent = createAssistantAgent() val testCreatorAgent = createTestCreatorAgent() val conversationEvaluator = ConversationEvaluator(embeddingModel) + val improvementAgent = createImprovementAgent() // Initialize Qdrant collection runBlocking { @@ -101,6 +105,7 @@ fun startServer( AdlAssistantMutation(assistantAgent), AdlValidationMutation(), AdlTestCreatorMutation(testCreatorAgent), + UseCaseImprovementMutation(improvementAgent), ) } server { diff --git a/adl-server/src/main/kotlin/agents/UseCaseImprovementAgent.kt b/adl-server/src/main/kotlin/agents/UseCaseImprovementAgent.kt new file mode 100644 index 00000000..71d53a43 --- /dev/null +++ b/adl-server/src/main/kotlin/agents/UseCaseImprovementAgent.kt @@ -0,0 +1,52 @@ + +// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +// +// SPDX-License-Identifier: Apache-2.0 +package org.eclipse.lmos.adl.server.agents + +import org.eclipse.lmos.arc.agents.ConversationAgent +import org.eclipse.lmos.arc.agents.agents +import org.eclipse.lmos.arc.agents.dsl.extensions.local +import org.eclipse.lmos.arc.agents.dsl.extensions.processUseCases +import org.eclipse.lmos.arc.agents.dsl.extensions.time +import org.eclipse.lmos.arc.agents.dsl.get +import org.eclipse.lmos.arc.assistants.support.filters.UnresolvedDetector +import org.eclipse.lmos.arc.assistants.support.filters.UseCaseResponseHandler +import org.eclipse.lmos.arc.assistants.support.usecases.UseCase + +fun createImprovementAgent(): ConversationAgent = agents { + agent { + name = "improvement_agent" + filterOutput { + -"```json" + -"```" + } + prompt { + """ + ### Role + You are an expert AI assistant specialized in analyzing and improving Use Cases for conversational agents. + + ### Definition of a Use Case + Use Cases define the expected behaviour of an assistant that is meant to help users with their issues. + + ### Task + Analyze the provided Use Case and suggest specific improvements to enhance its clarity, completeness, and effectiveness. Focus on identifying any gaps, ambiguities, or areas where additional detail could improve the Use Case. + + ### Output Format + Return ONLY a valid JSON object with the following structure: + + { + "improvements": [ + { + "issue": "String describing the identified issue", + "suggestion": "String detailing the suggested improvement", + "improved_use_case": "String with the revised section of the Use Case reflecting the improvement" + } + ] + } + + Ensure that your suggestions are actionable and directly address the identified issues. + """ + } + } +}.getAgents().first() as ConversationAgent diff --git a/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt index fe83f22d..7328db8c 100644 --- a/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt @@ -33,7 +33,7 @@ class AdlStorageMutation( ): StorageResult { log.info("Storing ADL with id: {} with {} examples", id, examples.size) adlStorage.store(Adl(id, content, tags, createdAt ?: now().toString(), examples)) - val storedCount = useCaseStore.storeUtterances(content, examples) + val storedCount = useCaseStore.storeUtterances(id, examples) log.debug("Successfully stored ADL with id: {}. Generated {} embeddings.", id, storedCount) return StorageResult( storedExamplesCount = storedCount, diff --git a/adl-server/src/main/kotlin/inbound/mutation/UseCaseImprovementMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/UseCaseImprovementMutation.kt new file mode 100644 index 00000000..11a27c6a --- /dev/null +++ b/adl-server/src/main/kotlin/inbound/mutation/UseCaseImprovementMutation.kt @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +// +// SPDX-License-Identifier: Apache-2.0 +package org.eclipse.lmos.adl.server.inbound.mutation + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import com.expediagroup.graphql.server.operations.Mutation +import kotlinx.serialization.Serializable +import org.eclipse.lmos.arc.agents.ConversationAgent +import org.eclipse.lmos.arc.agents.agent.process +import org.eclipse.lmos.arc.core.getOrThrow + +/** + * GraphQL mutation for improving use cases. + */ +class UseCaseImprovementMutation(private val improvementAgent: ConversationAgent) : Mutation { + + /** + * Analyzes the provided Use Case and suggests improvements. + * @param useCase The Use Case content to analyze. + * @return A UseCaseImprovementResponse object containing the suggested improvements. + */ + @GraphQLDescription("Analyzes the provided Use Case and suggests improvements.") + suspend fun improveUseCase(useCase: String): UseCaseImprovementResponse { + return improvementAgent.process(useCase).getOrThrow() + } +} + +@Serializable +@GraphQLDescription("Response containing improvements for a Use Case") +data class UseCaseImprovementResponse( + val improvements: List +) + +@Serializable +@GraphQLDescription("A specific improvement suggestion for a Use Case") +data class UseCaseImprovement( + val issue: String, + val suggestion: String, + val improved_use_case: String +) diff --git a/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt b/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt index 38154948..b738b4d5 100644 --- a/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt +++ b/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt @@ -39,8 +39,19 @@ class AdlQuery( } @GraphQLDescription("Returns a list of all stored ADLs.") - suspend fun list(): List { - return adlStorage.list() + suspend fun list( + @GraphQLDescription("Optional search criteria to filter ADLs by relevance") searchTerm: SearchCriteria? = null, + ): List { + val allAdls = adlStorage.list() + if (searchTerm == null || searchTerm.term.isBlank()) { + return allAdls + } + val matches = useCaseStore.search(searchTerm.term, searchTerm.limit, searchTerm.threshold.toFloat()) + val scores = matches.groupBy { it.useCaseId }.mapValues { it.value.maxOf { match -> match.score } } + + return allAdls.filter { it.id in scores.keys } + .map { it.copy(relevance = scores[it.id]?.toDouble()) } + .sortedByDescending { it.relevance } } @GraphQLDescription("Returns a single ADL by ID.") @@ -97,3 +108,13 @@ data class Example( @param:GraphQLDescription("The examples that matched the query") val example: String, ) + +@GraphQLDescription("Search criteria for ADLs") +data class SearchCriteria( + @param:GraphQLDescription("The search term") + val term: String, + @param:GraphQLDescription("Maximum number of results to return") + val limit: Int = 50, + @param:GraphQLDescription("Minimum similarity score (0.0 to 1.0)") + val threshold: Double = 0.5, +) diff --git a/adl-server/src/main/kotlin/model/Adl.kt b/adl-server/src/main/kotlin/model/Adl.kt index 84a5504b..dd75263c 100644 --- a/adl-server/src/main/kotlin/model/Adl.kt +++ b/adl-server/src/main/kotlin/model/Adl.kt @@ -21,5 +21,11 @@ data class Adl( val createdAt: String, @GraphQLDescription("Examples included in the ADL") - val examples: List = emptyList() + val examples: List = emptyList(), + + @GraphQLDescription("Relevance score") + val relevance: Double? = null, + + @GraphQLDescription("Version of the ADL") + val version: String = "1.0.0", ) diff --git a/arc-agents/src/main/kotlin/agent/Extensions.kt b/arc-agents/src/main/kotlin/agent/Extensions.kt index 8d1883d7..08f2e56d 100644 --- a/arc-agents/src/main/kotlin/agent/Extensions.kt +++ b/arc-agents/src/main/kotlin/agent/Extensions.kt @@ -39,7 +39,7 @@ val agentInputJson = Json { encodeDefaults = true; ignoreUnknownKeys = true } * @return A Result containing the processed output of type R or an AgentFailedException. */ suspend inline fun ConversationAgent.process(input: T) = result { - val inputString = agentInputJson.encodeToString(input) + val inputString = if (input is String) input else agentInputJson.encodeToString(input) val result = ask(inputString) failWith { it } agentInputJson.decodeFromString(result) } From 15e60e88ad1a5c92515bddfc3cfc76173f430239 Mon Sep 17 00:00:00 2001 From: "patrick.whelan@telekom.de" Date: Fri, 23 Jan 2026 13:05:26 +0100 Subject: [PATCH 07/40] feat: add Dockerfile for building and running native image of adl-server --- adl-server/Dockerfile | 35 +++++++++++++++++++ adl-server/build.gradle.kts | 25 +++++++++++++ adl-server/src/main/kotlin/AdlServer.kt | 6 ++-- .../mutation/AdlTestCreatorMutation.kt | 4 +-- .../kotlin/inbound/query/TestCaseQuery.kt | 22 ++++-------- .../InMemoryTestCaseRepository.kt | 2 +- 6 files changed, 73 insertions(+), 21 deletions(-) create mode 100644 adl-server/Dockerfile diff --git a/adl-server/Dockerfile b/adl-server/Dockerfile new file mode 100644 index 00000000..d3dddb07 --- /dev/null +++ b/adl-server/Dockerfile @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +# +# SPDX-License-Identifier: Apache-2.0 + +# Stage 1: Build the native image +FROM ghcr.io/graalvm/native-image-community:21 AS build + +WORKDIR /app + +# Copy the entire repository to ensure all dependent modules are available +COPY . . + +# Ensure gradlew is executable +RUN chmod +x gradlew + +RUN ./gradlew :adl-server:clean + +# Build the native image using Gradle +# -x test skips tests +# --info provides more detailed logs to diagnose the error +# -Dorg.gradle.jvmargs sets memory limits for the Gradle daemon to prevent it from consuming all RAM before Native Image runs +RUN ./gradlew :adl-server:nativeCompile -x test --info -Dorg.gradle.jvmargs="-Xmx2g" + +# Stage 2: Create the runtime image +FROM gcr.io/distroless/base-debian12 + +WORKDIR /app + +# Copy the native binary from the build stage +COPY --from=build /app/adl-server/build/native/nativeCompile/adl-server /app/server + +EXPOSE 8080 + +# Set the entrypoint to the native binary +ENTRYPOINT ["/app/server"] diff --git a/adl-server/build.gradle.kts b/adl-server/build.gradle.kts index 78ac22d0..86a8cdb4 100644 --- a/adl-server/build.gradle.kts +++ b/adl-server/build.gradle.kts @@ -31,6 +31,16 @@ graalvmNative { buildArgs.add("--initialize-at-build-time=kotlinx.serialization.json.ClassDiscriminatorMode") buildArgs.add("--initialize-at-build-time=kotlinx.serialization.modules.SerializersModuleKt") + buildArgs.add("--initialize-at-build-time=io.grpc.netty.shaded.io.netty") + buildArgs.add("--initialize-at-run-time=io.grpc.netty.shaded.io.netty.handler.ssl.BouncyCastleAlpnSslUtils") + buildArgs.add("--initialize-at-run-time=io.grpc.netty.shaded.io.netty.buffer.AbstractReferenceCountedByteBuf") + buildArgs.add("--initialize-at-run-time=io.grpc.netty.shaded.io.netty.buffer.PooledByteBuf") + buildArgs.add("--initialize-at-run-time=io.grpc.netty.shaded.io.netty.buffer.UnpooledHeapByteBuf") + buildArgs.add("--initialize-at-run-time=io.grpc.netty.shaded.io.netty.buffer.ByteBuf") + buildArgs.add("--initialize-at-build-time=org.bouncycastle") + buildArgs.add("--initialize-at-build-time=kotlinx.io.files.PathsJvmKt") + buildArgs.add("--initialize-at-build-time=kotlinx.io.files.FileSystemJvmKt") + buildArgs.add("-H:+InstallExitHandlers") buildArgs.add("-H:+ReportUnsupportedElementsAtRuntime") buildArgs.add("-H:+ReportExceptionStackTraces") @@ -55,6 +65,16 @@ graalvmNative { buildArgs.add("--initialize-at-build-time=kotlinx.serialization.json.ClassDiscriminatorMode") buildArgs.add("--initialize-at-build-time=kotlinx.serialization.modules.SerializersModuleKt") + buildArgs.add("--initialize-at-build-time=io.grpc.netty.shaded.io.netty") + buildArgs.add("--initialize-at-run-time=io.grpc.netty.shaded.io.netty.handler.ssl.BouncyCastleAlpnSslUtils") + buildArgs.add("--initialize-at-run-time=io.grpc.netty.shaded.io.netty.buffer.AbstractReferenceCountedByteBuf") + buildArgs.add("--initialize-at-run-time=io.grpc.netty.shaded.io.netty.buffer.PooledByteBuf") + buildArgs.add("--initialize-at-run-time=io.grpc.netty.shaded.io.netty.buffer.UnpooledHeapByteBuf") + buildArgs.add("--initialize-at-run-time=io.grpc.netty.shaded.io.netty.buffer.ByteBuf") + buildArgs.add("--initialize-at-build-time=org.bouncycastle") + buildArgs.add("--initialize-at-build-time=kotlinx.io.files.PathsJvmKt") + buildArgs.add("--initialize-at-build-time=kotlinx.io.files.FileSystemJvmKt") + buildArgs.add("-H:+InstallExitHandlers") buildArgs.add("-H:+ReportUnsupportedElementsAtRuntime") buildArgs.add("-H:+ReportExceptionStackTraces") @@ -108,6 +128,11 @@ dependencies { implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure") implementation("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure-spi") + // Bouncy Castle + implementation("org.bouncycastle:bctls-jdk18on:1.78.1") + implementation("org.bouncycastle:bcpkix-jdk18on:1.78.1") + implementation("org.bouncycastle:bcprov-jdk18on:1.78.1") + // Test dependencies testImplementation(libs.ktor.client.core) testImplementation(libs.ktor.client.cio.jvm) diff --git a/adl-server/src/main/kotlin/AdlServer.kt b/adl-server/src/main/kotlin/AdlServer.kt index f0d1c3b8..21ca44cb 100644 --- a/adl-server/src/main/kotlin/AdlServer.kt +++ b/adl-server/src/main/kotlin/AdlServer.kt @@ -40,6 +40,7 @@ import org.eclipse.lmos.adl.server.sessions.InMemorySessions import org.eclipse.lmos.adl.server.storage.memory.InMemoryAdlStorage import org.eclipse.lmos.adl.server.templates.TemplateLoader import org.eclipse.lmos.adl.server.agents.createImprovementAgent +import org.eclipse.lmos.adl.server.repositories.InMemoryTestCaseRepository import java.security.Security fun startServer( @@ -62,6 +63,7 @@ fun startServer( val testCreatorAgent = createTestCreatorAgent() val conversationEvaluator = ConversationEvaluator(embeddingModel) val improvementAgent = createImprovementAgent() + val testCaseRepository = InMemoryTestCaseRepository() // Initialize Qdrant collection runBlocking { @@ -95,7 +97,7 @@ fun startServer( queries = listOf( AdlQuery(useCaseStore, adlStorage), AdlExampleQuery(exampleAgent), - TestCaseQuery(), + TestCaseQuery(testCaseRepository), ) mutations = listOf( AdlCompilerMutation(), @@ -104,7 +106,7 @@ fun startServer( AdlEvalMutation(evalAgent, conversationEvaluator), AdlAssistantMutation(assistantAgent), AdlValidationMutation(), - AdlTestCreatorMutation(testCreatorAgent), + AdlTestCreatorMutation(testCreatorAgent, testCaseRepository), UseCaseImprovementMutation(improvementAgent), ) } diff --git a/adl-server/src/main/kotlin/inbound/mutation/AdlTestCreatorMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/AdlTestCreatorMutation.kt index 90785af2..ecc411b1 100644 --- a/adl-server/src/main/kotlin/inbound/mutation/AdlTestCreatorMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/AdlTestCreatorMutation.kt @@ -8,13 +8,11 @@ import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Mutation import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import org.eclipse.lmos.adl.server.model.SimpleMessage import org.eclipse.lmos.arc.agents.ConversationAgent import org.eclipse.lmos.arc.agents.agent.process import org.eclipse.lmos.arc.assistants.support.usecases.toUseCases import org.eclipse.lmos.arc.core.getOrThrow import org.eclipse.lmos.adl.server.repositories.TestCaseRepository -import java.util.UUID /** * GraphQL Mutation for creating test cases using the TestCreatorAgent. @@ -77,7 +75,7 @@ data class TestCase( @GraphQLDescription("The ID of the use case this test belongs to") val useCaseId: String? = null, @GraphQLDescription("The title of the test case") - val title: String, + val name: String, @GraphQLDescription("The description of the test case") val description: String, @SerialName("expected_conversation") diff --git a/adl-server/src/main/kotlin/inbound/query/TestCaseQuery.kt b/adl-server/src/main/kotlin/inbound/query/TestCaseQuery.kt index 8aec05a5..9005c8dd 100644 --- a/adl-server/src/main/kotlin/inbound/query/TestCaseQuery.kt +++ b/adl-server/src/main/kotlin/inbound/query/TestCaseQuery.kt @@ -8,25 +8,17 @@ import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Query import org.eclipse.lmos.adl.server.inbound.mutation.TestCase import org.eclipse.lmos.adl.server.model.SimpleMessage +import org.eclipse.lmos.adl.server.repositories.TestCaseRepository @GraphQLDescription("GraphQL Query for fetching test cases for a use case.") -class TestCaseQuery : Query { +class TestCaseQuery( + private val testCaseRepository: TestCaseRepository, +) : Query { @GraphQLDescription("Fetches test cases for a given use case ID.") - fun testCases( + suspend fun testCases( @GraphQLDescription("The ID of the use case.") useCaseId: String ): List { - // Mock data for now - return listOf( - TestCase( - id = "1", - name = "Basic Greeting", - description = "A simple conversation to test the AI's greeting capabilities.", - expectedConversation = listOf( - SimpleMessage(role = "user", content = "Hello!"), - SimpleMessage(role = "assistant", content = "Hi there! How can I help you today?") - ) - ) - ) + return testCaseRepository.findByUseCaseId(useCaseId) } -} \ No newline at end of file +} diff --git a/adl-server/src/main/kotlin/repositories/InMemoryTestCaseRepository.kt b/adl-server/src/main/kotlin/repositories/InMemoryTestCaseRepository.kt index 20688b50..65f5a1ff 100644 --- a/adl-server/src/main/kotlin/repositories/InMemoryTestCaseRepository.kt +++ b/adl-server/src/main/kotlin/repositories/InMemoryTestCaseRepository.kt @@ -4,7 +4,7 @@ package org.eclipse.lmos.adl.server.repositories -import org.eclipse.lmos.adl.server.inbound.TestCase +import org.eclipse.lmos.adl.server.inbound.query.TestCase import java.util.concurrent.ConcurrentHashMap /** From f2942d31fd40cfa50e09c0ebad57d00316672fcb Mon Sep 17 00:00:00 2001 From: "patrick.whelan@telekom.de" Date: Mon, 26 Jan 2026 16:27:29 +0100 Subject: [PATCH 08/40] feat: enhance test case management with CRUD operations and execution functionality --- adl-server/src/main/kotlin/AdlServer.kt | 7 +- .../inbound/mutation/AdlStorageMutation.kt | 2 +- ...CreatorMutation.kt => TestCaseMutation.kt} | 75 +++++++----- .../kotlin/inbound/query/TestCaseQuery.kt | 2 +- .../main/kotlin/models/ConversationTurn.kt | 19 ++++ adl-server/src/main/kotlin/models/TestCase.kt | 21 ++++ .../main/kotlin/models/TestExecutionResult.kt | 29 +++++ .../src/main/kotlin/models/TestRunResult.kt | 20 ++++ .../InMemoryTestCaseRepository.kt | 6 +- .../kotlin/repositories/TestCaseRepository.kt | 9 +- .../src/main/kotlin/services/TestExecutor.kt | 107 ++++++++++++++++++ adl-server/src/main/resources/assistant.md | 22 +++- 12 files changed, 284 insertions(+), 35 deletions(-) rename adl-server/src/main/kotlin/inbound/mutation/{AdlTestCreatorMutation.kt => TestCaseMutation.kt} (54%) create mode 100644 adl-server/src/main/kotlin/models/ConversationTurn.kt create mode 100644 adl-server/src/main/kotlin/models/TestCase.kt create mode 100644 adl-server/src/main/kotlin/models/TestExecutionResult.kt create mode 100644 adl-server/src/main/kotlin/models/TestRunResult.kt create mode 100644 adl-server/src/main/kotlin/services/TestExecutor.kt diff --git a/adl-server/src/main/kotlin/AdlServer.kt b/adl-server/src/main/kotlin/AdlServer.kt index 21ca44cb..9dc14fbd 100644 --- a/adl-server/src/main/kotlin/AdlServer.kt +++ b/adl-server/src/main/kotlin/AdlServer.kt @@ -29,19 +29,19 @@ import org.eclipse.lmos.adl.server.inbound.mutation.AdlEvalMutation import org.eclipse.lmos.adl.server.inbound.query.AdlExampleQuery import org.eclipse.lmos.adl.server.inbound.mutation.AdlStorageMutation import org.eclipse.lmos.adl.server.inbound.query.AdlQuery -import org.eclipse.lmos.adl.server.inbound.mutation.AdlTestCreatorMutation +import org.eclipse.lmos.adl.server.inbound.mutation.TestCreatorMutation import org.eclipse.lmos.adl.server.inbound.mutation.UseCaseImprovementMutation import org.eclipse.lmos.adl.server.inbound.mutation.AdlValidationMutation import org.eclipse.lmos.adl.server.inbound.GlobalExceptionHandler import org.eclipse.lmos.adl.server.inbound.mutation.SystemPromptMutation import org.eclipse.lmos.adl.server.inbound.query.TestCaseQuery import org.eclipse.lmos.adl.server.services.ConversationEvaluator +import org.eclipse.lmos.adl.server.services.TestExecutor import org.eclipse.lmos.adl.server.sessions.InMemorySessions import org.eclipse.lmos.adl.server.storage.memory.InMemoryAdlStorage import org.eclipse.lmos.adl.server.templates.TemplateLoader import org.eclipse.lmos.adl.server.agents.createImprovementAgent import org.eclipse.lmos.adl.server.repositories.InMemoryTestCaseRepository -import java.security.Security fun startServer( wait: Boolean = true, @@ -64,6 +64,7 @@ fun startServer( val conversationEvaluator = ConversationEvaluator(embeddingModel) val improvementAgent = createImprovementAgent() val testCaseRepository = InMemoryTestCaseRepository() + val testExecutor = TestExecutor(assistantAgent, adlStorage, testCaseRepository, conversationEvaluator) // Initialize Qdrant collection runBlocking { @@ -106,7 +107,7 @@ fun startServer( AdlEvalMutation(evalAgent, conversationEvaluator), AdlAssistantMutation(assistantAgent), AdlValidationMutation(), - AdlTestCreatorMutation(testCreatorAgent, testCaseRepository), + TestCreatorMutation(testCreatorAgent, testCaseRepository, testExecutor), UseCaseImprovementMutation(improvementAgent), ) } diff --git a/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt index 7328db8c..2e9e0c72 100644 --- a/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt @@ -32,7 +32,7 @@ class AdlStorageMutation( @GraphQLDescription("Examples") examples: List, ): StorageResult { log.info("Storing ADL with id: {} with {} examples", id, examples.size) - adlStorage.store(Adl(id, content, tags, createdAt ?: now().toString(), examples)) + adlStorage.store(Adl(id, content.trim(), tags, createdAt ?: now().toString(), examples)) val storedCount = useCaseStore.storeUtterances(id, examples) log.debug("Successfully stored ADL with id: {}. Generated {} embeddings.", id, storedCount) return StorageResult( diff --git a/adl-server/src/main/kotlin/inbound/mutation/AdlTestCreatorMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/TestCaseMutation.kt similarity index 54% rename from adl-server/src/main/kotlin/inbound/mutation/AdlTestCreatorMutation.kt rename to adl-server/src/main/kotlin/inbound/mutation/TestCaseMutation.kt index ecc411b1..e18f1be0 100644 --- a/adl-server/src/main/kotlin/inbound/mutation/AdlTestCreatorMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/TestCaseMutation.kt @@ -6,20 +6,24 @@ package org.eclipse.lmos.adl.server.inbound.mutation import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Mutation -import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable +import org.eclipse.lmos.adl.server.models.TestCase +import org.eclipse.lmos.adl.server.models.TestRunResult +import org.eclipse.lmos.adl.server.models.ConversationTurn +import org.eclipse.lmos.adl.server.services.TestExecutor +import org.eclipse.lmos.adl.server.repositories.TestCaseRepository import org.eclipse.lmos.arc.agents.ConversationAgent import org.eclipse.lmos.arc.agents.agent.process import org.eclipse.lmos.arc.assistants.support.usecases.toUseCases import org.eclipse.lmos.arc.core.getOrThrow -import org.eclipse.lmos.adl.server.repositories.TestCaseRepository /** * GraphQL Mutation for creating test cases using the TestCreatorAgent. */ -class AdlTestCreatorMutation( +class TestCreatorMutation( private val testCreatorAgent: ConversationAgent, private val testCaseRepository: TestCaseRepository, + private val testExecutor: TestExecutor, ) : Mutation { @GraphQLDescription("Generates test cases for a provided Use Case.") @@ -44,6 +48,37 @@ class AdlTestCreatorMutation( testCases.forEach { testCaseRepository.save(it) } return NewTestsResponse(testCases.size, useCaseId) } + + @GraphQLDescription("Executes tests for a given Use Case.") + suspend fun executeTests( + @GraphQLDescription("The Use Case ID") useCaseId: String, + @GraphQLDescription("The Test Case ID") testCaseId: String? = null, + ): TestRunResult { + return testExecutor.executeTests(useCaseId, testCaseId) + } + + @GraphQLDescription("Deletes a test case by its ID.") + suspend fun deleteTest( + @GraphQLDescription("The ID of the test case to delete") id: String, + ): Boolean { + return testCaseRepository.delete(id) + } + + @GraphQLDescription("Updates a single Test Case.") + suspend fun updateTest( + @GraphQLDescription("The updated Test Case data") input: UpdateTestCaseInput, + ): TestCase { + val existing = testCaseRepository.findById(input.id) + ?: throw IllegalArgumentException("Test Case with ID ${input.id} not found") + + val updated = existing.copy( + name = input.name ?: existing.name, + description = input.description ?: existing.description, + expectedConversation = input.expectedConversation ?: existing.expectedConversation + ) + + return testCaseRepository.save(updated) + } } /** @@ -66,30 +101,16 @@ data class TestCreatorInput( ) /** - * Represents a generated test case. - */ -@Serializable -data class TestCase( - @GraphQLDescription("The unique identifier of the test case") - val id: String = java.util.UUID.randomUUID().toString(), - @GraphQLDescription("The ID of the use case this test belongs to") - val useCaseId: String? = null, - @GraphQLDescription("The title of the test case") - val name: String, - @GraphQLDescription("The description of the test case") - val description: String, - @SerialName("expected_conversation") - @GraphQLDescription("The expected conversation flow") - val expectedConversation: List, -) - -/** - * Represents a turn in a conversation. + * Input for updating test cases. */ @Serializable -data class ConversationTurn( - @GraphQLDescription("The role of the speaker (e.g., user, assistant)") - val role: String, - @GraphQLDescription("The content of the message") - val content: String, +data class UpdateTestCaseInput( + @GraphQLDescription("The ID of the test case to update") + val id: String, + @GraphQLDescription("The new name of the test case") + val name: String? = null, + @GraphQLDescription("The new description of the test case") + val description: String? = null, + @GraphQLDescription("The new expected conversation") + val expectedConversation: List? = null, ) diff --git a/adl-server/src/main/kotlin/inbound/query/TestCaseQuery.kt b/adl-server/src/main/kotlin/inbound/query/TestCaseQuery.kt index 9005c8dd..36254a98 100644 --- a/adl-server/src/main/kotlin/inbound/query/TestCaseQuery.kt +++ b/adl-server/src/main/kotlin/inbound/query/TestCaseQuery.kt @@ -6,7 +6,7 @@ package org.eclipse.lmos.adl.server.inbound.query import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Query -import org.eclipse.lmos.adl.server.inbound.mutation.TestCase +import org.eclipse.lmos.adl.server.models.TestCase import org.eclipse.lmos.adl.server.model.SimpleMessage import org.eclipse.lmos.adl.server.repositories.TestCaseRepository diff --git a/adl-server/src/main/kotlin/models/ConversationTurn.kt b/adl-server/src/main/kotlin/models/ConversationTurn.kt new file mode 100644 index 00000000..c1af0c18 --- /dev/null +++ b/adl-server/src/main/kotlin/models/ConversationTurn.kt @@ -0,0 +1,19 @@ +// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +// +// SPDX-License-Identifier: Apache-2.0 + +package org.eclipse.lmos.adl.server.models + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import kotlinx.serialization.Serializable + +/** + * Represents a turn in a conversation. + */ +@Serializable +data class ConversationTurn( + @GraphQLDescription("The role of the speaker (e.g., user, assistant)") + val role: String, + @GraphQLDescription("The content of the message") + val content: String, +) diff --git a/adl-server/src/main/kotlin/models/TestCase.kt b/adl-server/src/main/kotlin/models/TestCase.kt new file mode 100644 index 00000000..d314fd4e --- /dev/null +++ b/adl-server/src/main/kotlin/models/TestCase.kt @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +// +// SPDX-License-Identifier: Apache-2.0 + +package org.eclipse.lmos.adl.server.models + +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable + +/** + * Represents a generated test case. + */ +@Serializable +data class TestCase( + val id: String = java.util.UUID.randomUUID().toString(), + val useCaseId: String? = null, + val name: String, + val description: String, + @SerialName("expected_conversation") + val expectedConversation: List, +) diff --git a/adl-server/src/main/kotlin/models/TestExecutionResult.kt b/adl-server/src/main/kotlin/models/TestExecutionResult.kt new file mode 100644 index 00000000..647f9617 --- /dev/null +++ b/adl-server/src/main/kotlin/models/TestExecutionResult.kt @@ -0,0 +1,29 @@ +// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +// +// SPDX-License-Identifier: Apache-2.0 + +package org.eclipse.lmos.adl.server.models + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import kotlinx.serialization.Serializable +import org.eclipse.lmos.adl.server.agents.EvalOutput + +/** + * Result of a test execution. + */ +@Serializable +@GraphQLDescription("Result of a test execution") +data class TestExecutionResult( + @GraphQLDescription("The ID of the test case") + val testCaseId: String, + @GraphQLDescription("The Name of the test case") + val testCaseName: String, + @GraphQLDescription("The status of the test execution (PASS/FAIL)") + val status: String, + @GraphQLDescription("The evaluation score") + val score: Int, + @GraphQLDescription("The actual conversation that took place") + val actualConversation: List, + @GraphQLDescription("Detailed evaluation output") + val details: EvalOutput, +) diff --git a/adl-server/src/main/kotlin/models/TestRunResult.kt b/adl-server/src/main/kotlin/models/TestRunResult.kt new file mode 100644 index 00000000..5c8b9652 --- /dev/null +++ b/adl-server/src/main/kotlin/models/TestRunResult.kt @@ -0,0 +1,20 @@ +// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +// +// SPDX-License-Identifier: Apache-2.0 + +package org.eclipse.lmos.adl.server.models + +import com.expediagroup.graphql.generator.annotations.GraphQLDescription +import kotlinx.serialization.Serializable + +/** + * Result of a test suite execution. + */ +@Serializable +@GraphQLDescription("Result of a test suite execution") +data class TestRunResult( + @GraphQLDescription("The overall score of the test suite (0-100)") + val overallScore: Double, + @GraphQLDescription("The results of individual test cases") + val results: List, +) diff --git a/adl-server/src/main/kotlin/repositories/InMemoryTestCaseRepository.kt b/adl-server/src/main/kotlin/repositories/InMemoryTestCaseRepository.kt index 65f5a1ff..920ce525 100644 --- a/adl-server/src/main/kotlin/repositories/InMemoryTestCaseRepository.kt +++ b/adl-server/src/main/kotlin/repositories/InMemoryTestCaseRepository.kt @@ -4,7 +4,7 @@ package org.eclipse.lmos.adl.server.repositories -import org.eclipse.lmos.adl.server.inbound.query.TestCase +import org.eclipse.lmos.adl.server.models.TestCase import java.util.concurrent.ConcurrentHashMap /** @@ -29,4 +29,8 @@ class InMemoryTestCaseRepository : TestCaseRepository { override suspend fun findByUseCaseId(useCaseId: String): List { return store.values.filter { it.useCaseId == useCaseId } } + + override suspend fun delete(id: String): Boolean { + return store.remove(id) != null + } } diff --git a/adl-server/src/main/kotlin/repositories/TestCaseRepository.kt b/adl-server/src/main/kotlin/repositories/TestCaseRepository.kt index b2d6bc9a..303f3071 100644 --- a/adl-server/src/main/kotlin/repositories/TestCaseRepository.kt +++ b/adl-server/src/main/kotlin/repositories/TestCaseRepository.kt @@ -4,7 +4,7 @@ package org.eclipse.lmos.adl.server.repositories -import org.eclipse.lmos.adl.server.inbound.TestCase +import org.eclipse.lmos.adl.server.models.TestCase /** * Repository for managing [TestCase] entities. @@ -36,4 +36,11 @@ interface TestCaseRepository { * @return A list of matching test cases. */ suspend fun findByUseCaseId(useCaseId: String): List + + /** + * Deletes a [TestCase] by its ID. + * @param id The ID of the test case to delete. + * @return True if the test case was deleted, false otherwise. + */ + suspend fun delete(id: String): Boolean } diff --git a/adl-server/src/main/kotlin/services/TestExecutor.kt b/adl-server/src/main/kotlin/services/TestExecutor.kt new file mode 100644 index 00000000..631d1183 --- /dev/null +++ b/adl-server/src/main/kotlin/services/TestExecutor.kt @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +// +// SPDX-License-Identifier: Apache-2.0 + +package org.eclipse.lmos.adl.server.services + +import org.eclipse.lmos.adl.server.model.SimpleMessage +import org.eclipse.lmos.adl.server.models.ConversationTurn +import org.eclipse.lmos.adl.server.models.TestCase +import org.eclipse.lmos.adl.server.models.TestExecutionResult +import org.eclipse.lmos.adl.server.models.TestRunResult +import org.eclipse.lmos.adl.server.repositories.TestCaseRepository +import org.eclipse.lmos.adl.server.storage.AdlStorage +import org.eclipse.lmos.arc.agents.ConversationAgent +import org.eclipse.lmos.arc.agents.conversation.Conversation +import org.eclipse.lmos.arc.agents.conversation.ConversationMessage +import org.eclipse.lmos.arc.agents.conversation.UserMessage +import org.eclipse.lmos.arc.agents.conversation.AssistantMessage +import org.eclipse.lmos.arc.agents.conversation.latest +import org.eclipse.lmos.arc.assistants.support.usecases.toUseCases +import org.eclipse.lmos.arc.core.Failure +import org.eclipse.lmos.arc.core.Success + +/** + * Service for executing tests. + */ +class TestExecutor( + private val assistantAgent: ConversationAgent, + private val adlStorage: AdlStorage, + private val testCaseRepository: TestCaseRepository, + private val conversationEvaluator: ConversationEvaluator, +) { + + suspend fun executeTests(useCaseId: String, testCaseId: String? = null): TestRunResult { + val adl = adlStorage.get(useCaseId) ?: throw IllegalArgumentException("Use Case not found: $useCaseId") + val useCases = adl.content.toUseCases() + + val testCases = if (testCaseId != null) { + val testCase = testCaseRepository.findById(testCaseId) + ?: throw IllegalArgumentException("Test Case not found: $testCaseId") + listOf(testCase) + } else { + testCaseRepository.findByUseCaseId(useCaseId) + } + + val results = testCases.map { testCase -> + executeTestCase(testCase, useCases) + } + + val overallScore = if (results.isNotEmpty()) results.map { it.score }.average() else 0.0 + + return TestRunResult( + overallScore = overallScore, + results = results, + ) + } + + private suspend fun executeTestCase(testCase: TestCase, useCases: Any): TestExecutionResult { + val transcript = mutableListOf() + val actualConversation = mutableListOf() + var failureReason: String? = null + + try { + for (turn in testCase.expectedConversation) { + if (turn.role == "user") { + val userMsg = UserMessage(turn.content) + transcript.add(userMsg) + actualConversation.add(SimpleMessage("user", turn.content)) + + val conv = Conversation(transcript = transcript) + val result = assistantAgent.execute(conv, setOf(useCases)) + + when (result) { + is Success -> { + val assistantMsg = result.value.latest() ?: AssistantMessage("") + transcript.add(assistantMsg) + actualConversation.add(SimpleMessage("assistant", assistantMsg.content)) + } + is Failure -> { + failureReason = "Agent execution failed: ${result.reason.message}" + break + } + } + } + } + } catch (e: Exception) { + failureReason = "Exception: ${e.message}" + } + + val evalOutput = conversationEvaluator.evaluate( + actualConversation, + testCase.expectedConversation.map { SimpleMessage(it.role, it.content) }, + ) + + val finalVerdict = if (failureReason != null) "fail" else evalOutput.verdict + val finalReasons = if (failureReason != null) (evalOutput.reasons + failureReason) else evalOutput.reasons + + return TestExecutionResult( + testCaseId = testCase.id, + testCaseName = testCase.name, + status = if (finalVerdict == "fail") "FAIL" else "PASS", + score = evalOutput.score, + actualConversation = actualConversation.map { ConversationTurn(it.role, it.content) }, + details = evalOutput.copy(verdict = finalVerdict, reasons = finalReasons as MutableList), + ) + } +} diff --git a/adl-server/src/main/resources/assistant.md b/adl-server/src/main/resources/assistant.md index ee536bd5..d1a7802f 100644 --- a/adl-server/src/main/resources/assistant.md +++ b/adl-server/src/main/resources/assistant.md @@ -13,7 +13,7 @@ NO_ANSWER. 7. Call any functions specified in the applicable use case when required. 8. Generate your response using the instructions in the selected use case, but make sure it aligns with current conversation context. 9. Skip asking questions if the answer can already be derived from the conversation. - +10. Keep your responses concise and to the point. ## Use Case & Step Handling @@ -78,4 +78,24 @@ You can try rephrasing your question or providing a bit more detail so I can bet $$TIME$$ ## Available Use Cases + +### UseCase: off_topic +#### Description +The customer is asking a question or making a statement that is unrelated to any of the defined use cases. + +#### Solution +Politely inform the customer that their question or +statement is outside the scope of your assistance capabilities. + +---- + +### UseCase: unclear_request +#### Description +The customer's request is ambiguous or lacks sufficient detail to determine the appropriate use case. + +#### Solution +Ask the customer for clarification or additional details to better understand their request. + +---- + $$USE_CASES$$ \ No newline at end of file From e29bbef61acfb836ed688c8ab2af8ab8876773c9 Mon Sep 17 00:00:00 2001 From: "patrick.whelan@telekom.de" Date: Mon, 26 Jan 2026 18:07:48 +0100 Subject: [PATCH 09/40] feat: add AdlExampleMutation for generating examples of ADL use cases --- adl-server/src/main/kotlin/AdlServer.kt | 6 +++--- .../AdlExampleQuery.kt => mutation/AdlExampleMutation.kt} | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) rename adl-server/src/main/kotlin/inbound/{query/AdlExampleQuery.kt => mutation/AdlExampleMutation.kt} (90%) diff --git a/adl-server/src/main/kotlin/AdlServer.kt b/adl-server/src/main/kotlin/AdlServer.kt index 9dc14fbd..50b4b33b 100644 --- a/adl-server/src/main/kotlin/AdlServer.kt +++ b/adl-server/src/main/kotlin/AdlServer.kt @@ -26,7 +26,6 @@ import org.eclipse.lmos.adl.server.embeddings.QdrantUseCaseEmbeddingsStore import org.eclipse.lmos.adl.server.inbound.mutation.AdlAssistantMutation import org.eclipse.lmos.adl.server.inbound.mutation.AdlCompilerMutation import org.eclipse.lmos.adl.server.inbound.mutation.AdlEvalMutation -import org.eclipse.lmos.adl.server.inbound.query.AdlExampleQuery import org.eclipse.lmos.adl.server.inbound.mutation.AdlStorageMutation import org.eclipse.lmos.adl.server.inbound.query.AdlQuery import org.eclipse.lmos.adl.server.inbound.mutation.TestCreatorMutation @@ -41,6 +40,7 @@ import org.eclipse.lmos.adl.server.sessions.InMemorySessions import org.eclipse.lmos.adl.server.storage.memory.InMemoryAdlStorage import org.eclipse.lmos.adl.server.templates.TemplateLoader import org.eclipse.lmos.adl.server.agents.createImprovementAgent +import org.eclipse.lmos.adl.server.inbound.mutation.AdlExampleMutation import org.eclipse.lmos.adl.server.repositories.InMemoryTestCaseRepository fun startServer( @@ -97,7 +97,6 @@ fun startServer( ) queries = listOf( AdlQuery(useCaseStore, adlStorage), - AdlExampleQuery(exampleAgent), TestCaseQuery(testCaseRepository), ) mutations = listOf( @@ -109,7 +108,8 @@ fun startServer( AdlValidationMutation(), TestCreatorMutation(testCreatorAgent, testCaseRepository, testExecutor), UseCaseImprovementMutation(improvementAgent), - ) + AdlExampleMutation(exampleAgent), + ) } server { diff --git a/adl-server/src/main/kotlin/inbound/query/AdlExampleQuery.kt b/adl-server/src/main/kotlin/inbound/mutation/AdlExampleMutation.kt similarity index 90% rename from adl-server/src/main/kotlin/inbound/query/AdlExampleQuery.kt rename to adl-server/src/main/kotlin/inbound/mutation/AdlExampleMutation.kt index 4f37a3ef..0ae20172 100644 --- a/adl-server/src/main/kotlin/inbound/query/AdlExampleQuery.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/AdlExampleMutation.kt @@ -2,10 +2,10 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.eclipse.lmos.adl.server.inbound.query +package org.eclipse.lmos.adl.server.inbound.mutation import com.expediagroup.graphql.generator.annotations.GraphQLDescription -import com.expediagroup.graphql.server.operations.Query +import com.expediagroup.graphql.server.operations.Mutation import org.eclipse.lmos.arc.agents.ConversationAgent import org.eclipse.lmos.arc.agents.conversation.AssistantMessage import org.eclipse.lmos.arc.agents.conversation.Conversation @@ -15,9 +15,9 @@ import org.eclipse.lmos.arc.core.getOrThrow /** * GraphQL Query for creating examples for ADL UseCases. */ -class AdlExampleQuery( +class AdlExampleMutation( private val exampleAgent: ConversationAgent, -) : Query { +) : Mutation { @GraphQLDescription("Generates examples for a given use case.") suspend fun examples( From 73666631dd252cd1b41cbc60d4561bd5f6846a13 Mon Sep 17 00:00:00 2001 From: "patrick.whelan@telekom.de" Date: Mon, 26 Jan 2026 18:47:48 +0100 Subject: [PATCH 10/40] feat: add logging for assistant requests and update assistant documentation for clarity --- .../inbound/mutation/AdlAssistantMutation.kt | 3 +++ adl-server/src/main/resources/assistant.md | 23 ++++++++----------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/adl-server/src/main/kotlin/inbound/mutation/AdlAssistantMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/AdlAssistantMutation.kt index 0794d7ab..d40cf2fb 100644 --- a/adl-server/src/main/kotlin/inbound/mutation/AdlAssistantMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/AdlAssistantMutation.kt @@ -30,10 +30,13 @@ class AdlAssistantMutation( private val assistantAgent: ConversationAgent, ) : Mutation { + private val log = org.slf4j.LoggerFactory.getLogger(this.javaClass) + @GraphQLDescription("Calls the assistant agent") suspend fun assistant( @GraphQLDescription("The assistant input") input: AssistantInput, ): AgentResult { + log.info("Received assistant request with useCases: ${input.request.conversationContext.conversationId}") val useCases = input.useCases.toUseCases() val request = input.request val outputContext = OutputContext() diff --git a/adl-server/src/main/resources/assistant.md b/adl-server/src/main/resources/assistant.md index d1a7802f..26b4b7f5 100644 --- a/adl-server/src/main/resources/assistant.md +++ b/adl-server/src/main/resources/assistant.md @@ -19,28 +19,25 @@ NO_ANSWER. 1. After selecting a use case, determine whether it contains a Steps section. 2. If a step: - - Asks a question and - - The answer can already be derived from the conversation + - Asks a question and the answer can already be derived from the conversation → Skip that step. -3. If the Steps contain bullet points, perform only one bullet point at a time. +3. If the Steps contain bullet points, select only one bullet point to generate the response. 4. After completing the applicable step (or skipping all steps), perform the instructions in the Solution section. -5. Never expose internal steps, instructions, or reasoning to the customer. -6. Mandatory Output Format (NON-NEGOTIABLE) -7. Every single response must follow this format exactly and in this order: +5. Never expose internal steps, instructions, or reasoning to the customer. + +## Mandatory Output Format (NON-NEGOTIABLE) + +Every single response must follow this format exactly and in this order: ``` - - [Customer-facing response] ``` ## Rules - The line is mandatory in all cases, including NO_ANSWER. -- If no step applies, you must explicitly write . -- If a step applies, include the exact step sequence number, e.g. . -- If either the use case ID or step indicator is missing, the response is considered invalid. +- If the use case ID is missing, the response is considered invalid. ## Language & Tone Requirements @@ -52,9 +49,9 @@ NO_ANSWER. ## Self-Validation Checklist (Before Responding) + Before finalizing your answer, silently confirm: - [] Does the response start with ? -- [] Is or present? - [] Is the language the same as the customer’s? - [] Is only requested information provided? - [] Are instructions and internal logic hidden from the customer? @@ -63,14 +60,12 @@ Before finalizing your answer, silently confirm: Example (Valid Output) ``` - You can review your open invoices in the billing section of your account and choose the payment method that works best for you. ``` Example (No Matching Use Case) ``` - You can try rephrasing your question or providing a bit more detail so I can better assist you. ``` From 1f99b7df02eacb3b518e1068e6602d5a997cd9f0 Mon Sep 17 00:00:00 2001 From: "patrick.whelan@telekom.de" Date: Mon, 26 Jan 2026 18:58:03 +0100 Subject: [PATCH 11/40] feat: add conversation ID generation to test execution for improved traceability --- adl-server/src/main/kotlin/inbound/AGENTS.md | 11 +++++ .../inbound/mutation/TestCaseMutation.kt | 28 +++++++++++ .../kotlin/services/ConversationEvaluator.kt | 5 ++ .../src/main/kotlin/services/TestExecutor.kt | 4 +- adl-server/src/main/resources/assistant.md | 48 +++++++++---------- 5 files changed, 69 insertions(+), 27 deletions(-) create mode 100644 adl-server/src/main/kotlin/inbound/AGENTS.md diff --git a/adl-server/src/main/kotlin/inbound/AGENTS.md b/adl-server/src/main/kotlin/inbound/AGENTS.md new file mode 100644 index 00000000..14e40212 --- /dev/null +++ b/adl-server/src/main/kotlin/inbound/AGENTS.md @@ -0,0 +1,11 @@ + +# ADL Server Inbound Endpoints + +## Coding Guidelines +- Each query and mutation MUST have a KDoc comment explaining its purpose. +- Each query and mutation MUST have unit tests covering at least 80% of its functionality. +- Each query and mutation MUST start with a info log statement indicating the start of the operation. \ No newline at end of file diff --git a/adl-server/src/main/kotlin/inbound/mutation/TestCaseMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/TestCaseMutation.kt index e18f1be0..d3b062b2 100644 --- a/adl-server/src/main/kotlin/inbound/mutation/TestCaseMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/TestCaseMutation.kt @@ -79,6 +79,19 @@ class TestCreatorMutation( return testCaseRepository.save(updated) } + + @GraphQLDescription("Adds a new Test Case manually.") + suspend fun addTest( + @GraphQLDescription("The new Test Case data") input: AddTestCaseInput, + ): TestCase { + val testCase = TestCase( + useCaseId = input.useCaseId, + name = input.name, + description = input.description, + expectedConversation = input.expectedConversation + ) + return testCaseRepository.save(testCase) + } } /** @@ -114,3 +127,18 @@ data class UpdateTestCaseInput( @GraphQLDescription("The new expected conversation") val expectedConversation: List? = null, ) + +/** + * Input for adding test cases manually. + */ +@Serializable +data class AddTestCaseInput( + @GraphQLDescription("The Use Case ID associated with this test") + val useCaseId: String, + @GraphQLDescription("The name of the test case") + val name: String, + @GraphQLDescription("The description of the test case") + val description: String, + @GraphQLDescription("The expected conversation") + val expectedConversation: List, +) diff --git a/adl-server/src/main/kotlin/services/ConversationEvaluator.kt b/adl-server/src/main/kotlin/services/ConversationEvaluator.kt index 1afa164f..9c96c4fd 100644 --- a/adl-server/src/main/kotlin/services/ConversationEvaluator.kt +++ b/adl-server/src/main/kotlin/services/ConversationEvaluator.kt @@ -38,6 +38,11 @@ class ConversationEvaluator( continue } + if(expected.role == "user") { + // User messages should always match exactly + continue + } + val actualEmb = embeddingModel.embed(actual.content).content() val expectedEmb = embeddingModel.embed(expected.content).content() val similarity = CosineSimilarity.between(actualEmb, expectedEmb) diff --git a/adl-server/src/main/kotlin/services/TestExecutor.kt b/adl-server/src/main/kotlin/services/TestExecutor.kt index 631d1183..b2277a8b 100644 --- a/adl-server/src/main/kotlin/services/TestExecutor.kt +++ b/adl-server/src/main/kotlin/services/TestExecutor.kt @@ -20,6 +20,7 @@ import org.eclipse.lmos.arc.agents.conversation.latest import org.eclipse.lmos.arc.assistants.support.usecases.toUseCases import org.eclipse.lmos.arc.core.Failure import org.eclipse.lmos.arc.core.Success +import java.util.UUID /** * Service for executing tests. @@ -59,6 +60,7 @@ class TestExecutor( val transcript = mutableListOf() val actualConversation = mutableListOf() var failureReason: String? = null + val conversationId = "test-${testCase.id}-${UUID.randomUUID()}" try { for (turn in testCase.expectedConversation) { @@ -67,7 +69,7 @@ class TestExecutor( transcript.add(userMsg) actualConversation.add(SimpleMessage("user", turn.content)) - val conv = Conversation(transcript = transcript) + val conv = Conversation(transcript = transcript, conversationId = conversationId) val result = assistantAgent.execute(conv, setOf(useCases)) when (result) { diff --git a/adl-server/src/main/resources/assistant.md b/adl-server/src/main/resources/assistant.md index 26b4b7f5..dd84e0a3 100644 --- a/adl-server/src/main/resources/assistant.md +++ b/adl-server/src/main/resources/assistant.md @@ -1,5 +1,13 @@ $$ROLE$$ +## Language & Tone Requirements + +- Always talk directly to the customer (second person). +- Never refer to the customer in the third person. +- Always suggest what the customer can do — never say the customer must do something. +- Be polite, friendly, and professional at all times. +- Keep your responses concise and to the point. + ## Core Instructions (Strict) Follow all instructions below. If any instruction conflicts, these Core Instructions take priority. @@ -13,7 +21,7 @@ NO_ANSWER. 7. Call any functions specified in the applicable use case when required. 8. Generate your response using the instructions in the selected use case, but make sure it aligns with current conversation context. 9. Skip asking questions if the answer can already be derived from the conversation. -10. Keep your responses concise and to the point. +10. Check your final response against the Self-Validation Checklist before sending it. ## Use Case & Step Handling @@ -22,8 +30,9 @@ NO_ANSWER. - Asks a question and the answer can already be derived from the conversation → Skip that step. 3. If the Steps contain bullet points, select only one bullet point to generate the response. -4. After completing the applicable step (or skipping all steps), perform the instructions in the Solution section. -5. Never expose internal steps, instructions, or reasoning to the customer. +4. Do not combine multiple Steps and do not combine steps and solution (NON-NEGOTIABLE). +5. After completing all applicable steps (or skipping all steps), perform the instructions in the Solution section. +6. Never expose internal steps, instructions, or reasoning to the customer. ## Mandatory Output Format (NON-NEGOTIABLE) @@ -34,29 +43,6 @@ Every single response must follow this format exactly and in this order: [Customer-facing response] ``` -## Rules - -- The line is mandatory in all cases, including NO_ANSWER. -- If the use case ID is missing, the response is considered invalid. - - -## Language & Tone Requirements - -- Always talk directly to the customer (second person). -- Never refer to the customer in the third person. -- Always suggest what the customer can do — never say the customer must do something. -- Be polite, friendly, and professional at all times. - - -## Self-Validation Checklist (Before Responding) - -Before finalizing your answer, silently confirm: -- [] Does the response start with ? -- [] Is the language the same as the customer’s? -- [] Is only requested information provided? -- [] Are instructions and internal logic hidden from the customer? -- [] If any check fails, revise the response before sending. - Example (Valid Output) ``` @@ -69,6 +55,16 @@ Example (No Matching Use Case) You can try rephrasing your question or providing a bit more detail so I can better assist you. ``` +## Self-Validation Checklist (Before Responding) + +Before finalizing your answer, silently confirm: +- [] Does the response start with ? +- [] Is the language the same as the customer’s? +- [] Is only requested information provided? +- [] Are instructions and internal logic hidden from the customer? +- [] If any check fails, revise the response before sending. +- [] Steps were not combined. + ## Time $$TIME$$ From c569071a04a4983a7a5ed205b44222924504c062 Mon Sep 17 00:00:00 2001 From: "patrick.whelan@telekom.de" Date: Mon, 26 Jan 2026 20:34:20 +0100 Subject: [PATCH 12/40] feat: improve similarity scoring in ConversationEvaluator and refine assistant response guidelines --- .../kotlin/services/ConversationEvaluator.kt | 5 +- adl-server/src/main/resources/assistant.md | 100 +++++++++++------- 2 files changed, 63 insertions(+), 42 deletions(-) diff --git a/adl-server/src/main/kotlin/services/ConversationEvaluator.kt b/adl-server/src/main/kotlin/services/ConversationEvaluator.kt index 9c96c4fd..b762f112 100644 --- a/adl-server/src/main/kotlin/services/ConversationEvaluator.kt +++ b/adl-server/src/main/kotlin/services/ConversationEvaluator.kt @@ -19,6 +19,7 @@ class ConversationEvaluator( failureThreshold: Double = 0.8, ): EvalOutput { val n = minOf(conversation.size, expectedConversation.size) + var compared = 0 var totalSimilarity = 0.0 val reasons = mutableListOf() @@ -43,6 +44,8 @@ class ConversationEvaluator( continue } + compared++ + val actualEmb = embeddingModel.embed(actual.content).content() val expectedEmb = embeddingModel.embed(expected.content).content() val similarity = CosineSimilarity.between(actualEmb, expectedEmb) @@ -61,7 +64,7 @@ class ConversationEvaluator( } // If one is empty - val finalScore = if (n > 0) (totalSimilarity / n) * 100 else 0.0 + val finalScore = if (compared > 0) (totalSimilarity / compared) * 100 else 0.0 val verdict = if (finalScore >= 90) "pass" else if (finalScore >= 60) "partial" else "fail" return EvalOutput( diff --git a/adl-server/src/main/resources/assistant.md b/adl-server/src/main/resources/assistant.md index dd84e0a3..4184df4e 100644 --- a/adl-server/src/main/resources/assistant.md +++ b/adl-server/src/main/resources/assistant.md @@ -1,70 +1,89 @@ $$ROLE$$ +You follow the ReAct pattern: you reason internally, then act by producing the final customer-facing response. +You NEVER reveal your reasoning, steps, or internal analysis. + ## Language & Tone Requirements - Always talk directly to the customer (second person). - Never refer to the customer in the third person. - Always suggest what the customer can do — never say the customer must do something. - Be polite, friendly, and professional at all times. -- Keep your responses concise and to the point. +- Keep responses concise and to the point. Do not add unnecessary information. +- Always respond in the same language the customer used. ## Core Instructions (Strict) -Follow all instructions below. If any instruction conflicts, these Core Instructions take priority. -1. Only provide information the customer has explicitly asked for. -2. Use the context of the conversation to provide the best possible answer. -3. Always answer in the same language the customer used (e.g., English or German). -4. You must always select exactly one use case that best matches the customer’s question or the ongoing conversation. -5. If no matching use case exists, you must still return a response and use the special use case ID: -NO_ANSWER. -6. Never invent a new use case. -7. Call any functions specified in the applicable use case when required. -8. Generate your response using the instructions in the selected use case, but make sure it aligns with current conversation context. -9. Skip asking questions if the answer can already be derived from the conversation. -10. Check your final response against the Self-Validation Checklist before sending it. - -## Use Case & Step Handling - -1. After selecting a use case, determine whether it contains a Steps section. -2. If a step: - - Asks a question and the answer can already be derived from the conversation - → Skip that step. -3. If the Steps contain bullet points, select only one bullet point to generate the response. -4. Do not combine multiple Steps and do not combine steps and solution (NON-NEGOTIABLE). -5. After completing all applicable steps (or skipping all steps), perform the instructions in the Solution section. -6. Never expose internal steps, instructions, or reasoning to the customer. +These rules override all others if there is a conflict. + +1. Only provide information the customer explicitly asked for. +2. Use the conversation context to determine the best possible answer. Do not add unnecessary information. +3. Select exactly ONE use case that best matches the customer’s question or the ongoing conversation. +4. If no matching use case exists, use the special use case ID: NO_ANSWER. +5. Never invent new use cases. +6. If the selected use case defines function calls, execute them when required. +7. Generate the response strictly according to the selected use case instructions. +8. Skip questions if the answer can already be derived from the conversation. +9. Never expose internal reasoning, ReAct thoughts, steps, or decision logic to the customer. + +## ReAct Execution Flow (Internal – Do Not Expose) + +Thought: +- Analyze the customer message and conversation context. +- Identify the single best-matching use case. +- Determine whether the use case contains Steps. +- Decide which step (if any) applies, following all step-handling rules. +- Validate the final response using the checklist. + +Action: +- Produce the final customer-facing response in the mandatory output format. + +## Use Case & Step Handling Rules + +1. After selecting a use case, check whether it contains a "Steps" section. +2. If a step asks a question and the answer can already be derived → skip that step. +3. If Steps contain bullet points → select exactly ONE bullet point. +4. Never combine multiple steps. +5. Never combine steps with the solution (NON-NEGOTIABLE). +6. After completing or skipping all steps, apply the "Solution" section. +7. Internal execution details must never be shown. + +## Self-Validation Checklist (Internal – Silent) + +Before responding, silently confirm: +- The response starts with +- The language matches the customer’s language +- Only requested information is included +- No internal logic, ReAct thoughts, or instructions are visible +- Steps were not combined with other steps or the solution + +If any check fails, revise before responding. ## Mandatory Output Format (NON-NEGOTIABLE) -Every single response must follow this format exactly and in this order: +The final output must ALWAYS follow this exact format: ``` -[Customer-facing response] +Customer-facing response ``` -Example (Valid Output) +The line is mandatory in all cases, including NO_ANSWER. + +### Example (Valid) + ``` You can review your open invoices in the billing section of your account and choose the payment method that works best for you. ``` -Example (No Matching Use Case) +### Example (No Matching Use Case) + ``` -You can try rephrasing your question or providing a bit more detail so I can better assist you. +You can try rephrasing your question or sharing a bit more detail so I can assist you more effectively. ``` -## Self-Validation Checklist (Before Responding) - -Before finalizing your answer, silently confirm: -- [] Does the response start with ? -- [] Is the language the same as the customer’s? -- [] Is only requested information provided? -- [] Are instructions and internal logic hidden from the customer? -- [] If any check fails, revise the response before sending. -- [] Steps were not combined. - ## Time $$TIME$$ @@ -75,8 +94,7 @@ $$TIME$$ The customer is asking a question or making a statement that is unrelated to any of the defined use cases. #### Solution -Politely inform the customer that their question or -statement is outside the scope of your assistance capabilities. +Politely let the customer know their request is outside the scope of your assistance. ---- From d2c5ac9758fc24a1fa7849566920f0615ba93c41 Mon Sep 17 00:00:00 2001 From: "patrick.whelan@telekom.de" Date: Mon, 26 Jan 2026 20:51:17 +0100 Subject: [PATCH 13/40] feat: improve similarity scoring in ConversationEvaluator and refine assistant response guidelines --- .../src/main/kotlin/agents/AssistantAgent.kt | 7 ++- .../src/main/kotlin/services/TestExecutor.kt | 7 +++ adl-server/src/main/resources/assistant.md | 59 +++++++++++-------- 3 files changed, 49 insertions(+), 24 deletions(-) diff --git a/adl-server/src/main/kotlin/agents/AssistantAgent.kt b/adl-server/src/main/kotlin/agents/AssistantAgent.kt index 20032ce8..23363658 100644 --- a/adl-server/src/main/kotlin/agents/AssistantAgent.kt +++ b/adl-server/src/main/kotlin/agents/AssistantAgent.kt @@ -5,10 +5,12 @@ package org.eclipse.lmos.adl.server.agents import org.eclipse.lmos.arc.agents.ConversationAgent import org.eclipse.lmos.arc.agents.agents +import org.eclipse.lmos.arc.agents.dsl.extensions.getCurrentUseCases import org.eclipse.lmos.arc.agents.dsl.extensions.local import org.eclipse.lmos.arc.agents.dsl.extensions.processUseCases import org.eclipse.lmos.arc.agents.dsl.extensions.time import org.eclipse.lmos.arc.agents.dsl.get +import org.eclipse.lmos.arc.agents.retry import org.eclipse.lmos.arc.assistants.support.filters.UnresolvedDetector import org.eclipse.lmos.arc.assistants.support.filters.UseCaseResponseHandler import org.eclipse.lmos.arc.assistants.support.usecases.UseCase @@ -20,11 +22,14 @@ fun createAssistantAgent(): ConversationAgent = agents { -"```json" -"```" +UseCaseResponseHandler() + if(outputMessage.content.contains("NEXT_USE_CASE")) { + retry("Detected NEXT_USE_CASE usage, retrying to avoid infinite loop.", max = 10) + } +UnresolvedDetector { "UNRESOLVED" } } prompt { val role = local("role.md")!! - val useCases = processUseCases(useCases = get>()) + val useCases = processUseCases(useCases = get>(), fallbackLimit = 3) val prompt = local("assistant.md")!! .replace("\$\$ROLE\$\$", role) .replace("\$\$USE_CASES\$\$", useCases) diff --git a/adl-server/src/main/kotlin/services/TestExecutor.kt b/adl-server/src/main/kotlin/services/TestExecutor.kt index b2277a8b..289ea5b3 100644 --- a/adl-server/src/main/kotlin/services/TestExecutor.kt +++ b/adl-server/src/main/kotlin/services/TestExecutor.kt @@ -57,6 +57,13 @@ class TestExecutor( } private suspend fun executeTestCase(testCase: TestCase, useCases: Any): TestExecutionResult { + val results = (1..5).map { + runSingleTestCase(testCase, useCases) + } + return results.minByOrNull { it.score } ?: results.first() + } + + private suspend fun runSingleTestCase(testCase: TestCase, useCases: Any): TestExecutionResult { val transcript = mutableListOf() val actualConversation = mutableListOf() var failureReason: String? = null diff --git a/adl-server/src/main/resources/assistant.md b/adl-server/src/main/resources/assistant.md index 4184df4e..7933cc5f 100644 --- a/adl-server/src/main/resources/assistant.md +++ b/adl-server/src/main/resources/assistant.md @@ -9,7 +9,8 @@ You NEVER reveal your reasoning, steps, or internal analysis. - Never refer to the customer in the third person. - Always suggest what the customer can do — never say the customer must do something. - Be polite, friendly, and professional at all times. -- Keep responses concise and to the point. Do not add unnecessary information. +- Keep responses concise and to the point. +- **IMPORTANT** Do not add unnecessary information nor assumptions to your answers. - Always respond in the same language the customer used. ## Core Instructions (Strict) @@ -19,24 +20,22 @@ These rules override all others if there is a conflict. 1. Only provide information the customer explicitly asked for. 2. Use the conversation context to determine the best possible answer. Do not add unnecessary information. 3. Select exactly ONE use case that best matches the customer’s question or the ongoing conversation. -4. If no matching use case exists, use the special use case ID: NO_ANSWER. -5. Never invent new use cases. -6. If the selected use case defines function calls, execute them when required. -7. Generate the response strictly according to the selected use case instructions. -8. Skip questions if the answer can already be derived from the conversation. -9. Never expose internal reasoning, ReAct thoughts, steps, or decision logic to the customer. +4. Never invent new use cases. +5. If the selected use case defines function calls, execute them when required. +6. Generate the response strictly according to the selected use case instructions. +7. Skip questions if the answer can already be derived from the conversation. +8. Never expose internal reasoning, ReAct thoughts, steps, or decision logic to the customer. ## ReAct Execution Flow (Internal – Do Not Expose) Thought: -- Analyze the customer message and conversation context. -- Identify the single best-matching use case. -- Determine whether the use case contains Steps. -- Decide which step (if any) applies, following all step-handling rules. -- Validate the final response using the checklist. - -Action: -- Produce the final customer-facing response in the mandatory output format. +Question: the input question you must answer. +Thought: you should always think about what to do. +Action: the action to take, examine the conversation so far and determine the best use case to apply and generate a response based on its instructions of the selected use case. +Observation: the result of the action. +(Note: this Thought/Action/Observation can repeat N times) +Thought: I now know the final answer. +Output: the final answer to the original input question. ## Use Case & Step Handling Rules @@ -47,15 +46,17 @@ Action: 5. Never combine steps with the solution (NON-NEGOTIABLE). 6. After completing or skipping all steps, apply the "Solution" section. 7. Internal execution details must never be shown. +8. If the selected use case solution instructs to ask the customer a question that has already been answered in the conversation context, return special command "NEXT_USE_CASE" to get next set of use cases. ## Self-Validation Checklist (Internal – Silent) Before responding, silently confirm: -- The response starts with -- The language matches the customer’s language -- Only requested information is included -- No internal logic, ReAct thoughts, or instructions are visible -- Steps were not combined with other steps or the solution +- [] The response starts with +- [] The language matches the customer’s language +- [] Only requested information is included +- [] No internal logic, ReAct thoughts, or instructions are visible +- [] Steps were not combined with other steps or the solution +- [] Questions already answered were not asked again If any check fails, revise before responding. @@ -77,11 +78,20 @@ The line is mandatory in all cases, including NO_ANSWER. You can review your open invoices in the billing section of your account and choose the payment method that works best for you. ``` -### Example (No Matching Use Case) +### Example (Use Case instructs to ask a question already answered) ``` - -You can try rephrasing your question or sharing a bit more detail so I can assist you more effectively. +### UseCase: buy_phone +#### Description +The customer wants to buy a phone. + +#### Solution +Ask the customer if they are interested in purchasing a mobile phone or landline phone. + +---- + +User: I want to buy a mobile phone. +Assistant: NEXT_USE_CASE ``` ## Time @@ -105,6 +115,9 @@ The customer's request is ambiguous or lacks sufficient detail to determine the #### Solution Ask the customer for clarification or additional details to better understand their request. +#### Fallback Solution +Politely let the customer know their request is outside the scope of your assistance. + ---- $$USE_CASES$$ \ No newline at end of file From 1b5a71e42615898c404df2633044026edb4983b4 Mon Sep 17 00:00:00 2001 From: "patrick.whelan@telekom.de" Date: Mon, 26 Jan 2026 21:40:17 +0100 Subject: [PATCH 14/40] feat: improve similarity scoring in ConversationEvaluator and refine assistant response guidelines --- adl-server/Dockerfile | 34 ++--- adl-server/GraalVMDockerfile | 35 +++++ adl-server/README.md | 18 +++ adl-server/src/main/kotlin/AdlServer.kt | 31 ++++- .../embeddings/UseCaseEmbeddingsStore.kt | 14 -- .../inbound/mutation/AdlStorageMutation.kt | 26 +++- .../src/main/kotlin/inbound/query/AdlQuery.kt | 10 +- .../src/main/kotlin/{model => models}/Adl.kt | 0 .../kotlin/{model => models}/SimpleMessage.kt | 0 .../src/main/kotlin/repositories/AGENTS.md | 14 ++ .../AdlRepository.kt} | 6 +- .../UseCaseEmbeddingsRepository.kt | 30 ++++ .../impl/InMemoryAdlRepository.kt} | 9 +- .../{ => impl}/InMemoryTestCaseRepository.kt | 3 +- .../impl/InMemoryUseCaseEmbeddingsStore.kt | 131 ++++++++++++++++++ .../impl}/QdrantUseCaseEmbeddingsStore.kt | 27 ++-- .../src/main/kotlin/services/TestExecutor.kt | 6 +- adl-server/src/main/resources/assistant.md | 16 +-- 18 files changed, 317 insertions(+), 93 deletions(-) create mode 100644 adl-server/GraalVMDockerfile delete mode 100644 adl-server/src/main/kotlin/embeddings/UseCaseEmbeddingsStore.kt rename adl-server/src/main/kotlin/{model => models}/Adl.kt (100%) rename adl-server/src/main/kotlin/{model => models}/SimpleMessage.kt (100%) create mode 100644 adl-server/src/main/kotlin/repositories/AGENTS.md rename adl-server/src/main/kotlin/{storage/AdlStorage.kt => repositories/AdlRepository.kt} (79%) create mode 100644 adl-server/src/main/kotlin/repositories/UseCaseEmbeddingsRepository.kt rename adl-server/src/main/kotlin/{storage/memory/InMemoryAdlStorage.kt => repositories/impl/InMemoryAdlRepository.kt} (79%) rename adl-server/src/main/kotlin/repositories/{ => impl}/InMemoryTestCaseRepository.kt (88%) create mode 100644 adl-server/src/main/kotlin/repositories/impl/InMemoryUseCaseEmbeddingsStore.kt rename adl-server/src/main/kotlin/{embeddings => repositories/impl}/QdrantUseCaseEmbeddingsStore.kt (94%) diff --git a/adl-server/Dockerfile b/adl-server/Dockerfile index d3dddb07..71765e81 100644 --- a/adl-server/Dockerfile +++ b/adl-server/Dockerfile @@ -2,34 +2,16 @@ # # SPDX-License-Identifier: Apache-2.0 -# Stage 1: Build the native image -FROM ghcr.io/graalvm/native-image-community:21 AS build - -WORKDIR /app - -# Copy the entire repository to ensure all dependent modules are available -COPY . . - +FROM gradle:8-jdk24-corretto AS build +WORKDIR /home/gradle/project +COPY --chown=gradle:gradle . . # Ensure gradlew is executable RUN chmod +x gradlew +# Skip tests to speed up the build +RUN ./gradlew :adl-server:installDist --no-daemon -x test -RUN ./gradlew :adl-server:clean - -# Build the native image using Gradle -# -x test skips tests -# --info provides more detailed logs to diagnose the error -# -Dorg.gradle.jvmargs sets memory limits for the Gradle daemon to prevent it from consuming all RAM before Native Image runs -RUN ./gradlew :adl-server:nativeCompile -x test --info -Dorg.gradle.jvmargs="-Xmx2g" - -# Stage 2: Create the runtime image -FROM gcr.io/distroless/base-debian12 - +FROM eclipse-temurin:21-jre-jammy WORKDIR /app - -# Copy the native binary from the build stage -COPY --from=build /app/adl-server/build/native/nativeCompile/adl-server /app/server - +COPY --from=build /home/gradle/project/adl-server/build/install/adl-server/ /app/ EXPOSE 8080 - -# Set the entrypoint to the native binary -ENTRYPOINT ["/app/server"] +ENTRYPOINT ["/app/bin/adl-server"] diff --git a/adl-server/GraalVMDockerfile b/adl-server/GraalVMDockerfile new file mode 100644 index 00000000..d3dddb07 --- /dev/null +++ b/adl-server/GraalVMDockerfile @@ -0,0 +1,35 @@ +# SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +# +# SPDX-License-Identifier: Apache-2.0 + +# Stage 1: Build the native image +FROM ghcr.io/graalvm/native-image-community:21 AS build + +WORKDIR /app + +# Copy the entire repository to ensure all dependent modules are available +COPY . . + +# Ensure gradlew is executable +RUN chmod +x gradlew + +RUN ./gradlew :adl-server:clean + +# Build the native image using Gradle +# -x test skips tests +# --info provides more detailed logs to diagnose the error +# -Dorg.gradle.jvmargs sets memory limits for the Gradle daemon to prevent it from consuming all RAM before Native Image runs +RUN ./gradlew :adl-server:nativeCompile -x test --info -Dorg.gradle.jvmargs="-Xmx2g" + +# Stage 2: Create the runtime image +FROM gcr.io/distroless/base-debian12 + +WORKDIR /app + +# Copy the native binary from the build stage +COPY --from=build /app/adl-server/build/native/nativeCompile/adl-server /app/server + +EXPOSE 8080 + +# Set the entrypoint to the native binary +ENTRYPOINT ["/app/server"] diff --git a/adl-server/README.md b/adl-server/README.md index 2093cde9..974b7b1b 100644 --- a/adl-server/README.md +++ b/adl-server/README.md @@ -37,6 +37,24 @@ The server can be configured using the following environment variables: By default, the server listens on port `8080`. You can override the port by setting the environment variable `ADL_SERVER_PORT`. +### Docker + +You can also run the server using Docker. + +#### Build the Docker Image + +Run the following command from the root of the repository: + +```sh +docker build -f adl-server/Dockerfile -t adl-server . +``` + +#### Run the Docker Container + +```sh +docker run -p 8080:8080 -e OPENAI_API_KEY=your-api-key adl-server +``` + ### GraphQL Endpoint The main endpoint is available at: diff --git a/adl-server/src/main/kotlin/AdlServer.kt b/adl-server/src/main/kotlin/AdlServer.kt index 50b4b33b..0788d789 100644 --- a/adl-server/src/main/kotlin/AdlServer.kt +++ b/adl-server/src/main/kotlin/AdlServer.kt @@ -9,6 +9,7 @@ import com.expediagroup.graphql.server.ktor.defaultGraphQLStatusPages import com.expediagroup.graphql.server.ktor.graphQLPostRoute import com.expediagroup.graphql.server.ktor.graphiQLRoute import dev.langchain4j.model.embedding.onnx.allminilml6v2.AllMiniLmL6V2EmbeddingModel +import io.ktor.http.ContentType import io.ktor.server.application.* import io.ktor.server.cio.* import io.ktor.server.engine.* @@ -17,12 +18,15 @@ import io.ktor.server.plugins.statuspages.* import io.ktor.server.routing.routing import io.ktor.http.HttpHeaders import io.ktor.http.HttpMethod +import io.ktor.server.http.content.staticResources +import io.ktor.server.response.respond +import io.ktor.server.response.respondText +import io.ktor.server.routing.contentType import kotlinx.coroutines.runBlocking import org.eclipse.lmos.adl.server.agents.createAssistantAgent import org.eclipse.lmos.adl.server.agents.createEvalAgent import org.eclipse.lmos.adl.server.agents.createExampleAgent import org.eclipse.lmos.adl.server.agents.createTestCreatorAgent -import org.eclipse.lmos.adl.server.embeddings.QdrantUseCaseEmbeddingsStore import org.eclipse.lmos.adl.server.inbound.mutation.AdlAssistantMutation import org.eclipse.lmos.adl.server.inbound.mutation.AdlCompilerMutation import org.eclipse.lmos.adl.server.inbound.mutation.AdlEvalMutation @@ -37,11 +41,15 @@ import org.eclipse.lmos.adl.server.inbound.query.TestCaseQuery import org.eclipse.lmos.adl.server.services.ConversationEvaluator import org.eclipse.lmos.adl.server.services.TestExecutor import org.eclipse.lmos.adl.server.sessions.InMemorySessions -import org.eclipse.lmos.adl.server.storage.memory.InMemoryAdlStorage +import org.eclipse.lmos.adl.server.repositories.impl.InMemoryAdlRepository import org.eclipse.lmos.adl.server.templates.TemplateLoader import org.eclipse.lmos.adl.server.agents.createImprovementAgent import org.eclipse.lmos.adl.server.inbound.mutation.AdlExampleMutation -import org.eclipse.lmos.adl.server.repositories.InMemoryTestCaseRepository +import org.eclipse.lmos.adl.server.repositories.AdlRepository +import org.eclipse.lmos.adl.server.repositories.impl.InMemoryTestCaseRepository +import org.eclipse.lmos.adl.server.repositories.UseCaseEmbeddingsRepository +import org.eclipse.lmos.adl.server.repositories.impl.InMemoryUseCaseEmbeddingsStore +import java.io.File fun startServer( wait: Boolean = true, @@ -53,8 +61,9 @@ fun startServer( val templateLoader = TemplateLoader() val sessions = InMemorySessions() val embeddingModel = AllMiniLmL6V2EmbeddingModel() - val useCaseStore = QdrantUseCaseEmbeddingsStore(embeddingModel, qdrantConfig) - val adlStorage = InMemoryAdlStorage() + // val useCaseStore: UseCaseEmbeddingsRepository = QdrantUseCaseEmbeddingsStore(embeddingModel, qdrantConfig) + val useCaseStore: UseCaseEmbeddingsRepository = InMemoryUseCaseEmbeddingsStore(embeddingModel) + val adlStorage: AdlRepository = InMemoryAdlRepository() // Agents val exampleAgent = createExampleAgent() @@ -109,7 +118,7 @@ fun startServer( TestCreatorMutation(testCreatorAgent, testCaseRepository, testExecutor), UseCaseImprovementMutation(improvementAgent), AdlExampleMutation(exampleAgent), - ) + ) } server { @@ -126,6 +135,16 @@ fun startServer( routing { graphiQLRoute() graphQLPostRoute() + + staticResources("/", "static") { + fallback { requestedPath, call -> + // read from classpath + if (requestedPath.startsWith("prompts")) call.respondText( + text = this::class.java.classLoader.getResource("static/prompts.html")!!.readText(), + contentType = ContentType.Text.Html, + ) + } + } } module() diff --git a/adl-server/src/main/kotlin/embeddings/UseCaseEmbeddingsStore.kt b/adl-server/src/main/kotlin/embeddings/UseCaseEmbeddingsStore.kt deleted file mode 100644 index 6a7013a1..00000000 --- a/adl-server/src/main/kotlin/embeddings/UseCaseEmbeddingsStore.kt +++ /dev/null @@ -1,14 +0,0 @@ -// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others -// -// SPDX-License-Identifier: Apache-2.0 -package org.eclipse.lmos.adl.server.embeddings - -/** - * Interface for storing UseCase embeddings. - */ -interface UseCaseEmbeddingsStore { - suspend fun storeUtterances(id: String, examples: List): Int - suspend fun storeUseCase(adl: String, examples: List = emptyList()): Int - suspend fun deleteByUseCaseId(useCaseId: String) - suspend fun clear() -} diff --git a/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt index 2e9e0c72..2737a5d7 100644 --- a/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt @@ -6,19 +6,18 @@ package org.eclipse.lmos.adl.server.inbound.mutation import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Mutation -import org.eclipse.lmos.adl.server.embeddings.UseCaseEmbeddingsStore import org.eclipse.lmos.adl.server.model.Adl -import org.eclipse.lmos.adl.server.storage.AdlStorage +import org.eclipse.lmos.adl.server.repositories.AdlRepository +import org.eclipse.lmos.adl.server.repositories.UseCaseEmbeddingsRepository import org.slf4j.LoggerFactory -import java.time.Instant import java.time.Instant.now /** * GraphQL Mutation for storing UseCases in the Embeddings store. */ class AdlStorageMutation( - private val useCaseStore: UseCaseEmbeddingsStore, - private val adlStorage: AdlStorage, + private val useCaseStore: UseCaseEmbeddingsRepository, + private val adlStorage: AdlRepository, ) : Mutation { private val log = LoggerFactory.getLogger(this::class.java) @@ -41,6 +40,23 @@ class AdlStorageMutation( ) } + @GraphQLDescription("Updates the tags of an existing ADL.") + suspend fun updateTags( + @GraphQLDescription("The unique ID of the ADL") id: String, + @GraphQLDescription("The new list of tags") tags: List, + ): StorageResult { + log.info("Updating tags for ADL with id: {}", id) + val existingAdl = adlStorage.get(id) ?: throw IllegalArgumentException("ADL with id $id not found") + + val updatedAdl = existingAdl.copy(tags = tags) + adlStorage.store(updatedAdl) + + return StorageResult( + storedExamplesCount = existingAdl.examples.size, + message = "Tags successfully updated", + ) + } + @GraphQLDescription("Deletes a UseCase from the embeddings store.") suspend fun delete( @GraphQLDescription("The unique ID of the UseCase to delete") id: String, diff --git a/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt b/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt index b738b4d5..d3f1afb2 100644 --- a/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt +++ b/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt @@ -6,18 +6,18 @@ package org.eclipse.lmos.adl.server.inbound.query import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Query -import org.eclipse.lmos.adl.server.embeddings.QdrantUseCaseEmbeddingsStore -import org.eclipse.lmos.adl.server.embeddings.UseCaseSearchResult import org.eclipse.lmos.adl.server.model.Adl import org.eclipse.lmos.adl.server.model.SimpleMessage -import org.eclipse.lmos.adl.server.storage.AdlStorage +import org.eclipse.lmos.adl.server.repositories.AdlRepository +import org.eclipse.lmos.adl.server.repositories.UseCaseEmbeddingsRepository +import org.eclipse.lmos.adl.server.repositories.UseCaseSearchResult /** * GraphQL Query for searching UseCases based on conversation embeddings. */ class AdlQuery( - private val useCaseStore: QdrantUseCaseEmbeddingsStore, - private val adlStorage: AdlStorage, + private val useCaseStore: UseCaseEmbeddingsRepository, + private val adlStorage: AdlRepository, ) : Query { @GraphQLDescription("Returns the supported version of the ALD.") diff --git a/adl-server/src/main/kotlin/model/Adl.kt b/adl-server/src/main/kotlin/models/Adl.kt similarity index 100% rename from adl-server/src/main/kotlin/model/Adl.kt rename to adl-server/src/main/kotlin/models/Adl.kt diff --git a/adl-server/src/main/kotlin/model/SimpleMessage.kt b/adl-server/src/main/kotlin/models/SimpleMessage.kt similarity index 100% rename from adl-server/src/main/kotlin/model/SimpleMessage.kt rename to adl-server/src/main/kotlin/models/SimpleMessage.kt diff --git a/adl-server/src/main/kotlin/repositories/AGENTS.md b/adl-server/src/main/kotlin/repositories/AGENTS.md new file mode 100644 index 00000000..7937d728 --- /dev/null +++ b/adl-server/src/main/kotlin/repositories/AGENTS.md @@ -0,0 +1,14 @@ + + +# ADL Server Repository + +Contains Repository classes for accessing and managing data related to the ADL Server application. + +## Coding Guidelines + +- Each Repository class is represented as a Kotlin interface. +- Implementations of these interfaces are located in the `repositories.impl` package. \ No newline at end of file diff --git a/adl-server/src/main/kotlin/storage/AdlStorage.kt b/adl-server/src/main/kotlin/repositories/AdlRepository.kt similarity index 79% rename from adl-server/src/main/kotlin/storage/AdlStorage.kt rename to adl-server/src/main/kotlin/repositories/AdlRepository.kt index 332aa9cc..b9dd51d7 100644 --- a/adl-server/src/main/kotlin/storage/AdlStorage.kt +++ b/adl-server/src/main/kotlin/repositories/AdlRepository.kt @@ -1,13 +1,13 @@ // SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others // // SPDX-License-Identifier: Apache-2.0 -package org.eclipse.lmos.adl.server.storage +package org.eclipse.lmos.adl.server.repositories import org.eclipse.lmos.adl.server.model.Adl -interface AdlStorage { +interface AdlRepository { suspend fun store(adl: Adl): Adl suspend fun get(id: String): Adl? suspend fun list(): List suspend fun deleteById(id: String) -} +} \ No newline at end of file diff --git a/adl-server/src/main/kotlin/repositories/UseCaseEmbeddingsRepository.kt b/adl-server/src/main/kotlin/repositories/UseCaseEmbeddingsRepository.kt new file mode 100644 index 00000000..b1dc1bf8 --- /dev/null +++ b/adl-server/src/main/kotlin/repositories/UseCaseEmbeddingsRepository.kt @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +// +// SPDX-License-Identifier: Apache-2.0 +package org.eclipse.lmos.adl.server.repositories + +import org.eclipse.lmos.adl.server.model.SimpleMessage + +/** + * Interface for storing UseCase embeddings. + */ +interface UseCaseEmbeddingsRepository : AutoCloseable { + suspend fun initialize() + suspend fun storeUtterances(id: String, examples: List): Int + suspend fun storeUseCase(adl: String, examples: List = emptyList()): Int + suspend fun search(query: String, limit: Int = 5, scoreThreshold: Float = 0.0f): List + suspend fun searchByConversation(messages: List, limit: Int = 5, scoreThreshold: Float = 0.0f): List + suspend fun deleteByUseCaseId(useCaseId: String) + suspend fun clear() + suspend fun count(): Long +} + +/** + * Result of a UseCase similarity search. + */ +data class UseCaseSearchResult( + val useCaseId: String, + val example: String, + val score: Float, + val content: String, +) diff --git a/adl-server/src/main/kotlin/storage/memory/InMemoryAdlStorage.kt b/adl-server/src/main/kotlin/repositories/impl/InMemoryAdlRepository.kt similarity index 79% rename from adl-server/src/main/kotlin/storage/memory/InMemoryAdlStorage.kt rename to adl-server/src/main/kotlin/repositories/impl/InMemoryAdlRepository.kt index a8a3fe0b..b4d49f63 100644 --- a/adl-server/src/main/kotlin/storage/memory/InMemoryAdlStorage.kt +++ b/adl-server/src/main/kotlin/repositories/impl/InMemoryAdlRepository.kt @@ -1,14 +1,13 @@ // SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others // // SPDX-License-Identifier: Apache-2.0 +package org.eclipse.lmos.adl.server.repositories.impl -package org.eclipse.lmos.adl.server.storage.memory - +import org.eclipse.lmos.adl.server.repositories.AdlRepository import org.eclipse.lmos.adl.server.model.Adl -import org.eclipse.lmos.adl.server.storage.AdlStorage import java.util.concurrent.ConcurrentHashMap -class InMemoryAdlStorage : AdlStorage { +class InMemoryAdlRepository : AdlRepository { private val storage = ConcurrentHashMap() override suspend fun store(adl: Adl): Adl { @@ -27,4 +26,4 @@ class InMemoryAdlStorage : AdlStorage { override suspend fun deleteById(id: String) { storage.remove(id) } -} +} \ No newline at end of file diff --git a/adl-server/src/main/kotlin/repositories/InMemoryTestCaseRepository.kt b/adl-server/src/main/kotlin/repositories/impl/InMemoryTestCaseRepository.kt similarity index 88% rename from adl-server/src/main/kotlin/repositories/InMemoryTestCaseRepository.kt rename to adl-server/src/main/kotlin/repositories/impl/InMemoryTestCaseRepository.kt index 920ce525..30ec8290 100644 --- a/adl-server/src/main/kotlin/repositories/InMemoryTestCaseRepository.kt +++ b/adl-server/src/main/kotlin/repositories/impl/InMemoryTestCaseRepository.kt @@ -2,8 +2,9 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.eclipse.lmos.adl.server.repositories +package org.eclipse.lmos.adl.server.repositories.impl +import org.eclipse.lmos.adl.server.repositories.TestCaseRepository import org.eclipse.lmos.adl.server.models.TestCase import java.util.concurrent.ConcurrentHashMap diff --git a/adl-server/src/main/kotlin/repositories/impl/InMemoryUseCaseEmbeddingsStore.kt b/adl-server/src/main/kotlin/repositories/impl/InMemoryUseCaseEmbeddingsStore.kt new file mode 100644 index 00000000..fec44195 --- /dev/null +++ b/adl-server/src/main/kotlin/repositories/impl/InMemoryUseCaseEmbeddingsStore.kt @@ -0,0 +1,131 @@ +// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +// +// SPDX-License-Identifier: Apache-2.0 + +package org.eclipse.lmos.adl.server.repositories.impl + +import dev.langchain4j.data.document.Metadata +import dev.langchain4j.data.segment.TextSegment +import dev.langchain4j.model.embedding.EmbeddingModel +import dev.langchain4j.store.embedding.EmbeddingSearchRequest +import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore +import org.eclipse.lmos.adl.server.model.SimpleMessage +import org.eclipse.lmos.adl.server.repositories.UseCaseEmbeddingsRepository +import org.eclipse.lmos.adl.server.repositories.UseCaseSearchResult +import org.eclipse.lmos.arc.assistants.support.usecases.toUseCases +import java.util.concurrent.ConcurrentHashMap + +class InMemoryUseCaseEmbeddingsStore( + private val embeddingModel: EmbeddingModel +) : UseCaseEmbeddingsRepository { + + private val store = InMemoryEmbeddingStore() + // Manage mapping of UseCaseId to embedding IDs in the store + private val useCaseIdToEmbeddingIds = ConcurrentHashMap>() + + override suspend fun initialize() { + // In-Memory does not require initialization + } + + override suspend fun storeUtterances(id: String, examples: List): Int { + deleteByUseCaseId(id) + return storeExamples(id, "", examples) + } + + override suspend fun storeUseCase(adl: String, examples: List): Int { + val parsedUseCases = adl.toUseCases() + var count = 0 + parsedUseCases.forEach { useCase -> + deleteByUseCaseId(useCase.id) + val allExamples = (parseExamples(useCase.examples) + examples).distinct() + count += storeExamples(useCase.id, adl, allExamples) + } + return count + } + + private fun storeExamples(useCaseId: String, content: String, examples: List): Int { + val segments = examples.map { example -> + TextSegment.from(example, Metadata.from(mapOf( + PAYLOAD_USECASE_ID to useCaseId, + PAYLOAD_EXAMPLE to example, + PAYLOAD_CONTENT to content + ))) + } + + if (segments.isEmpty()) return 0 + + val embeddings = embeddingModel.embedAll(segments).content() + val ids = store.addAll(embeddings, segments) + + useCaseIdToEmbeddingIds.computeIfAbsent(useCaseId) { mutableListOf() }.addAll(ids) + + return ids.size + } + + override suspend fun search(query: String, limit: Int, scoreThreshold: Float): List { + val embedding = embeddingModel.embed(query).content() + val request = EmbeddingSearchRequest.builder() + .queryEmbedding(embedding) + .maxResults(limit) + .minScore(scoreThreshold.toDouble()) + .build() + val results = store.search(request).matches() + + return results.map { match -> + UseCaseSearchResult( + useCaseId = match.embedded().metadata().getString(PAYLOAD_USECASE_ID) ?: "", + example = match.embedded().metadata().getString(PAYLOAD_EXAMPLE) ?: "", + score = match.score().toFloat(), + content = match.embedded().metadata().getString(PAYLOAD_CONTENT) ?: "" + ) + } + } + + override suspend fun searchByConversation( + messages: List, + limit: Int, + scoreThreshold: Float + ): List { + // Filter last user messages + return messages.filter { it.role == "user" && it.content.length > 5 } + .takeLast(5) + .flatMap { search(it.content, limit, scoreThreshold) } + } + + override suspend fun deleteByUseCaseId(useCaseId: String) { + val ids = useCaseIdToEmbeddingIds.remove(useCaseId) + if (!ids.isNullOrEmpty()) { + store.removeAll(ids) + } + } + + override suspend fun clear() { + val allIds = useCaseIdToEmbeddingIds.values.flatten() + if (allIds.isNotEmpty()) { + store.removeAll(allIds) + useCaseIdToEmbeddingIds.clear() + } + } + + override suspend fun count(): Long { + return useCaseIdToEmbeddingIds.values.sumOf { it.size }.toLong() + } + + override fun close() { + // Nothing to do for In-Memory + } + + private fun parseExamples(examples: String): List { + return examples.lines() + .map { it.trim() } + .filter { it.isNotEmpty() } + .map { it.removePrefix("-").trim() } + .filter { it.isNotEmpty() } + } + + companion object { + private const val PAYLOAD_USECASE_ID = "usecase_id" + private const val PAYLOAD_EXAMPLE = "example" + private const val PAYLOAD_CONTENT = "content" + } +} diff --git a/adl-server/src/main/kotlin/embeddings/QdrantUseCaseEmbeddingsStore.kt b/adl-server/src/main/kotlin/repositories/impl/QdrantUseCaseEmbeddingsStore.kt similarity index 94% rename from adl-server/src/main/kotlin/embeddings/QdrantUseCaseEmbeddingsStore.kt rename to adl-server/src/main/kotlin/repositories/impl/QdrantUseCaseEmbeddingsStore.kt index 88deff97..d70127af 100644 --- a/adl-server/src/main/kotlin/embeddings/QdrantUseCaseEmbeddingsStore.kt +++ b/adl-server/src/main/kotlin/repositories/impl/QdrantUseCaseEmbeddingsStore.kt @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -package org.eclipse.lmos.adl.server.embeddings +package org.eclipse.lmos.adl.server.repositories.impl import dev.langchain4j.model.embedding.EmbeddingModel import io.qdrant.client.PointIdFactory.id @@ -17,6 +17,8 @@ import io.qdrant.client.grpc.Points.ScoredPoint import kotlinx.coroutines.guava.await import org.eclipse.lmos.adl.server.QdrantConfig import org.eclipse.lmos.adl.server.model.SimpleMessage +import org.eclipse.lmos.adl.server.repositories.UseCaseEmbeddingsRepository +import org.eclipse.lmos.adl.server.repositories.UseCaseSearchResult import org.eclipse.lmos.arc.assistants.support.usecases.toUseCases import java.util.UUID import java.util.concurrent.ExecutionException @@ -31,7 +33,7 @@ import java.util.concurrent.ExecutionException class QdrantUseCaseEmbeddingsStore( private val embeddingModel: EmbeddingModel, private val config: QdrantConfig = QdrantConfig(), -) : AutoCloseable, UseCaseEmbeddingsStore { +) : AutoCloseable, UseCaseEmbeddingsRepository { private val client: QdrantClient by lazy { QdrantClient( @@ -42,7 +44,7 @@ class QdrantUseCaseEmbeddingsStore( /** * Initializes the Qdrant collection if it doesn't exist. */ - suspend fun initialize() { + override suspend fun initialize() { try { val collections = client.listCollectionsAsync().await() if (!collections.contains(config.collectionName)) { @@ -136,7 +138,7 @@ class QdrantUseCaseEmbeddingsStore( * @param scoreThreshold The minimum similarity score (0.0 to 1.0). * @return List of matching UseCase embeddings with their scores. */ - suspend fun search(query: String, limit: Int = 5, scoreThreshold: Float = 0.0f): List { + override suspend fun search(query: String, limit: Int, scoreThreshold: Float): List { val queryEmbedding = embeddingModel.embed(query).content().vector() return searchByVector(queryEmbedding.toList(), limit, scoreThreshold) } @@ -178,10 +180,10 @@ class QdrantUseCaseEmbeddingsStore( * @param scoreThreshold The minimum similarity score (0.0 to 1.0). * @return List of matching UseCase embeddings with their scores. */ - suspend fun searchByConversation( + override suspend fun searchByConversation( messages: List, - limit: Int = 5, - scoreThreshold: Float = 0.0f, + limit: Int, + scoreThreshold: Float, ): List { val combinedQuery = messages.filter { it.role == "user" && it.content.length > 5 }.takeLast(5).flatMap { search(it.content, limit, scoreThreshold) @@ -231,7 +233,7 @@ class QdrantUseCaseEmbeddingsStore( /** * Gets the total number of embeddings stored. */ - suspend fun count(): Long { + override suspend fun count(): Long { return try { client.countAsync(config.collectionName).await() } catch (e: ExecutionException) { @@ -280,12 +282,3 @@ class QdrantUseCaseEmbeddingsStore( } } -/** - * Result of a UseCase similarity search. - */ -data class UseCaseSearchResult( - val useCaseId: String, - val example: String, - val score: Float, - val content: String, -) diff --git a/adl-server/src/main/kotlin/services/TestExecutor.kt b/adl-server/src/main/kotlin/services/TestExecutor.kt index 289ea5b3..9d6bc117 100644 --- a/adl-server/src/main/kotlin/services/TestExecutor.kt +++ b/adl-server/src/main/kotlin/services/TestExecutor.kt @@ -9,8 +9,8 @@ import org.eclipse.lmos.adl.server.models.ConversationTurn import org.eclipse.lmos.adl.server.models.TestCase import org.eclipse.lmos.adl.server.models.TestExecutionResult import org.eclipse.lmos.adl.server.models.TestRunResult +import org.eclipse.lmos.adl.server.repositories.AdlRepository import org.eclipse.lmos.adl.server.repositories.TestCaseRepository -import org.eclipse.lmos.adl.server.storage.AdlStorage import org.eclipse.lmos.arc.agents.ConversationAgent import org.eclipse.lmos.arc.agents.conversation.Conversation import org.eclipse.lmos.arc.agents.conversation.ConversationMessage @@ -27,7 +27,7 @@ import java.util.UUID */ class TestExecutor( private val assistantAgent: ConversationAgent, - private val adlStorage: AdlStorage, + private val adlStorage: AdlRepository, private val testCaseRepository: TestCaseRepository, private val conversationEvaluator: ConversationEvaluator, ) { @@ -57,7 +57,7 @@ class TestExecutor( } private suspend fun executeTestCase(testCase: TestCase, useCases: Any): TestExecutionResult { - val results = (1..5).map { + val results = (1..2).map { runSingleTestCase(testCase, useCases) } return results.minByOrNull { it.score } ?: results.first() diff --git a/adl-server/src/main/resources/assistant.md b/adl-server/src/main/resources/assistant.md index 7933cc5f..1c64f3d9 100644 --- a/adl-server/src/main/resources/assistant.md +++ b/adl-server/src/main/resources/assistant.md @@ -28,14 +28,14 @@ These rules override all others if there is a conflict. ## ReAct Execution Flow (Internal – Do Not Expose) -Thought: -Question: the input question you must answer. -Thought: you should always think about what to do. -Action: the action to take, examine the conversation so far and determine the best use case to apply and generate a response based on its instructions of the selected use case. -Observation: the result of the action. -(Note: this Thought/Action/Observation can repeat N times) -Thought: I now know the final answer. -Output: the final answer to the original input question. +The following is for internal execution only and must never appear in the output: + +- Thought: Analyze the customer question. +- Action: Select the best matching use case and apply its rules. +- Observation: Process the result. +- (Repeat if necessary.) +- Thought: Final answer is ready. +- Output: Produce the customer-facing response. ## Use Case & Step Handling Rules From 5bb6f08cd95f9888a01250d923372a6550c8b50f Mon Sep 17 00:00:00 2001 From: "patrick.whelan@telekom.de" Date: Wed, 28 Jan 2026 20:10:45 +0100 Subject: [PATCH 15/40] feat: add metadata support to tools and implement related tests --- adl-server/src/main/kotlin/inbound/AdlTestQuery.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/adl-server/src/main/kotlin/inbound/AdlTestQuery.kt b/adl-server/src/main/kotlin/inbound/AdlTestQuery.kt index 0d8c57bd..8b29d407 100644 --- a/adl-server/src/main/kotlin/inbound/AdlTestQuery.kt +++ b/adl-server/src/main/kotlin/inbound/AdlTestQuery.kt @@ -6,6 +6,7 @@ package org.eclipse.lmos.adl.server.inbound import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Query +import org.eclipse.lmos.adl.server.models.TestCase import org.eclipse.lmos.adl.server.repositories.TestCaseRepository /** From 7f861188d86ece38e59d85a28ec5e8bedab274ed Mon Sep 17 00:00:00 2001 From: "patrick.whelan@telekom.de" Date: Wed, 28 Jan 2026 20:23:20 +0100 Subject: [PATCH 16/40] feat: add metadata support to tools and implement related tests --- adl-server/build.gradle.kts | 4 ++++ .../src/main/kotlin/embeddings/ConversationEmbedder.kt | 2 +- .../src/main/kotlin/embeddings/ConversationToTextStrategy.kt | 2 +- .../src/main/kotlin/inbound/mutation/AdlEvalMutation.kt | 2 +- adl-server/src/main/kotlin/inbound/query/AdlQuery.kt | 2 +- adl-server/src/main/kotlin/inbound/query/TestCaseQuery.kt | 1 - adl-server/src/main/kotlin/models/SimpleMessage.kt | 5 ++++- .../main/kotlin/repositories/UseCaseEmbeddingsRepository.kt | 2 +- .../repositories/impl/InMemoryUseCaseEmbeddingsStore.kt | 2 +- .../kotlin/repositories/impl/QdrantUseCaseEmbeddingsStore.kt | 2 +- adl-server/src/main/kotlin/services/ConversationEvaluator.kt | 2 +- adl-server/src/main/kotlin/services/TestExecutor.kt | 2 +- 12 files changed, 17 insertions(+), 11 deletions(-) diff --git a/adl-server/build.gradle.kts b/adl-server/build.gradle.kts index 86a8cdb4..a615d628 100644 --- a/adl-server/build.gradle.kts +++ b/adl-server/build.gradle.kts @@ -12,6 +12,10 @@ application { mainClass = "org.eclipse.lmos.adl.server.AdlServerKt" } +tasks.shadowJar { + isZip64 = true +} + graalvmNative { binaries { named("main") { diff --git a/adl-server/src/main/kotlin/embeddings/ConversationEmbedder.kt b/adl-server/src/main/kotlin/embeddings/ConversationEmbedder.kt index 3123f3d9..819e5157 100644 --- a/adl-server/src/main/kotlin/embeddings/ConversationEmbedder.kt +++ b/adl-server/src/main/kotlin/embeddings/ConversationEmbedder.kt @@ -5,7 +5,7 @@ package org.eclipse.lmos.adl.server.embeddings import dev.langchain4j.model.embedding.EmbeddingModel -import org.eclipse.lmos.adl.server.model.SimpleMessage +import org.eclipse.lmos.adl.server.models.SimpleMessage /** * Creates a single embedding for an entire conversation. diff --git a/adl-server/src/main/kotlin/embeddings/ConversationToTextStrategy.kt b/adl-server/src/main/kotlin/embeddings/ConversationToTextStrategy.kt index c690771d..a8c29825 100644 --- a/adl-server/src/main/kotlin/embeddings/ConversationToTextStrategy.kt +++ b/adl-server/src/main/kotlin/embeddings/ConversationToTextStrategy.kt @@ -4,7 +4,7 @@ package org.eclipse.lmos.adl.server.embeddings -import org.eclipse.lmos.adl.server.model.SimpleMessage +import org.eclipse.lmos.adl.server.models.SimpleMessage /** diff --git a/adl-server/src/main/kotlin/inbound/mutation/AdlEvalMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/AdlEvalMutation.kt index d7479016..f1593b67 100644 --- a/adl-server/src/main/kotlin/inbound/mutation/AdlEvalMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/AdlEvalMutation.kt @@ -8,7 +8,7 @@ import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Mutation import kotlinx.serialization.Serializable import org.eclipse.lmos.adl.server.agents.EvalOutput -import org.eclipse.lmos.adl.server.model.SimpleMessage +import org.eclipse.lmos.adl.server.models.SimpleMessage import org.eclipse.lmos.adl.server.services.ConversationEvaluator import org.eclipse.lmos.arc.agents.ConversationAgent import org.eclipse.lmos.arc.agents.agent.process diff --git a/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt b/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt index d3f1afb2..a3b827a6 100644 --- a/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt +++ b/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt @@ -7,7 +7,7 @@ package org.eclipse.lmos.adl.server.inbound.query import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Query import org.eclipse.lmos.adl.server.model.Adl -import org.eclipse.lmos.adl.server.model.SimpleMessage +import org.eclipse.lmos.adl.server.models.SimpleMessage import org.eclipse.lmos.adl.server.repositories.AdlRepository import org.eclipse.lmos.adl.server.repositories.UseCaseEmbeddingsRepository import org.eclipse.lmos.adl.server.repositories.UseCaseSearchResult diff --git a/adl-server/src/main/kotlin/inbound/query/TestCaseQuery.kt b/adl-server/src/main/kotlin/inbound/query/TestCaseQuery.kt index 36254a98..cf4f125c 100644 --- a/adl-server/src/main/kotlin/inbound/query/TestCaseQuery.kt +++ b/adl-server/src/main/kotlin/inbound/query/TestCaseQuery.kt @@ -7,7 +7,6 @@ package org.eclipse.lmos.adl.server.inbound.query import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Query import org.eclipse.lmos.adl.server.models.TestCase -import org.eclipse.lmos.adl.server.model.SimpleMessage import org.eclipse.lmos.adl.server.repositories.TestCaseRepository @GraphQLDescription("GraphQL Query for fetching test cases for a use case.") diff --git a/adl-server/src/main/kotlin/models/SimpleMessage.kt b/adl-server/src/main/kotlin/models/SimpleMessage.kt index 3fed2643..bd8d5ba0 100644 --- a/adl-server/src/main/kotlin/models/SimpleMessage.kt +++ b/adl-server/src/main/kotlin/models/SimpleMessage.kt @@ -1,4 +1,7 @@ -package org.eclipse.lmos.adl.server.model +// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +// +// SPDX-License-Identifier: Apache-2.0 +package org.eclipse.lmos.adl.server.models import com.expediagroup.graphql.generator.annotations.GraphQLDescription import kotlinx.serialization.Serializable diff --git a/adl-server/src/main/kotlin/repositories/UseCaseEmbeddingsRepository.kt b/adl-server/src/main/kotlin/repositories/UseCaseEmbeddingsRepository.kt index b1dc1bf8..4b273d69 100644 --- a/adl-server/src/main/kotlin/repositories/UseCaseEmbeddingsRepository.kt +++ b/adl-server/src/main/kotlin/repositories/UseCaseEmbeddingsRepository.kt @@ -3,7 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 package org.eclipse.lmos.adl.server.repositories -import org.eclipse.lmos.adl.server.model.SimpleMessage +import org.eclipse.lmos.adl.server.models.SimpleMessage /** * Interface for storing UseCase embeddings. diff --git a/adl-server/src/main/kotlin/repositories/impl/InMemoryUseCaseEmbeddingsStore.kt b/adl-server/src/main/kotlin/repositories/impl/InMemoryUseCaseEmbeddingsStore.kt index fec44195..2bf11992 100644 --- a/adl-server/src/main/kotlin/repositories/impl/InMemoryUseCaseEmbeddingsStore.kt +++ b/adl-server/src/main/kotlin/repositories/impl/InMemoryUseCaseEmbeddingsStore.kt @@ -9,7 +9,7 @@ import dev.langchain4j.data.segment.TextSegment import dev.langchain4j.model.embedding.EmbeddingModel import dev.langchain4j.store.embedding.EmbeddingSearchRequest import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore -import org.eclipse.lmos.adl.server.model.SimpleMessage +import org.eclipse.lmos.adl.server.models.SimpleMessage import org.eclipse.lmos.adl.server.repositories.UseCaseEmbeddingsRepository import org.eclipse.lmos.adl.server.repositories.UseCaseSearchResult import org.eclipse.lmos.arc.assistants.support.usecases.toUseCases diff --git a/adl-server/src/main/kotlin/repositories/impl/QdrantUseCaseEmbeddingsStore.kt b/adl-server/src/main/kotlin/repositories/impl/QdrantUseCaseEmbeddingsStore.kt index d70127af..03c3040d 100644 --- a/adl-server/src/main/kotlin/repositories/impl/QdrantUseCaseEmbeddingsStore.kt +++ b/adl-server/src/main/kotlin/repositories/impl/QdrantUseCaseEmbeddingsStore.kt @@ -16,7 +16,7 @@ import io.qdrant.client.grpc.Points.PointStruct import io.qdrant.client.grpc.Points.ScoredPoint import kotlinx.coroutines.guava.await import org.eclipse.lmos.adl.server.QdrantConfig -import org.eclipse.lmos.adl.server.model.SimpleMessage +import org.eclipse.lmos.adl.server.models.SimpleMessage import org.eclipse.lmos.adl.server.repositories.UseCaseEmbeddingsRepository import org.eclipse.lmos.adl.server.repositories.UseCaseSearchResult import org.eclipse.lmos.arc.assistants.support.usecases.toUseCases diff --git a/adl-server/src/main/kotlin/services/ConversationEvaluator.kt b/adl-server/src/main/kotlin/services/ConversationEvaluator.kt index b762f112..fff162b1 100644 --- a/adl-server/src/main/kotlin/services/ConversationEvaluator.kt +++ b/adl-server/src/main/kotlin/services/ConversationEvaluator.kt @@ -7,7 +7,7 @@ import dev.langchain4j.model.embedding.EmbeddingModel import dev.langchain4j.store.embedding.CosineSimilarity import org.eclipse.lmos.adl.server.agents.EvalEvidence import org.eclipse.lmos.adl.server.agents.EvalOutput -import org.eclipse.lmos.adl.server.model.SimpleMessage +import org.eclipse.lmos.adl.server.models.SimpleMessage import kotlin.math.roundToInt class ConversationEvaluator( diff --git a/adl-server/src/main/kotlin/services/TestExecutor.kt b/adl-server/src/main/kotlin/services/TestExecutor.kt index 9d6bc117..0c93428f 100644 --- a/adl-server/src/main/kotlin/services/TestExecutor.kt +++ b/adl-server/src/main/kotlin/services/TestExecutor.kt @@ -4,7 +4,7 @@ package org.eclipse.lmos.adl.server.services -import org.eclipse.lmos.adl.server.model.SimpleMessage +import org.eclipse.lmos.adl.server.models.SimpleMessage import org.eclipse.lmos.adl.server.models.ConversationTurn import org.eclipse.lmos.adl.server.models.TestCase import org.eclipse.lmos.adl.server.models.TestExecutionResult From 6b041c65341557ec0cdb0c41d5988b54345b3366 Mon Sep 17 00:00:00 2001 From: "patrick.whelan@telekom.de" Date: Wed, 28 Jan 2026 20:23:20 +0100 Subject: [PATCH 17/40] feat: add metadata support to tools and implement related tests --- adl-server/src/main/kotlin/AdlServer.kt | 2 + .../src/main/kotlin/agents/AssistantAgent.kt | 29 +++- .../agents/extensions/ConversationGuide.kt | 148 ++++++++++++++++++ .../inbound/mutation/AdlAssistantMutation.kt | 1 + .../inbound/rest/OpenAICompletionsHandler.kt | 80 ++++++++++ .../src/main/kotlin/models/OpenAIModels.kt | 54 +++++++ .../kotlin/services/ConversationEvaluator.kt | 5 +- adl-server/src/main/resources/assistant.md | 147 ++++++++--------- .../src/main/resources/examples/buy_a_car.md | 13 ++ 9 files changed, 401 insertions(+), 78 deletions(-) create mode 100644 adl-server/src/main/kotlin/agents/extensions/ConversationGuide.kt create mode 100644 adl-server/src/main/kotlin/inbound/rest/OpenAICompletionsHandler.kt create mode 100644 adl-server/src/main/kotlin/models/OpenAIModels.kt create mode 100644 adl-server/src/main/resources/examples/buy_a_car.md diff --git a/adl-server/src/main/kotlin/AdlServer.kt b/adl-server/src/main/kotlin/AdlServer.kt index 0788d789..e9c12db3 100644 --- a/adl-server/src/main/kotlin/AdlServer.kt +++ b/adl-server/src/main/kotlin/AdlServer.kt @@ -45,6 +45,7 @@ import org.eclipse.lmos.adl.server.repositories.impl.InMemoryAdlRepository import org.eclipse.lmos.adl.server.templates.TemplateLoader import org.eclipse.lmos.adl.server.agents.createImprovementAgent import org.eclipse.lmos.adl.server.inbound.mutation.AdlExampleMutation +import org.eclipse.lmos.adl.server.inbound.rest.openAICompletions import org.eclipse.lmos.adl.server.repositories.AdlRepository import org.eclipse.lmos.adl.server.repositories.impl.InMemoryTestCaseRepository import org.eclipse.lmos.adl.server.repositories.UseCaseEmbeddingsRepository @@ -133,6 +134,7 @@ fun startServer( } routing { + openAICompletions(assistantAgent) graphiQLRoute() graphQLPostRoute() diff --git a/adl-server/src/main/kotlin/agents/AssistantAgent.kt b/adl-server/src/main/kotlin/agents/AssistantAgent.kt index 23363658..733e4172 100644 --- a/adl-server/src/main/kotlin/agents/AssistantAgent.kt +++ b/adl-server/src/main/kotlin/agents/AssistantAgent.kt @@ -3,6 +3,7 @@ // SPDX-License-Identifier: Apache-2.0 package org.eclipse.lmos.adl.server.agents +import org.eclipse.lmos.adl.server.agents.extensions.conversationGuide import org.eclipse.lmos.arc.agents.ConversationAgent import org.eclipse.lmos.arc.agents.agents import org.eclipse.lmos.arc.agents.dsl.extensions.getCurrentUseCases @@ -10,29 +11,45 @@ import org.eclipse.lmos.arc.agents.dsl.extensions.local import org.eclipse.lmos.arc.agents.dsl.extensions.processUseCases import org.eclipse.lmos.arc.agents.dsl.extensions.time import org.eclipse.lmos.arc.agents.dsl.get +import org.eclipse.lmos.arc.agents.events.LoggingEventHandler import org.eclipse.lmos.arc.agents.retry import org.eclipse.lmos.arc.assistants.support.filters.UnresolvedDetector import org.eclipse.lmos.arc.assistants.support.filters.UseCaseResponseHandler +import org.eclipse.lmos.arc.assistants.support.usecases.Conditional import org.eclipse.lmos.arc.assistants.support.usecases.UseCase -fun createAssistantAgent(): ConversationAgent = agents { +fun createAssistantAgent(): ConversationAgent = agents( + handlers = listOf(LoggingEventHandler()) +) { agent { name = "assistant_agent" filterOutput { -"```json" -"```" +UseCaseResponseHandler() - if(outputMessage.content.contains("NEXT_USE_CASE")) { - retry("Detected NEXT_USE_CASE usage, retrying to avoid infinite loop.", max = 10) - } +UnresolvedDetector { "UNRESOLVED" } } + conversationGuide() prompt { val role = local("role.md")!! - val useCases = processUseCases(useCases = get>(), fallbackLimit = 3) + + // Convert steps to conditionals in use cases + val useCases = get>().map { uc -> + if (uc.steps.isNotEmpty()) { + val convertedSteps = uc.steps.filter { it.text.isNotEmpty() }.mapIndexed { i, step -> + step.copy(conditions = step.conditions + "step_${i + 1}") + } + uc.copy(solution = convertedSteps + uc.solution.map { s -> + s.copy(conditions = s.conditions + "else") + }, steps = emptyList()) + } else uc + } + val useCasesPrompt = processUseCases(useCases = useCases, fallbackLimit = 3) + + // Output the final prompt val prompt = local("assistant.md")!! .replace("\$\$ROLE\$\$", role) - .replace("\$\$USE_CASES\$\$", useCases) + .replace("\$\$USE_CASES\$\$", useCasesPrompt) .replace("\$\$TIME\$\$", time()) prompt } diff --git a/adl-server/src/main/kotlin/agents/extensions/ConversationGuide.kt b/adl-server/src/main/kotlin/agents/extensions/ConversationGuide.kt new file mode 100644 index 00000000..fcf97aa1 --- /dev/null +++ b/adl-server/src/main/kotlin/agents/extensions/ConversationGuide.kt @@ -0,0 +1,148 @@ +// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +// +// SPDX-License-Identifier: Apache-2.0 +package org.eclipse.lmos.adl.server.agents.extensions + +import org.eclipse.lmos.arc.agents.ArcException +import org.eclipse.lmos.arc.agents.RetrySignal +import org.eclipse.lmos.arc.agents.conversation.AssistantMessage +import org.eclipse.lmos.arc.agents.conversation.Conversation +import org.eclipse.lmos.arc.agents.conversation.ConversationMessage +import org.eclipse.lmos.arc.agents.conversation.SystemMessage +import org.eclipse.lmos.arc.agents.conversation.UserMessage +import org.eclipse.lmos.arc.agents.dsl.AgentDefinition +import org.eclipse.lmos.arc.agents.dsl.AgentInputFilter +import org.eclipse.lmos.arc.agents.dsl.AgentOutputFilter +import org.eclipse.lmos.arc.agents.dsl.DSLContext +import org.eclipse.lmos.arc.agents.dsl.InputFilterContext +import org.eclipse.lmos.arc.agents.dsl.OutputFilterContext +import org.eclipse.lmos.arc.agents.dsl.extensions.getCurrentAgent +import org.eclipse.lmos.arc.agents.dsl.get +import org.eclipse.lmos.arc.agents.dsl.getOptional +import org.eclipse.lmos.arc.agents.llm.ChatCompleterProvider +import org.eclipse.lmos.arc.agents.llm.ChatCompletionSettings +import org.eclipse.lmos.arc.agents.retry +import org.eclipse.lmos.arc.core.Failure +import org.eclipse.lmos.arc.core.getOrNull +import org.eclipse.lmos.arc.core.result +import org.slf4j.LoggerFactory + +/** + * Agent definition plugin to improve responses by checking if + * the questions asked by the Agent can be answered from the conversation history. + */ +fun AgentDefinition.conversationGuide() { + filterInput { + +InputHintProvider() + } + filterOutput { + +ConversationGuider() + } +} + +/** + * Key for storing retry reason in the RetrySignal. + */ +const val RESPONSE_GUIDE_RETRY_REASON = "RESPONSE_GUIDE_RETRY_REASON" + +/** + * Filter to prevent the agent from asking questions that can be answered from the conversation history. + */ +class ConversationGuider(private val retryMax: Int = 4, private val model: String? = null) : AgentOutputFilter { + private val log = LoggerFactory.getLogger(ConversationGuider::class.java) + private val noAnswerReturn = "NO_ANSWER" + private val outputDivider = "Final Answer >>" + + private val symbolRegex = "<(.*?)>".toRegex(RegexOption.IGNORE_CASE) + + private fun system() = + """ + ## Goal + You are an AI assistant designed to answer questions by referencing information previously shared in this conversation. + + ## Instructions + - You *must* only use information explicitly stated in the conversation history to formulate your answer. + - Do not introduce new external information or make assumptions. + - If the answer cannot be derived from the conversation history, return "$noAnswerReturn". + - Formulate your response so that it is clear what information you are using from the conversation history. + + Use the following format in your response: + + Question: the input question you must answer. + Thought: you should always think about what to do. + Action: the action to take, examine the conversation so far and extract the answer or conclude that the answer cannot be derived from the conversation. + Observation: the result of the action. + (Note: this Thought/Action/Observation can repeat N times) + Thought: I now know the final answer. + $outputDivider the final answer to the original input question. + + """ + + override suspend fun filter(message: ConversationMessage, context: OutputFilterContext): ConversationMessage? { + if (!message.content.contains("?")) return message + + log.debug("Checking Agent response: ${message.content}") + val conversation = context.get() + val cleanOutputMessage = message.content.clean() + val result = + context.callLLM( + buildList { + add(SystemMessage(system())) + conversation.transcript.forEach { add(it) } + add(UserMessage(cleanOutputMessage)) + }, + ) + + if (result is Failure) { + log.warn("ResponseHelper failed!", result.reason) + return message + } + + val output = result.getOrNull()?.content?.substringAfter(outputDivider) ?: return message + val agentName = context.getCurrentAgent()?.name ?: return message + if (!output.contains(noAnswerReturn, ignoreCase = true)) { + val previousHistory = context.getOptional()?.details ?: emptyMap() + val newHistory = mapOf(cleanOutputMessage to output) + log.info("Retrying $agentName...") + context.retry(max = retryMax, details = previousHistory + newHistory, reason = RESPONSE_GUIDE_RETRY_REASON) + } + return message + } + + suspend fun DSLContext.callLLM(messages: List) = result { + val chatCompleterProvider = get() + val chatCompleter = chatCompleterProvider.provideByModel(model = model) + return chatCompleter.complete( + messages, + null, + settings = ChatCompletionSettings(temperature = 0.0, seed = 42), + ) + } + + private fun String.clean(): String = replace(symbolRegex, "").trim() +} + +/** + * Filter to provide input hints based on the response helper's history. + */ +class InputHintProvider : AgentInputFilter { + private val log = LoggerFactory.getLogger(ConversationGuider::class.java) + + override suspend fun filter(message: ConversationMessage, context: InputFilterContext): ConversationMessage? { + val retry = context.getOptional() + val history = retry?.details + return if (RESPONSE_GUIDE_RETRY_REASON == retry?.reason && history != null) { + log.info("Updating input message hints: $history") + message.update( + """ + { + Customer: { message: "${message.content}" } + Notes: { Hint: "$history" } + } + """.trimIndent(), + ) + } else { + message + } + } +} diff --git a/adl-server/src/main/kotlin/inbound/mutation/AdlAssistantMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/AdlAssistantMutation.kt index d40cf2fb..87c39963 100644 --- a/adl-server/src/main/kotlin/inbound/mutation/AdlAssistantMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/AdlAssistantMutation.kt @@ -15,6 +15,7 @@ import org.eclipse.lmos.arc.agents.conversation.ConversationMessage import org.eclipse.lmos.arc.agents.conversation.UserMessage import org.eclipse.lmos.arc.agents.conversation.latest import org.eclipse.lmos.arc.agents.dsl.extensions.OutputContext +import org.eclipse.lmos.arc.agents.events.LoggingEventHandler import org.eclipse.lmos.arc.api.AgentRequest import org.eclipse.lmos.arc.api.AgentResult import org.eclipse.lmos.arc.api.AgentResultType.MESSAGE diff --git a/adl-server/src/main/kotlin/inbound/rest/OpenAICompletionsHandler.kt b/adl-server/src/main/kotlin/inbound/rest/OpenAICompletionsHandler.kt new file mode 100644 index 00000000..79b283dc --- /dev/null +++ b/adl-server/src/main/kotlin/inbound/rest/OpenAICompletionsHandler.kt @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +// +// SPDX-License-Identifier: Apache-2.0 +package org.eclipse.lmos.adl.server.inbound.rest + +import io.ktor.server.application.call +import io.ktor.server.request.receive +import io.ktor.server.response.respond +import io.ktor.server.routing.Route +import io.ktor.server.routing.post +import io.ktor.server.routing.route +import org.eclipse.lmos.adl.server.models.OpenAIChatCompletionRequest +import org.eclipse.lmos.adl.server.models.OpenAIChatCompletionResponse +import org.eclipse.lmos.adl.server.models.OpenAIChoice +import org.eclipse.lmos.adl.server.models.OpenAIMessage +import org.eclipse.lmos.adl.server.models.OpenAIUsage +import org.eclipse.lmos.arc.agents.ConversationAgent +import org.eclipse.lmos.arc.agents.conversation.AssistantMessage +import org.eclipse.lmos.arc.agents.conversation.Conversation +import org.eclipse.lmos.arc.agents.conversation.SystemMessage +import org.eclipse.lmos.arc.agents.conversation.UserMessage +import org.eclipse.lmos.arc.core.Failure +import org.eclipse.lmos.arc.core.Success +import java.util.UUID + +fun Route.openAICompletions(assistantAgent: ConversationAgent) { + route("/v1/chat/completions") { + post { + val request = call.receive() + + val messages = request.messages.map { msg -> + when (msg.role) { + "system" -> SystemMessage(content = msg.content) + "user" -> UserMessage(content = msg.content) + "assistant" -> AssistantMessage(content = msg.content) + else -> UserMessage(content = msg.content) + } + } + + val conversationId = call.request.headers["X-Conversation-Id"]?.takeIf { it.isNotBlank() } + ?: UUID.randomUUID().toString() + + // Create initial conversation from messages + val initialConversation = Conversation( + conversationId = conversationId, + transcript = messages + ) + + val result = assistantAgent.execute(initialConversation, emptySet()) + + val conversation = when (result) { + is Success -> result.value + is Failure -> throw RuntimeException("Agent execution failed: ${result.reason}") + } + + val lastMessage = conversation.transcript.lastOrNull() + val content = lastMessage?.content ?: "" + + val response = OpenAIChatCompletionResponse( + id = "chatcmpl-${UUID.randomUUID()}", + `object` = "chat.completion", + created = System.currentTimeMillis() / 1000, + model = request.model, + choices = listOf( + OpenAIChoice( + index = 0, + message = OpenAIMessage( + role = "assistant", + content = content + ), + finish_reason = "stop" + ) + ), + usage = OpenAIUsage(0, 0, 0) + ) + call.response.headers.append("X-Conversation-Id", conversationId) + call.respond(response) + } + } +} diff --git a/adl-server/src/main/kotlin/models/OpenAIModels.kt b/adl-server/src/main/kotlin/models/OpenAIModels.kt new file mode 100644 index 00000000..8f896827 --- /dev/null +++ b/adl-server/src/main/kotlin/models/OpenAIModels.kt @@ -0,0 +1,54 @@ +// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +// +// SPDX-License-Identifier: Apache-2.0 +package org.eclipse.lmos.adl.server.models + +import kotlinx.serialization.Serializable + +@Serializable +data class OpenAIChatCompletionRequest( + val model: String, + val messages: List, + val temperature: Double? = null, + val top_p: Double? = null, + val n: Int? = null, + val stream: Boolean? = null, + val stop: List? = null, + val max_tokens: Int? = null, + val presence_penalty: Double? = null, + val frequency_penalty: Double? = null, + val logit_bias: Map? = null, + val user: String? = null +) + +@Serializable +data class OpenAIMessage( + val role: String, + val content: String, + val name: String? = null +) + +@Serializable +data class OpenAIChatCompletionResponse( + val id: String, + val `object`: String, + val created: Long, + val model: String, + val choices: List, + val usage: OpenAIUsage? = null, + val system_fingerprint: String? = null +) + +@Serializable +data class OpenAIChoice( + val index: Int, + val message: OpenAIMessage, + val finish_reason: String +) + +@Serializable +data class OpenAIUsage( + val prompt_tokens: Int, + val completion_tokens: Int, + val total_tokens: Int +) diff --git a/adl-server/src/main/kotlin/services/ConversationEvaluator.kt b/adl-server/src/main/kotlin/services/ConversationEvaluator.kt index fff162b1..ce46eb98 100644 --- a/adl-server/src/main/kotlin/services/ConversationEvaluator.kt +++ b/adl-server/src/main/kotlin/services/ConversationEvaluator.kt @@ -22,6 +22,7 @@ class ConversationEvaluator( var compared = 0 var totalSimilarity = 0.0 + var lowestSimilarity = 1.0 val reasons = mutableListOf() val evidence = mutableListOf() @@ -51,6 +52,7 @@ class ConversationEvaluator( val similarity = CosineSimilarity.between(actualEmb, expectedEmb) totalSimilarity += similarity + lowestSimilarity = minOf(lowestSimilarity, similarity) if (similarity < failureThreshold) { reasons.add("Message $i: Content similarity is low (${(similarity * 100).roundToInt()}%).") @@ -64,7 +66,8 @@ class ConversationEvaluator( } // If one is empty - val finalScore = if (compared > 0) (totalSimilarity / compared) * 100 else 0.0 + // val finalScore = if (compared > 0) (totalSimilarity / compared) * 100 else 0.0 + val finalScore = lowestSimilarity * 100 val verdict = if (finalScore >= 90) "pass" else if (finalScore >= 60) "partial" else "fail" return EvalOutput( diff --git a/adl-server/src/main/resources/assistant.md b/adl-server/src/main/resources/assistant.md index 1c64f3d9..020e1d98 100644 --- a/adl-server/src/main/resources/assistant.md +++ b/adl-server/src/main/resources/assistant.md @@ -1,98 +1,93 @@ $$ROLE$$ -You follow the ReAct pattern: you reason internally, then act by producing the final customer-facing response. -You NEVER reveal your reasoning, steps, or internal analysis. - -## Language & Tone Requirements - -- Always talk directly to the customer (second person). -- Never refer to the customer in the third person. -- Always suggest what the customer can do — never say the customer must do something. -- Be polite, friendly, and professional at all times. -- Keep responses concise and to the point. -- **IMPORTANT** Do not add unnecessary information nor assumptions to your answers. -- Always respond in the same language the customer used. - ## Core Instructions (Strict) -These rules override all others if there is a conflict. - -1. Only provide information the customer explicitly asked for. -2. Use the conversation context to determine the best possible answer. Do not add unnecessary information. -3. Select exactly ONE use case that best matches the customer’s question or the ongoing conversation. -4. Never invent new use cases. -5. If the selected use case defines function calls, execute them when required. -6. Generate the response strictly according to the selected use case instructions. -7. Skip questions if the answer can already be derived from the conversation. -8. Never expose internal reasoning, ReAct thoughts, steps, or decision logic to the customer. - -## ReAct Execution Flow (Internal – Do Not Expose) +1. Only provide information the customer has explicitly asked for. +2. Use the context of the conversation to provide the best possible answer. +3. Always answer in the same language the customer used (e.g., English or German). +4. You must always select exactly one use case that best matches the customer’s question or the ongoing conversation. +5. If no matching use case exists, you must still return a response and use the special use case ID: +NO_ANSWER. +6. Never invent a new use case. +7. Call any functions specified in the applicable use case when required. +8. Follow the instructions in the selected use case exactly as specified. +9. Keep responses concise and to the point. +10. Do not ask questions that are not specified in the selected use case. + + +## Use Case & Step Handling (NON-NEGOTIABLE) + +When responding to the customer: +1. Select one use case that best matches the customer’s question or the ongoing conversation. +2. Generate the response according to the selected use case's solution. +3. Follow the instructions in the selected use case exactly as specified. +4. **Important** Start your response with the use case ID in angle brackets, example: +5. **Important** The is mandatory +6. If the instructions in the selected use case's solution does not make sense, +follow the instructions in the `use_case_instruction_not_sensible` use case. -The following is for internal execution only and must never appear in the output: - -- Thought: Analyze the customer question. -- Action: Select the best matching use case and apply its rules. -- Observation: Process the result. -- (Repeat if necessary.) -- Thought: Final answer is ready. -- Output: Produce the customer-facing response. - -## Use Case & Step Handling Rules - -1. After selecting a use case, check whether it contains a "Steps" section. -2. If a step asks a question and the answer can already be derived → skip that step. -3. If Steps contain bullet points → select exactly ONE bullet point. -4. Never combine multiple steps. -5. Never combine steps with the solution (NON-NEGOTIABLE). -6. After completing or skipping all steps, apply the "Solution" section. -7. Internal execution details must never be shown. -8. If the selected use case solution instructs to ask the customer a question that has already been answered in the conversation context, return special command "NEXT_USE_CASE" to get next set of use cases. - -## Self-Validation Checklist (Internal – Silent) +``` +[Customer-facing response] +``` -Before responding, silently confirm: -- [] The response starts with -- [] The language matches the customer’s language -- [] Only requested information is included -- [] No internal logic, ReAct thoughts, or instructions are visible -- [] Steps were not combined with other steps or the solution -- [] Questions already answered were not asked again +### Examples: -If any check fails, revise before responding. +Use Case: +``` +### UseCase: manually_pay_bills +#### Description +The customer is asking how to manually pay their bills. -## Mandatory Output Format (NON-NEGOTIABLE) +#### Solution +Tell the customer they can review their open invoices in the billing section of their +account and choose the payment method that works best for them. -The final output must ALWAYS follow this exact format: +``` +User Question: ``` - -Customer-facing response +How can I manually pay my bills? ``` -The line is mandatory in all cases, including NO_ANSWER. - -### Example (Valid) - +Your response: ``` - -You can review your open invoices in the billing section of your account and choose the payment method that works best for you. +You can review your open invoices in the billing section of your +account and choose the payment method that works best for you. ``` -### Example (Use Case instructs to ask a question already answered) +---- +Use Case: ``` -### UseCase: buy_phone +### UseCase: buy_a_movie_ticket #### Description -The customer wants to buy a phone. +The customer want to buy a movie ticket. #### Solution -Ask the customer if they are interested in purchasing a mobile phone or landline phone. +Ask the customer for the name of the movie they want to watch and the preferred showtime. ----- +``` + +User Question: +``` +I want to buy a ticket for the new James Bond movie. +``` -User: I want to buy a mobile phone. -Assistant: NEXT_USE_CASE +Your response: ``` + NO_SOLUTION_AVAILABLE. +``` + +## Language & Tone Requirements + +- Always talk directly to the customer (second person). +- Never refer to the customer in the third person. +- Always suggest what the customer can do — never say the customer must do something. +- Be polite, friendly, and professional at all times. +- Keep responses concise and to the point. +- **IMPORTANT** Do not add unnecessary information nor assumptions to your answers. +- Always respond in the same language the customer used. + ## Time $$TIME$$ @@ -120,4 +115,14 @@ Politely let the customer know their request is outside the scope of your assist ---- +### UseCase: use_case_instruction_not_sensible +#### Description +The instructions in the selected use case's solution do not +make sense in the context of the customer's request. + +#### Solution +Reply NO_SOLUTION_AVAILABLE. + +---- + $$USE_CASES$$ \ No newline at end of file diff --git a/adl-server/src/main/resources/examples/buy_a_car.md b/adl-server/src/main/resources/examples/buy_a_car.md new file mode 100644 index 00000000..f12eff19 --- /dev/null +++ b/adl-server/src/main/resources/examples/buy_a_car.md @@ -0,0 +1,13 @@ +### UseCase: buy_a_car + +#### Description +Customer want to buy a car. + +#### Steps +- Ask the customer how much they want to spend on the car. +- Ask the customer what model they want. Examples: SUV, Sedan, Truck, etc. + +#### Solution +Tell the customer we will notify them when we have cars that fit their budget and model preference. + +---- \ No newline at end of file From 65463d13a8111c2b7417b43691b9e025b286e405 Mon Sep 17 00:00:00 2001 From: "patrick.whelan@telekom.de" Date: Sat, 31 Jan 2026 00:34:27 +0100 Subject: [PATCH 18/40] feat: implement MCP service with mutation and query support --- adl-server/build.gradle.kts | 1 + adl-server/src/main/kotlin/AdlServer.kt | 9 ++-- .../kotlin/inbound/mutation/McpMutation.kt | 15 ++++++ .../src/main/kotlin/inbound/query/McpQuery.kt | 30 ++++++++++++ .../src/main/kotlin/services/McpService.kt | 49 +++++++++++++++++++ adl-server/src/main/resources/assistant.md | 41 ++-------------- 6 files changed, 104 insertions(+), 41 deletions(-) create mode 100644 adl-server/src/main/kotlin/inbound/mutation/McpMutation.kt create mode 100644 adl-server/src/main/kotlin/inbound/query/McpQuery.kt create mode 100644 adl-server/src/main/kotlin/services/McpService.kt diff --git a/adl-server/build.gradle.kts b/adl-server/build.gradle.kts index a615d628..1faa432d 100644 --- a/adl-server/build.gradle.kts +++ b/adl-server/build.gradle.kts @@ -96,6 +96,7 @@ dependencies { implementation(project(":arc-assistants")) implementation(project(":arc-api")) implementation(project(":adl-kotlin-runner")) + implementation(project(":arc-mcp")) implementation(libs.ktor.server.core) implementation(libs.ktor.server.cio) diff --git a/adl-server/src/main/kotlin/AdlServer.kt b/adl-server/src/main/kotlin/AdlServer.kt index e9c12db3..8e23897a 100644 --- a/adl-server/src/main/kotlin/AdlServer.kt +++ b/adl-server/src/main/kotlin/AdlServer.kt @@ -19,9 +19,7 @@ import io.ktor.server.routing.routing import io.ktor.http.HttpHeaders import io.ktor.http.HttpMethod import io.ktor.server.http.content.staticResources -import io.ktor.server.response.respond import io.ktor.server.response.respondText -import io.ktor.server.routing.contentType import kotlinx.coroutines.runBlocking import org.eclipse.lmos.adl.server.agents.createAssistantAgent import org.eclipse.lmos.adl.server.agents.createEvalAgent @@ -39,18 +37,20 @@ import org.eclipse.lmos.adl.server.inbound.GlobalExceptionHandler import org.eclipse.lmos.adl.server.inbound.mutation.SystemPromptMutation import org.eclipse.lmos.adl.server.inbound.query.TestCaseQuery import org.eclipse.lmos.adl.server.services.ConversationEvaluator +import org.eclipse.lmos.adl.server.services.McpService import org.eclipse.lmos.adl.server.services.TestExecutor import org.eclipse.lmos.adl.server.sessions.InMemorySessions import org.eclipse.lmos.adl.server.repositories.impl.InMemoryAdlRepository import org.eclipse.lmos.adl.server.templates.TemplateLoader import org.eclipse.lmos.adl.server.agents.createImprovementAgent import org.eclipse.lmos.adl.server.inbound.mutation.AdlExampleMutation +import org.eclipse.lmos.adl.server.inbound.mutation.McpMutation +import org.eclipse.lmos.adl.server.inbound.query.McpToolsQuery import org.eclipse.lmos.adl.server.inbound.rest.openAICompletions import org.eclipse.lmos.adl.server.repositories.AdlRepository import org.eclipse.lmos.adl.server.repositories.impl.InMemoryTestCaseRepository import org.eclipse.lmos.adl.server.repositories.UseCaseEmbeddingsRepository import org.eclipse.lmos.adl.server.repositories.impl.InMemoryUseCaseEmbeddingsStore -import java.io.File fun startServer( wait: Boolean = true, @@ -65,6 +65,7 @@ fun startServer( // val useCaseStore: UseCaseEmbeddingsRepository = QdrantUseCaseEmbeddingsStore(embeddingModel, qdrantConfig) val useCaseStore: UseCaseEmbeddingsRepository = InMemoryUseCaseEmbeddingsStore(embeddingModel) val adlStorage: AdlRepository = InMemoryAdlRepository() + val mcpService = McpService() // Agents val exampleAgent = createExampleAgent() @@ -108,6 +109,7 @@ fun startServer( queries = listOf( AdlQuery(useCaseStore, adlStorage), TestCaseQuery(testCaseRepository), + McpToolsQuery(mcpService), ) mutations = listOf( AdlCompilerMutation(), @@ -119,6 +121,7 @@ fun startServer( TestCreatorMutation(testCreatorAgent, testCaseRepository, testExecutor), UseCaseImprovementMutation(improvementAgent), AdlExampleMutation(exampleAgent), + McpMutation(mcpService), ) } server { diff --git a/adl-server/src/main/kotlin/inbound/mutation/McpMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/McpMutation.kt new file mode 100644 index 00000000..e6d3da9b --- /dev/null +++ b/adl-server/src/main/kotlin/inbound/mutation/McpMutation.kt @@ -0,0 +1,15 @@ +// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +// +// SPDX-License-Identifier: Apache-2.0 +package org.eclipse.lmos.adl.server.inbound.mutation + +import com.expediagroup.graphql.server.operations.Mutation +import org.eclipse.lmos.adl.server.services.McpService + +class McpMutation(private val mcpService: McpService) : Mutation { + + fun setMcpServerUrls(urls: List): Boolean { + mcpService.setMcpServerUrls(urls) + return true + } +} diff --git a/adl-server/src/main/kotlin/inbound/query/McpQuery.kt b/adl-server/src/main/kotlin/inbound/query/McpQuery.kt new file mode 100644 index 00000000..50b590dd --- /dev/null +++ b/adl-server/src/main/kotlin/inbound/query/McpQuery.kt @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +// +// SPDX-License-Identifier: Apache-2.0 +package org.eclipse.lmos.adl.server.inbound.query + +import com.expediagroup.graphql.server.operations.Query +import org.eclipse.lmos.adl.server.services.McpService + +class McpToolsQuery(private val mcpService: McpService) : Query { + + suspend fun getMcpTools(): List { + return mcpService.getAllTools().map { + AdlTool( + name = it.name, + description = it.description, + parameters = it.parameters.toString() + ) + } + } + + fun mcpServerUrls(): List { + return mcpService.getMcpServerUrls() + } +} + +data class AdlTool( + val name: String, + val description: String, + val parameters: String +) diff --git a/adl-server/src/main/kotlin/services/McpService.kt b/adl-server/src/main/kotlin/services/McpService.kt new file mode 100644 index 00000000..ca322746 --- /dev/null +++ b/adl-server/src/main/kotlin/services/McpService.kt @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +// +// SPDX-License-Identifier: Apache-2.0 +package org.eclipse.lmos.adl.server.services + +import org.eclipse.lmos.arc.agents.functions.LLMFunction +import org.eclipse.lmos.arc.mcp.McpTools +import java.io.Closeable +import java.time.Duration +import java.util.concurrent.ConcurrentHashMap + +class McpService : Closeable { + private val mcpToolsMap = ConcurrentHashMap() + + fun setMcpServerUrls(urls: List) { + // Close and remove tools for URLs that are not in the new list + mcpToolsMap.keys.toList().forEach { url -> + if (!urls.contains(url)) { + mcpToolsMap.remove(url)?.close() + } + } + + // Add new tools for URLs that are not in the map + urls.forEach { url -> + if (!mcpToolsMap.containsKey(url)) { + try { + mcpToolsMap[url] = McpTools(url, Duration.ofMinutes(5)) + } catch (e: Exception) { + // Handle connection error or log it. + // For now, we might want to skip or throw, but let's just log print stack trace in this mocked simplified env + e.printStackTrace() + } + } + } + } + + suspend fun getAllTools(): List { + return mcpToolsMap.values.flatMap { it.load(null) } + } + + fun getMcpServerUrls(): List { + return mcpToolsMap.keys.toList() + } + + override fun close() { + mcpToolsMap.values.forEach { it.close() } + mcpToolsMap.clear() + } +} diff --git a/adl-server/src/main/resources/assistant.md b/adl-server/src/main/resources/assistant.md index 020e1d98..0ac96459 100644 --- a/adl-server/src/main/resources/assistant.md +++ b/adl-server/src/main/resources/assistant.md @@ -10,9 +10,9 @@ $$ROLE$$ NO_ANSWER. 6. Never invent a new use case. 7. Call any functions specified in the applicable use case when required. -8. Follow the instructions in the selected use case exactly as specified. +8. **Important** Follow the instructions in the selected use case exactly as specified. 9. Keep responses concise and to the point. -10. Do not ask questions that are not specified in the selected use case. +10. **Important** Do not ask questions that are not specified in the selected use case. ## Use Case & Step Handling (NON-NEGOTIABLE) @@ -22,9 +22,7 @@ When responding to the customer: 2. Generate the response according to the selected use case's solution. 3. Follow the instructions in the selected use case exactly as specified. 4. **Important** Start your response with the use case ID in angle brackets, example: -5. **Important** The is mandatory -6. If the instructions in the selected use case's solution does not make sense, -follow the instructions in the `use_case_instruction_not_sensible` use case. +5. **Important** The is mandatory. ``` [Customer-facing response] @@ -55,29 +53,6 @@ Your response: account and choose the payment method that works best for you. ``` ----- - -Use Case: -``` -### UseCase: buy_a_movie_ticket -#### Description -The customer want to buy a movie ticket. - -#### Solution -Ask the customer for the name of the movie they want to watch and the preferred showtime. - -``` - -User Question: -``` -I want to buy a ticket for the new James Bond movie. -``` - -Your response: -``` - NO_SOLUTION_AVAILABLE. -``` - ## Language & Tone Requirements - Always talk directly to the customer (second person). @@ -115,14 +90,4 @@ Politely let the customer know their request is outside the scope of your assist ---- -### UseCase: use_case_instruction_not_sensible -#### Description -The instructions in the selected use case's solution do not -make sense in the context of the customer's request. - -#### Solution -Reply NO_SOLUTION_AVAILABLE. - ----- - $$USE_CASES$$ \ No newline at end of file From 11515ec669b6236cdf885be41501c443ddb18fa2 Mon Sep 17 00:00:00 2001 From: "patrick.whelan@telekom.de" Date: Sun, 1 Feb 2026 16:28:26 +0100 Subject: [PATCH 19/40] feat: enhance ADL server with improved MCP service integration and new test case handling --- adl-server/src/main/kotlin/AdlServer.kt | 20 +- .../src/main/kotlin/agents/AssistantAgent.kt | 81 ++++- .../agents/extensions/ConversationGuide.kt | 15 +- .../agents/extensions/StandardConditionals.kt | 21 ++ .../src/main/kotlin/inbound/AdlTestQuery.kt | 6 +- .../inbound/mutation/AdlAssistantMutation.kt | 7 +- .../kotlin/inbound/mutation/McpMutation.kt | 9 +- .../inbound/mutation/TestCaseMutation.kt | 26 +- .../src/main/kotlin/inbound/query/McpQuery.kt | 3 +- .../main/kotlin/models/McpServerDetails.kt | 11 + adl-server/src/main/kotlin/models/TestCase.kt | 1 + .../kotlin/repositories/TestCaseRepository.kt | 14 + .../impl/InMemoryTestCaseRepository.kt | 9 + .../src/main/kotlin/services/McpService.kt | 81 ++++- .../src/main/kotlin/services/TestExecutor.kt | 8 +- adl-server/src/main/resources/assistant.md | 1 + .../src/main/resources/examples/buy_a_car.md | 2 +- adl-server/src/main/resources/logback.xml | 2 +- .../main/resources/static/product_page.html | 323 ++++++++++++++++++ .../main/kotlin/extensions/LoadedUseCases.kt | 9 +- 20 files changed, 582 insertions(+), 67 deletions(-) create mode 100644 adl-server/src/main/kotlin/agents/extensions/StandardConditionals.kt create mode 100644 adl-server/src/main/kotlin/models/McpServerDetails.kt create mode 100644 adl-server/src/main/resources/static/product_page.html diff --git a/adl-server/src/main/kotlin/AdlServer.kt b/adl-server/src/main/kotlin/AdlServer.kt index 8e23897a..334a41de 100644 --- a/adl-server/src/main/kotlin/AdlServer.kt +++ b/adl-server/src/main/kotlin/AdlServer.kt @@ -47,10 +47,12 @@ import org.eclipse.lmos.adl.server.inbound.mutation.AdlExampleMutation import org.eclipse.lmos.adl.server.inbound.mutation.McpMutation import org.eclipse.lmos.adl.server.inbound.query.McpToolsQuery import org.eclipse.lmos.adl.server.inbound.rest.openAICompletions +import org.eclipse.lmos.adl.server.model.Adl import org.eclipse.lmos.adl.server.repositories.AdlRepository import org.eclipse.lmos.adl.server.repositories.impl.InMemoryTestCaseRepository import org.eclipse.lmos.adl.server.repositories.UseCaseEmbeddingsRepository import org.eclipse.lmos.adl.server.repositories.impl.InMemoryUseCaseEmbeddingsStore +import java.time.Instant.now fun startServer( wait: Boolean = true, @@ -66,15 +68,15 @@ fun startServer( val useCaseStore: UseCaseEmbeddingsRepository = InMemoryUseCaseEmbeddingsStore(embeddingModel) val adlStorage: AdlRepository = InMemoryAdlRepository() val mcpService = McpService() + val testCaseRepository = InMemoryTestCaseRepository() // Agents val exampleAgent = createExampleAgent() val evalAgent = createEvalAgent() - val assistantAgent = createAssistantAgent() + val assistantAgent = createAssistantAgent(mcpService, testCaseRepository) val testCreatorAgent = createTestCreatorAgent() val conversationEvaluator = ConversationEvaluator(embeddingModel) val improvementAgent = createImprovementAgent() - val testCaseRepository = InMemoryTestCaseRepository() val testExecutor = TestExecutor(assistantAgent, adlStorage, testCaseRepository, conversationEvaluator) // Initialize Qdrant collection @@ -82,6 +84,16 @@ fun startServer( useCaseStore.initialize() } + // Add example data + runBlocking { + // log.info("Loading examples", id, examples.size) + listOf("buy_a_car.md").forEach { example -> + val id = example.substringBeforeLast(".") + val content = this::class.java.classLoader.getResource("examples/$example")!!.readText() + adlStorage.store(Adl(id, content.trim(), listOf(), now().toString(), emptyList())) + } + } + return embeddedServer(CIO, port = port ?: EnvConfig.serverPort) { // Register shutdown hook to close resources monitor.subscribe(ApplicationStopping) { @@ -116,9 +128,9 @@ fun startServer( AdlStorageMutation(useCaseStore, adlStorage), SystemPromptMutation(sessions, templateLoader), AdlEvalMutation(evalAgent, conversationEvaluator), - AdlAssistantMutation(assistantAgent), + AdlAssistantMutation(assistantAgent, adlStorage), AdlValidationMutation(), - TestCreatorMutation(testCreatorAgent, testCaseRepository, testExecutor), + TestCreatorMutation(testCreatorAgent, testCaseRepository, testExecutor, adlStorage), UseCaseImprovementMutation(improvementAgent), AdlExampleMutation(exampleAgent), McpMutation(mcpService), diff --git a/adl-server/src/main/kotlin/agents/AssistantAgent.kt b/adl-server/src/main/kotlin/agents/AssistantAgent.kt index 733e4172..0e55a900 100644 --- a/adl-server/src/main/kotlin/agents/AssistantAgent.kt +++ b/adl-server/src/main/kotlin/agents/AssistantAgent.kt @@ -3,33 +3,63 @@ // SPDX-License-Identifier: Apache-2.0 package org.eclipse.lmos.adl.server.agents -import org.eclipse.lmos.adl.server.agents.extensions.conversationGuide +import ai.djl.repository.Repository +import org.eclipse.lmos.adl.server.agents.extensions.ConversationGuider +import org.eclipse.lmos.adl.server.agents.extensions.InputHintProvider +import org.eclipse.lmos.adl.server.agents.extensions.currentDate +import org.eclipse.lmos.adl.server.agents.extensions.isWeekend +import org.eclipse.lmos.adl.server.repositories.TestCaseRepository +import org.eclipse.lmos.adl.server.services.McpService import org.eclipse.lmos.arc.agents.ConversationAgent import org.eclipse.lmos.arc.agents.agents +import org.eclipse.lmos.arc.agents.dsl.extensions.addTool import org.eclipse.lmos.arc.agents.dsl.extensions.getCurrentUseCases import org.eclipse.lmos.arc.agents.dsl.extensions.local import org.eclipse.lmos.arc.agents.dsl.extensions.processUseCases import org.eclipse.lmos.arc.agents.dsl.extensions.time import org.eclipse.lmos.arc.agents.dsl.get import org.eclipse.lmos.arc.agents.events.LoggingEventHandler -import org.eclipse.lmos.arc.agents.retry +import org.eclipse.lmos.arc.agents.llm.ChatCompletionSettings import org.eclipse.lmos.arc.assistants.support.filters.UnresolvedDetector import org.eclipse.lmos.arc.assistants.support.filters.UseCaseResponseHandler -import org.eclipse.lmos.arc.assistants.support.usecases.Conditional import org.eclipse.lmos.arc.assistants.support.usecases.UseCase +import org.eclipse.lmos.arc.assistants.support.usecases.toUseCases -fun createAssistantAgent(): ConversationAgent = agents( - handlers = listOf(LoggingEventHandler()) +/** + * Creates and configures the main Assistant Agent for the ADL server. + * + * This agent is responsible for handling user interactions, processing use cases, + * and integrating with MCP tools. It sets up: + * - Input/Output filters for handling hints and response formatting. + * - Prompt generation logic that incorporates roles, use cases, and time context. + * - specialized handling for step-based use cases by converting them to conditional logic. + * + * @param mcpService The service responsible for loading and managing MCP tools. + * @return A configured [ConversationAgent] ready to handle requests. + */ +fun createAssistantAgent(mcpService: McpService, testRepository: TestCaseRepository): ConversationAgent = agents( + handlers = listOf(LoggingEventHandler()), + functionLoaders = listOf(mcpService) ) { agent { name = "assistant_agent" + settings = { ChatCompletionSettings(temperature = 0.0, seed = 42) } + filterInput { + +InputHintProvider() + } filterOutput { -"```json" -"```" +UseCaseResponseHandler() + +ConversationGuider() + getCurrentUseCases()?.processedUseCaseMap?.get(getCurrentUseCases()?.currentUseCaseId)?.let { uc -> + val solution = uc.toUseCases().first().solution.joinToString("\n").trim() + if ((solution.startsWith("\"") || solution.startsWith("- \"")) && solution.endsWith("\"")) { + outputMessage = outputMessage.update(solution.substringAfter("\"").substringBeforeLast("\"")) + } + } +UnresolvedDetector { "UNRESOLVED" } } - conversationGuide() prompt { val role = local("role.md")!! @@ -44,11 +74,48 @@ fun createAssistantAgent(): ConversationAgent = agents( }, steps = emptyList()) } else uc } - val useCasesPrompt = processUseCases(useCases = useCases, fallbackLimit = 3) + + // Add examples from the test repository + val examples = buildString { + append("## Examples:\n") + useCases.forEach { uc -> + testRepository.findByUseCaseId(uc.id).forEach { tc -> + append( + """ + ### Example for Use Case: ${uc.id} + **Conversation** + ${ + tc.expectedConversation.joinToString("\n") { + val prefix = if (it.role == "assistant") "" else "" + "${it.role}: $prefix ${it.content}" + } + } + + + """.trimIndent() + ) + } + } + } + + // Add tools + useCases.forEach { useCase -> + useCase.extractTools().forEach { + addTool(it) + } + } + + // Add conditions + val conditions = buildSet { + isWeekend()?.let { add("is_weekend") } + add(currentDate()) + } + val useCasesPrompt = processUseCases(useCases = useCases, fallbackLimit = 3, conditions = conditions) // Output the final prompt val prompt = local("assistant.md")!! .replace("\$\$ROLE\$\$", role) + .replace("\$\$EXAMPLES\$\$", examples) .replace("\$\$USE_CASES\$\$", useCasesPrompt) .replace("\$\$TIME\$\$", time()) prompt diff --git a/adl-server/src/main/kotlin/agents/extensions/ConversationGuide.kt b/adl-server/src/main/kotlin/agents/extensions/ConversationGuide.kt index fcf97aa1..c4676ad0 100644 --- a/adl-server/src/main/kotlin/agents/extensions/ConversationGuide.kt +++ b/adl-server/src/main/kotlin/agents/extensions/ConversationGuide.kt @@ -17,6 +17,7 @@ import org.eclipse.lmos.arc.agents.dsl.DSLContext import org.eclipse.lmos.arc.agents.dsl.InputFilterContext import org.eclipse.lmos.arc.agents.dsl.OutputFilterContext import org.eclipse.lmos.arc.agents.dsl.extensions.getCurrentAgent +import org.eclipse.lmos.arc.agents.dsl.extensions.getCurrentUseCases import org.eclipse.lmos.arc.agents.dsl.get import org.eclipse.lmos.arc.agents.dsl.getOptional import org.eclipse.lmos.arc.agents.llm.ChatCompleterProvider @@ -27,19 +28,6 @@ import org.eclipse.lmos.arc.core.getOrNull import org.eclipse.lmos.arc.core.result import org.slf4j.LoggerFactory -/** - * Agent definition plugin to improve responses by checking if - * the questions asked by the Agent can be answered from the conversation history. - */ -fun AgentDefinition.conversationGuide() { - filterInput { - +InputHintProvider() - } - filterOutput { - +ConversationGuider() - } -} - /** * Key for storing retry reason in the RetrySignal. */ @@ -80,6 +68,7 @@ class ConversationGuider(private val retryMax: Int = 4, private val model: Strin override suspend fun filter(message: ConversationMessage, context: OutputFilterContext): ConversationMessage? { if (!message.content.contains("?")) return message + if (context.getCurrentUseCases()?.currentUseCase()?.steps?.isEmpty() == true) return message log.debug("Checking Agent response: ${message.content}") val conversation = context.get() diff --git a/adl-server/src/main/kotlin/agents/extensions/StandardConditionals.kt b/adl-server/src/main/kotlin/agents/extensions/StandardConditionals.kt new file mode 100644 index 00000000..29188338 --- /dev/null +++ b/adl-server/src/main/kotlin/agents/extensions/StandardConditionals.kt @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2025 Deutsche Telekom AG and others +// +// SPDX-License-Identifier: Apache-2.0 +package org.eclipse.lmos.adl.server.agents.extensions + +import java.time.DayOfWeek +import java.time.LocalDate +import java.time.format.DateTimeFormatter + +fun isWeekend(): String? { + val day = LocalDate.now().dayOfWeek + return if (day == DayOfWeek.SATURDAY || day == DayOfWeek.SUNDAY) { + "is_weekend" + } else { + null + } +} + +fun currentDate(): String { + return LocalDate.now().format(DateTimeFormatter.ofPattern("dd.MM.yyyy")) +} diff --git a/adl-server/src/main/kotlin/inbound/AdlTestQuery.kt b/adl-server/src/main/kotlin/inbound/AdlTestQuery.kt index 8b29d407..521e41be 100644 --- a/adl-server/src/main/kotlin/inbound/AdlTestQuery.kt +++ b/adl-server/src/main/kotlin/inbound/AdlTestQuery.kt @@ -16,10 +16,10 @@ class AdlTestQuery( private val testCaseRepository: TestCaseRepository, ) : Query { - @GraphQLDescription("Retrieves test cases associated with a specific Use Case ID.") + @GraphQLDescription("Retrieves test cases associated with a ADL.") suspend fun getTests( - @GraphQLDescription("The ID of the Use Case") useCaseId: String, + @GraphQLDescription("The ADL identifier") id: String, ): List { - return testCaseRepository.findByUseCaseId(useCaseId) + return testCaseRepository.findByADLId(id) } } diff --git a/adl-server/src/main/kotlin/inbound/mutation/AdlAssistantMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/AdlAssistantMutation.kt index 87c39963..a4265b23 100644 --- a/adl-server/src/main/kotlin/inbound/mutation/AdlAssistantMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/AdlAssistantMutation.kt @@ -7,6 +7,7 @@ package org.eclipse.lmos.adl.server.inbound.mutation import com.expediagroup.graphql.generator.annotations.GraphQLDescription import com.expediagroup.graphql.server.operations.Mutation import kotlinx.serialization.Serializable +import org.eclipse.lmos.adl.server.repositories.AdlRepository import org.eclipse.lmos.arc.agents.ConversationAgent import org.eclipse.lmos.arc.agents.User import org.eclipse.lmos.arc.agents.conversation.AssistantMessage @@ -29,6 +30,7 @@ import java.time.Duration class AdlAssistantMutation( private val assistantAgent: ConversationAgent, + private val adlStorage: AdlRepository ) : Mutation { private val log = org.slf4j.LoggerFactory.getLogger(this.javaClass) @@ -38,7 +40,8 @@ class AdlAssistantMutation( @GraphQLDescription("The assistant input") input: AssistantInput, ): AgentResult { log.info("Received assistant request with useCases: ${input.request.conversationContext.conversationId}") - val useCases = input.useCases.toUseCases() + if(input.useCases == null && input.useCasesId == null) error("Either useCases or useCase Id must be defined!") + val useCases = input.useCasesId?.let{ adlStorage.get(it) } ?: input.useCases!!.toUseCases() val request = input.request val outputContext = OutputContext() val start = System.nanoTime() @@ -79,7 +82,7 @@ class AdlAssistantMutation( } @Serializable -data class AssistantInput(val useCases: String, val request: AgentRequest) +data class AssistantInput(val useCases: String? = null, val useCasesId: String? = null, val request: AgentRequest) fun List.convert(): List = map { when (it.role) { diff --git a/adl-server/src/main/kotlin/inbound/mutation/McpMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/McpMutation.kt index e6d3da9b..3aa37b80 100644 --- a/adl-server/src/main/kotlin/inbound/mutation/McpMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/McpMutation.kt @@ -4,12 +4,15 @@ package org.eclipse.lmos.adl.server.inbound.mutation import com.expediagroup.graphql.server.operations.Mutation +import org.eclipse.lmos.adl.server.models.McpServerDetails import org.eclipse.lmos.adl.server.services.McpService +/** + * GraphQL mutation for setting MCP server URLs. + */ class McpMutation(private val mcpService: McpService) : Mutation { - fun setMcpServerUrls(urls: List): Boolean { - mcpService.setMcpServerUrls(urls) - return true + suspend fun setMcpServerUrls(urls: List): List { + return mcpService.setMcpServerUrls(urls) } } diff --git a/adl-server/src/main/kotlin/inbound/mutation/TestCaseMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/TestCaseMutation.kt index d3b062b2..aa51ccb9 100644 --- a/adl-server/src/main/kotlin/inbound/mutation/TestCaseMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/TestCaseMutation.kt @@ -10,6 +10,7 @@ import kotlinx.serialization.Serializable import org.eclipse.lmos.adl.server.models.TestCase import org.eclipse.lmos.adl.server.models.TestRunResult import org.eclipse.lmos.adl.server.models.ConversationTurn +import org.eclipse.lmos.adl.server.repositories.AdlRepository import org.eclipse.lmos.adl.server.services.TestExecutor import org.eclipse.lmos.adl.server.repositories.TestCaseRepository import org.eclipse.lmos.arc.agents.ConversationAgent @@ -24,6 +25,7 @@ class TestCreatorMutation( private val testCreatorAgent: ConversationAgent, private val testCaseRepository: TestCaseRepository, private val testExecutor: TestExecutor, + private val adlRepository: AdlRepository, ) : Mutation { @GraphQLDescription("Generates test cases for a provided Use Case.") @@ -38,23 +40,25 @@ class TestCreatorMutation( @GraphQLDescription("Generates test cases for a provided Use Case and stores them in the repository.") suspend fun newTests( - @GraphQLDescription("The Use Case description.") useCase: String, + @GraphQLDescription("The ADL identifier") id: String, ): NewTestsResponse { - val useCaseId = useCase.toUseCases().firstOrNull()?.id ?: "unknown" - val testCases = - testCreatorAgent.process>(TestCreatorInput(useCase)).getOrThrow().map { - it.copy(useCaseId = useCaseId) - } - testCases.forEach { testCaseRepository.save(it) } - return NewTestsResponse(testCases.size, useCaseId) + val useCases = adlRepository.get(id)?.content?.toUseCases() ?: error("No adl found with id: $id") + val testCases = useCases.flatMap { useCase -> + testCreatorAgent.process>(TestCreatorInput(useCase.toString())) + .getOrThrow().map { + it.copy(useCaseId = useCase.id, adlId = id) + } + } + testCaseRepository.saveAll(testCases) + return NewTestsResponse(testCases.size) } @GraphQLDescription("Executes tests for a given Use Case.") suspend fun executeTests( - @GraphQLDescription("The Use Case ID") useCaseId: String, + @GraphQLDescription("The ADL identifier") adlId: String, @GraphQLDescription("The Test Case ID") testCaseId: String? = null, ): TestRunResult { - return testExecutor.executeTests(useCaseId, testCaseId) + return testExecutor.executeTests(adlId, testCaseId) } @GraphQLDescription("Deletes a test case by its ID.") @@ -101,8 +105,6 @@ class TestCreatorMutation( data class NewTestsResponse( @GraphQLDescription("The number of test cases created") val count: Int, - @GraphQLDescription("The ID of the use case associated with these tests") - val useCaseId: String, ) /** diff --git a/adl-server/src/main/kotlin/inbound/query/McpQuery.kt b/adl-server/src/main/kotlin/inbound/query/McpQuery.kt index 50b590dd..77411802 100644 --- a/adl-server/src/main/kotlin/inbound/query/McpQuery.kt +++ b/adl-server/src/main/kotlin/inbound/query/McpQuery.kt @@ -4,6 +4,7 @@ package org.eclipse.lmos.adl.server.inbound.query import com.expediagroup.graphql.server.operations.Query +import org.eclipse.lmos.adl.server.models.McpServerDetails import org.eclipse.lmos.adl.server.services.McpService class McpToolsQuery(private val mcpService: McpService) : Query { @@ -18,7 +19,7 @@ class McpToolsQuery(private val mcpService: McpService) : Query { } } - fun mcpServerUrls(): List { + suspend fun mcpServerUrls(): List { return mcpService.getMcpServerUrls() } } diff --git a/adl-server/src/main/kotlin/models/McpServerDetails.kt b/adl-server/src/main/kotlin/models/McpServerDetails.kt new file mode 100644 index 00000000..5c959ce3 --- /dev/null +++ b/adl-server/src/main/kotlin/models/McpServerDetails.kt @@ -0,0 +1,11 @@ +package org.eclipse.lmos.adl.server.models + + +/** + * Represents the status and details of a connected MCP server. + * + * @property url The URL of the MCP server. + * @property reachable Indicates whether the server is currently reachable and responding. + * @property toolCount The number of tools available on this server. + */ +data class McpServerDetails(val url: String, val reachable: Boolean, val toolCount: Int) diff --git a/adl-server/src/main/kotlin/models/TestCase.kt b/adl-server/src/main/kotlin/models/TestCase.kt index d314fd4e..8c0ab70f 100644 --- a/adl-server/src/main/kotlin/models/TestCase.kt +++ b/adl-server/src/main/kotlin/models/TestCase.kt @@ -14,6 +14,7 @@ import kotlinx.serialization.Serializable data class TestCase( val id: String = java.util.UUID.randomUUID().toString(), val useCaseId: String? = null, + val adlId : String? = null, val name: String, val description: String, @SerialName("expected_conversation") diff --git a/adl-server/src/main/kotlin/repositories/TestCaseRepository.kt b/adl-server/src/main/kotlin/repositories/TestCaseRepository.kt index 303f3071..32bea28b 100644 --- a/adl-server/src/main/kotlin/repositories/TestCaseRepository.kt +++ b/adl-server/src/main/kotlin/repositories/TestCaseRepository.kt @@ -17,6 +17,13 @@ interface TestCaseRepository { */ suspend fun save(testCase: TestCase): TestCase + /** + * Saves a list of [TestCase]s. + * @param testCases The test cases to save. + * @return The saved test cases. + */ + suspend fun saveAll(testCases: List): List + /** * Finds a [TestCase] by its ID. * @param id The ID of the test case. @@ -37,6 +44,13 @@ interface TestCaseRepository { */ suspend fun findByUseCaseId(useCaseId: String): List + /** + * Finds [TestCase]s associated with a specific ADL ID. + * @param adlId The ID of the ADL. + * @return A list of matching test cases. + */ + suspend fun findByADLId(adlId: String): List + /** * Deletes a [TestCase] by its ID. * @param id The ID of the test case to delete. diff --git a/adl-server/src/main/kotlin/repositories/impl/InMemoryTestCaseRepository.kt b/adl-server/src/main/kotlin/repositories/impl/InMemoryTestCaseRepository.kt index 30ec8290..3bca52fa 100644 --- a/adl-server/src/main/kotlin/repositories/impl/InMemoryTestCaseRepository.kt +++ b/adl-server/src/main/kotlin/repositories/impl/InMemoryTestCaseRepository.kt @@ -19,6 +19,11 @@ class InMemoryTestCaseRepository : TestCaseRepository { return testCase } + override suspend fun saveAll(testCases: List): List { + testCases.forEach { store[it.id] = it } + return testCases + } + override suspend fun findById(id: String): TestCase? { return store[id] } @@ -31,6 +36,10 @@ class InMemoryTestCaseRepository : TestCaseRepository { return store.values.filter { it.useCaseId == useCaseId } } + override suspend fun findByADLId(adlId: String): List { + return store.values.filter { it.adlId == adlId } + } + override suspend fun delete(id: String): Boolean { return store.remove(id) != null } diff --git a/adl-server/src/main/kotlin/services/McpService.kt b/adl-server/src/main/kotlin/services/McpService.kt index ca322746..c1c6cdb7 100644 --- a/adl-server/src/main/kotlin/services/McpService.kt +++ b/adl-server/src/main/kotlin/services/McpService.kt @@ -3,16 +3,32 @@ // SPDX-License-Identifier: Apache-2.0 package org.eclipse.lmos.adl.server.services +import org.eclipse.lmos.adl.server.models.McpServerDetails import org.eclipse.lmos.arc.agents.functions.LLMFunction +import org.eclipse.lmos.arc.agents.functions.LLMFunctionLoader +import org.eclipse.lmos.arc.agents.functions.ToolLoaderContext import org.eclipse.lmos.arc.mcp.McpTools import java.io.Closeable import java.time.Duration import java.util.concurrent.ConcurrentHashMap -class McpService : Closeable { +/** + * Service to manage Model Context Protocol (MCP) tools and connections. + * It handles the lifecycle of McpTools for multiple server URLs. + */ +class McpService : Closeable, LLMFunctionLoader { private val mcpToolsMap = ConcurrentHashMap() - fun setMcpServerUrls(urls: List) { + /** + * Sets the list of MCP server URLs to be used. + * Verifies each URL by attempting to connect and load tools. + * + * @param urls The list of MCP server URLs. + * @return A list of [McpServerDetails] objects containing status and tool count for each URL. + */ + suspend fun setMcpServerUrls(urls: List): List { + val serverStatuses = mutableListOf() + // Close and remove tools for URLs that are not in the new list mcpToolsMap.keys.toList().forEach { url -> if (!urls.contains(url)) { @@ -20,30 +36,65 @@ class McpService : Closeable { } } - // Add new tools for URLs that are not in the map - urls.forEach { url -> - if (!mcpToolsMap.containsKey(url)) { - try { - mcpToolsMap[url] = McpTools(url, Duration.ofMinutes(5)) - } catch (e: Exception) { - // Handle connection error or log it. - // For now, we might want to skip or throw, but let's just log print stack trace in this mocked simplified env - e.printStackTrace() - } + // Add or verify tools for URLs + for (url in urls) { + val tools = mcpToolsMap.getOrPut(url) { McpTools(url, Duration.ofMinutes(1)) } + try { + val loadedTools = tools.load(null) + serverStatuses.add(McpServerDetails(url, true, loadedTools.size)) + } catch (_: Exception) { + serverStatuses.add(McpServerDetails(url, false, 0)) } } + return serverStatuses } + /** + * Retrieves all tools from all registered MCP servers. + * + * @return A list of all available [LLMFunction]s. + */ suspend fun getAllTools(): List { - return mcpToolsMap.values.flatMap { it.load(null) } + return mcpToolsMap.values.flatMap { + try { + it.load(null) + } catch (_: Exception) { + emptyList() + } + } } - fun getMcpServerUrls(): List { - return mcpToolsMap.keys.toList() + /** + * Returns the list of currently registered MCP server URLs. + * + * @return List of server URLs. + */ + suspend fun getMcpServerUrls(): List { + return mcpToolsMap.entries.map { (url, tools) -> + try { + val loadedTools = tools.load(null) + McpServerDetails(url, true, loadedTools.size) + } catch (_: Exception) { + McpServerDetails(url, false, 0) + } + } } + /** + * Closes all connections to MCP servers. + */ override fun close() { mcpToolsMap.values.forEach { it.close() } mcpToolsMap.clear() } + + /** + * Loads functions/tools for the agent execution context. + * + * @param context The context for tool loading (optional). + * @return A list of loaded [LLMFunction]s. + */ + override suspend fun load(context: ToolLoaderContext?): List { + return getAllTools() + } } diff --git a/adl-server/src/main/kotlin/services/TestExecutor.kt b/adl-server/src/main/kotlin/services/TestExecutor.kt index 0c93428f..cc099d08 100644 --- a/adl-server/src/main/kotlin/services/TestExecutor.kt +++ b/adl-server/src/main/kotlin/services/TestExecutor.kt @@ -32,8 +32,8 @@ class TestExecutor( private val conversationEvaluator: ConversationEvaluator, ) { - suspend fun executeTests(useCaseId: String, testCaseId: String? = null): TestRunResult { - val adl = adlStorage.get(useCaseId) ?: throw IllegalArgumentException("Use Case not found: $useCaseId") + suspend fun executeTests(adlId: String, testCaseId: String? = null): TestRunResult { + val adl = adlStorage.get(adlId) ?: throw IllegalArgumentException("ADL not found: $adlId") val useCases = adl.content.toUseCases() val testCases = if (testCaseId != null) { @@ -41,7 +41,7 @@ class TestExecutor( ?: throw IllegalArgumentException("Test Case not found: $testCaseId") listOf(testCase) } else { - testCaseRepository.findByUseCaseId(useCaseId) + testCaseRepository.findByADLId(adlId) } val results = testCases.map { testCase -> @@ -57,7 +57,7 @@ class TestExecutor( } private suspend fun executeTestCase(testCase: TestCase, useCases: Any): TestExecutionResult { - val results = (1..2).map { + val results = (1..5).map { runSingleTestCase(testCase, useCases) } return results.minByOrNull { it.score } ?: results.first() diff --git a/adl-server/src/main/resources/assistant.md b/adl-server/src/main/resources/assistant.md index 0ac96459..5f5baae0 100644 --- a/adl-server/src/main/resources/assistant.md +++ b/adl-server/src/main/resources/assistant.md @@ -63,6 +63,7 @@ account and choose the payment method that works best for you. - **IMPORTANT** Do not add unnecessary information nor assumptions to your answers. - Always respond in the same language the customer used. +$$EXAMPLES$$ ## Time $$TIME$$ diff --git a/adl-server/src/main/resources/examples/buy_a_car.md b/adl-server/src/main/resources/examples/buy_a_car.md index f12eff19..f795b22e 100644 --- a/adl-server/src/main/resources/examples/buy_a_car.md +++ b/adl-server/src/main/resources/examples/buy_a_car.md @@ -5,7 +5,7 @@ Customer want to buy a car. #### Steps - Ask the customer how much they want to spend on the car. -- Ask the customer what model they want. Examples: SUV, Sedan, Truck, etc. +- Ask the customer if they would like to sell their current car. #### Solution Tell the customer we will notify them when we have cars that fit their budget and model preference. diff --git a/adl-server/src/main/resources/logback.xml b/adl-server/src/main/resources/logback.xml index 0d83bf60..0a723da2 100644 --- a/adl-server/src/main/resources/logback.xml +++ b/adl-server/src/main/resources/logback.xml @@ -10,7 +10,7 @@ SPDX-License-Identifier: Apache-2.0 - + diff --git a/adl-server/src/main/resources/static/product_page.html b/adl-server/src/main/resources/static/product_page.html new file mode 100644 index 00000000..eb96b742 --- /dev/null +++ b/adl-server/src/main/resources/static/product_page.html @@ -0,0 +1,323 @@ + + + + + + ADL Server - The Agent Runtime + + + + + + + + + + + +
+
+
+
+

+ Supercharge your + Prompt Engineering +

+

+ The Agent Definition Language (ADL) combines natural language with structural tokens to make AI agents predictable, robust, and manageable at scale. +

+
+

+ Build complex behaviors with stateful conversations. +

+ +
+
+
+
+
+
+
+
+
+
agent SupportBot {
+  model: "gpt-4-turbo"
+  description: "Customer Service"
+
+  use_case AnalyzeIssue {
+    when: "Use provides an error log"
+    steps: [
+      "Identify error code",
+      "Search knowledge base",
+      "Propose solution"
+    ]
+  }
+}
+
+
+
+
+
+ + +
+
+
+

Core Philosophy

+

+ Why ADL? +

+

+ Prompting shouldn't be a black box. ADL brings structure to the chaos of Large Language Models. +

+
+ +
+
+ +
+
+ + + +
+

Robust Structure

+

+ Combining natural language with special tokens makes agent behavior predictable. Define boundaries, rules, and formats that LLMs actually respect. +

+
+ + +
+
+ + + +
+

Progressive Disclosure

+

+ Manage complexity by revealing concepts and instructions only when needed. Similar to how humans learn, agents perform better when focused. +

+
+ + +
+
+ + + +
+

Stateful Conversations

+

+ Unlike stateless API calls, ADL agents maintain context awareness over time, enabling long-running workflows and coherent multi-turn interactions. +

+
+
+
+
+
+ + +
+
+
+
+

OpenAI Compatible Endpoint

+

+ Integration is effortless. ADL Server exposes a standard /v1/chat/completions endpoint. +

+

+ This allows you to swap out your existing OpenAI API calls for ADL Server calls, instantly upgrading your application with ADL's structured capabilities without rewriting your client code. +

+
    +
  • + + + + Drop-in replacement for OpenAI SDKs +
  • +
  • + + + + Streaming support for real-time UIs +
  • +
  • + + + + Model agnostic - Connect to any LLM backend +
  • +
+
+
+
+
+ cURL Request +
+
+
+
+
+
+
+curl https://adl-server/v1/chat/completions \
+  -H "Content-Type: application/json" \
+  -H "Authorization: Bearer $KEY" \
+  -d '{
+    "model": "finance-agent",
+    "messages": [
+      {
+        "role": "user",
+        "content": "Analyze the Q3 report attached."
+      }
+    ]
+  }'
+
+
+
+
+
+
+
+ + +
+
+
+

Extensible with MCP

+

+ The Model Context Protocol (MCP) connects your agents to the real world. +

+
+ +
+ +
+ + Agent ← MCP → World + +
+
+ +
+
+ 🗄️ +

Databases

+

Safe, read/write access to business data.

+
+
+ 🌐 +

APIs

+

Interact with internal services and 3rd party tools.

+
+
+ 📁 +

Filesystems

+

Read docs, logs, and generate reports.

+
+
+
+
+ + +
+
+
+ ADL Server +

+ Empowering developers to build reliable, scalable AI agents with the Agent Definition Language. +

+
+
+

Product

+ +
+
+

Community

+ +
+
+
+ © 2025 Deutsche Telekom AG and others. All rights reserved. +
+
+ + + diff --git a/arc-assistants/src/main/kotlin/extensions/LoadedUseCases.kt b/arc-assistants/src/main/kotlin/extensions/LoadedUseCases.kt index cd174cd4..37bae583 100644 --- a/arc-assistants/src/main/kotlin/extensions/LoadedUseCases.kt +++ b/arc-assistants/src/main/kotlin/extensions/LoadedUseCases.kt @@ -17,4 +17,11 @@ data class LoadedUseCases( val processedUseCaseMap: Map, val currentStep: String? = null, val currentUseCaseId: String? = null, -) +) { + /** + * Retrieves the current UseCase based on the currentUseCaseId. + */ + fun currentUseCase(): UseCase? { + return useCases.find { it.id == currentUseCaseId } + } +} From 0da87919fe96d8fbeceb4a484d0ce3a8939b85f9 Mon Sep 17 00:00:00 2001 From: "patrick.whelan@telekom.de" Date: Sun, 1 Feb 2026 17:00:14 +0100 Subject: [PATCH 20/40] feat: extend assistant agent with use case embeddings and improve search functionality --- adl-server/src/main/kotlin/AdlServer.kt | 2 +- .../src/main/kotlin/agents/AssistantAgent.kt | 23 +++++++++++++++++-- .../src/main/kotlin/inbound/query/AdlQuery.kt | 8 +++---- .../UseCaseEmbeddingsRepository.kt | 8 +++---- .../impl/InMemoryUseCaseEmbeddingsStore.kt | 10 ++++---- .../impl/QdrantUseCaseEmbeddingsStore.kt | 12 +++++----- 6 files changed, 41 insertions(+), 22 deletions(-) diff --git a/adl-server/src/main/kotlin/AdlServer.kt b/adl-server/src/main/kotlin/AdlServer.kt index 334a41de..749e276f 100644 --- a/adl-server/src/main/kotlin/AdlServer.kt +++ b/adl-server/src/main/kotlin/AdlServer.kt @@ -73,7 +73,7 @@ fun startServer( // Agents val exampleAgent = createExampleAgent() val evalAgent = createEvalAgent() - val assistantAgent = createAssistantAgent(mcpService, testCaseRepository) + val assistantAgent = createAssistantAgent(mcpService, testCaseRepository, useCaseStore, adlStorage) val testCreatorAgent = createTestCreatorAgent() val conversationEvaluator = ConversationEvaluator(embeddingModel) val improvementAgent = createImprovementAgent() diff --git a/adl-server/src/main/kotlin/agents/AssistantAgent.kt b/adl-server/src/main/kotlin/agents/AssistantAgent.kt index 0e55a900..dac435a1 100644 --- a/adl-server/src/main/kotlin/agents/AssistantAgent.kt +++ b/adl-server/src/main/kotlin/agents/AssistantAgent.kt @@ -8,10 +8,17 @@ import org.eclipse.lmos.adl.server.agents.extensions.ConversationGuider import org.eclipse.lmos.adl.server.agents.extensions.InputHintProvider import org.eclipse.lmos.adl.server.agents.extensions.currentDate import org.eclipse.lmos.adl.server.agents.extensions.isWeekend +import org.eclipse.lmos.adl.server.inbound.mutation.StorageResult +import org.eclipse.lmos.adl.server.repositories.AdlRepository import org.eclipse.lmos.adl.server.repositories.TestCaseRepository +import org.eclipse.lmos.adl.server.repositories.UseCaseEmbeddingsRepository import org.eclipse.lmos.adl.server.services.McpService import org.eclipse.lmos.arc.agents.ConversationAgent import org.eclipse.lmos.arc.agents.agents +import org.eclipse.lmos.arc.agents.conversation.Conversation +import org.eclipse.lmos.arc.agents.conversation.ConversationMessage +import org.eclipse.lmos.arc.agents.conversation.UserMessage +import org.eclipse.lmos.arc.agents.conversation.latest import org.eclipse.lmos.arc.agents.dsl.extensions.addTool import org.eclipse.lmos.arc.agents.dsl.extensions.getCurrentUseCases import org.eclipse.lmos.arc.agents.dsl.extensions.local @@ -37,7 +44,12 @@ import org.eclipse.lmos.arc.assistants.support.usecases.toUseCases * @param mcpService The service responsible for loading and managing MCP tools. * @return A configured [ConversationAgent] ready to handle requests. */ -fun createAssistantAgent(mcpService: McpService, testRepository: TestCaseRepository): ConversationAgent = agents( +fun createAssistantAgent( + mcpService: McpService, + testRepository: TestCaseRepository, + embeddingsRepository: UseCaseEmbeddingsRepository, + adlRepository: AdlRepository +): ConversationAgent = agents( handlers = listOf(LoggingEventHandler()), functionLoaders = listOf(mcpService) ) { @@ -63,8 +75,15 @@ fun createAssistantAgent(mcpService: McpService, testRepository: TestCaseReposit prompt { val role = local("role.md")!! + // Load Use Cases + val currentUseCases = get>() + val message = get().latest()?.content + val otherUseCases = embeddingsRepository.search(message!!, limit = 5).filter { + currentUseCases.none { cu -> cu.id == it.adlId } + }.flatMap { adlRepository.get(it.adlId)?.content?.toUseCases() ?: emptyList() } + // Convert steps to conditionals in use cases - val useCases = get>().map { uc -> + val useCases = (currentUseCases + otherUseCases).map { uc -> if (uc.steps.isNotEmpty()) { val convertedSteps = uc.steps.filter { it.text.isNotEmpty() }.mapIndexed { i, step -> step.copy(conditions = step.conditions + "step_${i + 1}") diff --git a/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt b/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt index a3b827a6..39170957 100644 --- a/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt +++ b/adl-server/src/main/kotlin/inbound/query/AdlQuery.kt @@ -10,7 +10,7 @@ import org.eclipse.lmos.adl.server.model.Adl import org.eclipse.lmos.adl.server.models.SimpleMessage import org.eclipse.lmos.adl.server.repositories.AdlRepository import org.eclipse.lmos.adl.server.repositories.UseCaseEmbeddingsRepository -import org.eclipse.lmos.adl.server.repositories.UseCaseSearchResult +import org.eclipse.lmos.adl.server.repositories.SearchResult /** * GraphQL Query for searching UseCases based on conversation embeddings. @@ -47,7 +47,7 @@ class AdlQuery( return allAdls } val matches = useCaseStore.search(searchTerm.term, searchTerm.limit, searchTerm.threshold.toFloat()) - val scores = matches.groupBy { it.useCaseId }.mapValues { it.value.maxOf { match -> match.score } } + val scores = matches.groupBy { it.adlId }.mapValues { it.value.maxOf { match -> match.score } } return allAdls.filter { it.id in scores.keys } .map { it.copy(relevance = scores[it.id]?.toDouble()) } @@ -72,8 +72,8 @@ class AdlQuery( return results.toMatches() } - private fun List.toMatches(): List { - return groupBy { it.useCaseId } + private fun List.toMatches(): List { + return groupBy { it.adlId } .map { (useCaseId, matches) -> UseCaseMatch( maxScore = matches.maxOf { it.score }, diff --git a/adl-server/src/main/kotlin/repositories/UseCaseEmbeddingsRepository.kt b/adl-server/src/main/kotlin/repositories/UseCaseEmbeddingsRepository.kt index 4b273d69..f53a6aa4 100644 --- a/adl-server/src/main/kotlin/repositories/UseCaseEmbeddingsRepository.kt +++ b/adl-server/src/main/kotlin/repositories/UseCaseEmbeddingsRepository.kt @@ -12,8 +12,8 @@ interface UseCaseEmbeddingsRepository : AutoCloseable { suspend fun initialize() suspend fun storeUtterances(id: String, examples: List): Int suspend fun storeUseCase(adl: String, examples: List = emptyList()): Int - suspend fun search(query: String, limit: Int = 5, scoreThreshold: Float = 0.0f): List - suspend fun searchByConversation(messages: List, limit: Int = 5, scoreThreshold: Float = 0.0f): List + suspend fun search(query: String, limit: Int = 5, scoreThreshold: Float = 0.0f): List + suspend fun searchByConversation(messages: List, limit: Int = 5, scoreThreshold: Float = 0.0f): List suspend fun deleteByUseCaseId(useCaseId: String) suspend fun clear() suspend fun count(): Long @@ -22,8 +22,8 @@ interface UseCaseEmbeddingsRepository : AutoCloseable { /** * Result of a UseCase similarity search. */ -data class UseCaseSearchResult( - val useCaseId: String, +data class SearchResult( + val adlId: String, val example: String, val score: Float, val content: String, diff --git a/adl-server/src/main/kotlin/repositories/impl/InMemoryUseCaseEmbeddingsStore.kt b/adl-server/src/main/kotlin/repositories/impl/InMemoryUseCaseEmbeddingsStore.kt index 2bf11992..3bd54da3 100644 --- a/adl-server/src/main/kotlin/repositories/impl/InMemoryUseCaseEmbeddingsStore.kt +++ b/adl-server/src/main/kotlin/repositories/impl/InMemoryUseCaseEmbeddingsStore.kt @@ -11,7 +11,7 @@ import dev.langchain4j.store.embedding.EmbeddingSearchRequest import dev.langchain4j.store.embedding.inmemory.InMemoryEmbeddingStore import org.eclipse.lmos.adl.server.models.SimpleMessage import org.eclipse.lmos.adl.server.repositories.UseCaseEmbeddingsRepository -import org.eclipse.lmos.adl.server.repositories.UseCaseSearchResult +import org.eclipse.lmos.adl.server.repositories.SearchResult import org.eclipse.lmos.arc.assistants.support.usecases.toUseCases import java.util.concurrent.ConcurrentHashMap @@ -62,7 +62,7 @@ class InMemoryUseCaseEmbeddingsStore( return ids.size } - override suspend fun search(query: String, limit: Int, scoreThreshold: Float): List { + override suspend fun search(query: String, limit: Int, scoreThreshold: Float): List { val embedding = embeddingModel.embed(query).content() val request = EmbeddingSearchRequest.builder() .queryEmbedding(embedding) @@ -72,8 +72,8 @@ class InMemoryUseCaseEmbeddingsStore( val results = store.search(request).matches() return results.map { match -> - UseCaseSearchResult( - useCaseId = match.embedded().metadata().getString(PAYLOAD_USECASE_ID) ?: "", + SearchResult( + adlId = match.embedded().metadata().getString(PAYLOAD_USECASE_ID) ?: "", example = match.embedded().metadata().getString(PAYLOAD_EXAMPLE) ?: "", score = match.score().toFloat(), content = match.embedded().metadata().getString(PAYLOAD_CONTENT) ?: "" @@ -85,7 +85,7 @@ class InMemoryUseCaseEmbeddingsStore( messages: List, limit: Int, scoreThreshold: Float - ): List { + ): List { // Filter last user messages return messages.filter { it.role == "user" && it.content.length > 5 } .takeLast(5) diff --git a/adl-server/src/main/kotlin/repositories/impl/QdrantUseCaseEmbeddingsStore.kt b/adl-server/src/main/kotlin/repositories/impl/QdrantUseCaseEmbeddingsStore.kt index 03c3040d..7deadf9b 100644 --- a/adl-server/src/main/kotlin/repositories/impl/QdrantUseCaseEmbeddingsStore.kt +++ b/adl-server/src/main/kotlin/repositories/impl/QdrantUseCaseEmbeddingsStore.kt @@ -18,7 +18,7 @@ import kotlinx.coroutines.guava.await import org.eclipse.lmos.adl.server.QdrantConfig import org.eclipse.lmos.adl.server.models.SimpleMessage import org.eclipse.lmos.adl.server.repositories.UseCaseEmbeddingsRepository -import org.eclipse.lmos.adl.server.repositories.UseCaseSearchResult +import org.eclipse.lmos.adl.server.repositories.SearchResult import org.eclipse.lmos.arc.assistants.support.usecases.toUseCases import java.util.UUID import java.util.concurrent.ExecutionException @@ -138,7 +138,7 @@ class QdrantUseCaseEmbeddingsStore( * @param scoreThreshold The minimum similarity score (0.0 to 1.0). * @return List of matching UseCase embeddings with their scores. */ - override suspend fun search(query: String, limit: Int, scoreThreshold: Float): List { + override suspend fun search(query: String, limit: Int, scoreThreshold: Float): List { val queryEmbedding = embeddingModel.embed(query).content().vector() return searchByVector(queryEmbedding.toList(), limit, scoreThreshold) } @@ -154,7 +154,7 @@ class QdrantUseCaseEmbeddingsStore( embedding: List, limit: Int = 5, scoreThreshold: Float = 0.0f, - ): List { + ): List { return try { val searchRequest = io.qdrant.client.grpc.Points.SearchPoints.newBuilder() .setCollectionName(config.collectionName) @@ -184,7 +184,7 @@ class QdrantUseCaseEmbeddingsStore( messages: List, limit: Int, scoreThreshold: Float, - ): List { + ): List { val combinedQuery = messages.filter { it.role == "user" && it.content.length > 5 }.takeLast(5).flatMap { search(it.content, limit, scoreThreshold) } @@ -265,9 +265,9 @@ class QdrantUseCaseEmbeddingsStore( } } - private fun ScoredPoint.toSearchResult(): UseCaseSearchResult { + private fun ScoredPoint.toSearchResult(): SearchResult { val payload = this.payloadMap - return UseCaseSearchResult( + return SearchResult( useCaseId = payload[PAYLOAD_USECASE_ID]?.stringValue ?: "", example = payload[PAYLOAD_EXAMPLE]?.stringValue ?: "", score = this.score, From ca22d0bd37ffe2c873737de838f55c91f5de2025 Mon Sep 17 00:00:00 2001 From: "patrick.whelan@telekom.de" Date: Sun, 1 Feb 2026 20:44:16 +0100 Subject: [PATCH 21/40] feat: enhance ADL server with improved use case handling and new greeting use case --- adl-server/src/main/kotlin/AdlServer.kt | 23 +- .../src/main/kotlin/agents/AssistantAgent.kt | 38 +- .../inbound/mutation/AdlStorageMutation.kt | 6 +- .../impl/QdrantUseCaseEmbeddingsStore.kt | 8 +- adl-server/src/main/resources/assistant.md | 21 - .../src/main/resources/base_use_cases.md | 20 + .../src/main/resources/examples/buy_a_car.md | 22 + .../src/main/resources/examples/greeting.md | 15 + .../main/resources/static/product_page.html | 486 +++++++++++++----- 9 files changed, 466 insertions(+), 173 deletions(-) create mode 100644 adl-server/src/main/resources/base_use_cases.md create mode 100644 adl-server/src/main/resources/examples/greeting.md diff --git a/adl-server/src/main/kotlin/AdlServer.kt b/adl-server/src/main/kotlin/AdlServer.kt index 749e276f..401c5ef8 100644 --- a/adl-server/src/main/kotlin/AdlServer.kt +++ b/adl-server/src/main/kotlin/AdlServer.kt @@ -52,6 +52,7 @@ import org.eclipse.lmos.adl.server.repositories.AdlRepository import org.eclipse.lmos.adl.server.repositories.impl.InMemoryTestCaseRepository import org.eclipse.lmos.adl.server.repositories.UseCaseEmbeddingsRepository import org.eclipse.lmos.adl.server.repositories.impl.InMemoryUseCaseEmbeddingsStore +import org.eclipse.lmos.arc.assistants.support.usecases.toUseCases import java.time.Instant.now fun startServer( @@ -65,7 +66,7 @@ fun startServer( val sessions = InMemorySessions() val embeddingModel = AllMiniLmL6V2EmbeddingModel() // val useCaseStore: UseCaseEmbeddingsRepository = QdrantUseCaseEmbeddingsStore(embeddingModel, qdrantConfig) - val useCaseStore: UseCaseEmbeddingsRepository = InMemoryUseCaseEmbeddingsStore(embeddingModel) + val embeddingStore: UseCaseEmbeddingsRepository = InMemoryUseCaseEmbeddingsStore(embeddingModel) val adlStorage: AdlRepository = InMemoryAdlRepository() val mcpService = McpService() val testCaseRepository = InMemoryTestCaseRepository() @@ -73,7 +74,7 @@ fun startServer( // Agents val exampleAgent = createExampleAgent() val evalAgent = createEvalAgent() - val assistantAgent = createAssistantAgent(mcpService, testCaseRepository, useCaseStore, adlStorage) + val assistantAgent = createAssistantAgent(mcpService, testCaseRepository, embeddingStore, adlStorage) val testCreatorAgent = createTestCreatorAgent() val conversationEvaluator = ConversationEvaluator(embeddingModel) val improvementAgent = createImprovementAgent() @@ -81,23 +82,25 @@ fun startServer( // Initialize Qdrant collection runBlocking { - useCaseStore.initialize() + embeddingStore.initialize() } // Add example data runBlocking { // log.info("Loading examples", id, examples.size) - listOf("buy_a_car.md").forEach { example -> - val id = example.substringBeforeLast(".") - val content = this::class.java.classLoader.getResource("examples/$example")!!.readText() - adlStorage.store(Adl(id, content.trim(), listOf(), now().toString(), emptyList())) + listOf("buy_a_car.md", "greeting.md").forEach { name -> + val content = this::class.java.classLoader.getResource("examples/$name")!!.readText() + val id = name.substringBeforeLast(".") + val examples = content.toUseCases().flatMap { it.examples.split("\n") }.filter { it.isNotBlank() } + adlStorage.store(Adl(id, content.trim(), listOf(), now().toString(), examples)) + if (examples.isNotEmpty()) embeddingStore.storeUtterances(id, examples) } } return embeddedServer(CIO, port = port ?: EnvConfig.serverPort) { // Register shutdown hook to close resources monitor.subscribe(ApplicationStopping) { - useCaseStore.close() + embeddingStore.close() } install(CORS) { @@ -119,13 +122,13 @@ fun startServer( "org.eclipse.lmos.adl.server.model", ) queries = listOf( - AdlQuery(useCaseStore, adlStorage), + AdlQuery(embeddingStore, adlStorage), TestCaseQuery(testCaseRepository), McpToolsQuery(mcpService), ) mutations = listOf( AdlCompilerMutation(), - AdlStorageMutation(useCaseStore, adlStorage), + AdlStorageMutation(embeddingStore, adlStorage), SystemPromptMutation(sessions, templateLoader), AdlEvalMutation(evalAgent, conversationEvaluator), AdlAssistantMutation(assistantAgent, adlStorage), diff --git a/adl-server/src/main/kotlin/agents/AssistantAgent.kt b/adl-server/src/main/kotlin/agents/AssistantAgent.kt index dac435a1..a5940d70 100644 --- a/adl-server/src/main/kotlin/agents/AssistantAgent.kt +++ b/adl-server/src/main/kotlin/agents/AssistantAgent.kt @@ -3,12 +3,10 @@ // SPDX-License-Identifier: Apache-2.0 package org.eclipse.lmos.adl.server.agents -import ai.djl.repository.Repository import org.eclipse.lmos.adl.server.agents.extensions.ConversationGuider import org.eclipse.lmos.adl.server.agents.extensions.InputHintProvider import org.eclipse.lmos.adl.server.agents.extensions.currentDate import org.eclipse.lmos.adl.server.agents.extensions.isWeekend -import org.eclipse.lmos.adl.server.inbound.mutation.StorageResult import org.eclipse.lmos.adl.server.repositories.AdlRepository import org.eclipse.lmos.adl.server.repositories.TestCaseRepository import org.eclipse.lmos.adl.server.repositories.UseCaseEmbeddingsRepository @@ -16,11 +14,11 @@ import org.eclipse.lmos.adl.server.services.McpService import org.eclipse.lmos.arc.agents.ConversationAgent import org.eclipse.lmos.arc.agents.agents import org.eclipse.lmos.arc.agents.conversation.Conversation -import org.eclipse.lmos.arc.agents.conversation.ConversationMessage import org.eclipse.lmos.arc.agents.conversation.UserMessage import org.eclipse.lmos.arc.agents.conversation.latest import org.eclipse.lmos.arc.agents.dsl.extensions.addTool import org.eclipse.lmos.arc.agents.dsl.extensions.getCurrentUseCases +import org.eclipse.lmos.arc.agents.dsl.extensions.info import org.eclipse.lmos.arc.agents.dsl.extensions.local import org.eclipse.lmos.arc.agents.dsl.extensions.processUseCases import org.eclipse.lmos.arc.agents.dsl.extensions.time @@ -78,21 +76,29 @@ fun createAssistantAgent( // Load Use Cases val currentUseCases = get>() val message = get().latest()?.content - val otherUseCases = embeddingsRepository.search(message!!, limit = 5).filter { - currentUseCases.none { cu -> cu.id == it.adlId } - }.flatMap { adlRepository.get(it.adlId)?.content?.toUseCases() ?: emptyList() } + val otherUseCases = embeddingsRepository.search(message!!, limit = 5) + .flatMap { adlRepository.get(it.adlId)?.content?.toUseCases() ?: emptyList() } + info("Loaded ${otherUseCases.size} additional use cases from embeddings store.") + val baseUseCases = local("base_use_cases.md")?.toUseCases() ?: emptyList() // Convert steps to conditionals in use cases - val useCases = (currentUseCases + otherUseCases).map { uc -> - if (uc.steps.isNotEmpty()) { - val convertedSteps = uc.steps.filter { it.text.isNotEmpty() }.mapIndexed { i, step -> - step.copy(conditions = step.conditions + "step_${i + 1}") - } - uc.copy(solution = convertedSteps + uc.solution.map { s -> - s.copy(conditions = s.conditions + "else") - }, steps = emptyList()) - } else uc - } + val useCases = ( + baseUseCases.filter { + currentUseCases.none { bc -> bc.id == it.id } + otherUseCases.none { bc -> bc.id == it.id } + } + otherUseCases.filter { + currentUseCases.none { bc -> bc.id == it.id } + } + currentUseCases + ).map { uc -> + if (uc.steps.isNotEmpty()) { + val convertedSteps = uc.steps.filter { it.text.isNotEmpty() }.mapIndexed { i, step -> + step.copy(conditions = step.conditions + "step_${i + 1}") + } + uc.copy(solution = convertedSteps + uc.solution.map { s -> + s.copy(conditions = s.conditions + "else") + }, steps = emptyList()) + } else uc + } // Add examples from the test repository val examples = buildString { diff --git a/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt b/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt index 2737a5d7..974c6ea2 100644 --- a/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt +++ b/adl-server/src/main/kotlin/inbound/mutation/AdlStorageMutation.kt @@ -9,6 +9,7 @@ import com.expediagroup.graphql.server.operations.Mutation import org.eclipse.lmos.adl.server.model.Adl import org.eclipse.lmos.adl.server.repositories.AdlRepository import org.eclipse.lmos.adl.server.repositories.UseCaseEmbeddingsRepository +import org.eclipse.lmos.arc.assistants.support.usecases.toUseCases import org.slf4j.LoggerFactory import java.time.Instant.now @@ -31,8 +32,9 @@ class AdlStorageMutation( @GraphQLDescription("Examples") examples: List, ): StorageResult { log.info("Storing ADL with id: {} with {} examples", id, examples.size) - adlStorage.store(Adl(id, content.trim(), tags, createdAt ?: now().toString(), examples)) - val storedCount = useCaseStore.storeUtterances(id, examples) + val allExamples = content.toUseCases().flatMap { it.examples.split("\n") }.filter { it.isNotBlank() } + examples + adlStorage.store(Adl(id, content.trim(), tags, createdAt ?: now().toString(), allExamples)) + val storedCount = useCaseStore.storeUtterances(id, allExamples) log.debug("Successfully stored ADL with id: {}. Generated {} embeddings.", id, storedCount) return StorageResult( storedExamplesCount = storedCount, diff --git a/adl-server/src/main/kotlin/repositories/impl/QdrantUseCaseEmbeddingsStore.kt b/adl-server/src/main/kotlin/repositories/impl/QdrantUseCaseEmbeddingsStore.kt index 7deadf9b..78653abc 100644 --- a/adl-server/src/main/kotlin/repositories/impl/QdrantUseCaseEmbeddingsStore.kt +++ b/adl-server/src/main/kotlin/repositories/impl/QdrantUseCaseEmbeddingsStore.kt @@ -204,7 +204,7 @@ class QdrantUseCaseEmbeddingsStore( io.qdrant.client.grpc.Points.Condition.newBuilder() .setField( io.qdrant.client.grpc.Points.FieldCondition.newBuilder() - .setKey(PAYLOAD_USECASE_ID) + .setKey(PAYLOAD_ADL_ID) .setMatch( io.qdrant.client.grpc.Points.Match.newBuilder() .setKeyword(useCaseId), @@ -259,7 +259,7 @@ class QdrantUseCaseEmbeddingsStore( example: String, ): Map { return buildMap { - put(PAYLOAD_USECASE_ID, value(useCaseId)) + put(PAYLOAD_ADL_ID, value(useCaseId)) put(PAYLOAD_EXAMPLE, value(example)) put(PAYLOAD_CONTENT, value(useCase)) } @@ -268,7 +268,7 @@ class QdrantUseCaseEmbeddingsStore( private fun ScoredPoint.toSearchResult(): SearchResult { val payload = this.payloadMap return SearchResult( - useCaseId = payload[PAYLOAD_USECASE_ID]?.stringValue ?: "", + adlId = payload[PAYLOAD_ADL_ID]?.stringValue ?: "", example = payload[PAYLOAD_EXAMPLE]?.stringValue ?: "", score = this.score, content = payload[PAYLOAD_CONTENT]?.stringValue ?: "", @@ -276,7 +276,7 @@ class QdrantUseCaseEmbeddingsStore( } companion object { - private const val PAYLOAD_USECASE_ID = "usecase_id" + private const val PAYLOAD_ADL_ID = "adl_id" private const val PAYLOAD_EXAMPLE = "example" private const val PAYLOAD_CONTENT = "content" } diff --git a/adl-server/src/main/resources/assistant.md b/adl-server/src/main/resources/assistant.md index 5f5baae0..4879119b 100644 --- a/adl-server/src/main/resources/assistant.md +++ b/adl-server/src/main/resources/assistant.md @@ -70,25 +70,4 @@ $$TIME$$ ## Available Use Cases -### UseCase: off_topic -#### Description -The customer is asking a question or making a statement that is unrelated to any of the defined use cases. - -#### Solution -Politely let the customer know their request is outside the scope of your assistance. - ----- - -### UseCase: unclear_request -#### Description -The customer's request is ambiguous or lacks sufficient detail to determine the appropriate use case. - -#### Solution -Ask the customer for clarification or additional details to better understand their request. - -#### Fallback Solution -Politely let the customer know their request is outside the scope of your assistance. - ----- - $$USE_CASES$$ \ No newline at end of file diff --git a/adl-server/src/main/resources/base_use_cases.md b/adl-server/src/main/resources/base_use_cases.md new file mode 100644 index 00000000..97478f0a --- /dev/null +++ b/adl-server/src/main/resources/base_use_cases.md @@ -0,0 +1,20 @@ +### UseCase: off_topic +#### Description +The customer is asking a question or making a statement that is unrelated to any of the defined use cases. + +#### Solution +Politely let the customer know their request is outside the scope of your assistance. + +---- + +### UseCase: unclear_request +#### Description +The customer's request is ambiguous or lacks sufficient detail to determine the appropriate use case. + +#### Solution +Ask the customer for clarification or additional details to better understand their request. + +#### Fallback Solution +Politely let the customer know their request is outside the scope of your assistance. + +---- \ No newline at end of file diff --git a/adl-server/src/main/resources/examples/buy_a_car.md b/adl-server/src/main/resources/examples/buy_a_car.md index f795b22e..3e972c74 100644 --- a/adl-server/src/main/resources/examples/buy_a_car.md +++ b/adl-server/src/main/resources/examples/buy_a_car.md @@ -10,4 +10,26 @@ Customer want to buy a car. #### Solution Tell the customer we will notify them when we have cars that fit their budget and model preference. +#### Examples +- I'm looking to buy a car, but I'm not sure where to start. +- Can you help me find a car to buy? +- I want to purchase a car. What are my options? +- I'm interested in buying a car. Can you assist me? +- Could you guide me through the car purchasing process? +- I need help finding the right car to buy. +- I'm planning to buy a car soon. Can you provide some advice? +- What's the process for buying a car? +- I'm curious about buying a new vehicle. What do I need to know? +- I want to buy a car but need some help figuring it out. +- Looking to buy a car within a certain budget. Can you help? +- Could you help me with buying a new car? +- Thinking about purchasing a car, but I'm not sure about model options. +- Can you help me with the car buying process? +- I’d like to buy a car, but I’m a bit overwhelmed. +- Can you assist me with purchasing a car? +- I need advice on buying a car. +- How can I buy a new car? +- I'm planning to buy a car and need some guidance. +- Urgently need to buy a car. Can you advise me on this? + ---- \ No newline at end of file diff --git a/adl-server/src/main/resources/examples/greeting.md b/adl-server/src/main/resources/examples/greeting.md new file mode 100644 index 00000000..adc75460 --- /dev/null +++ b/adl-server/src/main/resources/examples/greeting.md @@ -0,0 +1,15 @@ +### UseCase: greeting + +#### Description +Customer is saying hello or greeting the agent. + +#### Solution +"Hello! I am the ADL AI assistant. How can I assist you today?" + +#### Examples +- Hi +- Hello +- Good morning +- Good afternoon + +---- \ No newline at end of file diff --git a/adl-server/src/main/resources/static/product_page.html b/adl-server/src/main/resources/static/product_page.html index eb96b742..88c41560 100644 --- a/adl-server/src/main/resources/static/product_page.html +++ b/adl-server/src/main/resources/static/product_page.html @@ -14,15 +14,15 @@ sans: ['Inter', 'sans-serif'], }, colors: { - iron: { - red: '#8B0000', // Deep Red - redlight: '#A52A2A', // Lighter Red - gold: '#D4AF37', // Metallic Gold - goldlight: '#F4D03F', // Lighter Gold - reactor: '#00BFFF', // Arc Reactor Blue - dark: '#0f1012', // Almost Black - panel: '#1a1c22', // Dark Grey Panel - border: '#2d3340' // Borders + nature: { + primary: '#3a5a40', // Hunter Green + light: '#588157', // Sage + accent: '#d4a373', // Tan/Brown + sky: '#5F9EA0', // Soft Blue-Green + dark: '#1c1917', // Stone 900 + panel: '#ffffff', // White + border: '#e7e5e4', // Stone 200 + page: '#fafaf9' // Stone 50 } } } @@ -31,41 +31,41 @@ - + -