diff --git a/build.gradle.kts b/build.gradle.kts index 9352c73..e50638f 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,6 +1,7 @@ plugins { kotlin("jvm") version "2.0.20" id("org.jetbrains.kotlinx.kover") version "0.8.3" + id("org.cadixdev.licenser") version "0.6.1" `maven-publish` } @@ -8,9 +9,19 @@ subprojects { apply(plugin = "org.jetbrains.kotlin.jvm") apply(plugin = "org.jetbrains.kotlinx.kover") apply(plugin = "maven-publish") + apply(plugin = "org.cadixdev.licenser") group = "ai.ancf.lmos" - version = "0.1.0-SNAPSHOT" + version = "0.1.3-SNAPSHOT" + + license { + header(rootProject.file("LICENSE")) + include("**/*.java") + include("**/*.kt") + include("**/*.yaml") + exclude("**/*.properties") + } + dependencies { testImplementation(kotlin("test")) diff --git a/kotlin-wot-binding-http/src/main/kotlin/http/HttpClientConfig.kt b/kotlin-wot-binding-http/src/main/kotlin/http/HttpClientConfig.kt index 18f68fd..3971e53 100644 --- a/kotlin-wot-binding-http/src/main/kotlin/http/HttpClientConfig.kt +++ b/kotlin-wot-binding-http/src/main/kotlin/http/HttpClientConfig.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package http import ai.ancf.lmos.wot.security.SecurityScheme diff --git a/kotlin-wot-binding-http/src/main/kotlin/http/HttpProtocolClient.kt b/kotlin-wot-binding-http/src/main/kotlin/http/HttpProtocolClient.kt index a2ee53f..3b618a8 100644 --- a/kotlin-wot-binding-http/src/main/kotlin/http/HttpProtocolClient.kt +++ b/kotlin-wot-binding-http/src/main/kotlin/http/HttpProtocolClient.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.http import ai.ancf.lmos.wot.content.Content @@ -124,8 +130,15 @@ fun createHttpClient(): HttpClient { engine { proxy = ProxyBuilder.http(proxyUrl) } + install(HttpTimeout) { + requestTimeoutMillis = 50000 + } } } else { - HttpClient(CIO) + HttpClient(CIO) { + install(HttpTimeout) { + requestTimeoutMillis = 50000 + } + } } } \ No newline at end of file diff --git a/kotlin-wot-binding-http/src/main/kotlin/http/HttpProtocolClientFactory.kt b/kotlin-wot-binding-http/src/main/kotlin/http/HttpProtocolClientFactory.kt index cf42bf7..48222b1 100644 --- a/kotlin-wot-binding-http/src/main/kotlin/http/HttpProtocolClientFactory.kt +++ b/kotlin-wot-binding-http/src/main/kotlin/http/HttpProtocolClientFactory.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.http import ai.anfc.lmos.wot.binding.ProtocolClient diff --git a/kotlin-wot-binding-http/src/main/kotlin/http/HttpProtocolServer.kt b/kotlin-wot-binding-http/src/main/kotlin/http/HttpProtocolServer.kt index d8962c8..4307547 100644 --- a/kotlin-wot-binding-http/src/main/kotlin/http/HttpProtocolServer.kt +++ b/kotlin-wot-binding-http/src/main/kotlin/http/HttpProtocolServer.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.http import ai.ancf.lmos.wot.JsonMapper diff --git a/kotlin-wot-binding-http/src/main/kotlin/http/HttpsProtocolClientFactory.kt b/kotlin-wot-binding-http/src/main/kotlin/http/HttpsProtocolClientFactory.kt index 901356d..a586aff 100644 --- a/kotlin-wot-binding-http/src/main/kotlin/http/HttpsProtocolClientFactory.kt +++ b/kotlin-wot-binding-http/src/main/kotlin/http/HttpsProtocolClientFactory.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.http /** diff --git a/kotlin-wot-binding-http/src/main/kotlin/http/routes/AbstractRoute.kt b/kotlin-wot-binding-http/src/main/kotlin/http/routes/AbstractRoute.kt index e9db82a..93e0d3a 100644 --- a/kotlin-wot-binding-http/src/main/kotlin/http/routes/AbstractRoute.kt +++ b/kotlin-wot-binding-http/src/main/kotlin/http/routes/AbstractRoute.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.http.routes import ai.ancf.lmos.wot.binding.http.routes.AbstractRoute diff --git a/kotlin-wot-binding-http/src/main/kotlin/http/routes/ThingsRoute.kt b/kotlin-wot-binding-http/src/main/kotlin/http/routes/ThingsRoute.kt index 702befb..448e6b0 100644 --- a/kotlin-wot-binding-http/src/main/kotlin/http/routes/ThingsRoute.kt +++ b/kotlin-wot-binding-http/src/main/kotlin/http/routes/ThingsRoute.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.http.routes import ai.ancf.lmos.wot.content.Content diff --git a/kotlin-wot-binding-http/src/test/kotlin/http/HttpProtocolClientFactoryTest.kt b/kotlin-wot-binding-http/src/test/kotlin/http/HttpProtocolClientFactoryTest.kt index 889d35b..7d10156 100644 --- a/kotlin-wot-binding-http/src/test/kotlin/http/HttpProtocolClientFactoryTest.kt +++ b/kotlin-wot-binding-http/src/test/kotlin/http/HttpProtocolClientFactoryTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.http import kotlin.test.Test diff --git a/kotlin-wot-binding-http/src/test/kotlin/http/HttpProtocolClientTest.kt b/kotlin-wot-binding-http/src/test/kotlin/http/HttpProtocolClientTest.kt index e4c8c2e..dfe55e1 100644 --- a/kotlin-wot-binding-http/src/test/kotlin/http/HttpProtocolClientTest.kt +++ b/kotlin-wot-binding-http/src/test/kotlin/http/HttpProtocolClientTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.http import ai.ancf.lmos.wot.content.Content import ai.ancf.lmos.wot.security.BasicSecurityScheme diff --git a/kotlin-wot-binding-http/src/test/kotlin/http/HttpProtocolServerTest.kt b/kotlin-wot-binding-http/src/test/kotlin/http/HttpProtocolServerTest.kt index fc451c9..275fbc8 100644 --- a/kotlin-wot-binding-http/src/test/kotlin/http/HttpProtocolServerTest.kt +++ b/kotlin-wot-binding-http/src/test/kotlin/http/HttpProtocolServerTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.http import ai.ancf.lmos.wot.Servient diff --git a/kotlin-wot-binding-http/src/test/kotlin/http/HttpsProtocolClientFactoryTest.kt b/kotlin-wot-binding-http/src/test/kotlin/http/HttpsProtocolClientFactoryTest.kt index 3038b2f..dd4f6ab 100644 --- a/kotlin-wot-binding-http/src/test/kotlin/http/HttpsProtocolClientFactoryTest.kt +++ b/kotlin-wot-binding-http/src/test/kotlin/http/HttpsProtocolClientFactoryTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.http import kotlin.test.Test diff --git a/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttClientConfig.kt b/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttClientConfig.kt index e1b906d..720ee9e 100644 --- a/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttClientConfig.kt +++ b/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttClientConfig.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.mqtt import java.util.* diff --git a/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttProtocolClient.kt b/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttProtocolClient.kt index 790ec4a..b54b0e5 100644 --- a/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttProtocolClient.kt +++ b/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttProtocolClient.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.mqtt import ai.ancf.lmos.wot.content.Content diff --git a/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttProtocolClientFactory.kt b/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttProtocolClientFactory.kt index c38152d..610652e 100644 --- a/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttProtocolClientFactory.kt +++ b/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttProtocolClientFactory.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.mqtt import ai.anfc.lmos.wot.binding.ProtocolClient diff --git a/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttProtocolException.kt b/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttProtocolException.kt index 422f565..fc0890a 100644 --- a/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttProtocolException.kt +++ b/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttProtocolException.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.mqtt diff --git a/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttProtocolServer.kt b/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttProtocolServer.kt index 3b14c13..695c743 100644 --- a/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttProtocolServer.kt +++ b/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttProtocolServer.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.mqtt import ai.ancf.lmos.wot.Servient diff --git a/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttsProtocolClientFactory.kt b/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttsProtocolClientFactory.kt index c01be92..5849b40 100644 --- a/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttsProtocolClientFactory.kt +++ b/kotlin-wot-binding-mqtt/src/main/kotlin/mqtt/MqttsProtocolClientFactory.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.mqtt import ai.anfc.lmos.wot.binding.ProtocolClient diff --git a/kotlin-wot-binding-mqtt/src/test/kotlin/integration/MqttProtocolClientFactoryTest.kt b/kotlin-wot-binding-mqtt/src/test/kotlin/integration/MqttProtocolClientFactoryTest.kt index e8ffc5a..63a522c 100644 --- a/kotlin-wot-binding-mqtt/src/test/kotlin/integration/MqttProtocolClientFactoryTest.kt +++ b/kotlin-wot-binding-mqtt/src/test/kotlin/integration/MqttProtocolClientFactoryTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.mqtt import kotlin.test.Test diff --git a/kotlin-wot-binding-mqtt/src/test/kotlin/integration/MqttProtocolClientTest.kt b/kotlin-wot-binding-mqtt/src/test/kotlin/integration/MqttProtocolClientTest.kt index 4f81ff8..be79795 100644 --- a/kotlin-wot-binding-mqtt/src/test/kotlin/integration/MqttProtocolClientTest.kt +++ b/kotlin-wot-binding-mqtt/src/test/kotlin/integration/MqttProtocolClientTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.mqtt import ai.ancf.lmos.wot.content.Content diff --git a/kotlin-wot-binding-mqtt/src/test/kotlin/integration/MqttProtocolServerTest.kt b/kotlin-wot-binding-mqtt/src/test/kotlin/integration/MqttProtocolServerTest.kt index 5ad88ad..5c6870b 100644 --- a/kotlin-wot-binding-mqtt/src/test/kotlin/integration/MqttProtocolServerTest.kt +++ b/kotlin-wot-binding-mqtt/src/test/kotlin/integration/MqttProtocolServerTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.mqtt import ai.ancf.lmos.wot.JsonMapper diff --git a/kotlin-wot-binding-websocket/src/main/kotlin/websocket/HttpClientConfig.kt b/kotlin-wot-binding-websocket/src/main/kotlin/websocket/HttpClientConfig.kt index fd98b72..a773c9c 100644 --- a/kotlin-wot-binding-websocket/src/main/kotlin/websocket/HttpClientConfig.kt +++ b/kotlin-wot-binding-websocket/src/main/kotlin/websocket/HttpClientConfig.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.websocket import ai.ancf.lmos.wot.security.SecurityScheme diff --git a/kotlin-wot-binding-websocket/src/main/kotlin/websocket/Messages.kt b/kotlin-wot-binding-websocket/src/main/kotlin/websocket/Messages.kt index 84acb4e..b245825 100644 --- a/kotlin-wot-binding-websocket/src/main/kotlin/websocket/Messages.kt +++ b/kotlin-wot-binding-websocket/src/main/kotlin/websocket/Messages.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.websocket import com.fasterxml.jackson.annotation.* diff --git a/kotlin-wot-binding-websocket/src/main/kotlin/websocket/SecureWebSocketProtocolClientFactory.kt b/kotlin-wot-binding-websocket/src/main/kotlin/websocket/SecureWebSocketProtocolClientFactory.kt index 6db0b9b..7ae3528 100644 --- a/kotlin-wot-binding-websocket/src/main/kotlin/websocket/SecureWebSocketProtocolClientFactory.kt +++ b/kotlin-wot-binding-websocket/src/main/kotlin/websocket/SecureWebSocketProtocolClientFactory.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.websocket /** diff --git a/kotlin-wot-binding-websocket/src/main/kotlin/websocket/WebSocketProtocolClient.kt b/kotlin-wot-binding-websocket/src/main/kotlin/websocket/WebSocketProtocolClient.kt index 96c065f..62da62c 100644 --- a/kotlin-wot-binding-websocket/src/main/kotlin/websocket/WebSocketProtocolClient.kt +++ b/kotlin-wot-binding-websocket/src/main/kotlin/websocket/WebSocketProtocolClient.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.websocket import ai.ancf.lmos.wot.JsonMapper @@ -91,6 +97,7 @@ class WebSocketProtocolClient( requestAndReply(resource.form, message) } finally { resourceChannels[resource.name]?.close() + resourceChannels.remove(resource.name) } } @@ -105,7 +112,7 @@ class WebSocketProtocolClient( resourceChannels[resource.name] = channel return channel.consumeAsFlow().onCompletion { - resourceChannels.remove(resource.name) + unlinkResource(resource, resourceType) } } diff --git a/kotlin-wot-binding-websocket/src/main/kotlin/websocket/WebSocketProtocolClientFactory.kt b/kotlin-wot-binding-websocket/src/main/kotlin/websocket/WebSocketProtocolClientFactory.kt index 1c8ae2e..e3d9155 100644 --- a/kotlin-wot-binding-websocket/src/main/kotlin/websocket/WebSocketProtocolClientFactory.kt +++ b/kotlin-wot-binding-websocket/src/main/kotlin/websocket/WebSocketProtocolClientFactory.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.websocket import ai.anfc.lmos.wot.binding.ProtocolClient diff --git a/kotlin-wot-binding-websocket/src/main/kotlin/websocket/WebSocketProtocolServer.kt b/kotlin-wot-binding-websocket/src/main/kotlin/websocket/WebSocketProtocolServer.kt index f8f739e..eaa777c 100644 --- a/kotlin-wot-binding-websocket/src/main/kotlin/websocket/WebSocketProtocolServer.kt +++ b/kotlin-wot-binding-websocket/src/main/kotlin/websocket/WebSocketProtocolServer.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.websocket import ai.ancf.lmos.wot.JsonMapper @@ -206,11 +212,22 @@ fun Application.setupRoutingWithWebSockets(servient: Servient) { webSocket("/ws") { val sessionId = this.call.request.headers["Sec-WebSocket-Key"] ?: UUID.randomUUID().toString() - handleWebSocketSession(sessionId, servient) + try { + handleWebSocketSession(sessionId, servient) + } finally { + // This will be triggered when the WebSocket connection is closed + cleanUp(sessionId, servient) + } } } } +fun cleanUp(sessionId: String, servient: Servient) { + servient.things.values.forEach { thing -> + thing.unregisterAllListeners(sessionId) + } +} + @WithSpan(kind = SpanKind.SERVER) suspend fun DefaultWebSocketServerSession.handleWebSocketSession( @SpanAttribute("websocket.session.id") sessionId: String, @@ -483,6 +500,7 @@ suspend fun DefaultWebSocketServerSession.handleSubscribeEvent(thing: ExposedThi } } } + @WithSpan(kind = SpanKind.SERVER) suspend fun DefaultWebSocketServerSession.handleUnsubscribeEvent(thing: ExposedThing, message: UnsubscribeEventMessage, @SpanAttribute("thingId") thingId: String, @SpanAttribute("sessionId") sessionId: String) { val eventName = message.event diff --git a/kotlin-wot-binding-websocket/src/test/kotlin/websocket/WebSocketProtocolClientTest.kt b/kotlin-wot-binding-websocket/src/test/kotlin/websocket/WebSocketProtocolClientTest.kt index c6e3ffd..3be3213 100644 --- a/kotlin-wot-binding-websocket/src/test/kotlin/websocket/WebSocketProtocolClientTest.kt +++ b/kotlin-wot-binding-websocket/src/test/kotlin/websocket/WebSocketProtocolClientTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package websocket import ai.ancf.lmos.wot.Servient diff --git a/kotlin-wot-binding-websocket/src/test/kotlin/websocket/WebSocketProtocolServerTest.kt b/kotlin-wot-binding-websocket/src/test/kotlin/websocket/WebSocketProtocolServerTest.kt index 4bde912..0420ebc 100644 --- a/kotlin-wot-binding-websocket/src/test/kotlin/websocket/WebSocketProtocolServerTest.kt +++ b/kotlin-wot-binding-websocket/src/test/kotlin/websocket/WebSocketProtocolServerTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package websocket import ai.ancf.lmos.wot.JsonMapper diff --git a/kotlin-wot-binding-websocket/src/test/kotlin/websocket/WebsocketProtocolClientFactoryTest.kt b/kotlin-wot-binding-websocket/src/test/kotlin/websocket/WebsocketProtocolClientFactoryTest.kt index d5f31a2..b3a6bba 100644 --- a/kotlin-wot-binding-websocket/src/test/kotlin/websocket/WebsocketProtocolClientFactoryTest.kt +++ b/kotlin-wot-binding-websocket/src/test/kotlin/websocket/WebsocketProtocolClientFactoryTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.binding.mqtt import ai.ancf.lmos.wot.binding.websocket.SecureWebSocketProtocolClientFactory diff --git a/kotlin-wot-integration-tests/build.gradle.kts b/kotlin-wot-integration-tests/build.gradle.kts index a38c0cd..6e381e2 100644 --- a/kotlin-wot-integration-tests/build.gradle.kts +++ b/kotlin-wot-integration-tests/build.gradle.kts @@ -3,14 +3,14 @@ import org.springframework.boot.gradle.tasks.run.BootRun import java.net.URI plugins { - kotlin("plugin.spring") version "1.9.10" - id("org.springframework.boot") version "3.1.5" // Use the latest compatible version - id("io.spring.dependency-management") version "1.1.3" + kotlin("plugin.spring") version "1.9.25" + id("org.springframework.boot") version "3.4.2" + id("io.spring.dependency-management") version "1.1.7" } -tasks.named("test") { - enabled = false -} +//tasks.named("test") { +// enabled = false +//} dependencies { // Replace the following with the starter dependencies of specific modules you wish to use @@ -18,7 +18,8 @@ dependencies { api(project(":kotlin-wot-binding-websocket")) api(project(":kotlin-wot-binding-mqtt")) api(project(":kotlin-wot-spring-boot-starter")) - api(project(":kotlin-wot-lmos-protocol")) + api(project(":lmos-kotlin-sdk-client")) + api(project(":lmos-kotlin-sdk-server")) implementation("org.eclipse.lmos:arc-azure-client:0.1.0-SNAPSHOT") api("org.eclipse.lmos:arc-spring-boot-starter:0.1.0-SNAPSHOT") @@ -40,10 +41,13 @@ dependencies { //implementation("io.opentelemetry:opentelemetry-exporter-otlp") implementation("dev.langchain4j:langchain4j-azure-open-ai:1.0.0-beta1") + implementation("org.jsoup:jsoup:1.7.2") + //implementation("dev.langchain4j:langchain4j:0.35.0") testImplementation("org.springframework.boot:spring-boot-starter-test") testImplementation("com.hivemq:hivemq-mqtt-client:1.3.3") implementation("org.testcontainers:testcontainers:1.20.3") + testImplementation("app.cash.turbine:turbine:1.2.0") } diff --git a/kotlin-wot-integration-tests/src/main/kotlin/integration/AgentApplication.kt b/kotlin-wot-integration-tests/src/main/kotlin/integration/AgentApplication.kt index 287f1f1..2db9473 100644 --- a/kotlin-wot-integration-tests/src/main/kotlin/integration/AgentApplication.kt +++ b/kotlin-wot-integration-tests/src/main/kotlin/integration/AgentApplication.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.integration import org.springframework.boot.autoconfigure.SpringBootApplication diff --git a/kotlin-wot-integration-tests/src/main/kotlin/integration/AgentConfiguration.kt b/kotlin-wot-integration-tests/src/main/kotlin/integration/AgentConfiguration.kt index 7f37d1e..5d70cac 100644 --- a/kotlin-wot-integration-tests/src/main/kotlin/integration/AgentConfiguration.kt +++ b/kotlin-wot-integration-tests/src/main/kotlin/integration/AgentConfiguration.kt @@ -1,12 +1,21 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.integration -import ai.ancf.lmos.wot.JsonMapper import ai.ancf.lmos.wot.Wot -import ai.ancf.lmos.wot.security.SecurityScheme +import ai.ancf.lmos.wot.integration.ThingToFunctionsMapper.mapDataSchemaToParam +import ai.ancf.lmos.wot.integration.ThingToFunctionsMapper.mapSchemaToJsonNode +import ai.ancf.lmos.wot.security.NoSecurityScheme import ai.ancf.lmos.wot.thing.schema.WoTConsumedThing -import com.fasterxml.jackson.module.kotlin.readValue +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.node.TextNode import kotlinx.coroutines.runBlocking +import org.eclipse.lmos.arc.agents.dsl.AllTools import org.eclipse.lmos.arc.agents.functions.LLMFunction import org.eclipse.lmos.arc.spring.Agents import org.eclipse.lmos.arc.spring.Functions @@ -22,14 +31,15 @@ import org.springframework.context.annotation.Configuration @EnableConfigurationProperties(ToolProperties::class) class AgentConfiguration { - private lateinit var thingDescriptionsMap : Map + private lateinit var thingDescriptionsMap: Map - private val log : Logger = LoggerFactory.getLogger(AgentConfiguration::class.java) + private val log: Logger = LoggerFactory.getLogger(AgentConfiguration::class.java) @Bean fun chatArcAgent(agent: Agents) = agent { name = "ChatAgent" - prompt { """ + prompt { + """ You are a professional smart home assistant. ## Instructions @@ -63,7 +73,8 @@ class AgentConfiguration { - "Set the thermostat to 72°F." - "Lock the front door." - """.trimIndent() } + """.trimIndent() + } model = { "GPT-4o" } filterInput { -"Hello world" } tools = listOf("devices") @@ -81,11 +92,15 @@ class AgentConfiguration { name = "ScraperAgent" prompt { "You can scrape a page by using the scraper tool." } model = { "GPT-4o" } - tools = listOf("fetchContent") + tools = AllTools } @Bean - fun agentEventListener(applicationEventPublisher: ApplicationEventPublisher) = ArcEventListener(applicationEventPublisher) + fun agentEventListener(applicationEventPublisher: ApplicationEventPublisher) = + ArcEventListener(applicationEventPublisher) + + + /* @Bean fun discoverTools(toolProperties: ToolProperties, functions: Functions, wot: Wot) : List = runBlocking { @@ -108,12 +123,42 @@ class AgentConfiguration { } } -} + */ + + @Bean + fun scraperToolFunctions(toolProperties: ToolProperties, functions: Functions, wot: Wot, scraperTool: ScraperTool) : List = runBlocking { + + val thingDescription = wot.requestThingDescription(toolProperties.tools["scraper"]?.url!!, NoSecurityScheme()) + val consumedThing = wot.consume(thingDescription) + + thingDescription.actions.flatMap { (actionName, action) -> + val actionParams = action.input?.let { listOf(Pair(mapDataSchemaToParam(it), true)) } ?: emptyList() + functions(actionName, action.description ?: "No Description available", "scraper", actionParams) { input -> + try { + val jsonInput : JsonNode = action.output?.let { mapSchemaToJsonNode(it, input.first()!!) } ?: TextNode(input.first()!!) + consumedThing.invokeAction(actionName, jsonInput).asText() + ?: "Function call failed" + } catch (e: Exception) { + "Function call failed" + } + } + } -data class Resources( - val milk: Int, - val water: Int , - val chocolate : Int, - val coffeeBeans: Int -) + } + /* + @Bean + fun discoverLocalTools(functions: Functions, wot: Wot, scraperTool: ScraperTool) : List{ + log.info("Map Scraper to LLM Functions") + val exposedThing = ExposedThingBuilder.createExposedThing(wot, scraperTool, ScraperTool::class) + return if(exposedThing != null) { + ThingToFunctionsMapper + .mapThingDescriptionToFunctions2(functions, "scraper", + exposedThing.getThingDescription()).toList() + }else{ + emptyList() + } + } + */ + +} diff --git a/kotlin-wot-integration-tests/src/main/kotlin/integration/ArcConversationalAgent.kt b/kotlin-wot-integration-tests/src/main/kotlin/integration/ArcConversationalAgent.kt index ed72a3a..6ed1c74 100644 --- a/kotlin-wot-integration-tests/src/main/kotlin/integration/ArcConversationalAgent.kt +++ b/kotlin-wot-integration-tests/src/main/kotlin/integration/ArcConversationalAgent.kt @@ -1,35 +1,32 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.integration -import ai.ancf.lmos.wot.protocol.ConversationalAgent -import io.opentelemetry.api.trace.Span -import io.opentelemetry.instrumentation.annotations.SpanAttribute + +import ai.ancf.lmos.sdk.agents.ConversationalAgent +import ai.ancf.lmos.sdk.model.AgentRequest +import ai.ancf.lmos.sdk.model.AgentResult +import integration.executeAgent import io.opentelemetry.instrumentation.annotations.WithSpan import org.eclipse.lmos.arc.agents.AgentProvider -import org.eclipse.lmos.arc.agents.User -import org.eclipse.lmos.arc.agents.conversation.AssistantMessage -import org.eclipse.lmos.arc.agents.conversation.latest -import org.eclipse.lmos.arc.agents.conversation.toConversation import org.eclipse.lmos.arc.agents.getAgentByName -import org.eclipse.lmos.arc.core.getOrThrow import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.stereotype.Component @Component -class ArcConversationalAgent(agentProvider: AgentProvider) : ConversationalAgent { +class ArcConversationalAgent(agentProvider: AgentProvider) : ConversationalAgent { private val agent = agentProvider.getAgentByName("ChatAgent") as org.eclipse.lmos.arc.agents.ChatAgent private val log : Logger = LoggerFactory.getLogger(ArcConversationalAgent::class.java) @WithSpan - override suspend fun chat(@SpanAttribute message: String): String { - log.info("Chat input: $message") - val assistantMessage = agent.execute(message.toConversation(User("myId"))).getOrThrow().latest() - ?: throw RuntimeException("No Assistant response") - - val currentSpan = Span.current() - currentSpan.setAttribute("assistantMessage", assistantMessage.content) - return assistantMessage.content + override suspend fun chat(message: AgentRequest): AgentResult { + return executeAgent(message, agent::execute) } } \ No newline at end of file diff --git a/kotlin-wot-integration-tests/src/main/kotlin/integration/ArcEventListener.kt b/kotlin-wot-integration-tests/src/main/kotlin/integration/ArcEventListener.kt index fc1756f..a1c1d80 100644 --- a/kotlin-wot-integration-tests/src/main/kotlin/integration/ArcEventListener.kt +++ b/kotlin-wot-integration-tests/src/main/kotlin/integration/ArcEventListener.kt @@ -1,6 +1,14 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.integration + +import ai.ancf.lmos.sdk.model.AgentEvent import ai.ancf.lmos.wot.JsonMapper import org.eclipse.lmos.arc.agents.events.Event import org.eclipse.lmos.arc.agents.events.EventHandler @@ -10,8 +18,14 @@ import org.springframework.context.ApplicationEventPublisher class ArcEventListener(private val applicationEventPublisher: ApplicationEventPublisher) : EventHandler { override fun onEvent(event: Event) { - applicationEventPublisher.publishEvent(AgentEvent(JsonMapper.instance.writeValueAsString(event))) + applicationEventPublisher.publishEvent(SpringApplicationAgentEvent( + AgentEvent( + event::class.simpleName.toString(), + JsonMapper.instance.writeValueAsString(event), + event.context["conversationId"], + event.context["turnId"]) + )) } } -data class AgentEvent(val message: String) : ApplicationEvent(message) \ No newline at end of file +data class SpringApplicationAgentEvent(val event: AgentEvent) : ApplicationEvent(event) \ No newline at end of file diff --git a/kotlin-wot-integration-tests/src/main/kotlin/integration/ArcHelper.kt b/kotlin-wot-integration-tests/src/main/kotlin/integration/ArcHelper.kt new file mode 100644 index 0000000..30067e0 --- /dev/null +++ b/kotlin-wot-integration-tests/src/main/kotlin/integration/ArcHelper.kt @@ -0,0 +1,35 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package integration + +import ai.ancf.lmos.sdk.model.AgentRequest +import ai.ancf.lmos.sdk.model.AgentResult +import ai.ancf.lmos.sdk.model.Message +import org.eclipse.lmos.arc.agents.AgentFailedException +import org.eclipse.lmos.arc.agents.User +import org.eclipse.lmos.arc.agents.conversation.AssistantMessage +import org.eclipse.lmos.arc.agents.conversation.Conversation +import org.eclipse.lmos.arc.agents.conversation.latest +import org.eclipse.lmos.arc.agents.conversation.toConversation +import org.eclipse.lmos.arc.core.Result +import org.eclipse.lmos.arc.core.getOrThrow + +suspend fun executeAgent(message: AgentRequest, function: suspend (Conversation) -> Result): AgentResult { + val lastMessage : String = message.messages.last().content + val assistantMessage = function(lastMessage.toConversation(User("myId"))).getOrThrow().latest() + ?: throw RuntimeException("No Assistant response") + return AgentResult( + messages = listOf( + Message( + role = "assistant", + content = assistantMessage.content, + turnId = assistantMessage.turnId + ) + ) + ) +} + diff --git a/kotlin-wot-integration-tests/src/main/kotlin/integration/Broker.kt b/kotlin-wot-integration-tests/src/main/kotlin/integration/Broker.kt index cb63ffe..1c0f4ab 100644 --- a/kotlin-wot-integration-tests/src/main/kotlin/integration/Broker.kt +++ b/kotlin-wot-integration-tests/src/main/kotlin/integration/Broker.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package integration import kotlinx.coroutines.runBlocking diff --git a/kotlin-wot-integration-tests/src/main/kotlin/integration/ChatAgent.kt b/kotlin-wot-integration-tests/src/main/kotlin/integration/ChatAgent.kt index b46f34c..d24a218 100644 --- a/kotlin-wot-integration-tests/src/main/kotlin/integration/ChatAgent.kt +++ b/kotlin-wot-integration-tests/src/main/kotlin/integration/ChatAgent.kt @@ -1,8 +1,16 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.integration -import ai.ancf.lmos.wot.JsonMapper -import ai.ancf.lmos.wot.protocol.ConversationalAgent -import ai.ancf.lmos.wot.protocol.LMOSContext +import ai.ancf.lmos.sdk.LMOSContext +import ai.ancf.lmos.sdk.agents.ConversationalAgent +import ai.ancf.lmos.sdk.model.AgentEvent +import ai.ancf.lmos.sdk.model.AgentRequest +import ai.ancf.lmos.sdk.model.AgentResult import ai.ancf.lmos.wot.protocol.LMOSThingType import ai.ancf.lmos.wot.reflection.annotations.* import kotlinx.coroutines.CoroutineScope @@ -20,25 +28,25 @@ import org.springframework.stereotype.Component @Link(href = "lmos/capabilities", rel = "service-meta", type = "application/json") @VersionInfo(instance = "1.0.0") @Component -class ChatAgent(private val arcAgent: ConversationalAgent): ApplicationListener { +class ChatAgent(private val arcAgent: ConversationalAgent): ApplicationListener { - private val agentEventFlow = MutableSharedFlow(replay = 1) // Replay last emitted value + private val agentEventFlow = MutableSharedFlow(replay = 1) // Replay last emitted value @Action(title = "Chat", description = "Ask the agent a question.") @ActionInput(title = "The question", description = "A question") - @ActionOutput(title = "The question", description = "A question") - suspend fun chat(message: String) : String { + @ActionOutput(title = "The answer", description = "The Answer") + suspend fun chat(message: AgentRequest) : AgentResult { return arcAgent.chat(message) } @Event(title = "Agent Event", description = "An event from the agent.") - fun agentEvent() : Flow { + fun agentEvent() : Flow { return agentEventFlow } - override fun onApplicationEvent(event: AgentEvent) { + override fun onApplicationEvent(event: SpringApplicationAgentEvent) { CoroutineScope(Dispatchers.IO).launch { - agentEventFlow.emit(JsonMapper.instance.writeValueAsString(event)) + agentEventFlow.emit(event.event) } } } diff --git a/kotlin-wot-integration-tests/src/main/kotlin/integration/ConversationalAgent.kt b/kotlin-wot-integration-tests/src/main/kotlin/integration/ConversationalAgent.kt deleted file mode 100644 index dc10e91..0000000 --- a/kotlin-wot-integration-tests/src/main/kotlin/integration/ConversationalAgent.kt +++ /dev/null @@ -1,48 +0,0 @@ -package integration - -import ai.ancf.lmos.wot.Servient -import ai.ancf.lmos.wot.Wot -import ai.ancf.lmos.wot.binding.http.HttpProtocolClientFactory -import ai.ancf.lmos.wot.binding.http.HttpsProtocolClientFactory -import ai.ancf.lmos.wot.binding.websocket.WebSocketProtocolClientFactory -import ai.ancf.lmos.wot.protocol.ConsumedConversationalAgent -import ai.ancf.lmos.wot.protocol.ConversationalAgent -import ai.ancf.lmos.wot.protocol.EventListener -import ai.ancf.lmos.wot.thing.ConsumedThing -import io.opentelemetry.instrumentation.annotations.WithSpan -import org.slf4j.Logger -import org.slf4j.LoggerFactory - - -class WotConversationalAgent private constructor(private val thing : ConsumedThing) : - ConsumedConversationalAgent { - - private val log : Logger = LoggerFactory.getLogger(ConversationalAgent::class.java) - - companion object { - suspend fun create(wot: Wot, url: String): ConsumedConversationalAgent { - return WotConversationalAgent(wot.consume(wot.requestThingDescription(url)) as ConsumedThing) - } - - suspend fun create(url: String): ConsumedConversationalAgent { - val wot = Wot.create(Servient(clientFactories = listOf(HttpProtocolClientFactory(), HttpsProtocolClientFactory(), - WebSocketProtocolClientFactory() - ))) - return create(wot, url) - } - } - - @WithSpan - override suspend fun chat(message: String): String { - return try { - thing.invokeAction(actionName = "chat", input = message) - } catch (e: Exception) { - log.error("Failed to receive an answer", e) - "Failed to receive an answer" - } - } - - override suspend fun consumeEvent(eventName: String, listener: EventListener) { - thing.subscribeEvent(eventName, { listener.handleEvent(it.value().asText()) }) - } -} \ No newline at end of file diff --git a/kotlin-wot-integration-tests/src/main/kotlin/integration/ResearcherAgent.kt b/kotlin-wot-integration-tests/src/main/kotlin/integration/ResearcherAgent.kt index 3136f6c..32c6251 100644 --- a/kotlin-wot-integration-tests/src/main/kotlin/integration/ResearcherAgent.kt +++ b/kotlin-wot-integration-tests/src/main/kotlin/integration/ResearcherAgent.kt @@ -1,20 +1,24 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.integration +import ai.ancf.lmos.sdk.model.AgentRequest +import ai.ancf.lmos.sdk.model.AgentResult import ai.ancf.lmos.wot.protocol.LMOSContext import ai.ancf.lmos.wot.protocol.LMOSThingType import ai.ancf.lmos.wot.reflection.annotations.Action import ai.ancf.lmos.wot.reflection.annotations.Context import ai.ancf.lmos.wot.reflection.annotations.Thing import ai.ancf.lmos.wot.reflection.annotations.VersionInfo +import integration.executeAgent import kotlinx.coroutines.flow.MutableSharedFlow import org.eclipse.lmos.arc.agents.AgentProvider -import org.eclipse.lmos.arc.agents.User -import org.eclipse.lmos.arc.agents.conversation.AssistantMessage -import org.eclipse.lmos.arc.agents.conversation.latest -import org.eclipse.lmos.arc.agents.conversation.toConversation import org.eclipse.lmos.arc.agents.getAgentByName -import org.eclipse.lmos.arc.core.getOrThrow import org.springframework.stereotype.Component @@ -30,10 +34,8 @@ class ResearcherAgent(agentProvider: AgentProvider) { val agent = agentProvider.getAgentByName("ResearcherAgent") as org.eclipse.lmos.arc.agents.ChatAgent @Action(title = "Chat", description = "Ask the agent a question.") - suspend fun chat(message : String) : String { - val assistantMessage = agent.execute(message.toConversation(User("myId"))).getOrThrow().latest() ?: - throw RuntimeException("No Assistant response") - return assistantMessage.content + suspend fun chat(message : AgentRequest) : AgentResult { + return executeAgent(message, agent::execute) } } diff --git a/kotlin-wot-integration-tests/src/main/kotlin/integration/ScraperAgent.kt b/kotlin-wot-integration-tests/src/main/kotlin/integration/ScraperAgent.kt index acac5d4..ae20360 100644 --- a/kotlin-wot-integration-tests/src/main/kotlin/integration/ScraperAgent.kt +++ b/kotlin-wot-integration-tests/src/main/kotlin/integration/ScraperAgent.kt @@ -1,18 +1,27 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.integration +import ai.ancf.lmos.sdk.model.AgentEvent +import ai.ancf.lmos.sdk.model.AgentRequest +import ai.ancf.lmos.sdk.model.AgentResult import ai.ancf.lmos.wot.protocol.LMOSContext import ai.ancf.lmos.wot.protocol.LMOSThingType import ai.ancf.lmos.wot.reflection.annotations.* +import integration.executeAgent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.launch import org.eclipse.lmos.arc.agents.AgentProvider -import org.eclipse.lmos.arc.agents.User -import org.eclipse.lmos.arc.agents.conversation.AssistantMessage -import org.eclipse.lmos.arc.agents.conversation.latest -import org.eclipse.lmos.arc.agents.conversation.toConversation import org.eclipse.lmos.arc.agents.getAgentByName -import org.eclipse.lmos.arc.core.getOrThrow +import org.springframework.context.ApplicationListener import org.springframework.stereotype.Component @@ -21,23 +30,26 @@ import org.springframework.stereotype.Component @Context(prefix = LMOSContext.prefix, url = LMOSContext.url) @VersionInfo(instance = "1.0.0") @Component -class ScraperAgent(agentProvider: AgentProvider) { +class ScraperAgent(agentProvider: AgentProvider) : ApplicationListener { - private val messageFlow = MutableSharedFlow(replay = 1) // Replay last emitted value + private val agentEventFlow = MutableSharedFlow() val agent = agentProvider.getAgentByName("ScraperAgent") as org.eclipse.lmos.arc.agents.ChatAgent - @Event(description = "HTML Content of the scraped web site") - fun contentRetrieved() : Flow { - return messageFlow + @Action(title = "chat", description = "Ask the agent a question.") + suspend fun chat(message: AgentRequest) : AgentResult { + return executeAgent(message, agent::execute) } - @Action(title = "chat", description = "Ask the agent a question.") - suspend fun chat(message: String) : String { - val assistantMessage = agent.execute(message.toConversation(User("myId"))).getOrThrow().latest() ?: - throw RuntimeException("No Assistant response") - messageFlow.emit(assistantMessage.content) - return assistantMessage.content + @Event(title = "Agent Event", description = "An event from the agent.") + fun agentEvent() : Flow { + return agentEventFlow + } + + override fun onApplicationEvent(event: SpringApplicationAgentEvent) { + CoroutineScope(Dispatchers.IO).launch { + agentEventFlow.emit(event.event) + } } } diff --git a/kotlin-wot-integration-tests/src/main/kotlin/integration/ScraperTool.kt b/kotlin-wot-integration-tests/src/main/kotlin/integration/ScraperTool.kt new file mode 100644 index 0000000..688e1c4 --- /dev/null +++ b/kotlin-wot-integration-tests/src/main/kotlin/integration/ScraperTool.kt @@ -0,0 +1,40 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package ai.ancf.lmos.wot.integration + + +import ai.ancf.lmos.wot.protocol.LMOSContext +import ai.ancf.lmos.wot.protocol.LMOSThingType +import ai.ancf.lmos.wot.reflection.annotations.* +import io.opentelemetry.api.trace.Span +import io.opentelemetry.instrumentation.annotations.WithSpan +import org.jsoup.Jsoup +import org.springframework.stereotype.Component + + +@Thing(id= "scraperTool", title="Tool", + description= "An HTML scraper.", type = LMOSThingType.TOOL) +@VersionInfo(instance = "1.0.0") +@Context(prefix = LMOSContext.prefix, url = LMOSContext.url) +@Component +class ScraperTool() { + + @Action(title = "Fetch Content", description = "Fetches the content from the specified URL.") + @ActionInput(title = "url", description = "The URL to fetch content from.") + @ActionOutput(title = "content", description = "The content fetched from the URL.") + @WithSpan + suspend fun fetchContent(url: String): String { + Span.current().setAttribute("lmos.agent.scraper.input.url", url) + return try { + val document = Jsoup.connect(url).get() + document.outerHtml() + } catch (e: Exception) { + "Error fetching content" + } + } +} + diff --git a/kotlin-wot-integration-tests/src/main/kotlin/integration/ThingToFunctionsMapper.kt b/kotlin-wot-integration-tests/src/main/kotlin/integration/ThingToFunctionsMapper.kt index 1c482f9..17bd5af 100644 --- a/kotlin-wot-integration-tests/src/main/kotlin/integration/ThingToFunctionsMapper.kt +++ b/kotlin-wot-integration-tests/src/main/kotlin/integration/ThingToFunctionsMapper.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.integration @@ -37,14 +43,13 @@ object ThingToFunctionsMapper { suspend fun requestThingDescription(wot: Wot, functions: Functions, group: String, url: String, securityScheme: SecurityScheme = NoSecurityScheme()): List { val thingDescription = wot.requestThingDescription(url, securityScheme) - val consumedThings = consumeThings(wot, setOf(thingDescription)) - val retrieveAllFunction = createRetrieveAllFunction(functions, group, setOf(thingDescription)) - mapAllThingFunctions(functions, group, consumedThings) - val allFunctions = retrieveAllFunction+ functionCache.values.flatten() + val consumedThing = wot.consume(thingDescription) + mapThingDescriptionToFunctions2(functions, group, thingDescription) + val allFunctions = functionCache.values.flatten() return allFunctions } - private fun createRetrieveAllFunction(functions: Functions, group: String, thingDescriptions: Set): List { + fun createRetrieveAllFunction(functions: Functions, group: String, thingDescriptions: Set): List { return functions("retrieveAllThings", "Returns the metadata information of all available devices.", group) { summarizeThingDescriptions(thingDescriptions) } @@ -57,7 +62,7 @@ object ThingToFunctionsMapper { } private fun mapAllThingFunctions(functions: Functions, group: String, consumedThings: List): List { - return consumedThings.flatMap { mapThingDescriptionToFunctions(functions, group, it) } + return consumedThings.flatMap { mapThingDescriptionToFunctions(functions, group, it.getThingDescription()) } } private fun summarizeThingDescriptions(things: Set): String { @@ -81,8 +86,7 @@ object ThingToFunctionsMapper { return thing.properties.entries.joinToString("\n ") { (key, property) -> "$key: ${property.title} - ${property.description}" } } - private fun mapThingDescriptionToFunctions(functions: Functions, group: String, thing: WoTConsumedThing): Set { - val thingDescription = thing.getThingDescription() + fun mapThingDescriptionToFunctions(functions: Functions, group: String, thingDescription: WoTThingDescription): Set { val defaultParams = createDefaultParams() val actionFunctions = createActionFunctions(functions, group, thingDescription, defaultParams) val propertyFunctions = createPropertyFunctions(functions, group, thingDescription, defaultParams) @@ -90,6 +94,12 @@ object ThingToFunctionsMapper { return actionFunctions.toSet() + propertyFunctions.toSet() + readAllPropertiesFunction.toSet() } + fun mapThingDescriptionToFunctions2(functions: Functions, group: String, thingDescription: WoTThingDescription): Set { + val actionFunctions = createActionFunctions(functions, group, thingDescription, emptyList()) + val propertyFunctions = createPropertyFunctions(functions, group, thingDescription, emptyList()) + return actionFunctions.toSet() + propertyFunctions.toSet() + } + private fun createDefaultParams(): List> { return listOf(Pair(ParameterSchema("thingId", "The unique identifier of the thing", ParameterType("string"), emptyList()), true)) } @@ -100,13 +110,13 @@ object ThingToFunctionsMapper { val params = defaultParams + actionParams functionCache.getOrPut(actionName) { functions(actionName, action.description ?: "No Description available", group, params) { (thingId, input) -> - invokeAction(thingDescription.id, actionName, input, action.input) + invokeAction(thingId, actionName, input, action.input) } } } } - private suspend fun invokeAction(thingId: String, actionName: String, input: String?, inputSchema: DataSchema<*>?): String { + private suspend fun invokeAction(thingId: String?, actionName: String, input: String?, inputSchema: DataSchema<*>?): String { return try { val jsonInput : JsonNode = inputSchema?.let { mapSchemaToJsonNode(it, input!!) } ?: TextNode(input) thingDescriptionsMap[thingId]?.invokeAction(actionName, jsonInput)?.asText() @@ -201,7 +211,7 @@ object ThingToFunctionsMapper { } } - private fun mapSchemaToJsonNode(schema: DataSchema<*>, value: String): JsonNode { + fun mapSchemaToJsonNode(schema: DataSchema<*>, value: String): JsonNode { return when (schema) { is StringSchema -> TextNode(value) is IntegerSchema -> IntNode(value.toIntOrNull() ?: 0) diff --git a/kotlin-wot-integration-tests/src/main/kotlin/integration/ToolProperties.kt b/kotlin-wot-integration-tests/src/main/kotlin/integration/ToolProperties.kt index b77844f..68ae52c 100644 --- a/kotlin-wot-integration-tests/src/main/kotlin/integration/ToolProperties.kt +++ b/kotlin-wot-integration-tests/src/main/kotlin/integration/ToolProperties.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.integration diff --git a/kotlin-wot-integration-tests/src/main/resources/application.yaml b/kotlin-wot-integration-tests/src/main/resources/application.yaml index 10508eb..e8e90dc 100644 --- a/kotlin-wot-integration-tests/src/main/resources/application.yaml +++ b/kotlin-wot-integration-tests/src/main/resources/application.yaml @@ -1,3 +1,9 @@ +# +# SPDX-FileCopyrightText: Robert Winkler +# +# SPDX-License-Identifier: Apache-2.0 +# + spring: application: name: chat-agent # Service name for tracing (appears in Langfuse UI as the source service) @@ -28,15 +34,15 @@ arc: - id: GPT-4o model-name: GPT35T-1106 api-key: dummy - client: azure + client: langchain4j-azure url: https://gpt4-uk.openai.azure.com tools: scraper: url: "http://localhost:9099/scraper" - devices: - url: "https://plugfest.webthings.io/.well-known/wot" - isDirectory: true - securityScheme: "bearer" + #devices: + # url: "https://plugfest.webthings.io/.well-known/wot" + # isDirectory: true + # securityScheme: "bearer" wot: servient: @@ -52,7 +58,7 @@ wot: port: 8181 http: server: - enabled: true + enabled: false host: localhost port: 9080 mqtt: diff --git a/kotlin-wot-integration-tests/src/test/kotlin/integration/AgentsTest.kt b/kotlin-wot-integration-tests/src/test/kotlin/integration/AgentsTest.kt index 5aba083..8433d0e 100644 --- a/kotlin-wot-integration-tests/src/test/kotlin/integration/AgentsTest.kt +++ b/kotlin-wot-integration-tests/src/test/kotlin/integration/AgentsTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.integration import ai.ancf.lmos.wot.JsonMapper diff --git a/kotlin-wot-integration-tests/src/test/kotlin/integration/QuickTest.kt b/kotlin-wot-integration-tests/src/test/kotlin/integration/QuickTest.kt index 9f85091..d8af622 100644 --- a/kotlin-wot-integration-tests/src/test/kotlin/integration/QuickTest.kt +++ b/kotlin-wot-integration-tests/src/test/kotlin/integration/QuickTest.kt @@ -1,8 +1,22 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.integration -import integration.WotConversationalAgent +import ai.ancf.lmos.sdk.agents.WotConversationalAgent +import ai.ancf.lmos.sdk.agents.lastMessage +import ai.ancf.lmos.sdk.agents.toAgentRequest +import ai.ancf.lmos.sdk.model.AgentEvent +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.runBlocking +import java.util.concurrent.CountDownLatch import kotlin.test.Test class QuickTest { @@ -21,27 +35,35 @@ class QuickTest { //val command = "What is the state of my lamp?" val command = "Turn all lamps on" println("User: $command") - val answer = agent.chat(command) - println("Agent: $answer") + val answer = agent.chat(command.toAgentRequest()) + println("Agent: $answer.lastMessage()") //latch.await() } @Test fun `scrape a URL`() = runBlocking { - //val latch = CountDownLatch(3) + val latch = CountDownLatch(3) + + val agent = WotConversationalAgent.create("http://localhost:8181/scraper") - val agent = WotConversationalAgent.create("http://localhost:9080/scraper") /* - agent.consumeEvent("agentEvent") { + agent.consumeEvent("agentEvent", AgentEvent::class) { println("Event: $it") latch.countDown() } */ + + agent.consumeEvent("agentEvent", AgentEvent::class).onEach { + println("Event: $it") + latch.countDown() + }.launchIn(CoroutineScope(Dispatchers.IO)) + + //val command = "What is the state of my lamp?" val command = "Scrape the page https://eclipse.dev/lmos/\"" println("User: $command") - val answer = agent.chat(command) - println("Agent: $answer") - //latch.await() + val answer = agent.chat(command.toAgentRequest()) + println("Agent: ${answer.lastMessage()}") + latch.await() } } \ No newline at end of file diff --git a/kotlin-wot-integration-tests/src/test/kotlin/integration/TestWebThings.kt b/kotlin-wot-integration-tests/src/test/kotlin/integration/TestWebThings.kt index ce335ad..f693409 100644 --- a/kotlin-wot-integration-tests/src/test/kotlin/integration/TestWebThings.kt +++ b/kotlin-wot-integration-tests/src/test/kotlin/integration/TestWebThings.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.integration import ai.ancf.lmos.wot.Servient diff --git a/kotlin-wot-integration-tests/src/test/kotlin/integration/ThingAgentApplicationTest.kt b/kotlin-wot-integration-tests/src/test/kotlin/integration/ThingAgentApplicationTest.kt index b0fde2f..5d69914 100644 --- a/kotlin-wot-integration-tests/src/test/kotlin/integration/ThingAgentApplicationTest.kt +++ b/kotlin-wot-integration-tests/src/test/kotlin/integration/ThingAgentApplicationTest.kt @@ -1,7 +1,15 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.integration +import ai.ancf.lmos.sdk.agents.WotConversationalAgent +import ai.ancf.lmos.sdk.agents.lastMessage +import ai.ancf.lmos.sdk.agents.toAgentRequest import ai.ancf.lmos.wot.Wot -import integration.WotConversationalAgent import kotlinx.coroutines.runBlocking import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Autowired @@ -22,8 +30,8 @@ class ThingAgentApplicationTest { @Test fun testChat() = runBlocking { val agent = WotConversationalAgent.create(wot, "http://localhost:$port/chatagent") - val answer = agent.chat("What is the state of my lamps?") - logger.info(answer) + val answer = agent.chat("What is the state of my lamps?".toAgentRequest()) + logger.info(answer.lastMessage()) } @Test @@ -35,13 +43,13 @@ class ThingAgentApplicationTest { val latch = CountDownLatch(1) - scraperAgent.consumeEvent("contentRetrieved") { - val summary: String = researchAgent.chat("Summarize $it for me") + scraperAgent.consumeEvent("contentRetrieved", String::class) { + val summary: String = researchAgent.chat("Summarize $it for me".toAgentRequest()).lastMessage() logger.info(summary) latch.countDown() } - scraperAgent.chat("Retrieve content from https://eclipse.dev/lmos/docs/lmos_protocol/introduction") + scraperAgent.chat("Retrieve content from https://eclipse.dev/lmos/docs/lmos_protocol/introduction".toAgentRequest()) latch.await() diff --git a/kotlin-wot-integration-tests/src/test/kotlin/integration/WoTIntegrationTest.kt b/kotlin-wot-integration-tests/src/test/kotlin/integration/WoTIntegrationTest.kt index 07c0773..562a551 100644 --- a/kotlin-wot-integration-tests/src/test/kotlin/integration/WoTIntegrationTest.kt +++ b/kotlin-wot-integration-tests/src/test/kotlin/integration/WoTIntegrationTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.integration import ai.ancf.lmos.wot.Servient diff --git a/kotlin-wot-lmos-protocol/src/main/kotlin/protocol/ConversationalAgent.kt b/kotlin-wot-lmos-protocol/src/main/kotlin/protocol/ConversationalAgent.kt deleted file mode 100644 index 487e810..0000000 --- a/kotlin-wot-lmos-protocol/src/main/kotlin/protocol/ConversationalAgent.kt +++ /dev/null @@ -1,15 +0,0 @@ -package ai.ancf.lmos.wot.protocol - -interface ConversationalAgent { - suspend fun chat(message: I): O -} - -interface ConsumedConversationalAgent: ConversationalAgent { - suspend fun consumeEvent(eventName: String, listener: EventListener) -} - -fun interface EventListener { - suspend fun handleEvent(data: E) -} - - diff --git a/kotlin-wot-lmos-protocol/src/main/kotlin/protocol/LmosProtocol.kt b/kotlin-wot-lmos-protocol/src/main/kotlin/protocol/LmosProtocol.kt index 51e54bb..776fa83 100644 --- a/kotlin-wot-lmos-protocol/src/main/kotlin/protocol/LmosProtocol.kt +++ b/kotlin-wot-lmos-protocol/src/main/kotlin/protocol/LmosProtocol.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.protocol class LMOSThingType { diff --git a/kotlin-wot-reflection/src/main/kotlin/reflection/ConsumedThingBuilder.kt b/kotlin-wot-reflection/src/main/kotlin/reflection/ConsumedThingBuilder.kt index 9aa7e13..ff7facc 100644 --- a/kotlin-wot-reflection/src/main/kotlin/reflection/ConsumedThingBuilder.kt +++ b/kotlin-wot-reflection/src/main/kotlin/reflection/ConsumedThingBuilder.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.reflection /* diff --git a/kotlin-wot-reflection/src/main/kotlin/reflection/ExposedThingBuilder.kt b/kotlin-wot-reflection/src/main/kotlin/reflection/ExposedThingBuilder.kt index 33ae630..416565f 100644 --- a/kotlin-wot-reflection/src/main/kotlin/reflection/ExposedThingBuilder.kt +++ b/kotlin-wot-reflection/src/main/kotlin/reflection/ExposedThingBuilder.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.reflection import ai.ancf.lmos.wot.JsonMapper diff --git a/kotlin-wot-reflection/src/main/kotlin/reflection/annotations/Annotations.kt b/kotlin-wot-reflection/src/main/kotlin/reflection/annotations/Annotations.kt index 2b46142..5b4f83c 100644 --- a/kotlin-wot-reflection/src/main/kotlin/reflection/annotations/Annotations.kt +++ b/kotlin-wot-reflection/src/main/kotlin/reflection/annotations/Annotations.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.reflection.annotations import ai.ancf.lmos.wot.thing.DEFAULT_TYPE diff --git a/kotlin-wot-reflection/src/test/kotlin/reflection/AddHandlerTest.kt b/kotlin-wot-reflection/src/test/kotlin/reflection/AddHandlerTest.kt index 57fc40e..d9861c9 100644 --- a/kotlin-wot-reflection/src/test/kotlin/reflection/AddHandlerTest.kt +++ b/kotlin-wot-reflection/src/test/kotlin/reflection/AddHandlerTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.reflection import ai.ancf.lmos.wot.reflection.ExposedThingBuilder.addActionHandler diff --git a/kotlin-wot-reflection/src/test/kotlin/reflection/BuildObjectSchemaTest.kt b/kotlin-wot-reflection/src/test/kotlin/reflection/BuildObjectSchemaTest.kt index 428937a..1288437 100644 --- a/kotlin-wot-reflection/src/test/kotlin/reflection/BuildObjectSchemaTest.kt +++ b/kotlin-wot-reflection/src/test/kotlin/reflection/BuildObjectSchemaTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.reflection import ai.ancf.lmos.wot.reflection.ExposedThingBuilder.buildObjectSchema diff --git a/kotlin-wot-reflection/src/test/kotlin/reflection/ComplexThingTest.kt b/kotlin-wot-reflection/src/test/kotlin/reflection/ComplexThingTest.kt index 7a0aebb..dcfe404 100644 --- a/kotlin-wot-reflection/src/test/kotlin/reflection/ComplexThingTest.kt +++ b/kotlin-wot-reflection/src/test/kotlin/reflection/ComplexThingTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.reflection import ai.ancf.lmos.wot.Servient diff --git a/kotlin-wot-reflection/src/test/kotlin/reflection/ConsumedThingBuilderTest.kt b/kotlin-wot-reflection/src/test/kotlin/reflection/ConsumedThingBuilderTest.kt index dbc3431..c65b0be 100644 --- a/kotlin-wot-reflection/src/test/kotlin/reflection/ConsumedThingBuilderTest.kt +++ b/kotlin-wot-reflection/src/test/kotlin/reflection/ConsumedThingBuilderTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package reflection import ai.ancf.lmos.wot.Servient diff --git a/kotlin-wot-reflection/src/test/kotlin/reflection/MapTypeToSchemaTest.kt b/kotlin-wot-reflection/src/test/kotlin/reflection/MapTypeToSchemaTest.kt index fdd2c28..e4172f7 100644 --- a/kotlin-wot-reflection/src/test/kotlin/reflection/MapTypeToSchemaTest.kt +++ b/kotlin-wot-reflection/src/test/kotlin/reflection/MapTypeToSchemaTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.reflection import ai.ancf.lmos.wot.reflection.ExposedThingBuilder.mapTypeToSchema diff --git a/kotlin-wot-reflection/src/test/kotlin/reflection/SimpleThingTest.kt b/kotlin-wot-reflection/src/test/kotlin/reflection/SimpleThingTest.kt index e330b10..cbc0b7d 100644 --- a/kotlin-wot-reflection/src/test/kotlin/reflection/SimpleThingTest.kt +++ b/kotlin-wot-reflection/src/test/kotlin/reflection/SimpleThingTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.reflection import ai.ancf.lmos.wot.Servient diff --git a/kotlin-wot-reflection/src/test/kotlin/reflection/ToKotlinObjectTest.kt b/kotlin-wot-reflection/src/test/kotlin/reflection/ToKotlinObjectTest.kt index 0186fe0..e38ce68 100644 --- a/kotlin-wot-reflection/src/test/kotlin/reflection/ToKotlinObjectTest.kt +++ b/kotlin-wot-reflection/src/test/kotlin/reflection/ToKotlinObjectTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.reflection import ai.ancf.lmos.wot.JsonMapper diff --git a/kotlin-wot-reflection/src/test/kotlin/reflection/things/ComplexThing.kt b/kotlin-wot-reflection/src/test/kotlin/reflection/things/ComplexThing.kt index 951e371..8a4abd9 100644 --- a/kotlin-wot-reflection/src/test/kotlin/reflection/things/ComplexThing.kt +++ b/kotlin-wot-reflection/src/test/kotlin/reflection/things/ComplexThing.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.reflection.things import ai.ancf.lmos.wot.reflection.annotations.* diff --git a/kotlin-wot-reflection/src/test/kotlin/reflection/things/SimpleThing.kt b/kotlin-wot-reflection/src/test/kotlin/reflection/things/SimpleThing.kt index 0ea02b3..72e37e1 100644 --- a/kotlin-wot-reflection/src/test/kotlin/reflection/things/SimpleThing.kt +++ b/kotlin-wot-reflection/src/test/kotlin/reflection/things/SimpleThing.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.reflection.things import ai.ancf.lmos.wot.reflection.annotations.* diff --git a/kotlin-wot-reflection/src/test/kotlin/reflection/things/SimpleThingInterface.kt b/kotlin-wot-reflection/src/test/kotlin/reflection/things/SimpleThingInterface.kt index ff76639..ddafd29 100644 --- a/kotlin-wot-reflection/src/test/kotlin/reflection/things/SimpleThingInterface.kt +++ b/kotlin-wot-reflection/src/test/kotlin/reflection/things/SimpleThingInterface.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.reflection.things import ai.ancf.lmos.wot.reflection.annotations.Action diff --git a/kotlin-wot-spring-boot-starter/src/main/kotlin/spring/CredentialsProperties.kt b/kotlin-wot-spring-boot-starter/src/main/kotlin/spring/CredentialsProperties.kt index 6cb39be..910ba00 100644 --- a/kotlin-wot-spring-boot-starter/src/main/kotlin/spring/CredentialsProperties.kt +++ b/kotlin-wot-spring-boot-starter/src/main/kotlin/spring/CredentialsProperties.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.spring diff --git a/kotlin-wot-spring-boot-starter/src/main/kotlin/spring/HttpProperties.kt b/kotlin-wot-spring-boot-starter/src/main/kotlin/spring/HttpProperties.kt index d4fb305..526715d 100644 --- a/kotlin-wot-spring-boot-starter/src/main/kotlin/spring/HttpProperties.kt +++ b/kotlin-wot-spring-boot-starter/src/main/kotlin/spring/HttpProperties.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.spring import org.springframework.boot.context.properties.ConfigurationProperties diff --git a/kotlin-wot-spring-boot-starter/src/main/kotlin/spring/MqttProperties.kt b/kotlin-wot-spring-boot-starter/src/main/kotlin/spring/MqttProperties.kt index b5fc1e6..ee92940 100644 --- a/kotlin-wot-spring-boot-starter/src/main/kotlin/spring/MqttProperties.kt +++ b/kotlin-wot-spring-boot-starter/src/main/kotlin/spring/MqttProperties.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.spring import org.springframework.boot.context.properties.ConfigurationProperties diff --git a/kotlin-wot-spring-boot-starter/src/main/kotlin/spring/ServientAutoConfiguration.kt b/kotlin-wot-spring-boot-starter/src/main/kotlin/spring/ServientAutoConfiguration.kt index 545d232..be48f6f 100644 --- a/kotlin-wot-spring-boot-starter/src/main/kotlin/spring/ServientAutoConfiguration.kt +++ b/kotlin-wot-spring-boot-starter/src/main/kotlin/spring/ServientAutoConfiguration.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.spring import ai.ancf.lmos.wot.Servient diff --git a/kotlin-wot-spring-boot-starter/src/main/kotlin/spring/WoTRuntime.kt b/kotlin-wot-spring-boot-starter/src/main/kotlin/spring/WoTRuntime.kt index 393f17a..07c8257 100644 --- a/kotlin-wot-spring-boot-starter/src/main/kotlin/spring/WoTRuntime.kt +++ b/kotlin-wot-spring-boot-starter/src/main/kotlin/spring/WoTRuntime.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.spring import ai.ancf.lmos.wot.Servient diff --git a/kotlin-wot-spring-boot-starter/src/test/kotlin/spring/CredentialsPropertiesTest.kt b/kotlin-wot-spring-boot-starter/src/test/kotlin/spring/CredentialsPropertiesTest.kt index a1eaed6..ebe21d9 100644 --- a/kotlin-wot-spring-boot-starter/src/test/kotlin/spring/CredentialsPropertiesTest.kt +++ b/kotlin-wot-spring-boot-starter/src/test/kotlin/spring/CredentialsPropertiesTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package spring import ai.ancf.lmos.wot.credentials.ApiKeyCredentials diff --git a/kotlin-wot-spring-boot-starter/src/test/kotlin/spring/SpringApplicationTest.kt b/kotlin-wot-spring-boot-starter/src/test/kotlin/spring/SpringApplicationTest.kt index b64e85f..89ff010 100644 --- a/kotlin-wot-spring-boot-starter/src/test/kotlin/spring/SpringApplicationTest.kt +++ b/kotlin-wot-spring-boot-starter/src/test/kotlin/spring/SpringApplicationTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.spring import ai.ancf.lmos.wot.Servient diff --git a/kotlin-wot-spring-boot-starter/src/test/kotlin/spring/TestApplication.kt b/kotlin-wot-spring-boot-starter/src/test/kotlin/spring/TestApplication.kt index bcc7c5e..215ab69 100644 --- a/kotlin-wot-spring-boot-starter/src/test/kotlin/spring/TestApplication.kt +++ b/kotlin-wot-spring-boot-starter/src/test/kotlin/spring/TestApplication.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.spring import org.springframework.boot.autoconfigure.SpringBootApplication diff --git a/kotlin-wot-spring-boot-starter/src/test/resources/application.yaml b/kotlin-wot-spring-boot-starter/src/test/resources/application.yaml index 71e82d8..13b0945 100644 --- a/kotlin-wot-spring-boot-starter/src/test/resources/application.yaml +++ b/kotlin-wot-spring-boot-starter/src/test/resources/application.yaml @@ -1,3 +1,9 @@ +# +# SPDX-FileCopyrightText: Robert Winkler +# +# SPDX-License-Identifier: Apache-2.0 +# + wot: servient: security: diff --git a/kotlin-wot-tool-example/src/main/kotlin/example/HtmlTool.kt b/kotlin-wot-tool-example/src/main/kotlin/example/HtmlTool.kt index d554114..bdf6cc2 100644 --- a/kotlin-wot-tool-example/src/main/kotlin/example/HtmlTool.kt +++ b/kotlin-wot-tool-example/src/main/kotlin/example/HtmlTool.kt @@ -1,12 +1,17 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.example import ai.ancf.lmos.wot.protocol.LMOSContext import ai.ancf.lmos.wot.protocol.LMOSThingType -import ai.ancf.lmos.wot.reflection.annotations.Action -import ai.ancf.lmos.wot.reflection.annotations.Context -import ai.ancf.lmos.wot.reflection.annotations.Thing -import ai.ancf.lmos.wot.reflection.annotations.VersionInfo +import ai.ancf.lmos.wot.reflection.annotations.* +import io.opentelemetry.api.trace.Span +import io.opentelemetry.instrumentation.annotations.WithSpan import org.jsoup.Jsoup import org.springframework.stereotype.Component @@ -19,7 +24,11 @@ import org.springframework.stereotype.Component class HtmlTool() { @Action(title = "Fetch Content", description = "Fetches the content from the specified URL.") + @ActionInput(title = "url", description = "The URL to fetch content from.") + @ActionOutput(title = "content", description = "The content fetched from the URL.") + @WithSpan suspend fun fetchContent(url: String): String { + Span.current().setAttribute("lmos.agent.scraper.input.url", url) return try { val document = Jsoup.connect(url).get() document.outerHtml() diff --git a/kotlin-wot-tool-example/src/main/kotlin/example/ToolApplication.kt b/kotlin-wot-tool-example/src/main/kotlin/example/ToolApplication.kt index 5625972..7731e86 100644 --- a/kotlin-wot-tool-example/src/main/kotlin/example/ToolApplication.kt +++ b/kotlin-wot-tool-example/src/main/kotlin/example/ToolApplication.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.example import org.springframework.boot.autoconfigure.SpringBootApplication diff --git a/kotlin-wot-tool-example/src/main/resources/application.yaml b/kotlin-wot-tool-example/src/main/resources/application.yaml index 7b4d281..b237bc6 100644 --- a/kotlin-wot-tool-example/src/main/resources/application.yaml +++ b/kotlin-wot-tool-example/src/main/resources/application.yaml @@ -1,3 +1,9 @@ +# +# SPDX-FileCopyrightText: Robert Winkler +# +# SPDX-License-Identifier: Apache-2.0 +# + arc: ai: clients: diff --git a/kotlin-wot/src/main/kotlin/DefaultWot.kt b/kotlin-wot/src/main/kotlin/DefaultWot.kt index 75e4544..bcba803 100644 --- a/kotlin-wot/src/main/kotlin/DefaultWot.kt +++ b/kotlin-wot/src/main/kotlin/DefaultWot.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot diff --git a/kotlin-wot/src/main/kotlin/Servient.kt b/kotlin-wot/src/main/kotlin/Servient.kt index ee76736..34907d8 100644 --- a/kotlin-wot/src/main/kotlin/Servient.kt +++ b/kotlin-wot/src/main/kotlin/Servient.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot import ai.ancf.lmos.wot.content.ContentCodecException diff --git a/kotlin-wot/src/main/kotlin/Utils.kt b/kotlin-wot/src/main/kotlin/Utils.kt index 78ff175..85c975d 100644 --- a/kotlin-wot/src/main/kotlin/Utils.kt +++ b/kotlin-wot/src/main/kotlin/Utils.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot import ai.ancf.lmos.wot.thing.ThingDescription diff --git a/kotlin-wot/src/main/kotlin/Wot.kt b/kotlin-wot/src/main/kotlin/Wot.kt index f0ef7e4..c5d1820 100644 --- a/kotlin-wot/src/main/kotlin/Wot.kt +++ b/kotlin-wot/src/main/kotlin/Wot.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot import ai.ancf.lmos.wot.security.NoSecurityScheme @@ -43,8 +49,7 @@ interface Wot { suspend fun exploreDirectory(directoryUrl: String, securityScheme: SecurityScheme = NoSecurityScheme()): Set /** - * Accepts a `thing` argument of type [ThingDescription] and returns an [ ] object.

The result can be used to start exposing interfaces for thing - * interaction. Returns a failed future if thing with same id is already exposed. + * Accepts a `thing` argument of type [ThingDescription] and returns an [WoTExposedThing]. * * @param thingDescription * @return diff --git a/kotlin-wot/src/main/kotlin/binding/ProtocolClient.kt b/kotlin-wot/src/main/kotlin/binding/ProtocolClient.kt index f7c884b..f86c1ab 100644 --- a/kotlin-wot/src/main/kotlin/binding/ProtocolClient.kt +++ b/kotlin-wot/src/main/kotlin/binding/ProtocolClient.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.anfc.lmos.wot.binding import ai.ancf.lmos.wot.content.Content diff --git a/kotlin-wot/src/main/kotlin/binding/ProtocolClientException.kt b/kotlin-wot/src/main/kotlin/binding/ProtocolClientException.kt index aec900f..5f79dbb 100644 --- a/kotlin-wot/src/main/kotlin/binding/ProtocolClientException.kt +++ b/kotlin-wot/src/main/kotlin/binding/ProtocolClientException.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.anfc.lmos.wot.binding import ai.ancf.lmos.wot.ServientException diff --git a/kotlin-wot/src/main/kotlin/binding/ProtocolClientFactory.kt b/kotlin-wot/src/main/kotlin/binding/ProtocolClientFactory.kt index cc44b13..a97f862 100644 --- a/kotlin-wot/src/main/kotlin/binding/ProtocolClientFactory.kt +++ b/kotlin-wot/src/main/kotlin/binding/ProtocolClientFactory.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.anfc.lmos.wot.binding diff --git a/kotlin-wot/src/main/kotlin/binding/ProtocolClientNotImplementedException.kt b/kotlin-wot/src/main/kotlin/binding/ProtocolClientNotImplementedException.kt index 6af4cc0..a9944cb 100644 --- a/kotlin-wot/src/main/kotlin/binding/ProtocolClientNotImplementedException.kt +++ b/kotlin-wot/src/main/kotlin/binding/ProtocolClientNotImplementedException.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.anfc.lmos.wot.binding diff --git a/kotlin-wot/src/main/kotlin/binding/ProtocolServer.kt b/kotlin-wot/src/main/kotlin/binding/ProtocolServer.kt index 034ba00..895cf8a 100644 --- a/kotlin-wot/src/main/kotlin/binding/ProtocolServer.kt +++ b/kotlin-wot/src/main/kotlin/binding/ProtocolServer.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.anfc.lmos.wot.binding import ai.ancf.lmos.wot.Servient diff --git a/kotlin-wot/src/main/kotlin/binding/ProtocolServerException.kt b/kotlin-wot/src/main/kotlin/binding/ProtocolServerException.kt index b345852..04485f2 100644 --- a/kotlin-wot/src/main/kotlin/binding/ProtocolServerException.kt +++ b/kotlin-wot/src/main/kotlin/binding/ProtocolServerException.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.anfc.lmos.wot.binding import ai.ancf.lmos.wot.ServientException diff --git a/kotlin-wot/src/main/kotlin/binding/ProtocolServerNotImplementedException.kt b/kotlin-wot/src/main/kotlin/binding/ProtocolServerNotImplementedException.kt index 2c90af5..58dca1a 100644 --- a/kotlin-wot/src/main/kotlin/binding/ProtocolServerNotImplementedException.kt +++ b/kotlin-wot/src/main/kotlin/binding/ProtocolServerNotImplementedException.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.anfc.lmos.wot.binding diff --git a/kotlin-wot/src/main/kotlin/content/Content.kt b/kotlin-wot/src/main/kotlin/content/Content.kt index d2108c4..8ef1f80 100644 --- a/kotlin-wot/src/main/kotlin/content/Content.kt +++ b/kotlin-wot/src/main/kotlin/content/Content.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.content /** diff --git a/kotlin-wot/src/main/kotlin/content/ContentCodec.kt b/kotlin-wot/src/main/kotlin/content/ContentCodec.kt index a545ed7..e1977e8 100644 --- a/kotlin-wot/src/main/kotlin/content/ContentCodec.kt +++ b/kotlin-wot/src/main/kotlin/content/ContentCodec.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.content import ai.ancf.lmos.wot.thing.schema.DataSchema diff --git a/kotlin-wot/src/main/kotlin/content/ContentManager.kt b/kotlin-wot/src/main/kotlin/content/ContentManager.kt index b30d8e2..4eec81a 100644 --- a/kotlin-wot/src/main/kotlin/content/ContentManager.kt +++ b/kotlin-wot/src/main/kotlin/content/ContentManager.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.content import ai.ancf.lmos.wot.ServientException diff --git a/kotlin-wot/src/main/kotlin/content/JsonCodec.kt b/kotlin-wot/src/main/kotlin/content/JsonCodec.kt index b4f16fe..1d6a5b5 100644 --- a/kotlin-wot/src/main/kotlin/content/JsonCodec.kt +++ b/kotlin-wot/src/main/kotlin/content/JsonCodec.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.content import ai.ancf.lmos.wot.JsonMapper diff --git a/kotlin-wot/src/main/kotlin/content/LinkFormatCodec.kt b/kotlin-wot/src/main/kotlin/content/LinkFormatCodec.kt index 92fb372..421078e 100644 --- a/kotlin-wot/src/main/kotlin/content/LinkFormatCodec.kt +++ b/kotlin-wot/src/main/kotlin/content/LinkFormatCodec.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.content /* diff --git a/kotlin-wot/src/main/kotlin/credentials/Credentials.kt b/kotlin-wot/src/main/kotlin/credentials/Credentials.kt index 8024f73..f6010cc 100644 --- a/kotlin-wot/src/main/kotlin/credentials/Credentials.kt +++ b/kotlin-wot/src/main/kotlin/credentials/Credentials.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.credentials import ai.ancf.lmos.wot.thing.schema.WoTForm diff --git a/kotlin-wot/src/main/kotlin/credentials/DefaultCredentialsProvider.kt b/kotlin-wot/src/main/kotlin/credentials/DefaultCredentialsProvider.kt index 57df982..647fd40 100644 --- a/kotlin-wot/src/main/kotlin/credentials/DefaultCredentialsProvider.kt +++ b/kotlin-wot/src/main/kotlin/credentials/DefaultCredentialsProvider.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.credentials diff --git a/kotlin-wot/src/main/kotlin/filter/DiscoveryMethod.kt b/kotlin-wot/src/main/kotlin/filter/DiscoveryMethod.kt index a30fc63..6499fcb 100644 --- a/kotlin-wot/src/main/kotlin/filter/DiscoveryMethod.kt +++ b/kotlin-wot/src/main/kotlin/filter/DiscoveryMethod.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.filter /** diff --git a/kotlin-wot/src/main/kotlin/filter/ThingFilter.kt b/kotlin-wot/src/main/kotlin/filter/ThingFilter.kt index 732d70f..6c49206 100644 --- a/kotlin-wot/src/main/kotlin/filter/ThingFilter.kt +++ b/kotlin-wot/src/main/kotlin/filter/ThingFilter.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.filter import java.net.URI diff --git a/kotlin-wot/src/main/kotlin/filter/ThingQuery.kt b/kotlin-wot/src/main/kotlin/filter/ThingQuery.kt index b75bf81..2f6475b 100644 --- a/kotlin-wot/src/main/kotlin/filter/ThingQuery.kt +++ b/kotlin-wot/src/main/kotlin/filter/ThingQuery.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.filter import ai.ancf.lmos.wot.ServientException diff --git a/kotlin-wot/src/main/kotlin/thing/ConsumedThing.kt b/kotlin-wot/src/main/kotlin/thing/ConsumedThing.kt index 455d7c4..2d64501 100644 --- a/kotlin-wot/src/main/kotlin/thing/ConsumedThing.kt +++ b/kotlin-wot/src/main/kotlin/thing/ConsumedThing.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing import AugmentedForm @@ -29,7 +35,7 @@ import org.slf4j.LoggerFactory import java.io.File import java.io.IOException import java.net.MalformedURLException -import java.net.URL +import java.net.URI import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap @@ -337,7 +343,7 @@ data class ConsumedThing( if (subscribedEvents.containsKey(eventName)) { return flow { - throw IllegalStateException("ConsumedThing '$title' already has a function subscribed to $eventName. You can only subscribe once.") + throw ConsumedThingException("ConsumedThing '$title' already has a function subscribed to $eventName. You can only subscribe once.") } } @@ -345,8 +351,19 @@ data class ConsumedThing( val formWithoutURITemplates = handleUriVariables(this, eventAffordance, form, options) - return client.subscribeResource(Resource(id, eventName, formWithoutURITemplates), ResourceType.EVENT) + val subscription = InternalEventSubscription(this, eventName, client, form) + subscribedEvents[eventName] = subscription + + val flow = client.subscribeResource(Resource(id, eventName, formWithoutURITemplates), ResourceType.EVENT) .map { handleInteractionOutput(it, form, eventAffordance.data) } + .onCompletion { + error -> if (error != null && error !is CancellationException) { // Ignore cancellations + log.warn("Error while processing observe property for ${eventAffordance.title}: ${error.message}", error) + } + subscription.stop(options) + } + + return flow } @WithSpan(kind = SpanKind.CLIENT) @@ -363,7 +380,7 @@ data class ConsumedThing( val (client, form) = getClientFor(eventAffordance.forms, Operation.SUBSCRIBE_EVENT, options) if (subscribedEvents.containsKey(eventName)) { - throw IllegalStateException("ConsumedThing '$title' already has a function subscribed to $eventName. You can only subscribe once.") + throw ConsumedThingException("ConsumedThing '$title' already has a function subscribed to $eventName. You can only subscribe once.") } log.debug("ConsumedThing '$title' subscribing to ${form.href}") @@ -760,8 +777,8 @@ fun findFormIndexWithScoring( // Compare the origins of the URLs try { - val formUrl = URL(form.href) - val refFormUrl = URL(refForm.href) + val formUrl = URI(form.href).toURL() + val refFormUrl = URI(refForm.href).toURL() if (formUrl.protocol == refFormUrl.protocol && formUrl.host == refFormUrl.host) { score += 1 } diff --git a/kotlin-wot/src/main/kotlin/thing/ContextDeserializer.kt b/kotlin-wot/src/main/kotlin/thing/ContextDeserializer.kt index 5a9d35f..35b368a 100644 --- a/kotlin-wot/src/main/kotlin/thing/ContextDeserializer.kt +++ b/kotlin-wot/src/main/kotlin/thing/ContextDeserializer.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing import ai.ancf.lmos.wot.thing.schema.Context diff --git a/kotlin-wot/src/main/kotlin/thing/ContextSerializer.kt b/kotlin-wot/src/main/kotlin/thing/ContextSerializer.kt index 2bcb636..55a84ad 100644 --- a/kotlin-wot/src/main/kotlin/thing/ContextSerializer.kt +++ b/kotlin-wot/src/main/kotlin/thing/ContextSerializer.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing import ai.ancf.lmos.wot.thing.schema.Context diff --git a/kotlin-wot/src/main/kotlin/thing/ExposedThing.kt b/kotlin-wot/src/main/kotlin/thing/ExposedThing.kt index 6b74285..20a0d3a 100644 --- a/kotlin-wot/src/main/kotlin/thing/ExposedThing.kt +++ b/kotlin-wot/src/main/kotlin/thing/ExposedThing.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing @@ -76,6 +82,11 @@ data class ExposedThing( */ private val eventListenerRegistry: SessionAwareProtocolListenerRegistry = SessionAwareProtocolListenerRegistry() + fun unregisterAllListeners(sessionId: String){ + propertyListeners.unregisterAll(sessionId) + eventListenerRegistry.unregisterAll(sessionId) + } + override fun setPropertyReadHandler(propertyName: String, handler: PropertyReadHandler): ExposedThing { log.debug("ExposedThing '${this.title}' setting read handler for '$propertyName'") diff --git a/kotlin-wot/src/main/kotlin/thing/OperationsDeserializer.kt b/kotlin-wot/src/main/kotlin/thing/OperationsDeserializer.kt index 5572d54..64f2721 100644 --- a/kotlin-wot/src/main/kotlin/thing/OperationsDeserializer.kt +++ b/kotlin-wot/src/main/kotlin/thing/OperationsDeserializer.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing import ai.ancf.lmos.wot.thing.form.Operation diff --git a/kotlin-wot/src/main/kotlin/thing/ProtocolHelpers.kt b/kotlin-wot/src/main/kotlin/thing/ProtocolHelpers.kt index d77af50..934c782 100644 --- a/kotlin-wot/src/main/kotlin/thing/ProtocolHelpers.kt +++ b/kotlin-wot/src/main/kotlin/thing/ProtocolHelpers.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing import ai.ancf.lmos.wot.thing.form.Form diff --git a/kotlin-wot/src/main/kotlin/thing/ProtocolListenerRegistry.kt b/kotlin-wot/src/main/kotlin/thing/ProtocolListenerRegistry.kt index 5c156cd..c462480 100644 --- a/kotlin-wot/src/main/kotlin/thing/ProtocolListenerRegistry.kt +++ b/kotlin-wot/src/main/kotlin/thing/ProtocolListenerRegistry.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing import ai.ancf.lmos.wot.content.ContentManager @@ -9,6 +15,7 @@ import org.slf4j.Logger import org.slf4j.LoggerFactory import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentMap +import kotlin.coroutines.cancellation.CancellationException class ProtocolListenerRegistry { @@ -78,6 +85,9 @@ class ProtocolListenerRegistry { try { val content = ContentManager.valueToContent(interactionInputValue.value, contentType) listener.handle(content) + } catch (e: CancellationException) { + log.info("Cancellation exception while notifying listener", e) + throw e // Rethrow cancellation exception } catch (e: Exception) { log.error("Error while notifying listener", e) } @@ -91,6 +101,9 @@ class ProtocolListenerRegistry { try { val content = ContentManager.valueToContent(interactionInputValue.value, contentType) listener.handle(content) + } catch (e: CancellationException) { + log.info("Cancellation exception while notifying listener", e) + throw e // Rethrow cancellation exception } catch (e: Exception) { log.error("Error while notifying listener", e) } diff --git a/kotlin-wot/src/main/kotlin/thing/SessionAwareProtocolListenerRegistry.kt b/kotlin-wot/src/main/kotlin/thing/SessionAwareProtocolListenerRegistry.kt index 7a9591c..63a4dd1 100644 --- a/kotlin-wot/src/main/kotlin/thing/SessionAwareProtocolListenerRegistry.kt +++ b/kotlin-wot/src/main/kotlin/thing/SessionAwareProtocolListenerRegistry.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing import ai.ancf.lmos.wot.thing.schema.ContentListener diff --git a/kotlin-wot/src/main/kotlin/thing/ThingDescription.kt b/kotlin-wot/src/main/kotlin/thing/ThingDescription.kt index 638d4b8..6886c94 100644 --- a/kotlin-wot/src/main/kotlin/thing/ThingDescription.kt +++ b/kotlin-wot/src/main/kotlin/thing/ThingDescription.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing import ai.ancf.lmos.wot.JsonMapper diff --git a/kotlin-wot/src/main/kotlin/thing/TypeDeserializer.kt b/kotlin-wot/src/main/kotlin/thing/TypeDeserializer.kt index 115070a..b9ef37b 100644 --- a/kotlin-wot/src/main/kotlin/thing/TypeDeserializer.kt +++ b/kotlin-wot/src/main/kotlin/thing/TypeDeserializer.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing import ai.ancf.lmos.wot.thing.schema.Type diff --git a/kotlin-wot/src/main/kotlin/thing/TypeSerializer.kt b/kotlin-wot/src/main/kotlin/thing/TypeSerializer.kt index 1f52785..31dd85f 100644 --- a/kotlin-wot/src/main/kotlin/thing/TypeSerializer.kt +++ b/kotlin-wot/src/main/kotlin/thing/TypeSerializer.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing import ai.ancf.lmos.wot.thing.schema.Type diff --git a/kotlin-wot/src/main/kotlin/thing/UriTemplate.kt b/kotlin-wot/src/main/kotlin/thing/UriTemplate.kt index 0a6acb0..156864c 100644 --- a/kotlin-wot/src/main/kotlin/thing/UriTemplate.kt +++ b/kotlin-wot/src/main/kotlin/thing/UriTemplate.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing import java.util.regex.Pattern diff --git a/kotlin-wot/src/main/kotlin/thing/action/ThingAction.kt b/kotlin-wot/src/main/kotlin/thing/action/ThingAction.kt index b9c1c88..d149698 100644 --- a/kotlin-wot/src/main/kotlin/thing/action/ThingAction.kt +++ b/kotlin-wot/src/main/kotlin/thing/action/ThingAction.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.action import ai.ancf.lmos.wot.thing.schema.Type diff --git a/kotlin-wot/src/main/kotlin/thing/event/ThingEvent.kt b/kotlin-wot/src/main/kotlin/thing/event/ThingEvent.kt index 16d9bca..e8e06bd 100644 --- a/kotlin-wot/src/main/kotlin/thing/event/ThingEvent.kt +++ b/kotlin-wot/src/main/kotlin/thing/event/ThingEvent.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.event import ai.ancf.lmos.wot.thing.schema.Type diff --git a/kotlin-wot/src/main/kotlin/thing/form/AugmentedForm.kt b/kotlin-wot/src/main/kotlin/thing/form/AugmentedForm.kt index cd02a3e..1f4fcc0 100644 --- a/kotlin-wot/src/main/kotlin/thing/form/AugmentedForm.kt +++ b/kotlin-wot/src/main/kotlin/thing/form/AugmentedForm.kt @@ -1,3 +1,8 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ import ai.ancf.lmos.wot.security.SecurityScheme import ai.ancf.lmos.wot.thing.form.Form diff --git a/kotlin-wot/src/main/kotlin/thing/form/Form.kt b/kotlin-wot/src/main/kotlin/thing/form/Form.kt index adf6ec4..ca9a6af 100644 --- a/kotlin-wot/src/main/kotlin/thing/form/Form.kt +++ b/kotlin-wot/src/main/kotlin/thing/form/Form.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.form import ai.ancf.lmos.wot.thing.OperationsDeserializer diff --git a/kotlin-wot/src/main/kotlin/thing/form/Operation.kt b/kotlin-wot/src/main/kotlin/thing/form/Operation.kt index d33c05b..61ea0a3 100644 --- a/kotlin-wot/src/main/kotlin/thing/form/Operation.kt +++ b/kotlin-wot/src/main/kotlin/thing/form/Operation.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.form import com.fasterxml.jackson.annotation.JsonCreator diff --git a/kotlin-wot/src/main/kotlin/thing/schema/Context.kt b/kotlin-wot/src/main/kotlin/thing/schema/Context.kt index 5376d33..459a5c9 100644 --- a/kotlin-wot/src/main/kotlin/thing/schema/Context.kt +++ b/kotlin-wot/src/main/kotlin/thing/schema/Context.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.schema import ai.ancf.lmos.wot.thing.ContextDeserializer diff --git a/kotlin-wot/src/main/kotlin/thing/schema/DataSchema.kt b/kotlin-wot/src/main/kotlin/thing/schema/DataSchema.kt index e9dbae3..2395408 100644 --- a/kotlin-wot/src/main/kotlin/thing/schema/DataSchema.kt +++ b/kotlin-wot/src/main/kotlin/thing/schema/DataSchema.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.schema import ai.ancf.lmos.wot.WoTDSL diff --git a/kotlin-wot/src/main/kotlin/thing/schema/Exentions.kt b/kotlin-wot/src/main/kotlin/thing/schema/Exentions.kt index bef858f..ae29e7e 100644 --- a/kotlin-wot/src/main/kotlin/thing/schema/Exentions.kt +++ b/kotlin-wot/src/main/kotlin/thing/schema/Exentions.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.schema import ai.ancf.lmos.wot.JsonMapper diff --git a/kotlin-wot/src/main/kotlin/thing/schema/InteractionOutput.kt b/kotlin-wot/src/main/kotlin/thing/schema/InteractionOutput.kt index 8c37e49..f0599a3 100644 --- a/kotlin-wot/src/main/kotlin/thing/schema/InteractionOutput.kt +++ b/kotlin-wot/src/main/kotlin/thing/schema/InteractionOutput.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.schema import ai.ancf.lmos.wot.content.Content diff --git a/kotlin-wot/src/main/kotlin/thing/schema/Link.kt b/kotlin-wot/src/main/kotlin/thing/schema/Link.kt index 33fec02..bea07f7 100644 --- a/kotlin-wot/src/main/kotlin/thing/schema/Link.kt +++ b/kotlin-wot/src/main/kotlin/thing/schema/Link.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.schema import com.fasterxml.jackson.annotation.JsonInclude diff --git a/kotlin-wot/src/main/kotlin/thing/schema/Type.kt b/kotlin-wot/src/main/kotlin/thing/schema/Type.kt index 040a4af..e4cb9f2 100644 --- a/kotlin-wot/src/main/kotlin/thing/schema/Type.kt +++ b/kotlin-wot/src/main/kotlin/thing/schema/Type.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.schema import ai.ancf.lmos.wot.thing.TypeDeserializer diff --git a/kotlin-wot/src/main/kotlin/thing/schema/Validators.kt b/kotlin-wot/src/main/kotlin/thing/schema/Validators.kt index 80ca2e7..144b224 100644 --- a/kotlin-wot/src/main/kotlin/thing/schema/Validators.kt +++ b/kotlin-wot/src/main/kotlin/thing/schema/Validators.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.schema sealed class ValidationException(message: String) : Exception(message) diff --git a/kotlin-wot/src/main/kotlin/thing/schema/WoT.kt b/kotlin-wot/src/main/kotlin/thing/schema/WoT.kt index d52257c..3dc38a2 100644 --- a/kotlin-wot/src/main/kotlin/thing/schema/WoT.kt +++ b/kotlin-wot/src/main/kotlin/thing/schema/WoT.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.schema import ai.ancf.lmos.wot.JsonMapper diff --git a/kotlin-wot/src/main/kotlin/thing/schema/WoTThingDescription.kt b/kotlin-wot/src/main/kotlin/thing/schema/WoTThingDescription.kt index 5a0983b..3c18fb6 100644 --- a/kotlin-wot/src/main/kotlin/thing/schema/WoTThingDescription.kt +++ b/kotlin-wot/src/main/kotlin/thing/schema/WoTThingDescription.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.schema import ai.ancf.lmos.wot.WoTDSL diff --git a/kotlin-wot/src/main/kotlin/thing/security/APIKeySecurityScheme.kt b/kotlin-wot/src/main/kotlin/thing/security/APIKeySecurityScheme.kt index 431e3d6..f4555a9 100644 --- a/kotlin-wot/src/main/kotlin/thing/security/APIKeySecurityScheme.kt +++ b/kotlin-wot/src/main/kotlin/thing/security/APIKeySecurityScheme.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.security import com.fasterxml.jackson.annotation.JsonInclude diff --git a/kotlin-wot/src/main/kotlin/thing/security/BasicSecurityScheme.kt b/kotlin-wot/src/main/kotlin/thing/security/BasicSecurityScheme.kt index 1ddc483..8fb95d7 100644 --- a/kotlin-wot/src/main/kotlin/thing/security/BasicSecurityScheme.kt +++ b/kotlin-wot/src/main/kotlin/thing/security/BasicSecurityScheme.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.security import com.fasterxml.jackson.annotation.JsonInclude diff --git a/kotlin-wot/src/main/kotlin/thing/security/BearerSecurityScheme.kt b/kotlin-wot/src/main/kotlin/thing/security/BearerSecurityScheme.kt index ef021e9..3c0d6f6 100644 --- a/kotlin-wot/src/main/kotlin/thing/security/BearerSecurityScheme.kt +++ b/kotlin-wot/src/main/kotlin/thing/security/BearerSecurityScheme.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.security import com.fasterxml.jackson.annotation.JsonInclude diff --git a/kotlin-wot/src/main/kotlin/thing/security/CertSecurityScheme.kt b/kotlin-wot/src/main/kotlin/thing/security/CertSecurityScheme.kt index 7bca88c..cd86a7c 100644 --- a/kotlin-wot/src/main/kotlin/thing/security/CertSecurityScheme.kt +++ b/kotlin-wot/src/main/kotlin/thing/security/CertSecurityScheme.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.security import com.fasterxml.jackson.annotation.JsonInclude diff --git a/kotlin-wot/src/main/kotlin/thing/security/DigestSecurityScheme.kt b/kotlin-wot/src/main/kotlin/thing/security/DigestSecurityScheme.kt index 9d5a649..ae845fb 100644 --- a/kotlin-wot/src/main/kotlin/thing/security/DigestSecurityScheme.kt +++ b/kotlin-wot/src/main/kotlin/thing/security/DigestSecurityScheme.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.security import com.fasterxml.jackson.annotation.JsonInclude diff --git a/kotlin-wot/src/main/kotlin/thing/security/NoSecurityScheme.kt b/kotlin-wot/src/main/kotlin/thing/security/NoSecurityScheme.kt index 653e9fe..77f6eb6 100644 --- a/kotlin-wot/src/main/kotlin/thing/security/NoSecurityScheme.kt +++ b/kotlin-wot/src/main/kotlin/thing/security/NoSecurityScheme.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.security diff --git a/kotlin-wot/src/main/kotlin/thing/security/OAuth2SecurityScheme.kt b/kotlin-wot/src/main/kotlin/thing/security/OAuth2SecurityScheme.kt index 760247e..6468bc8 100644 --- a/kotlin-wot/src/main/kotlin/thing/security/OAuth2SecurityScheme.kt +++ b/kotlin-wot/src/main/kotlin/thing/security/OAuth2SecurityScheme.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.security import com.fasterxml.jackson.annotation.JsonInclude diff --git a/kotlin-wot/src/main/kotlin/thing/security/PSKSecurityScheme.kt b/kotlin-wot/src/main/kotlin/thing/security/PSKSecurityScheme.kt index 617f0f6..f1b96b7 100644 --- a/kotlin-wot/src/main/kotlin/thing/security/PSKSecurityScheme.kt +++ b/kotlin-wot/src/main/kotlin/thing/security/PSKSecurityScheme.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.security import com.fasterxml.jackson.annotation.JsonInclude diff --git a/kotlin-wot/src/main/kotlin/thing/security/PoPSecurityScheme.kt b/kotlin-wot/src/main/kotlin/thing/security/PoPSecurityScheme.kt index 9b52aaa..f245543 100644 --- a/kotlin-wot/src/main/kotlin/thing/security/PoPSecurityScheme.kt +++ b/kotlin-wot/src/main/kotlin/thing/security/PoPSecurityScheme.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.security import com.fasterxml.jackson.annotation.JsonInclude diff --git a/kotlin-wot/src/main/kotlin/thing/security/PublicSecurityScheme.kt b/kotlin-wot/src/main/kotlin/thing/security/PublicSecurityScheme.kt index bb9cad8..9c3b6a4 100644 --- a/kotlin-wot/src/main/kotlin/thing/security/PublicSecurityScheme.kt +++ b/kotlin-wot/src/main/kotlin/thing/security/PublicSecurityScheme.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.security import com.fasterxml.jackson.annotation.JsonInclude diff --git a/kotlin-wot/src/main/kotlin/thing/security/SecurityScheme.kt b/kotlin-wot/src/main/kotlin/thing/security/SecurityScheme.kt index 2afd9d4..571c79a 100644 --- a/kotlin-wot/src/main/kotlin/thing/security/SecurityScheme.kt +++ b/kotlin-wot/src/main/kotlin/thing/security/SecurityScheme.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.security import com.fasterxml.jackson.annotation.JsonIgnoreProperties diff --git a/kotlin-wot/src/main/kotlin/tracing/Tracing.kt b/kotlin-wot/src/main/kotlin/tracing/Tracing.kt index 0fd80c7..29add9f 100644 --- a/kotlin-wot/src/main/kotlin/tracing/Tracing.kt +++ b/kotlin-wot/src/main/kotlin/tracing/Tracing.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.tracing import io.opentelemetry.api.GlobalOpenTelemetry diff --git a/kotlin-wot/src/test/kotlin/ServientTest.kt b/kotlin-wot/src/test/kotlin/ServientTest.kt index a65a4e6..5c20cf9 100644 --- a/kotlin-wot/src/test/kotlin/ServientTest.kt +++ b/kotlin-wot/src/test/kotlin/ServientTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot import ai.ancf.lmos.wot.content.Content diff --git a/kotlin-wot/src/test/kotlin/WotTest.kt b/kotlin-wot/src/test/kotlin/WotTest.kt index bac9034..9f12391 100644 --- a/kotlin-wot/src/test/kotlin/WotTest.kt +++ b/kotlin-wot/src/test/kotlin/WotTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot import ai.ancf.lmos.wot.thing.ExposedThing diff --git a/kotlin-wot/src/test/kotlin/content/ContentManagerTest.kt b/kotlin-wot/src/test/kotlin/content/ContentManagerTest.kt index 4aab675..7810d4c 100644 --- a/kotlin-wot/src/test/kotlin/content/ContentManagerTest.kt +++ b/kotlin-wot/src/test/kotlin/content/ContentManagerTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.content import ai.ancf.lmos.wot.thing.schema.ObjectSchema diff --git a/kotlin-wot/src/test/kotlin/content/JsonCodecText.kt b/kotlin-wot/src/test/kotlin/content/JsonCodecText.kt index 3737ff2..f4b6bde 100644 --- a/kotlin-wot/src/test/kotlin/content/JsonCodecText.kt +++ b/kotlin-wot/src/test/kotlin/content/JsonCodecText.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.content import ai.ancf.lmos.wot.thing.schema.BooleanSchema diff --git a/kotlin-wot/src/test/kotlin/thing/ConsumedThingTest.kt b/kotlin-wot/src/test/kotlin/thing/ConsumedThingTest.kt index 8b450e3..02e5918 100644 --- a/kotlin-wot/src/test/kotlin/thing/ConsumedThingTest.kt +++ b/kotlin-wot/src/test/kotlin/thing/ConsumedThingTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing import ai.ancf.lmos.wot.JsonMapper @@ -5,6 +11,7 @@ import ai.ancf.lmos.wot.Servient import ai.ancf.lmos.wot.content.Content import ai.ancf.lmos.wot.content.ContentManager import ai.ancf.lmos.wot.content.JsonCodec +import ai.ancf.lmos.wot.content.toJsonContent import ai.ancf.lmos.wot.security.BasicSecurityScheme import ai.ancf.lmos.wot.thing.action.ThingAction import ai.ancf.lmos.wot.thing.event.ThingEvent @@ -15,14 +22,14 @@ import ai.anfc.lmos.wot.binding.ProtocolClient import ai.anfc.lmos.wot.binding.ProtocolClientFactory import ai.anfc.lmos.wot.binding.Resource import ai.anfc.lmos.wot.binding.ResourceType +import app.cash.turbine.test import com.fasterxml.jackson.databind.node.TextNode +import com.fasterxml.jackson.module.kotlin.treeToValue import io.mockk.* import kotlinx.coroutines.flow.emptyFlow +import kotlinx.coroutines.flow.flow import kotlinx.coroutines.runBlocking -import kotlin.test.BeforeTest -import kotlin.test.Test -import kotlin.test.assertEquals -import kotlin.test.assertNotNull +import kotlin.test.* class ConsumedThingTest { @@ -158,6 +165,59 @@ class ConsumedThingTest { assertEquals(true, subscription.active) } + @Test + fun `subscribe twice to the same event should fail`(): Unit = runBlocking { + val listener = mockk(relaxed = true) + coEvery { protocolClient.subscribeResource(any(), any()) } returns emptyFlow() + + consumedThing.subscribeEvent("testEvent", listener) + assertFailsWith { consumedThing.subscribeEvent("testEvent", listener) } + } + + @Test + fun `test consumeEvent`(): Unit = runBlocking { + coEvery { protocolClient.subscribeResource(any(), any()) } returns flow { + emit("testValue".toJsonContent()) + } + + consumedThing.consumeEvent("testEvent").test { + val item = awaitItem().value() + val value : String = JsonMapper.instance.treeToValue(item) + assertEquals("testValue", value) + awaitComplete() + } + + coVerify { protocolClient.unlinkResource(any(), any()) } + } + + @Test + fun `test consumeEvent with error`(): Unit = runBlocking { + coEvery { protocolClient.subscribeResource(any(), any()) } returns flow { + throw Exception("test error") + } + + consumedThing.consumeEvent("testEvent").test { + val error = awaitError() // Waits for the expected exception + assertTrue(error is Exception) + } + + coVerify { protocolClient.unlinkResource(any(), any()) } + } + + @Test + fun `consume twice to the same event should fail`(): Unit = runBlocking { + coEvery { protocolClient.subscribeResource(any(), any()) } returns emptyFlow() + + consumedThing.consumeEvent("testEvent") + consumedThing.consumeEvent("testEvent").test { + val error = awaitError() // Waits for the expected exception + assertTrue(error is ConsumedThingException) + } + + coVerify(exactly = 0) { protocolClient.unlinkResource(any(), any()) } + } + + @Test fun testEquals() { val thingDescription = ThingDescription( diff --git a/kotlin-wot/src/test/kotlin/thing/ContextTest.kt b/kotlin-wot/src/test/kotlin/thing/ContextTest.kt index 88e5220..e3da6ba 100644 --- a/kotlin-wot/src/test/kotlin/thing/ContextTest.kt +++ b/kotlin-wot/src/test/kotlin/thing/ContextTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing diff --git a/kotlin-wot/src/test/kotlin/thing/ExposedThingTest.kt b/kotlin-wot/src/test/kotlin/thing/ExposedThingTest.kt index f29efd8..1693d27 100644 --- a/kotlin-wot/src/test/kotlin/thing/ExposedThingTest.kt +++ b/kotlin-wot/src/test/kotlin/thing/ExposedThingTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing import ai.ancf.lmos.wot.JsonMapper diff --git a/kotlin-wot/src/test/kotlin/thing/ProtocolHelpersTest.kt b/kotlin-wot/src/test/kotlin/thing/ProtocolHelpersTest.kt index c4bc621..a4d08fc 100644 --- a/kotlin-wot/src/test/kotlin/thing/ProtocolHelpersTest.kt +++ b/kotlin-wot/src/test/kotlin/thing/ProtocolHelpersTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing diff --git a/kotlin-wot/src/test/kotlin/thing/ProtocolListenerRegistryTest.kt b/kotlin-wot/src/test/kotlin/thing/ProtocolListenerRegistryTest.kt index a5a78fe..f817237 100644 --- a/kotlin-wot/src/test/kotlin/thing/ProtocolListenerRegistryTest.kt +++ b/kotlin-wot/src/test/kotlin/thing/ProtocolListenerRegistryTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing import ai.ancf.lmos.wot.thing.form.Form diff --git a/kotlin-wot/src/test/kotlin/thing/SessionAwareProtocolListenerRegistryTest.kt b/kotlin-wot/src/test/kotlin/thing/SessionAwareProtocolListenerRegistryTest.kt index ddb6b7f..74fcb59 100644 --- a/kotlin-wot/src/test/kotlin/thing/SessionAwareProtocolListenerRegistryTest.kt +++ b/kotlin-wot/src/test/kotlin/thing/SessionAwareProtocolListenerRegistryTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing import ai.ancf.lmos.wot.thing.form.Form diff --git a/kotlin-wot/src/test/kotlin/thing/ThingDescriptionTest.kt b/kotlin-wot/src/test/kotlin/thing/ThingDescriptionTest.kt index b97d154..7df3ce8 100644 --- a/kotlin-wot/src/test/kotlin/thing/ThingDescriptionTest.kt +++ b/kotlin-wot/src/test/kotlin/thing/ThingDescriptionTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing import ai.ancf.lmos.wot.security.BasicSecurityScheme diff --git a/kotlin-wot/src/test/kotlin/thing/TypeTest.kt b/kotlin-wot/src/test/kotlin/thing/TypeTest.kt index 3226781..8504ad5 100644 --- a/kotlin-wot/src/test/kotlin/thing/TypeTest.kt +++ b/kotlin-wot/src/test/kotlin/thing/TypeTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing import ai.ancf.lmos.wot.JsonMapper diff --git a/kotlin-wot/src/test/kotlin/thing/UriTemplateTest.kt b/kotlin-wot/src/test/kotlin/thing/UriTemplateTest.kt index 55f1d49..e68cdad 100644 --- a/kotlin-wot/src/test/kotlin/thing/UriTemplateTest.kt +++ b/kotlin-wot/src/test/kotlin/thing/UriTemplateTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing import kotlin.test.Test diff --git a/kotlin-wot/src/test/kotlin/thing/action/ThingActionTest.kt b/kotlin-wot/src/test/kotlin/thing/action/ThingActionTest.kt index d0a9a60..89d0eea 100644 --- a/kotlin-wot/src/test/kotlin/thing/action/ThingActionTest.kt +++ b/kotlin-wot/src/test/kotlin/thing/action/ThingActionTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.action import ai.ancf.lmos.wot.JsonMapper diff --git a/kotlin-wot/src/test/kotlin/thing/credentials/DefaultCredentialsProviderTest.kt b/kotlin-wot/src/test/kotlin/thing/credentials/DefaultCredentialsProviderTest.kt index 670c99b..cf82c7c 100644 --- a/kotlin-wot/src/test/kotlin/thing/credentials/DefaultCredentialsProviderTest.kt +++ b/kotlin-wot/src/test/kotlin/thing/credentials/DefaultCredentialsProviderTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.credentials diff --git a/kotlin-wot/src/test/kotlin/thing/event/ThingEventTest.kt b/kotlin-wot/src/test/kotlin/thing/event/ThingEventTest.kt index 640e485..2eea6fa 100644 --- a/kotlin-wot/src/test/kotlin/thing/event/ThingEventTest.kt +++ b/kotlin-wot/src/test/kotlin/thing/event/ThingEventTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.event import ai.ancf.lmos.wot.JsonMapper diff --git a/kotlin-wot/src/test/kotlin/thing/form/AugmentedFormTest.kt b/kotlin-wot/src/test/kotlin/thing/form/AugmentedFormTest.kt index d8a75b4..33e4932 100644 --- a/kotlin-wot/src/test/kotlin/thing/form/AugmentedFormTest.kt +++ b/kotlin-wot/src/test/kotlin/thing/form/AugmentedFormTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.form import AugmentedForm diff --git a/kotlin-wot/src/test/kotlin/thing/form/FormTest.kt b/kotlin-wot/src/test/kotlin/thing/form/FormTest.kt index d42411c..ba6d3b3 100644 --- a/kotlin-wot/src/test/kotlin/thing/form/FormTest.kt +++ b/kotlin-wot/src/test/kotlin/thing/form/FormTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.form import ai.ancf.lmos.wot.JsonMapper diff --git a/kotlin-wot/src/test/kotlin/thing/form/OperationTest.kt b/kotlin-wot/src/test/kotlin/thing/form/OperationTest.kt index 55d1b2d..ec6b8ae 100644 --- a/kotlin-wot/src/test/kotlin/thing/form/OperationTest.kt +++ b/kotlin-wot/src/test/kotlin/thing/form/OperationTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.form import ai.ancf.lmos.wot.JsonMapper diff --git a/kotlin-wot/src/test/kotlin/thing/property/ThingPropertyTest.kt b/kotlin-wot/src/test/kotlin/thing/property/ThingPropertyTest.kt index 64b3775..111bb26 100644 --- a/kotlin-wot/src/test/kotlin/thing/property/ThingPropertyTest.kt +++ b/kotlin-wot/src/test/kotlin/thing/property/ThingPropertyTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.property diff --git a/kotlin-wot/src/test/kotlin/thing/schema/DataSchemaTest.kt b/kotlin-wot/src/test/kotlin/thing/schema/DataSchemaTest.kt index 4ed6b38..edf2ea2 100644 --- a/kotlin-wot/src/test/kotlin/thing/schema/DataSchemaTest.kt +++ b/kotlin-wot/src/test/kotlin/thing/schema/DataSchemaTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.schema import ai.ancf.lmos.wot.JsonMapper diff --git a/kotlin-wot/src/test/kotlin/thing/schema/DataSchemaValidationTest.kt b/kotlin-wot/src/test/kotlin/thing/schema/DataSchemaValidationTest.kt index efdde01..70cbfa1 100644 --- a/kotlin-wot/src/test/kotlin/thing/schema/DataSchemaValidationTest.kt +++ b/kotlin-wot/src/test/kotlin/thing/schema/DataSchemaValidationTest.kt @@ -1,3 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + package ai.ancf.lmos.wot.thing.schema import org.junit.jupiter.api.Assertions.assertFalse diff --git a/lmos-kotlin-sdk-base/build.gradle.kts b/lmos-kotlin-sdk-base/build.gradle.kts new file mode 100644 index 0000000..36c156e --- /dev/null +++ b/lmos-kotlin-sdk-base/build.gradle.kts @@ -0,0 +1,4 @@ +dependencies { + api(project(":kotlin-wot")) + implementation("org.jetbrains.kotlin:kotlin-reflect") +} \ No newline at end of file diff --git a/lmos-kotlin-sdk-base/src/main/kotlin/sdk/DataSchemaBuilder.kt b/lmos-kotlin-sdk-base/src/main/kotlin/sdk/DataSchemaBuilder.kt new file mode 100644 index 0000000..b7824b0 --- /dev/null +++ b/lmos-kotlin-sdk-base/src/main/kotlin/sdk/DataSchemaBuilder.kt @@ -0,0 +1,91 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package ai.ancf.lmos.sdk + +import ai.ancf.lmos.wot.thing.schema.* +import kotlin.reflect.KClass +import kotlin.reflect.KType +import kotlin.reflect.full.memberProperties + +object DataSchemaBuilder { + + // TODO: Move to kotlin-wot + + fun mapTypeToSchema(type: KType): DataSchema { + return when (type.classifier) { + Int::class -> IntegerSchema() + Float::class -> NumberSchema() + Double::class -> NumberSchema() + Long::class -> NumberSchema() + String::class -> StringSchema() + Boolean::class -> BooleanSchema() + List::class, Set::class -> { + val genericType = type.arguments.firstOrNull()?.type + val itemSchema = if (genericType != null) { + mapTypeToSchema(genericType) + } else { + StringSchema() + } + ArraySchema(items = itemSchema) + } + IntArray::class -> ArraySchema(items = IntegerSchema()) + FloatArray::class -> ArraySchema(items = NumberSchema()) + DoubleArray::class -> ArraySchema(items = NumberSchema()) + NumberSchema::class -> ArraySchema(items = NumberSchema()) + Array::class -> ArraySchema(items = StringSchema()) + Array::class -> ArraySchema(items = NumberSchema()) + Array::class -> ArraySchema(items = BooleanSchema()) + Array::class -> ArraySchema(items = IntegerSchema()) + Array::class -> ArraySchema(items = NumberSchema()) + Array::class -> ArraySchema(items = NumberSchema()) + Unit::class -> NullSchema() + else -> buildObjectSchema(type) + } + } + + private fun buildObjectSchema(type: KType): ObjectSchema { + if (type.classifier == Map::class) { + return buildMapSchema(type) + } + + val kClass = type.classifier as? KClass<*> + ?: throw IllegalArgumentException("Type is not a valid class: $type") + + val (properties, required) = extractProperties(kClass) + return ObjectSchema(properties, required) + } + + private fun buildMapSchema(type: KType): ObjectSchema { + val keyType = type.arguments.getOrNull(0)?.type + val valueType = type.arguments.getOrNull(1)?.type + + val keySchema = if (keyType != null) mapTypeToSchema(keyType) else StringSchema() + val valueSchema = if (valueType != null) mapTypeToSchema(valueType) else StringSchema() + + return ObjectSchema( + properties = mutableMapOf( + "keys" to ArraySchema(items = keySchema), + "values" to ArraySchema(items = valueSchema) + ) + ) + } + + private fun extractProperties(kClass: KClass<*>): Pair>, MutableList> { + val properties: MutableMap> = mutableMapOf() + val required: MutableList = mutableListOf() + + kClass.memberProperties.forEach { property -> + val returnType = property.returnType + val schemaForParam = mapTypeToSchema(returnType) + properties[property.name] = schemaForParam + if (!returnType.isMarkedNullable) { + required.add(property.name) + } + } + return Pair(properties, required) + } +} \ No newline at end of file diff --git a/lmos-kotlin-sdk-base/src/main/kotlin/sdk/LMOSThingTypes.kt b/lmos-kotlin-sdk-base/src/main/kotlin/sdk/LMOSThingTypes.kt new file mode 100644 index 0000000..8fbc17a --- /dev/null +++ b/lmos-kotlin-sdk-base/src/main/kotlin/sdk/LMOSThingTypes.kt @@ -0,0 +1,14 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package ai.ancf.lmos.sdk + +import ai.ancf.lmos.wot.thing.schema.Type + +object LMOSThingTypes { + val TOOL = Type("lmos:Tool") + val AGENT = Type("lmos:Agent") +} \ No newline at end of file diff --git a/lmos-kotlin-sdk-base/src/main/kotlin/sdk/LmosContext.kt b/lmos-kotlin-sdk-base/src/main/kotlin/sdk/LmosContext.kt new file mode 100644 index 0000000..9214cef --- /dev/null +++ b/lmos-kotlin-sdk-base/src/main/kotlin/sdk/LmosContext.kt @@ -0,0 +1,12 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package ai.ancf.lmos.sdk + +object LMOSContext { + const val prefix = "lmos" + const val url = "https://eclipse.dev/lmos/protocol/v1" +} \ No newline at end of file diff --git a/lmos-kotlin-sdk-base/src/main/kotlin/sdk/agents/ConversationalAgent.kt b/lmos-kotlin-sdk-base/src/main/kotlin/sdk/agents/ConversationalAgent.kt new file mode 100644 index 0000000..4785d10 --- /dev/null +++ b/lmos-kotlin-sdk-base/src/main/kotlin/sdk/agents/ConversationalAgent.kt @@ -0,0 +1,54 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package ai.ancf.lmos.sdk.agents + +import ai.ancf.lmos.sdk.model.AgentRequest +import ai.ancf.lmos.sdk.model.AgentResult +import ai.ancf.lmos.sdk.model.Message +import kotlinx.coroutines.flow.Flow +import kotlin.reflect.KClass + +interface ConversationalAgent { + suspend fun chat(message: AgentRequest): AgentResult +} + +interface ConsumedConversationalAgent: ConversationalAgent { + suspend fun consumeEvent(eventName: String, clazz: KClass, listener: EventListener) + suspend fun consumeEvent(eventName: String, clazz: KClass) : Flow +} + +fun interface EventListener { + suspend fun handleEvent(data: T) +} + +fun String.toAgentRequest(): AgentRequest { + return AgentRequest( + messages = listOf( + Message( + role = "user", + content = this + ) + ) + ) +} + +fun String.toAgentResult(): AgentResult { + return AgentResult( + messages = listOf( + Message( + role = "user", + content = this + ) + ) + ) +} + +fun AgentResult.lastMessage(): String { + return this.messages.last().content +} + + diff --git a/lmos-kotlin-sdk-base/src/main/kotlin/sdk/model/AgentEvent.kt b/lmos-kotlin-sdk-base/src/main/kotlin/sdk/model/AgentEvent.kt new file mode 100644 index 0000000..d619280 --- /dev/null +++ b/lmos-kotlin-sdk-base/src/main/kotlin/sdk/model/AgentEvent.kt @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package ai.ancf.lmos.sdk.model + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties + +@JsonIgnoreProperties(ignoreUnknown = true) +data class AgentEvent( + var type: String = "", + var payload: String = "", + var conversationId: String? = "", + var turnId: String? = "", +) \ No newline at end of file diff --git a/lmos-kotlin-sdk-base/src/main/kotlin/sdk/model/AgentRequest.kt b/lmos-kotlin-sdk-base/src/main/kotlin/sdk/model/AgentRequest.kt new file mode 100644 index 0000000..db3c32e --- /dev/null +++ b/lmos-kotlin-sdk-base/src/main/kotlin/sdk/model/AgentRequest.kt @@ -0,0 +1,37 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package ai.ancf.lmos.sdk.model + + +data class AgentRequest( + val messages: List, + val conversationContext: ConversationContext? = null, + val systemContext: List? = null, + val userContext: UserContext? = null, +) + +data class UserContext( + val userId: String? = null, + val userToken: String? = null, + val profile: List, +) + +data class ConversationContext( + val conversationId: String, + val turnId: String? = null, + val anonymizationEntities: List? = null, +) + +data class ProfileEntry( + val key: String, + val value: String, +) + +data class SystemContextEntry( + val key: String, + val value: String, +) \ No newline at end of file diff --git a/lmos-kotlin-sdk-base/src/main/kotlin/sdk/model/AgentResult.kt b/lmos-kotlin-sdk-base/src/main/kotlin/sdk/model/AgentResult.kt new file mode 100644 index 0000000..6abf1c7 --- /dev/null +++ b/lmos-kotlin-sdk-base/src/main/kotlin/sdk/model/AgentResult.kt @@ -0,0 +1,15 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package ai.ancf.lmos.sdk.model + + +data class AgentResult( + val status: String? = null, + val responseTime: Double = -1.0, + val messages: List, + val anonymizationEntities: List? = null, +) diff --git a/lmos-kotlin-sdk-base/src/main/kotlin/sdk/model/AnonymizationEntity.kt b/lmos-kotlin-sdk-base/src/main/kotlin/sdk/model/AnonymizationEntity.kt new file mode 100644 index 0000000..2a8c5a2 --- /dev/null +++ b/lmos-kotlin-sdk-base/src/main/kotlin/sdk/model/AnonymizationEntity.kt @@ -0,0 +1,9 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package ai.ancf.lmos.sdk.model + +data class AnonymizationEntity(val type: String, val value: String, val replacement: String) diff --git a/lmos-kotlin-sdk-base/src/main/kotlin/sdk/model/Message.kt b/lmos-kotlin-sdk-base/src/main/kotlin/sdk/model/Message.kt new file mode 100644 index 0000000..6bf2c5e --- /dev/null +++ b/lmos-kotlin-sdk-base/src/main/kotlin/sdk/model/Message.kt @@ -0,0 +1,17 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package ai.ancf.lmos.sdk.model + +data class Message( + val role: String, + val content: String, + val format: String = "text", + val turnId: String? = null, + val binaryData: List? = null, +) + +class BinaryData(val mimeType: String, val dataAsBase64: String? = null, val source: String? = null) \ No newline at end of file diff --git a/lmos-kotlin-sdk-client/build.gradle.kts b/lmos-kotlin-sdk-client/build.gradle.kts new file mode 100644 index 0000000..797f999 --- /dev/null +++ b/lmos-kotlin-sdk-client/build.gradle.kts @@ -0,0 +1,7 @@ +dependencies { + implementation(project(":kotlin-wot")) + implementation(project(":kotlin-wot-binding-http")) + implementation(project(":kotlin-wot-binding-websocket")) + api(project(":lmos-kotlin-sdk-base")) + implementation("org.slf4j:slf4j-api:2.0.16") +} \ No newline at end of file diff --git a/lmos-kotlin-sdk-client/src/main/kotlin/sdk/WotConversationalAgent.kt b/lmos-kotlin-sdk-client/src/main/kotlin/sdk/WotConversationalAgent.kt new file mode 100644 index 0000000..a37cb66 --- /dev/null +++ b/lmos-kotlin-sdk-client/src/main/kotlin/sdk/WotConversationalAgent.kt @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package ai.ancf.lmos.sdk.agents + +import ai.ancf.lmos.sdk.model.AgentRequest +import ai.ancf.lmos.sdk.model.AgentResult +import ai.ancf.lmos.wot.JsonMapper +import ai.ancf.lmos.wot.Servient +import ai.ancf.lmos.wot.Wot +import ai.ancf.lmos.wot.binding.http.HttpProtocolClientFactory +import ai.ancf.lmos.wot.binding.http.HttpsProtocolClientFactory +import ai.ancf.lmos.wot.binding.websocket.WebSocketProtocolClientFactory +import ai.ancf.lmos.wot.thing.ConsumedThing +import io.opentelemetry.instrumentation.annotations.WithSpan +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map +import org.slf4j.Logger +import org.slf4j.LoggerFactory +import kotlin.reflect.KClass + + +class WotConversationalAgent private constructor(private val thing : ConsumedThing) : + ConsumedConversationalAgent{ + + private val log : Logger = LoggerFactory.getLogger(ConversationalAgent::class.java) + + companion object { + suspend fun create(wot: Wot, url: String): ConsumedConversationalAgent { + return WotConversationalAgent(wot.consume(wot.requestThingDescription(url)) as ConsumedThing) + } + + suspend fun create(url: String): ConsumedConversationalAgent { + val wot = Wot.create(Servient(clientFactories = listOf( + HttpProtocolClientFactory(), + HttpsProtocolClientFactory(), + WebSocketProtocolClientFactory() + ))) + return create(wot, url) + } + } + + @WithSpan + override suspend fun chat(message: AgentRequest): AgentResult { + return try { + thing.invokeAction(actionName = "chat", input = message) + } catch (e: Exception) { + log.error("Failed to receive an answer", e) + throw e + } + } + + override suspend fun consumeEvent(eventName: String, clazz: KClass, listener: EventListener, ) { + thing.subscribeEvent(eventName, listener = { event -> + try{ + val parsedEvent = JsonMapper.instance.treeToValue(event.value(), clazz.java) + // Pass it to the listener + listener.handleEvent(parsedEvent) + } catch (e: Exception) { + log.error("Failed to parse event", e) + throw e + } + }) + } + + override suspend fun consumeEvent(eventName: String, clazz: KClass): Flow { + return thing.consumeEvent(eventName).map { event -> + try { + log.info("Event:" + event.value()) + JsonMapper.instance.treeToValue(event.value(), clazz.java) + } catch (e: Exception) { + log.error("Failed to parse event", e) + throw e + } + } + } +} \ No newline at end of file diff --git a/lmos-kotlin-sdk-server/build.gradle.kts b/lmos-kotlin-sdk-server/build.gradle.kts new file mode 100644 index 0000000..d8ebb13 --- /dev/null +++ b/lmos-kotlin-sdk-server/build.gradle.kts @@ -0,0 +1,29 @@ +dependencies { + api("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") + api(project(":lmos-kotlin-sdk-base")) + implementation(project(":kotlin-wot")) + implementation("org.slf4j:slf4j-api:2.0.16") + + testImplementation(platform("io.ktor:ktor-bom:3.1.0")) + testImplementation(project(":kotlin-wot-binding-http")) + testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.0") + testImplementation("io.ktor:ktor-client-okhttp") + testImplementation("io.ktor:ktor-client-json") + testImplementation("io.ktor:ktor-client-jackson") + testImplementation("io.ktor:ktor-client-serialization") + testImplementation("io.ktor:ktor-client-content-negotiation") + testImplementation("io.ktor:ktor-serialization-jackson") + testImplementation("org.assertj:assertj-core:3.24.2") +} + +tasks.register("listConfigurations") { + doLast { + configurations.forEach { config -> + println("Configuration: ${config.name}") + println(" Can be resolved: ${config.isCanBeResolved}") + println(" Can be consumed: ${config.isCanBeConsumed}") + println(" Extends from: ${config.extendsFrom.joinToString { it.name }}") + println() + } + } +} diff --git a/lmos-kotlin-sdk-server/src/main/kotlin/ai/ancf/lmos/kotlinsdk/server/LmosAgent.kt b/lmos-kotlin-sdk-server/src/main/kotlin/ai/ancf/lmos/kotlinsdk/server/LmosAgent.kt new file mode 100644 index 0000000..5fbe544 --- /dev/null +++ b/lmos-kotlin-sdk-server/src/main/kotlin/ai/ancf/lmos/kotlinsdk/server/LmosAgent.kt @@ -0,0 +1,20 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package ai.ancf.lmos.kotlinsdk.server + +import ai.ancf.lmos.sdk.model.AgentRequest +import ai.ancf.lmos.sdk.model.AgentResult + + +data class LmosAgent( + var id: String, + var title: String, + var description: String, + var capabilities: String, + var chat: (input: AgentRequest) -> AgentResult, + // var events: Flow +) \ No newline at end of file diff --git a/lmos-kotlin-sdk-server/src/main/kotlin/ai/ancf/lmos/kotlinsdk/server/LmosRuntime.kt b/lmos-kotlin-sdk-server/src/main/kotlin/ai/ancf/lmos/kotlinsdk/server/LmosRuntime.kt new file mode 100644 index 0000000..b9acebf --- /dev/null +++ b/lmos-kotlin-sdk-server/src/main/kotlin/ai/ancf/lmos/kotlinsdk/server/LmosRuntime.kt @@ -0,0 +1,80 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package ai.ancf.lmos.kotlinsdk.server + +import ai.ancf.lmos.sdk.DataSchemaBuilder +import ai.ancf.lmos.sdk.LMOSContext +import ai.ancf.lmos.sdk.LMOSThingTypes +import ai.ancf.lmos.sdk.model.AgentRequest +import ai.ancf.lmos.sdk.model.AgentResult +import ai.ancf.lmos.wot.Servient +import ai.ancf.lmos.wot.Wot +import ai.ancf.lmos.wot.thing.DEFAULT_CONTEXT +import ai.ancf.lmos.wot.thing.schema.Context +import ai.ancf.lmos.wot.thing.schema.DataSchema +import ai.ancf.lmos.wot.thing.schema.VersionInfo +import ai.ancf.lmos.wot.thing.schema.WoTExposedThing +import org.slf4j.LoggerFactory +import kotlin.reflect.full.createType + +class LmosRuntime(private val servient: Servient) { + + private val wot = Wot.create(servient) + + suspend fun start() { + servient.start() + log.info("LmosRuntime started") + } + + suspend fun stop() { + servient.shutdown() + log.info("LmosRuntime stopped") + } + + suspend fun add(agent: LmosAgent) { + val agentThing = agent.toThing(wot) + //servient.addThing(agentThing) + servient.expose(agentThing.getThingDescription().id) + } + + // TODO: Remove an agent? Currently not possible in kotlin-wot + + companion object { + private val log = LoggerFactory.getLogger(LmosRuntime::class.java) + } + +} + +private fun LmosAgent.toThing(wot: Wot): WoTExposedThing { + return wot.produce { + id = this@toThing.id + title = this@toThing.title + description = this@toThing.description + objectType = LMOSThingTypes.AGENT + + val context = Context(DEFAULT_CONTEXT) + context.addContext(LMOSContext.prefix, LMOSContext.url) + objectContext = context + + version = VersionInfo("1.0.0", null) // TODO: Is this something specified by the agent developer? Or LMOS specific? + + // TODO: Links to capabilities + + // TODO: Really ? + action("chat") { + title = "Chat" + description = "Chat with the agent" + safe = false + idempotent = false + // TODO: Is this unsafe cast needed? How to avoid it? + input = DataSchemaBuilder.mapTypeToSchema(AgentRequest::class.createType()) as DataSchema + output = DataSchemaBuilder.mapTypeToSchema(AgentResult::class.createType()) as DataSchema + } + + // TODO: add agent events + } +} \ No newline at end of file diff --git a/lmos-kotlin-sdk-server/src/test/kotlin/ai/ancf/lmos/kotlinsdk/server/LmosRuntimeTest.kt b/lmos-kotlin-sdk-server/src/test/kotlin/ai/ancf/lmos/kotlinsdk/server/LmosRuntimeTest.kt new file mode 100644 index 0000000..0e9a308 --- /dev/null +++ b/lmos-kotlin-sdk-server/src/test/kotlin/ai/ancf/lmos/kotlinsdk/server/LmosRuntimeTest.kt @@ -0,0 +1,84 @@ +/* + * SPDX-FileCopyrightText: Robert Winkler + * + * SPDX-License-Identifier: Apache-2.0 + */ + +package ai.ancf.lmos.kotlinsdk.server + + +import ai.ancf.lmos.sdk.model.AgentRequest +import ai.ancf.lmos.sdk.model.AgentResult +import ai.ancf.lmos.sdk.model.AnonymizationEntity +import ai.ancf.lmos.sdk.model.Message +import ai.ancf.lmos.wot.Servient +import ai.ancf.lmos.wot.binding.http.HttpProtocolServer +import com.fasterxml.jackson.databind.DeserializationFeature +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.SerializationFeature +import io.ktor.client.* +import io.ktor.client.call.* +import io.ktor.client.engine.okhttp.* +import io.ktor.client.plugins.contentnegotiation.* +import io.ktor.client.request.* +import io.ktor.serialization.jackson.* +import kotlinx.coroutines.runBlocking +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.api.Test + + +class LmosRuntimeTest { + + private val client = HttpClient(OkHttp) { + install(ContentNegotiation) { + jackson { + enable(SerializationFeature.INDENT_OUTPUT) + configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + } + } + } + + @Test + fun test() = runBlocking { + val lmosAgent = LmosAgent( + id = "my-agent-id", + title = "my-agent-title", + description = "my agent description", + capabilities = "capabilities", + chat = { agentRequest -> someAgentResult(agentRequest) }, + ) + + val servient = Servient(servers = listOf(HttpProtocolServer())) + val lmosRuntime = LmosRuntime(servient) + lmosRuntime.start() + lmosRuntime.add(lmosAgent) + + val response: JsonNode = client.get("http://localhost:8080/${lmosAgent.id}").body() + assertThat(response.isObject).isTrue() + assertThat(response.get("id").asText()).isEqualTo(lmosAgent.id) + + lmosRuntime.stop() + } + + private fun someAgentResult(agentRequest: AgentRequest) = AgentResult( + status = "some status", + responseTime = 1.0, + messages = listOf( + Message( + role = "some role", + content = "some content", + format = "some format", + turnId = "some turn id", + binaryData = null + ) + ), + anonymizationEntities = listOf( + AnonymizationEntity( + type = "some type", + value = "some value", + replacement = "some replacement", + ) + ) + ) + +} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 2b6d062..688c428 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -18,4 +18,7 @@ include("kotlin-wot-integration-tests") include("kotlin-wot-reflection") include("kotlin-wot-spring-boot-starter") include("kotlin-wot-tool-example") -include("kotlin-wot-lmos-protocol") \ No newline at end of file +include("kotlin-wot-lmos-protocol") +include("lmos-kotlin-sdk-base") +include("lmos-kotlin-sdk-client") +include("lmos-kotlin-sdk-server") \ No newline at end of file