Skip to content

Commit 9e41fe4

Browse files
author
Robert Winkler
committed
Allow tools to be configured
1 parent d85878a commit 9e41fe4

File tree

10 files changed

+159
-49
lines changed

10 files changed

+159
-49
lines changed

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ subprojects {
1010
apply(plugin = "maven-publish")
1111

1212
group = "ai.ancf.lmos"
13-
version = "1.0-SNAPSHOT"
13+
version = "0.1.0-SNAPSHOT"
1414

1515
dependencies {
1616
testImplementation(kotlin("test"))

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

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
package ai.ancf.lmos.wot.integration
22

33

4+
import ai.ancf.lmos.wot.JsonMapper
45
import ai.ancf.lmos.wot.Wot
5-
import ai.ancf.lmos.wot.security.BearerSecurityScheme
6+
import ai.ancf.lmos.wot.security.SecurityScheme
67
import ai.ancf.lmos.wot.thing.schema.WoTConsumedThing
8+
import com.fasterxml.jackson.module.kotlin.readValue
79
import kotlinx.coroutines.runBlocking
8-
import org.eclipse.lmos.arc.agents.dsl.AllTools
910
import org.eclipse.lmos.arc.agents.functions.LLMFunction
1011
import org.eclipse.lmos.arc.spring.Agents
1112
import org.eclipse.lmos.arc.spring.Functions
1213
import org.slf4j.Logger
1314
import org.slf4j.LoggerFactory
15+
import org.springframework.boot.context.properties.EnableConfigurationProperties
1416
import org.springframework.context.ApplicationEventPublisher
1517
import org.springframework.context.annotation.Bean
1618
import org.springframework.context.annotation.Configuration
1719

1820

1921
@Configuration
22+
@EnableConfigurationProperties(ToolProperties::class)
2023
class AgentConfiguration {
2124

2225
private lateinit var thingDescriptionsMap : Map<String, WoTConsumedThing>
@@ -63,7 +66,7 @@ class AgentConfiguration {
6366
""".trimIndent() }
6467
model = { "GPT-4o" }
6568
filterInput { -"Hello world" }
66-
tools = AllTools
69+
tools = listOf("devices")
6770
}
6871

6972
@Bean
@@ -76,20 +79,33 @@ class AgentConfiguration {
7679
@Bean
7780
fun scraperArcAgent(agent: Agents) = agent {
7881
name = "ScraperAgent"
79-
prompt { "You can retrieve content by scraping a given URL." }
82+
prompt { "You can scrape a page by using the scraper tool." }
8083
model = { "GPT-4o" }
81-
tools = AllTools
84+
tools = listOf("fetchContent")
8285
}
8386

8487
@Bean
8588
fun agentEventListener(applicationEventPublisher: ApplicationEventPublisher) = ArcEventListener(applicationEventPublisher)
8689

8790
@Bean
88-
fun discoverTools(functions: Functions, wot: Wot) : List<LLMFunction> = runBlocking {
89-
ThingToFunctionsMapper.exploreToolDirectory(
90-
wot, functions, "https://plugfest.webthings.io/.well-known/wot",
91-
BearerSecurityScheme()
92-
)
91+
fun discoverTools(toolProperties: ToolProperties, functions: Functions, wot: Wot) : List<LLMFunction> = runBlocking {
92+
toolProperties.tools.flatMap {
93+
log.info("Discovering tools for ${it.key}")
94+
log.info("Security scheme: ${it.value.securityScheme}")
95+
val json = """
96+
{
97+
"scheme": "${it.value.securityScheme}"
98+
}
99+
"""
100+
val securityScheme = JsonMapper.instance.readValue<SecurityScheme>(json)
101+
val createdFunctions = if (it.value.isDirectory) {
102+
ThingToFunctionsMapper.exploreToolDirectory(wot, functions, it.key, it.value.url, securityScheme)
103+
} else {
104+
ThingToFunctionsMapper.requestThingDescription(wot, functions, it.key, it.value.url, securityScheme)
105+
}
106+
log.info("Added ${createdFunctions.size} functions to group ${it.key}")
107+
createdFunctions
108+
}
93109
}
94110

95111
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,11 @@ class ScraperAgent(agentProvider: AgentProvider) {
3333
}
3434

3535
@Action(title = "chat", description = "Ask the agent a question.")
36-
suspend fun chat(message: String) {
36+
suspend fun chat(message: String) : String {
3737
val assistantMessage = agent.execute(message.toConversation(User("myId"))).getOrThrow().latest<AssistantMessage>() ?:
3838
throw RuntimeException("No Assistant response")
3939
messageFlow.emit(assistantMessage.content)
40+
return assistantMessage.content
4041
}
4142
}
4243

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

Lines changed: 33 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package ai.ancf.lmos.wot.integration
33

44
import ai.ancf.lmos.wot.JsonMapper
55
import ai.ancf.lmos.wot.Wot
6+
import ai.ancf.lmos.wot.security.NoSecurityScheme
67
import ai.ancf.lmos.wot.security.SecurityScheme
78
import ai.ancf.lmos.wot.thing.schema.*
89
import com.fasterxml.jackson.databind.JsonNode
@@ -25,17 +26,26 @@ object ThingToFunctionsMapper {
2526

2627
private val functionCache = mutableMapOf<String, List<LLMFunction>>()
2728

28-
suspend fun exploreToolDirectory(wot: Wot, functions: Functions, url: String, securityScheme: SecurityScheme): List<LLMFunction> {
29+
suspend fun exploreToolDirectory(wot: Wot, functions: Functions, group: String, url: String, securityScheme: SecurityScheme): List<LLMFunction> {
2930
val thingDescriptions = wot.exploreDirectory(url, securityScheme)
3031
val consumedThings = consumeThings(wot, thingDescriptions)
31-
val retrieveAllFunction = createRetrieveAllFunction(functions, thingDescriptions)
32-
mapAllThingFunctions(functions, consumedThings)
32+
val retrieveAllFunction = createRetrieveAllFunction(functions, group, thingDescriptions)
33+
mapAllThingFunctions(functions, group, consumedThings)
3334
val allFunctions = retrieveAllFunction+ functionCache.values.flatten()
3435
return allFunctions
3536
}
3637

37-
private fun createRetrieveAllFunction(functions: Functions, thingDescriptions: Set<WoTThingDescription>): List<LLMFunction> {
38-
return functions("retrieveAllThings", "Returns the metadata information of all available devices.", "all_things") {
38+
suspend fun requestThingDescription(wot: Wot, functions: Functions, group: String, url: String, securityScheme: SecurityScheme = NoSecurityScheme()): List<LLMFunction> {
39+
val thingDescription = wot.requestThingDescription(url, securityScheme)
40+
val consumedThings = consumeThings(wot, setOf(thingDescription))
41+
val retrieveAllFunction = createRetrieveAllFunction(functions, group, setOf(thingDescription))
42+
mapAllThingFunctions(functions, group, consumedThings)
43+
val allFunctions = retrieveAllFunction+ functionCache.values.flatten()
44+
return allFunctions
45+
}
46+
47+
private fun createRetrieveAllFunction(functions: Functions, group: String, thingDescriptions: Set<WoTThingDescription>): List<LLMFunction> {
48+
return functions("retrieveAllThings", "Returns the metadata information of all available devices.", group) {
3949
summarizeThingDescriptions(thingDescriptions)
4050
}
4151
}
@@ -46,8 +56,8 @@ object ThingToFunctionsMapper {
4656
}
4757
}
4858

49-
private fun mapAllThingFunctions(functions: Functions, consumedThings: List<WoTConsumedThing>): List<LLMFunction> {
50-
return consumedThings.flatMap { mapThingDescriptionToFunctions(functions, it) }
59+
private fun mapAllThingFunctions(functions: Functions, group: String, consumedThings: List<WoTConsumedThing>): List<LLMFunction> {
60+
return consumedThings.flatMap { mapThingDescriptionToFunctions(functions, group, it) }
5161
}
5262

5363
private fun summarizeThingDescriptions(things: Set<WoTThingDescription>): String {
@@ -71,25 +81,25 @@ object ThingToFunctionsMapper {
7181
return thing.properties.entries.joinToString("\n ") { (key, property) -> "$key: ${property.title} - ${property.description}" }
7282
}
7383

74-
private fun mapThingDescriptionToFunctions(functions: Functions, thing: WoTConsumedThing): Set<LLMFunction> {
84+
private fun mapThingDescriptionToFunctions(functions: Functions, group: String, thing: WoTConsumedThing): Set<LLMFunction> {
7585
val thingDescription = thing.getThingDescription()
7686
val defaultParams = createDefaultParams()
77-
val actionFunctions = createActionFunctions(functions, thingDescription, defaultParams)
78-
val propertyFunctions = createPropertyFunctions(functions, thingDescription, defaultParams)
79-
val readAllPropertiesFunction = createReadAllPropertiesFunction(functions, thingDescription)
87+
val actionFunctions = createActionFunctions(functions, group, thingDescription, defaultParams)
88+
val propertyFunctions = createPropertyFunctions(functions, group, thingDescription, defaultParams)
89+
val readAllPropertiesFunction = createReadAllPropertiesFunction(functions, group, thingDescription)
8090
return actionFunctions.toSet() + propertyFunctions.toSet() + readAllPropertiesFunction.toSet()
8191
}
8292

8393
private fun createDefaultParams(): List<Pair<ParameterSchema, Boolean>> {
8494
return listOf(Pair(ParameterSchema("thingId", "The unique identifier of the thing", ParameterType("string"), emptyList()), true))
8595
}
8696

87-
private fun createActionFunctions(functions: Functions, thingDescription: WoTThingDescription, defaultParams: List<Pair<ParameterSchema, Boolean>>): List<LLMFunction> {
97+
private fun createActionFunctions(functions: Functions, group: String, thingDescription: WoTThingDescription, defaultParams: List<Pair<ParameterSchema, Boolean>>): List<LLMFunction> {
8898
return thingDescription.actions.flatMap { (actionName, action) ->
8999
val actionParams = action.input?.let { listOf(Pair(mapDataSchemaToParam(it), true)) } ?: emptyList()
90100
val params = defaultParams + actionParams
91101
functionCache.getOrPut(actionName) {
92-
functions(actionName, action.description ?: "No Description available", thingDescription.title, params) { (thingId, input) ->
102+
functions(actionName, action.description ?: "No Description available", group, params) { (thingId, input) ->
93103
invokeAction(thingDescription.id, actionName, input, action.input)
94104
}
95105
}
@@ -107,36 +117,36 @@ object ThingToFunctionsMapper {
107117
}
108118
}
109119

110-
private fun createReadAllPropertiesFunction(functions: Functions, thingDescription: WoTThingDescription): List<LLMFunction> {
120+
private fun createReadAllPropertiesFunction(functions: Functions, group: String, thingDescription: WoTThingDescription): List<LLMFunction> {
111121
val functionKey = "readAllProperties"
112122
return functionCache.getOrPut(functionKey) {
113-
functions(functionKey, "Read all properties of a thing", "This function retrieves all properties of a thing") {
123+
functions(functionKey, "Read all properties of a thing", group) {
114124
thingDescriptionsMap[thingDescription.id]?.readAllProperties()?.map { (propertyName, futureValue) -> "$propertyName: ${futureValue.value().asText()}" }?.joinToString("\n") ?: "Function call failed"
115125
}
116126
}
117127
}
118128

119-
private fun createPropertyFunctions(functions: Functions, thingDescription: WoTThingDescription, defaultParams: List<Pair<ParameterSchema, Boolean>>): List<LLMFunction> {
129+
private fun createPropertyFunctions(functions: Functions, group: String, thingDescription: WoTThingDescription, defaultParams: List<Pair<ParameterSchema, Boolean>>): List<LLMFunction> {
120130
return thingDescription.properties.flatMap { (propertyName, property) ->
121131
when {
122-
property.readOnly -> createReadPropertyFunction(functions, thingDescription, propertyName)
123-
property.writeOnly -> createWritePropertyFunction(functions, thingDescription, propertyName, property, defaultParams)
124-
else -> createReadPropertyFunction(functions, thingDescription, propertyName) + createWritePropertyFunction(functions, thingDescription, propertyName, property, defaultParams)
132+
property.readOnly -> createReadPropertyFunction(functions, group, propertyName)
133+
property.writeOnly -> createWritePropertyFunction(functions, group, propertyName, property, defaultParams)
134+
else -> createReadPropertyFunction(functions, group, propertyName) + createWritePropertyFunction(functions, group, propertyName, property, defaultParams)
125135
}
126136
}
127137
}
128138

129139
private fun createReadPropertyFunction(
130140
functions: Functions,
131-
thingDescription: WoTThingDescription,
141+
group: String,
132142
propertyName: String
133143
) : List<LLMFunction> {
134144
val functionKey = "read_$propertyName"
135145
return functionCache.getOrPut(functionKey) {
136146
functions(
137147
functionKey,
138148
"Reads the value of the $propertyName property.",
139-
thingDescription.title
149+
group,
140150
) { (thingId) ->
141151
try {
142152
thingDescriptionsMap[thingId]?.readProperty(propertyName)?.value()?.asText() ?: "Function call failed"
@@ -150,7 +160,7 @@ object ThingToFunctionsMapper {
150160

151161
private fun createWritePropertyFunction(
152162
functions: Functions,
153-
thingDescription: WoTThingDescription,
163+
group: String,
154164
propertyName: String,
155165
property: PropertyAffordance<*>,
156166
defaultParams: List<Pair<ParameterSchema, Boolean>>
@@ -161,7 +171,7 @@ object ThingToFunctionsMapper {
161171
functions(
162172
functionKey,
163173
"Sets the value of the $propertyName property.",
164-
thingDescription.title,
174+
group,
165175
params
166176
) { (thingId, propertyValue) ->
167177
if (propertyValue != null) {
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package ai.ancf.lmos.wot.integration
2+
3+
4+
import org.springframework.boot.context.properties.ConfigurationProperties
5+
6+
@ConfigurationProperties(prefix = "arc.ai")
7+
data class ToolProperties(
8+
val tools: Map<String, Tool>
9+
)
10+
11+
data class Tool(
12+
val url: String,
13+
val isDirectory: Boolean = false,
14+
val securityScheme: String = "nosec"
15+
)

kotlin-wot-integration-tests/src/main/resources/application.yaml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,13 @@ arc:
3030
api-key: dummy
3131
client: azure
3232
url: https://gpt4-uk.openai.azure.com
33+
tools:
34+
scraper:
35+
url: "http://localhost:9099/scraper"
36+
devices:
37+
url: "https://plugfest.webthings.io/.well-known/wot"
38+
isDirectory: true
39+
securityScheme: "bearer"
3340

3441
wot:
3542
servient:
@@ -47,10 +54,7 @@ wot:
4754
server:
4855
enabled: true
4956
host: localhost
50-
port: 8080
51-
baseUrls:
52-
- http://localhost:8080
53-
- http://not-existing-external-url:8080
57+
port: 9080
5458
mqtt:
5559
server:
5660
enabled: false

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

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,4 +25,23 @@ class QuickTest {
2525
println("Agent: $answer")
2626
//latch.await()
2727
}
28+
29+
@Test
30+
fun `scrape a URL`() = runBlocking {
31+
//val latch = CountDownLatch(3)
32+
33+
val agent = WotConversationalAgent.create("http://localhost:9080/scraper")
34+
/*
35+
agent.consumeEvent("agentEvent") {
36+
println("Event: $it")
37+
latch.countDown()
38+
}
39+
*/
40+
//val command = "What is the state of my lamp?"
41+
val command = "Scrape the page https://eclipse.dev/lmos/\""
42+
println("User: $command")
43+
val answer = agent.chat(command)
44+
println("Agent: $answer")
45+
//latch.await()
46+
}
2847
}
Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package ai.ancf.lmos.wot.spring
22

33
import org.springframework.boot.context.properties.ConfigurationProperties
4+
import org.springframework.boot.context.properties.bind.ConstructorBinding
45
import org.springframework.validation.annotation.Validated
56

67
open class ServerProperties(
@@ -10,14 +11,19 @@ open class ServerProperties(
1011
var baseUrls: List<String>
1112
)
1213

14+
1315
@ConfigurationProperties(prefix = "wot.servient.http.server", ignoreUnknownFields = true)
1416
@Validated
15-
class HttpServerProperties : ServerProperties(
16-
baseUrls = listOf("http://localhost:8080")
17-
)
17+
class HttpServerProperties(
18+
enabled: Boolean = true,
19+
host: String = "0.0.0.0",
20+
port: Int = 8080
21+
) : ServerProperties(enabled, host, port, listOf("http://localhost:$port"))
1822

1923
@ConfigurationProperties(prefix = "wot.servient.websocket.server", ignoreUnknownFields = true)
2024
@Validated
21-
class WebsocketProperties : ServerProperties(
22-
baseUrls = listOf("ws://localhost:8080")
23-
)
25+
class WebsocketProperties(
26+
enabled: Boolean = true,
27+
host: String = "0.0.0.0",
28+
port: Int = 8080
29+
) : ServerProperties(enabled, host, port, listOf("ws://localhost:$port"))

0 commit comments

Comments
 (0)