Skip to content

Commit 09c5aff

Browse files
author
Robert Winkler
committed
Implemented exploreDirectory function
1 parent 0a2f803 commit 09c5aff

File tree

11 files changed

+205
-44
lines changed

11 files changed

+205
-44
lines changed

kotlin-wot-integration-tests/src/main/kotlin/integration/AgentConfiguration.kt

Lines changed: 124 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,34 @@ import ai.ancf.lmos.arc.agents.functions.ParameterSchema
66
import ai.ancf.lmos.arc.agents.functions.ParameterType
77
import ai.ancf.lmos.arc.spring.Agents
88
import ai.ancf.lmos.arc.spring.Functions
9-
import ai.ancf.lmos.wot.JsonMapper
109
import ai.ancf.lmos.wot.Wot
1110
import ai.ancf.lmos.wot.security.BearerSecurityScheme
1211
import ai.ancf.lmos.wot.security.SecurityScheme
1312
import ai.ancf.lmos.wot.thing.schema.*
1413
import com.fasterxml.jackson.databind.JsonNode
14+
import com.fasterxml.jackson.databind.node.BooleanNode
15+
import com.fasterxml.jackson.databind.node.DecimalNode
16+
import com.fasterxml.jackson.databind.node.IntNode
1517
import com.fasterxml.jackson.databind.node.TextNode
16-
import com.fasterxml.jackson.module.kotlin.convertValue
1718
import kotlinx.coroutines.runBlocking
19+
import org.slf4j.Logger
20+
import org.slf4j.LoggerFactory
1821
import org.springframework.context.annotation.Bean
1922
import org.springframework.context.annotation.Configuration
23+
import java.math.BigDecimal
2024

2125

2226
@Configuration
2327
class AgentConfiguration {
2428

29+
private lateinit var thingDescriptionsMap : Map<String, WoTConsumedThing>
30+
31+
private val log : Logger = LoggerFactory.getLogger(AgentConfiguration::class.java)
32+
2533
@Bean
2634
fun chatArcAgent(agent: Agents) = agent {
2735
name = "ChatAgent"
28-
prompt { "You are a helpful smart home agent that can control devices." }
36+
prompt { "You are a helpful smart home agent that can control devices. But never print thingIds to the customer" }
2937
model = { "GPT-4o" }
3038
tools = AllTools
3139
}
@@ -49,8 +57,40 @@ class AgentConfiguration {
4957
@Bean
5058
fun discoverTools(functions: Functions, wot: Wot) : List<LLMFunction> = runBlocking {
5159
//discoverTool(wot, functions, "http://localhost:8081/scraper")
60+
/*
5261
discoverTool(wot, functions, "https://plugfest.webthings.io/things/virtual-things-2",
5362
BearerSecurityScheme())
63+
*/
64+
exploreToolDirectory(wot, functions, "https://plugfest.webthings.io/.well-known/wot",
65+
BearerSecurityScheme())
66+
}
67+
68+
private suspend fun exploreToolDirectory(wot: Wot, functions: Functions, url : String,
69+
securityScheme: SecurityScheme) : List<LLMFunction> {
70+
val thingDescriptions = wot.exploreDirectory(url, securityScheme)
71+
72+
val retrieveAllFunction = functions(
73+
"retrieveAllThings",
74+
"Retrieves the metadata information of all things/devices available. " +
75+
"Can be used to understand which device types are available and retrieve the " +
76+
"thingIds to control multiple devices. The types tell you the capabilities of a device.",
77+
"all_things"
78+
) {
79+
summarizeThingDescriptions(thingDescriptions)
80+
}
81+
82+
val consumedThings = thingDescriptions.map { wot.consume(it) }
83+
thingDescriptionsMap = consumedThings.associateBy { it.getThingDescription().id }
84+
85+
return retrieveAllFunction + consumedThings
86+
.flatMap { mapThingDescriptionToFunctions(functions, it) }
87+
}
88+
89+
fun summarizeThingDescriptions(things: Set<WoTThingDescription>): String {
90+
return things.joinToString(separator = "\n") { thing ->
91+
val types = thing.objectType?.types?.joinToString(", ") ?: "N/A"
92+
"thingId: ${thing.id}, Title: ${thing.title ?: "N/A"}, Types: $types"
93+
}
5494
}
5595

5696
private suspend fun discoverTool(wot: Wot, functions: Functions, url : String,
@@ -60,28 +100,43 @@ class AgentConfiguration {
60100

61101
val thing = wot.consume(thingDescription)
62102

63-
return mapThingDescriptionToFunctions(thingDescription, functions, thing)
103+
return mapThingDescriptionToFunctions(functions, thing)
64104
}
65105

66106
private suspend fun mapThingDescriptionToFunctions(
67-
thingDescription: WoTThingDescription,
68107
functions: Functions,
69108
thing: WoTConsumedThing
70109
): List<LLMFunction> {
110+
val thingDescription = thing.getThingDescription()
111+
112+
val defaultParams = listOf(Pair(ParameterSchema(
113+
name = "thingId",
114+
description = "The unique identifier of the thing",
115+
type = ParameterType("string"),
116+
enum = emptyList()
117+
), true))
118+
71119
val actionFunctions = thingDescription.actions.flatMap { (actionName, action) ->
72120

73-
val params = action.input?.let { input ->
121+
val actionParams = action.input?.let { input ->
74122
listOf(Pair(mapDataSchemaToParam(input), true))
75-
}
123+
} ?: emptyList()
124+
125+
val params = defaultParams + actionParams
76126

77127
functions(
78128
actionName,
79129
action.description ?: "No Description available",
80130
thingDescription.title,
81-
params ?: emptyList(),
131+
params,
82132
) {
83-
(url) ->
84-
thing.invokeAction(actionName, TextNode(url)).asText()
133+
(thingId, input) ->
134+
try {
135+
thingDescriptionsMap[thingId]?.invokeAction(actionName, TextNode(input))?.asText()?: "Function call failed"
136+
}catch (e: Exception) {
137+
log.error("Error invoking action $actionName", e)
138+
"Function call failed"
139+
}
85140
}
86141
}
87142
val propertiesFunctions = functions(
@@ -101,38 +156,68 @@ class AgentConfiguration {
101156
property.description ?: "Can be used to read the $propertyName property",
102157
thingDescription.title
103158
) {
104-
thing.readProperty(propertyName).value().asText()
159+
(thingId) ->
160+
try {
161+
thingDescriptionsMap[thingId]?.readProperty(propertyName)?.value()?.asText() ?: "Function call failed"
162+
} catch (e: Exception) {
163+
log.error("Error reading property $propertyName", e)
164+
"Function call failed"
165+
}
105166
}
106167
} else if (property.writeOnly) {
168+
val params = defaultParams + listOf(Pair(mapDataSchemaToParam(property), true))
169+
107170
functions(
108171
"set$propertyName",
109172
property.description ?: "Can be used to set the $propertyName property",
110173
thingDescription.title,
111-
listOf(Pair(mapDataSchemaToParam(property), true))
174+
params
112175
) {
113-
(propertyValue) ->
114-
thing.writeProperty(propertyName, TextNode(propertyValue))
115-
"Property $propertyName set to $propertyValue"
176+
(thingId, propertyValue) ->
177+
if(propertyValue != null){
178+
try {
179+
val propertyAffordance = thing.getThingDescription().properties[propertyName]!!
180+
thingDescriptionsMap[thingId]?.writeProperty(propertyName, mapSchemaToJsonNode(propertyAffordance, propertyValue)) ?: "Function failed"
181+
"Property $propertyName set to $propertyValue"
182+
} catch (e: Exception) {
183+
log.error("Error writing property $propertyName", e)
184+
"Function call failed"
185+
}
186+
}else{
187+
"Function call failed"
188+
}
116189
}
117190
} else {
118191
functions(
119192
"read$propertyName",
120193
property.description ?: "Can be used to read the $propertyName property",
121194
thingDescription.title
122195
) {
123-
thing.readProperty(propertyName).value().asText()
196+
(thingId) ->
197+
try {
198+
thingDescriptionsMap[thingId]?.readProperty(propertyName)?.value()?.asText() ?: "Function failed"
199+
} catch (e: Exception) {
200+
log.error("Error reading property $propertyName", e)
201+
"Function call failed"
202+
}
124203
}
204+
val params = defaultParams + listOf(Pair(mapDataSchemaToParam(property), true))
125205
functions(
126206
"set$propertyName",
127207
property.description ?: "Can be used to set the $propertyName property",
128208
thingDescription.title,
129-
listOf(Pair(mapDataSchemaToParam(property), true))
209+
params
130210
) {
131-
(propertyValue) ->
211+
(thingId, propertyValue) ->
132212
if(propertyValue != null){
133-
val input : JsonNode = JsonMapper.instance.convertValue(propertyValue)
134-
thing.writeProperty(propertyName, input)
135-
"Property $propertyName set to $propertyValue"
213+
try {
214+
val propertyAffordance = thing.getThingDescription().properties[propertyName]!!
215+
thingDescriptionsMap[thingId]?.writeProperty(propertyName, mapSchemaToJsonNode(propertyAffordance, propertyValue)) ?: "Function failed"
216+
"Property $propertyName set to $propertyValue"
217+
} catch (e: Exception) {
218+
log.error("Error writing property $propertyName", e)
219+
"Function call failed"
220+
}
136221
}else{
137222
"Function call failed"
138223
}
@@ -142,7 +227,24 @@ class AgentConfiguration {
142227
return actionFunctions + propertyFunctions + propertiesFunctions
143228
}
144229

145-
230+
fun mapSchemaToJsonNode(schema: DataSchema<*>, value: String): JsonNode {
231+
return when (schema) {
232+
is StringSchema -> TextNode(value)
233+
is IntegerSchema -> {
234+
val intValue = value.toIntOrNull() ?: 0
235+
IntNode(intValue)
236+
}
237+
is NumberSchema -> {
238+
val numberValue = value.toBigDecimalOrNull() ?: BigDecimal.ZERO
239+
DecimalNode(numberValue)
240+
}
241+
is BooleanSchema -> {
242+
val boolValue = value?.toBooleanStrictOrNull() ?: false
243+
BooleanNode.valueOf(boolValue)
244+
}
245+
else -> throw IllegalArgumentException("Unsupported schema type: ${schema::class.simpleName}")
246+
}
247+
}
146248

147249
/*
148250

kotlin-wot-integration-tests/src/test/kotlin/integration/QuickTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ class QuickTest {
99
fun `should control my lamp`() = runBlocking {
1010
val agent = ConversationalAgent.create("http://localhost:8080/chatagent")
1111
//val command = "What is the state of my lamp?"
12-
val command = "Turn on the lamp"
12+
val command = "Set all my color lights to green"
1313
println("User: $command")
1414
val answer = agent.chat(command)
1515
println("Agent: $answer")

kotlin-wot/src/main/kotlin/DefaultWot.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,19 @@ class DefaultWot(private val servient: Servient) : Wot {
2424
'}'
2525
}
2626
@Throws(WotException::class)
27-
override suspend fun discover(filter: ThingFilter): Flow<WoTExposedThing> {
27+
override fun discover(filter: ThingFilter): Flow<WoTThingDescription> {
2828
return servient.discover(filter)
2929
}
3030

3131
@Throws(WotException::class)
32-
override suspend fun discover(): Flow<WoTExposedThing> {
32+
override fun discover(): Flow<WoTThingDescription> {
3333
return discover(ThingFilter(method = DiscoveryMethod.ANY))
3434
}
3535

36+
override suspend fun exploreDirectory(directoryUrl: String, securityScheme: SecurityScheme): Set<WoTThingDescription> {
37+
return servient.exploreDirectory(directoryUrl, securityScheme)
38+
}
39+
3640
override fun produce(thingDescription: WoTThingDescription): WoTExposedThing {
3741
val exposedThing = ExposedThing(servient, thingDescription)
3842
return if (servient.addThing(exposedThing)) {

kotlin-wot/src/main/kotlin/Servient.kt

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ import ai.ancf.lmos.wot.thing.filter.DiscoveryMethod.*
1010
import ai.ancf.lmos.wot.thing.filter.ThingFilter
1111
import ai.ancf.lmos.wot.thing.form.Form
1212
import ai.ancf.lmos.wot.thing.schema.WoTExposedThing
13+
import ai.ancf.lmos.wot.thing.schema.WoTThingDescription
1314
import ai.anfc.lmos.wot.binding.*
15+
import com.fasterxml.jackson.module.kotlin.treeToValue
1416
import kotlinx.coroutines.async
1517
import kotlinx.coroutines.awaitAll
1618
import kotlinx.coroutines.coroutineScope
@@ -314,7 +316,7 @@ class Servient(
314316
* @return
315317
*/
316318

317-
suspend fun discover(): Flow<WoTExposedThing> {
319+
fun discover(): Flow<WoTThingDescription> {
318320
return discover(ThingFilter(method = ANY))
319321
}
320322

@@ -331,7 +333,7 @@ class Servient(
331333
* @return
332334
*/
333335

334-
suspend fun discover(filter: ThingFilter): Flow<WoTExposedThing> {
336+
fun discover(filter: ThingFilter): Flow<WoTThingDescription> {
335337
return when (filter.method) {
336338
DIRECTORY -> discoverDirectory(filter)
337339
LOCAL -> discoverLocal(filter)
@@ -341,7 +343,7 @@ class Servient(
341343

342344
// Discover any available Things across all protocols
343345
@Throws(ServientException::class)
344-
private suspend fun discoverAny(filter: ThingFilter): Flow<WoTExposedThing> = flow {
346+
private fun discoverAny(filter: ThingFilter): Flow<WoTThingDescription> = flow {
345347
var foundAtLeastOne = false
346348
// Try to run a discovery with every available protocol binding
347349
for (factory in clientFactories.values) {
@@ -364,7 +366,7 @@ class Servient(
364366
emitAll(discoverLocal(filter))
365367
}
366368

367-
private suspend fun discoverDirectory(
369+
private fun discoverDirectory(
368370
filter: ThingFilter
369371
): Flow<ExposedThing> = flow {
370372
//val discoveredThings = filter.url?.let { fetchDirectory(it) } TODO
@@ -376,7 +378,7 @@ class Servient(
376378
filteredThings?.forEach { emit(it) } // Emit each thing one by one
377379
}
378380

379-
private fun discoverLocal(filter: ThingFilter): Flow<WoTExposedThing> = flow {
381+
private fun discoverLocal(filter: ThingFilter): Flow<WoTThingDescription> = flow {
380382
val myThings = things.values.toList()
381383

382384
// Apply the filter query if available
@@ -386,6 +388,16 @@ class Servient(
386388

387389
fun hasClientFor(scheme: String): Boolean = clientFactories.containsKey(scheme)
388390

391+
suspend fun exploreDirectory(directoryUrl: String, securityScheme: SecurityScheme): Set<WoTThingDescription> {
392+
val directoryThingDescription = fetch(directoryUrl, securityScheme)
393+
val consumedDirectory = ConsumedThing(this@Servient, directoryThingDescription)
394+
val thingsPropertyOutput = consumedDirectory.readProperty("things")
395+
val thingsAsJsonNode = thingsPropertyOutput.value()
396+
397+
val thingDescriptions : Set<ThingDescription> = JsonMapper.instance.treeToValue<Set<ThingDescription>>(thingsAsJsonNode)
398+
return thingDescriptions
399+
}
400+
389401
companion object {
390402
private val log = LoggerFactory.getLogger(Servient::class.java)
391403
val addresses: Set<String>

kotlin-wot/src/main/kotlin/Wot.kt

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,23 @@ interface Wot {
2424
* @param filter
2525
* @return
2626
*/
27-
suspend fun discover(filter: ThingFilter): Flow<WoTExposedThing>
27+
fun discover(filter: ThingFilter): Flow<WoTThingDescription>
2828

2929
/**
3030
* Starts the discovery process that will provide all available Things.
3131
*
3232
* @return
3333
*/
34+
fun discover(): Flow<WoTThingDescription>
3435

35-
suspend fun discover(): Flow<WoTExposedThing>
36+
/**
37+
* Starts the discovery process that will provide Things that match the `filter`
38+
* argument from a given Thing Directory.
39+
*
40+
* @param filter
41+
* @return
42+
*/
43+
suspend fun exploreDirectory(directoryUrl: String, securityScheme: SecurityScheme = NoSecurityScheme()): Set<WoTThingDescription>
3644

3745
/**
3846
* Accepts a `thing` argument of type [ThingDescription] and returns an [ ] object.<br></br> The result can be used to start exposing interfaces for thing

kotlin-wot/src/main/kotlin/content/ContentManager.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ object ContentManager {
147147
* @param contentType
148148
* @return
149149
*/
150-
internal fun getMediaType(contentType: String?): String {
150+
fun getMediaType(contentType: String?): String {
151151
if (contentType == null) {
152152
return DEFAULT_MEDIA_TYPE
153153
}

kotlin-wot/src/main/kotlin/thing/ConsumedThing.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ data class ConsumedThing(
7878

7979
// Check if returned media type matches the expected media type from TD
8080
form.response?.let { response ->
81-
if (content.type != response.contentType) {
81+
if (ContentManager.getMediaType(content.type) != ContentManager.getMediaType(response.contentType)) {
8282
throw IllegalArgumentException(
8383
"Unexpected type '${content.type}' in response. Expected '${response.contentType}'"
8484
)

0 commit comments

Comments
 (0)