test
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -38,3 +38,5 @@ build/
|
|||||||
### Mac OS ###
|
### Mac OS ###
|
||||||
.DS_Store
|
.DS_Store
|
||||||
/.idea/
|
/.idea/
|
||||||
|
/agent-persistence-test/
|
||||||
|
/a2a-messages/
|
||||||
|
|||||||
31
pom.xml
31
pom.xml
@@ -16,6 +16,7 @@
|
|||||||
<properties>
|
<properties>
|
||||||
<java.version>21</java.version>
|
<java.version>21</java.version>
|
||||||
<kotlin.version>2.2.10</kotlin.version>
|
<kotlin.version>2.2.10</kotlin.version>
|
||||||
|
<koog.version>0.5.3</koog.version>
|
||||||
</properties>
|
</properties>
|
||||||
|
|
||||||
<repositories>
|
<repositories>
|
||||||
@@ -50,6 +51,10 @@
|
|||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-starter</artifactId>
|
<artifactId>spring-boot-starter</artifactId>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-web</artifactId>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.jetbrains.kotlin</groupId>
|
<groupId>org.jetbrains.kotlin</groupId>
|
||||||
<artifactId>kotlin-reflect</artifactId>
|
<artifactId>kotlin-reflect</artifactId>
|
||||||
@@ -63,17 +68,37 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>ai.koog</groupId>
|
<groupId>ai.koog</groupId>
|
||||||
<artifactId>koog-agents-jvm</artifactId>
|
<artifactId>koog-agents-jvm</artifactId>
|
||||||
<version>0.5.0</version>
|
<version>${koog.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>ai.koog</groupId>
|
<groupId>ai.koog</groupId>
|
||||||
<artifactId>prompt-cache-redis-jvm</artifactId>
|
<artifactId>prompt-cache-redis-jvm</artifactId>
|
||||||
<version>0.5.0</version>
|
<version>${koog.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>ai.koog</groupId>
|
<groupId>ai.koog</groupId>
|
||||||
<artifactId>prompt-executor-model-jvm</artifactId>
|
<artifactId>prompt-executor-model-jvm</artifactId>
|
||||||
<version>0.5.0</version>
|
<version>${koog.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>ai.koog</groupId>
|
||||||
|
<artifactId>agents-features-a2a-client-jvm</artifactId>
|
||||||
|
<version>${koog.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>ai.koog</groupId>
|
||||||
|
<artifactId>agents-features-a2a-server-jvm</artifactId>
|
||||||
|
<version>${koog.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>ai.koog</groupId>
|
||||||
|
<artifactId>a2a-transport-server-jsonrpc-http-jvm</artifactId>
|
||||||
|
<version>${koog.version}</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>ai.koog</groupId>
|
||||||
|
<artifactId>a2a-transport-client-jsonrpc-http-jvm</artifactId>
|
||||||
|
<version>${koog.version}</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
<!-- Utilities -->
|
<!-- Utilities -->
|
||||||
|
|||||||
143
src/main/kotlin/nl/trivion/softwarefabric/agents/AgenTester2.kt
Normal file
143
src/main/kotlin/nl/trivion/softwarefabric/agents/AgenTester2.kt
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
package nl.trivion.softwarefabric.agents
|
||||||
|
|
||||||
|
import ai.koog.agents.core.agent.AIAgent
|
||||||
|
import ai.koog.agents.core.agent.config.AIAgentConfig
|
||||||
|
import ai.koog.agents.core.agent.singleRunStrategy
|
||||||
|
import ai.koog.agents.core.dsl.builder.strategy
|
||||||
|
import ai.koog.agents.core.tools.ToolRegistry
|
||||||
|
import ai.koog.agents.ext.tool.AskUser
|
||||||
|
import ai.koog.agents.ext.tool.SayToUser
|
||||||
|
import ai.koog.agents.snapshot.feature.Persistence
|
||||||
|
import ai.koog.agents.snapshot.providers.file.JVMFilePersistenceStorageProvider
|
||||||
|
import ai.koog.prompt.dsl.prompt
|
||||||
|
import ai.koog.prompt.executor.llms.all.simpleOllamaAIExecutor
|
||||||
|
import ai.koog.prompt.llm.LLMCapability
|
||||||
|
import ai.koog.prompt.llm.LLMProvider
|
||||||
|
import ai.koog.prompt.llm.LLModel
|
||||||
|
import ai.koog.prompt.llm.OllamaModels
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import java.nio.file.Files
|
||||||
|
import kotlin.io.path.exists
|
||||||
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This example demonstrates how to use the file-based checkpoint provider with a persistent agent.
|
||||||
|
*
|
||||||
|
* The JVMFileAgentCheckpointStorageProvider stores agent checkpoints in a file system,
|
||||||
|
* allowing the agent's state to persist across multiple runs.
|
||||||
|
*
|
||||||
|
* This example shows:
|
||||||
|
* 1. How to create a file-based checkpoint provider
|
||||||
|
* 2. How to configure an agent with the file-based checkpoint provider
|
||||||
|
* 3. How to run an agent that automatically creates checkpoints
|
||||||
|
* 4. How to restore an agent from a checkpoint
|
||||||
|
*/
|
||||||
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
|
fun main() = runBlocking {
|
||||||
|
// Create a temporary directory for storing checkpoints
|
||||||
|
val checkpointDir = Files.createTempDirectory("agent-checkpoints")
|
||||||
|
println("Checkpoint directory: $checkpointDir")
|
||||||
|
|
||||||
|
// Create the file-based checkpoint provider
|
||||||
|
val provider = JVMFilePersistenceStorageProvider(checkpointDir)
|
||||||
|
|
||||||
|
// Create a unique agent ID to identify this agent's checkpoints
|
||||||
|
val agentId = "persistent-agent-example3"
|
||||||
|
|
||||||
|
// Create tool registry with basic tools
|
||||||
|
val toolRegistry = ToolRegistry {
|
||||||
|
tool(AskUser)
|
||||||
|
tool(SayToUser)
|
||||||
|
}
|
||||||
|
|
||||||
|
val customModel: LLModel = LLModel(
|
||||||
|
provider = LLMProvider.Ollama,
|
||||||
|
id = "gpt-oss:20b",
|
||||||
|
capabilities = listOf(
|
||||||
|
LLMCapability.Temperature
|
||||||
|
),
|
||||||
|
contextLength = 2048
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create agent config with a system prompt
|
||||||
|
val agentConfig = AIAgentConfig(
|
||||||
|
prompt = prompt("persistent-agent") {
|
||||||
|
system("You are a helpful assistant that remembers conversations across sessions.")
|
||||||
|
},
|
||||||
|
model = customModel,
|
||||||
|
maxAgentIterations = 20
|
||||||
|
)
|
||||||
|
|
||||||
|
println("Creating and running agent with continuous persistence...")
|
||||||
|
|
||||||
|
simpleOllamaAIExecutor().use { executor ->
|
||||||
|
// Create the agent with the file-based checkpoint provider and continuous persistence
|
||||||
|
val agent = AIAgent(
|
||||||
|
promptExecutor = executor,
|
||||||
|
// strategy = SnapshotStrategy.strategy,
|
||||||
|
strategy = singleRunStrategy(),
|
||||||
|
agentConfig = agentConfig,
|
||||||
|
toolRegistry = toolRegistry,
|
||||||
|
id = agentId
|
||||||
|
) {
|
||||||
|
install(Persistence) {
|
||||||
|
storage = provider // Use the file-based checkpoint provider
|
||||||
|
enableAutomaticPersistence = true // Enable automatic checkpoint creation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the agent with an initial input
|
||||||
|
val result = agent.run("Hallo ik heet Erik")
|
||||||
|
println("Agent result: $result")
|
||||||
|
|
||||||
|
// Retrieve all checkpoints created during the agent's execution
|
||||||
|
val checkpoints = provider.getCheckpoints(agentId)
|
||||||
|
println("\nRetrieved ${checkpoints.size} checkpoints for agent $agentId")
|
||||||
|
|
||||||
|
// Print checkpoint details
|
||||||
|
checkpoints.forEachIndexed { index, checkpoint ->
|
||||||
|
println("Checkpoint ${index + 1}:")
|
||||||
|
println(" ID: ${checkpoint.checkpointId}")
|
||||||
|
println(" Created at: ${checkpoint.createdAt}")
|
||||||
|
println(" Node ID: ${checkpoint.nodeId}")
|
||||||
|
println(" Message history size: ${checkpoint.messageHistory.size}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the checkpoint files exist in the file system
|
||||||
|
val checkpointsDir = checkpointDir.resolve("checkpoints").resolve(agentId)
|
||||||
|
if (checkpointsDir.exists()) {
|
||||||
|
println("\nCheckpoint files in directory: ${checkpointsDir.toFile().list()?.joinToString()}")
|
||||||
|
}
|
||||||
|
|
||||||
|
println("\nNow creating a new agent instance with the same ID to demonstrate restoration...")
|
||||||
|
|
||||||
|
// Create a new agent instance with the same ID
|
||||||
|
// It will automatically restore from the latest checkpoint
|
||||||
|
val restoredAgent = AIAgent(
|
||||||
|
promptExecutor = executor,
|
||||||
|
// strategy = SnapshotStrategy.strategy,
|
||||||
|
strategy = singleRunStrategy(),
|
||||||
|
agentConfig = agentConfig,
|
||||||
|
toolRegistry = toolRegistry,
|
||||||
|
id = agentId
|
||||||
|
) {
|
||||||
|
install(Persistence) {
|
||||||
|
storage = provider // Use the file-based checkpoint provider
|
||||||
|
enableAutomaticPersistence = true // Enable automatic checkpoint creation
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the restored agent with a new input
|
||||||
|
// The agent will continue the conversation from where it left off
|
||||||
|
val restoredResult = restoredAgent.run("Hoe heet ik?")
|
||||||
|
println("Restored agent result: $restoredResult")
|
||||||
|
|
||||||
|
// Get the latest checkpoint after the second run
|
||||||
|
val latestCheckpoint = provider.getLatestCheckpoint(agentId)
|
||||||
|
println("\nLatest checkpoint after restoration:")
|
||||||
|
println(" ID: ${latestCheckpoint?.checkpointId}")
|
||||||
|
println(" Created at: ${latestCheckpoint?.createdAt}")
|
||||||
|
println(" Node ID: ${latestCheckpoint?.nodeId}")
|
||||||
|
println(" Message history size: ${latestCheckpoint?.messageHistory?.size}")
|
||||||
|
}
|
||||||
|
}
|
||||||
188
src/main/kotlin/nl/trivion/softwarefabric/agents/AgentTester.kt
Normal file
188
src/main/kotlin/nl/trivion/softwarefabric/agents/AgentTester.kt
Normal file
@@ -0,0 +1,188 @@
|
|||||||
|
package nl.trivion.softwarefabric.agents
|
||||||
|
|
||||||
|
import ai.koog.agents.core.agent.AIAgent
|
||||||
|
import ai.koog.agents.core.agent.config.AIAgentConfig
|
||||||
|
import ai.koog.agents.core.dsl.builder.forwardTo
|
||||||
|
import ai.koog.agents.core.dsl.builder.strategy
|
||||||
|
import ai.koog.agents.core.dsl.extension.nodeLLMRequest
|
||||||
|
import ai.koog.agents.features.eventHandler.feature.EventHandler
|
||||||
|
import ai.koog.agents.snapshot.feature.Persistence
|
||||||
|
import ai.koog.agents.snapshot.providers.InMemoryPersistenceStorageProvider
|
||||||
|
import ai.koog.agents.snapshot.providers.PersistenceStorageProvider
|
||||||
|
import ai.koog.agents.snapshot.providers.file.JVMFilePersistenceStorageProvider
|
||||||
|
import ai.koog.prompt.dsl.Prompt
|
||||||
|
import ai.koog.prompt.executor.llms.all.simpleOllamaAIExecutor
|
||||||
|
import ai.koog.prompt.llm.LLMCapability
|
||||||
|
import ai.koog.prompt.llm.LLMProvider
|
||||||
|
import ai.koog.prompt.llm.LLModel
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import org.slf4j.LoggerFactory
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
class AgentTester {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LoggerFactory.getLogger(AgentTester::class.java)
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun main(args: Array<String>) {
|
||||||
|
println("=== Simple Agent Persistence Test ===")
|
||||||
|
println()
|
||||||
|
|
||||||
|
// Parse command line arguments
|
||||||
|
val issueNumber = args.getOrNull(0)?.toIntOrNull() ?: 1
|
||||||
|
val message = args.getOrNull(1) ?: "Ik heet Erik."
|
||||||
|
|
||||||
|
println("Testing with:")
|
||||||
|
println("- Issue number: $issueNumber")
|
||||||
|
println("- Message: $message")
|
||||||
|
println()
|
||||||
|
|
||||||
|
val tester = AgentTester()
|
||||||
|
|
||||||
|
runBlocking {
|
||||||
|
tester.testAgent(issueNumber, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val promptExecutor = simpleOllamaAIExecutor("http://localhost:11434")
|
||||||
|
|
||||||
|
private val customModel: LLModel = LLModel(
|
||||||
|
provider = LLMProvider.Ollama,
|
||||||
|
id = "gpt-oss:20b",
|
||||||
|
capabilities = listOf(
|
||||||
|
LLMCapability.Temperature
|
||||||
|
),
|
||||||
|
contextLength = 2048
|
||||||
|
)
|
||||||
|
|
||||||
|
private val systemPrompt = """
|
||||||
|
You are a helpful assistant. Keep your responses concise and friendly.
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
private val agentConfig = AIAgentConfig(
|
||||||
|
prompt = Prompt.build("simple-chat") {
|
||||||
|
system(systemPrompt)
|
||||||
|
},
|
||||||
|
model = customModel,
|
||||||
|
maxAgentIterations = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun createAgent(agentId: String, provider: PersistenceStorageProvider<*>): AIAgent<String, String> {
|
||||||
|
|
||||||
|
return AIAgent(
|
||||||
|
promptExecutor = promptExecutor,
|
||||||
|
agentConfig = agentConfig,
|
||||||
|
id = agentId,
|
||||||
|
installFeatures = {
|
||||||
|
install(EventHandler) {
|
||||||
|
onAgentCompleted { result ->
|
||||||
|
log.info("✓ Agent completed")
|
||||||
|
log.info(" Result: ${result.result}")
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
install(Persistence) {
|
||||||
|
storage = provider
|
||||||
|
enableAutomaticPersistence = true
|
||||||
|
|
||||||
|
log.info("✓ Persistence installed with automatic persistence enabled")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun testAgent(issueNumber: Int, message: String) {
|
||||||
|
val agentId = "developer-agent-issue-$issueNumber"
|
||||||
|
|
||||||
|
println("Creating agent with ID: $agentId")
|
||||||
|
println("-".repeat(50))
|
||||||
|
|
||||||
|
try {
|
||||||
|
val persistenceDir = File("./agent-persistence-test")
|
||||||
|
|
||||||
|
if (!persistenceDir.exists()) {
|
||||||
|
persistenceDir.mkdirs()
|
||||||
|
log.info("Created persistence directory: ${persistenceDir.absolutePath}")
|
||||||
|
}
|
||||||
|
|
||||||
|
// val provider = JVMFilePersistenceStorageProvider(persistenceDir.toPath())
|
||||||
|
val provider = InMemoryPersistenceStorageProvider()
|
||||||
|
|
||||||
|
log.info("Creating agent with ID: $agentId")
|
||||||
|
log.info("Persistence directory: ${persistenceDir.absolutePath}")
|
||||||
|
|
||||||
|
var agent = createAgent(agentId, provider)
|
||||||
|
|
||||||
|
println()
|
||||||
|
println("Running agent with message: '$message'")
|
||||||
|
println("-".repeat(50))
|
||||||
|
|
||||||
|
var result = agent.run(message)
|
||||||
|
|
||||||
|
println()
|
||||||
|
println("=== RESULT ===")
|
||||||
|
println(result)
|
||||||
|
println()
|
||||||
|
|
||||||
|
agent = createAgent(agentId, provider)
|
||||||
|
result = agent.run("Hoe heet ik?")
|
||||||
|
agent.close()
|
||||||
|
|
||||||
|
|
||||||
|
println()
|
||||||
|
println("=== RESULT ===")
|
||||||
|
println(result)
|
||||||
|
println()
|
||||||
|
|
||||||
|
// Wacht even zodat persistence kan schrijven
|
||||||
|
kotlinx.coroutines.delay(1000)
|
||||||
|
|
||||||
|
// Check wat er is opgeslagen
|
||||||
|
val agentDir = File(persistenceDir, "checkpoints/" + agentId)
|
||||||
|
|
||||||
|
println("=== PERSISTENCE CHECK ===")
|
||||||
|
if (agentDir.exists()) {
|
||||||
|
println("Snapshot directory exists: ${agentDir.absolutePath}")
|
||||||
|
agentDir.listFiles()?.forEach {
|
||||||
|
println(" - ${it.name} (${it.length()} bytes, modified: ${java.util.Date(it.lastModified())})")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
println("WARNING: No snapshot directory found!")
|
||||||
|
}
|
||||||
|
println()
|
||||||
|
|
||||||
|
println("=== TEST COMPLETED ===")
|
||||||
|
println("To continue this conversation, run:")
|
||||||
|
println(" ./gradlew run --args=\"$issueNumber 'your next message'\"")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
log.error("Error during test: ${e.message}", e)
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Usage examples:
|
||||||
|
*
|
||||||
|
* Run with default values:
|
||||||
|
* ./gradlew run
|
||||||
|
*
|
||||||
|
* Run with specific issue number:
|
||||||
|
* ./gradlew run --args="5"
|
||||||
|
*
|
||||||
|
* Run with issue number and message:
|
||||||
|
* ./gradlew run --args="5 'Tell me a joke'"
|
||||||
|
*
|
||||||
|
* Continue conversation (reuse same issue number):
|
||||||
|
* ./gradlew run --args="5 'Tell me another one'"
|
||||||
|
*
|
||||||
|
* This simple agent has:
|
||||||
|
* - No tools
|
||||||
|
* - No custom strategy
|
||||||
|
* - Only basic chat functionality
|
||||||
|
* - Persistence enabled
|
||||||
|
*/
|
||||||
@@ -99,6 +99,14 @@ class DesignerAgent(private val giteaService: GiteaService) {
|
|||||||
(nodeSendToolResult forwardTo nodeExecuteTool)
|
(nodeSendToolResult forwardTo nodeExecuteTool)
|
||||||
onToolCall { true }
|
onToolCall { true }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
edge(
|
||||||
|
(nodeSendToolResult forwardTo nodeFinish)
|
||||||
|
onAssistantMessage {
|
||||||
|
log.info("content: ${it.content}")
|
||||||
|
it.content.startsWith("Question is send to")
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val systemPrompt = this::class.java.getResource("/prompts/designer-system-prompt.txt")
|
val systemPrompt = this::class.java.getResource("/prompts/designer-system-prompt.txt")
|
||||||
|
|||||||
@@ -0,0 +1,53 @@
|
|||||||
|
package nl.trivion.softwarefabric.agents
|
||||||
|
|
||||||
|
import ai.koog.agents.core.dsl.builder.AIAgentNodeDelegate
|
||||||
|
import ai.koog.agents.core.dsl.builder.AIAgentSubgraphBuilderBase
|
||||||
|
import ai.koog.agents.core.dsl.builder.forwardTo
|
||||||
|
import ai.koog.agents.core.dsl.builder.strategy
|
||||||
|
import ai.koog.agents.snapshot.feature.withPersistence
|
||||||
|
import kotlinx.serialization.json.JsonPrimitive
|
||||||
|
|
||||||
|
private fun AIAgentSubgraphBuilderBase<*, *>.simpleNode(
|
||||||
|
name: String? = null,
|
||||||
|
output: String,
|
||||||
|
): AIAgentNodeDelegate<String, String> = node(name) {
|
||||||
|
return@node it + output
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class TeleportState(var teleported: Boolean = false)
|
||||||
|
|
||||||
|
private fun AIAgentSubgraphBuilderBase<*, *>.teleportNode(
|
||||||
|
name: String? = null,
|
||||||
|
teleportState: TeleportState = TeleportState()
|
||||||
|
): AIAgentNodeDelegate<String, String> = node(name) {
|
||||||
|
if (!teleportState.teleported) {
|
||||||
|
teleportState.teleported = true
|
||||||
|
withPersistence {
|
||||||
|
setExecutionPoint(it, "Node1", listOf(), JsonPrimitive("Teleported!!!"))
|
||||||
|
return@withPersistence "Teleported"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return@node "$it\nAlready teleported, passing by"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
object SnapshotStrategy {
|
||||||
|
private val teleportState = TeleportState()
|
||||||
|
|
||||||
|
val strategy = strategy("test") {
|
||||||
|
val node1 by simpleNode(
|
||||||
|
"Node1",
|
||||||
|
output = "Node 1 output"
|
||||||
|
)
|
||||||
|
val node2 by simpleNode(
|
||||||
|
"Node2",
|
||||||
|
output = "Node 2 output"
|
||||||
|
)
|
||||||
|
val teleportNode by teleportNode(teleportState = teleportState)
|
||||||
|
|
||||||
|
edge(nodeStart forwardTo node1)
|
||||||
|
edge(node1 forwardTo node2)
|
||||||
|
edge(node2 forwardTo teleportNode)
|
||||||
|
edge(teleportNode forwardTo nodeFinish)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package nl.trivion.softwarefabric.client
|
||||||
|
|
||||||
|
import ai.koog.a2a.client.A2AClient
|
||||||
|
import ai.koog.a2a.client.UrlAgentCardResolver
|
||||||
|
import ai.koog.a2a.consts.A2AConsts
|
||||||
|
import ai.koog.a2a.model.Message
|
||||||
|
import ai.koog.a2a.model.MessageSendConfiguration
|
||||||
|
import ai.koog.a2a.model.MessageSendParams
|
||||||
|
import ai.koog.a2a.model.Role
|
||||||
|
import ai.koog.a2a.model.TextPart
|
||||||
|
import ai.koog.a2a.transport.Request
|
||||||
|
import ai.koog.a2a.transport.client.jsonrpc.http.HttpJSONRPCClientTransport
|
||||||
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
|
suspend fun main() {
|
||||||
|
val agentUrl = "http://localhost:8080/a2a"
|
||||||
|
|
||||||
|
val cardResolver = UrlAgentCardResolver(
|
||||||
|
baseUrl = agentUrl,
|
||||||
|
path = A2AConsts.AGENT_CARD_WELL_KNOWN_PATH,
|
||||||
|
)
|
||||||
|
|
||||||
|
val transport = HttpJSONRPCClientTransport(
|
||||||
|
url = agentUrl,
|
||||||
|
)
|
||||||
|
|
||||||
|
val a2aClient = A2AClient(
|
||||||
|
transport = transport,
|
||||||
|
agentCardResolver = cardResolver
|
||||||
|
)
|
||||||
|
|
||||||
|
// Initialize client and fetch the card
|
||||||
|
println("connect")
|
||||||
|
a2aClient.connect()
|
||||||
|
println("connected")
|
||||||
|
|
||||||
|
val message = Message(
|
||||||
|
role = Role.User,
|
||||||
|
parts = listOf(
|
||||||
|
TextPart("Hoe heet ik?")
|
||||||
|
),
|
||||||
|
contextId = "blog-project-456",
|
||||||
|
messageId = Uuid.random().toString()
|
||||||
|
)
|
||||||
|
|
||||||
|
val request = Request(
|
||||||
|
data = MessageSendParams(
|
||||||
|
message = message,
|
||||||
|
configuration = MessageSendConfiguration(
|
||||||
|
blocking = false, // Get first response
|
||||||
|
historyLength = 5 // Include context
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
val response = a2aClient.sendMessage(request)
|
||||||
|
println(response.data)
|
||||||
|
}
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
package nl.trivion.softwarefabric.controllers
|
||||||
|
|
||||||
|
import ai.koog.a2a.consts.A2AConsts
|
||||||
|
import ai.koog.a2a.model.*
|
||||||
|
import ai.koog.a2a.server.A2AServer
|
||||||
|
import ai.koog.a2a.server.agent.AgentExecutor
|
||||||
|
import ai.koog.a2a.server.session.RequestContext
|
||||||
|
import ai.koog.a2a.server.session.SessionEventProcessor
|
||||||
|
import ai.koog.a2a.transport.server.jsonrpc.http.HttpJSONRPCServerTransport
|
||||||
|
import ai.koog.agents.a2a.core.A2AMessage
|
||||||
|
import ai.koog.agents.a2a.core.toA2AMessage
|
||||||
|
import ai.koog.agents.a2a.core.toKoogMessage
|
||||||
|
import ai.koog.agents.a2a.server.feature.A2AAgentServer
|
||||||
|
import ai.koog.agents.a2a.server.feature.nodeA2ARespondMessage
|
||||||
|
import ai.koog.agents.core.agent.AIAgent
|
||||||
|
import ai.koog.agents.core.agent.config.AIAgentConfig
|
||||||
|
import ai.koog.agents.core.dsl.builder.strategy
|
||||||
|
import ai.koog.agents.features.eventHandler.feature.handleEvents
|
||||||
|
import ai.koog.prompt.dsl.Prompt
|
||||||
|
import ai.koog.prompt.executor.llms.all.simpleOllamaAIExecutor
|
||||||
|
import ai.koog.prompt.llm.LLMCapability
|
||||||
|
import ai.koog.prompt.llm.LLMProvider
|
||||||
|
import ai.koog.prompt.llm.LLModel
|
||||||
|
import io.ktor.server.cio.CIO
|
||||||
|
import nl.trivion.softwarefabric.storage.FileMessageStorage
|
||||||
|
import kotlin.uuid.ExperimentalUuidApi
|
||||||
|
import kotlin.uuid.Uuid
|
||||||
|
|
||||||
|
class A2AController2 {
|
||||||
|
|
||||||
|
@OptIn(ExperimentalUuidApi::class)
|
||||||
|
fun blogpostWritingStrategy() = strategy<MessageSendParams, A2AMessage>("blogpost-writer-strategy") {
|
||||||
|
val blogpostRequest by node<MessageSendParams, A2AMessage> { input ->
|
||||||
|
val userMessage = input.message.toKoogMessage().content
|
||||||
|
val contextId = input.message.contextId
|
||||||
|
val taskId = input.message.taskId
|
||||||
|
|
||||||
|
llm.writeSession {
|
||||||
|
appendPrompt { user(userMessage) }
|
||||||
|
val llmResponse = requestLLM()
|
||||||
|
A2AMessage(
|
||||||
|
messageId = Uuid.random().toString(),
|
||||||
|
role = Role.Agent,
|
||||||
|
parts = listOf(TextPart(llmResponse.content)),
|
||||||
|
contextId = contextId,
|
||||||
|
taskId = taskId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val sendMessage by nodeA2ARespondMessage()
|
||||||
|
|
||||||
|
nodeStart then blogpostRequest then sendMessage then nodeFinish
|
||||||
|
}
|
||||||
|
|
||||||
|
private val customModel: LLModel = LLModel(
|
||||||
|
provider = LLMProvider.Ollama,
|
||||||
|
id = "gpt-oss:20b",
|
||||||
|
capabilities = listOf(LLMCapability.Temperature),
|
||||||
|
contextLength = 2048
|
||||||
|
)
|
||||||
|
|
||||||
|
private val systemPrompt = """
|
||||||
|
You are a helpful assistant. Keep your responses concise and friendly.
|
||||||
|
""".trimIndent()
|
||||||
|
|
||||||
|
private val promptExecutor = simpleOllamaAIExecutor("http://localhost:11434")
|
||||||
|
|
||||||
|
suspend fun createBlogpostWritingAgent(
|
||||||
|
requestContext: RequestContext<MessageSendParams>,
|
||||||
|
eventProcessor: SessionEventProcessor
|
||||||
|
): AIAgent<MessageSendParams, A2AMessage> {
|
||||||
|
requestContext.messageStorage.save(requestContext.params.message)
|
||||||
|
// De A2AServer heeft al het inkomende bericht opgeslagen via de messageStorageFactory
|
||||||
|
// Haal de volledige geschiedenis op (inclusief het huidige bericht)
|
||||||
|
val messageHistory = requestContext.messageStorage.getAll().map { it.toKoogMessage() }
|
||||||
|
|
||||||
|
val agentConfig = AIAgentConfig(
|
||||||
|
prompt = Prompt.build("simple-chat") {
|
||||||
|
system(systemPrompt)
|
||||||
|
messages(messageHistory)
|
||||||
|
},
|
||||||
|
model = customModel,
|
||||||
|
maxAgentIterations = 5
|
||||||
|
)
|
||||||
|
|
||||||
|
return AIAgent(
|
||||||
|
promptExecutor = promptExecutor,
|
||||||
|
strategy = blogpostWritingStrategy(),
|
||||||
|
agentConfig = agentConfig
|
||||||
|
) {
|
||||||
|
install(A2AAgentServer) {
|
||||||
|
this.context = requestContext
|
||||||
|
this.eventProcessor = eventProcessor
|
||||||
|
}
|
||||||
|
|
||||||
|
handleEvents {
|
||||||
|
onAgentCompleted { ctx ->
|
||||||
|
val resultMessage = ctx.result as A2AMessage
|
||||||
|
// Sla het antwoord op via de messageStorage van de context
|
||||||
|
requestContext.messageStorage.save(resultMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class BlogpostAgentExecutor : AgentExecutor {
|
||||||
|
override suspend fun execute(
|
||||||
|
context: RequestContext<MessageSendParams>,
|
||||||
|
eventProcessor: SessionEventProcessor
|
||||||
|
) {
|
||||||
|
createBlogpostWritingAgent(context, eventProcessor)
|
||||||
|
.run(context.params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val agentCard = AgentCard(
|
||||||
|
name = "Blog Writer",
|
||||||
|
description = "AI agent that creates high-quality blog posts and articles",
|
||||||
|
url = "https://api.blog-writer.com/a2a/v1",
|
||||||
|
version = "1.0.0",
|
||||||
|
capabilities = AgentCapabilities(streaming = true),
|
||||||
|
defaultInputModes = listOf("text/plain"),
|
||||||
|
defaultOutputModes = listOf("text/markdown"),
|
||||||
|
skills = listOf(
|
||||||
|
AgentSkill(
|
||||||
|
id = "write-post",
|
||||||
|
name = "Blog Post Writing",
|
||||||
|
description = "Generate engaging blog posts on any topic",
|
||||||
|
tags = listOf("writing", "content", "blog"),
|
||||||
|
examples = listOf("Write a post about AI trends")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
suspend fun start() {
|
||||||
|
// Maak een file-based message storage aan
|
||||||
|
val fileMessageStorage = FileMessageStorage(
|
||||||
|
storageDir = "./a2a-messages"
|
||||||
|
)
|
||||||
|
|
||||||
|
val a2aServer = A2AServer(
|
||||||
|
agentExecutor = BlogpostAgentExecutor(),
|
||||||
|
agentCard = agentCard,
|
||||||
|
messageStorage = fileMessageStorage
|
||||||
|
)
|
||||||
|
|
||||||
|
val transport = HttpJSONRPCServerTransport(
|
||||||
|
requestHandler = a2aServer
|
||||||
|
)
|
||||||
|
|
||||||
|
transport.start(
|
||||||
|
engineFactory = CIO,
|
||||||
|
port = 8080,
|
||||||
|
path = "/a2a",
|
||||||
|
wait = true,
|
||||||
|
agentCard = agentCard,
|
||||||
|
agentCardPath = A2AConsts.AGENT_CARD_WELL_KNOWN_PATH
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun main() {
|
||||||
|
val controller = A2AController2()
|
||||||
|
controller.start()
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
package nl.trivion.softwarefabric.storage
|
||||||
|
|
||||||
|
import ai.koog.a2a.model.Message
|
||||||
|
import ai.koog.a2a.server.messages.MessageStorage
|
||||||
|
import ai.koog.a2a.server.exceptions.MessageOperationException
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
import kotlinx.serialization.SerializationException
|
||||||
|
import java.io.File
|
||||||
|
import java.nio.file.Paths
|
||||||
|
import kotlinx.coroutines.sync.Mutex
|
||||||
|
import kotlinx.coroutines.sync.withLock
|
||||||
|
|
||||||
|
/**
|
||||||
|
* File-based implementation of MessageStorage for A2A server.
|
||||||
|
* Stores messages per contextId in separate JSON files.
|
||||||
|
* Thread-safe implementation using Mutex.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* ```
|
||||||
|
* val messageStorage = FileMessageStorage(storageDir = "./a2a-messages")
|
||||||
|
* val a2aServer = A2AServer(
|
||||||
|
* agentExecutor = myExecutor,
|
||||||
|
* agentCard = myCard,
|
||||||
|
* messageStorage = messageStorage
|
||||||
|
* )
|
||||||
|
* ```
|
||||||
|
*/
|
||||||
|
class FileMessageStorage(
|
||||||
|
private val storageDir: String = "./a2a-messages"
|
||||||
|
) : MessageStorage {
|
||||||
|
|
||||||
|
private val json = Json {
|
||||||
|
prettyPrint = true
|
||||||
|
ignoreUnknownKeys = true
|
||||||
|
}
|
||||||
|
|
||||||
|
private val mutex = Mutex()
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Zorg ervoor dat de storage directory bestaat
|
||||||
|
File(storageDir).mkdirs()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getContextFile(contextId: String): File {
|
||||||
|
// Sanitize contextId voor bestandsnaam (verwijder ongeldige karakters)
|
||||||
|
val safeContextId = contextId.replace(Regex("[^a-zA-Z0-9._-]"), "_")
|
||||||
|
return Paths.get(storageDir, "$safeContextId.json").toFile()
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun save(message: Message) {
|
||||||
|
val contextId = message.contextId ?: throw MessageOperationException(
|
||||||
|
"Cannot save message without contextId"
|
||||||
|
)
|
||||||
|
|
||||||
|
mutex.withLock {
|
||||||
|
try {
|
||||||
|
val file = getContextFile(contextId)
|
||||||
|
val messages = if (file.exists()) {
|
||||||
|
try {
|
||||||
|
val content = file.readText()
|
||||||
|
json.decodeFromString<MutableList<Message>>(content)
|
||||||
|
} catch (e: SerializationException) {
|
||||||
|
throw MessageOperationException(
|
||||||
|
"Failed to parse existing messages for context $contextId",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
mutableListOf()
|
||||||
|
}
|
||||||
|
|
||||||
|
messages.add(message)
|
||||||
|
|
||||||
|
val jsonContent = json.encodeToString(messages)
|
||||||
|
file.writeText(jsonContent)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is MessageOperationException) throw e
|
||||||
|
throw MessageOperationException(
|
||||||
|
"Failed to save message for context $contextId",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getByContext(contextId: String): List<Message> {
|
||||||
|
return mutex.withLock {
|
||||||
|
try {
|
||||||
|
val file = getContextFile(contextId)
|
||||||
|
if (file.exists()) {
|
||||||
|
val content = file.readText()
|
||||||
|
json.decodeFromString<List<Message>>(content)
|
||||||
|
} else {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
} catch (e: SerializationException) {
|
||||||
|
throw MessageOperationException(
|
||||||
|
"Failed to read messages for context $contextId",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
throw MessageOperationException(
|
||||||
|
"Failed to retrieve messages for context $contextId",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteByContext(contextId: String) {
|
||||||
|
mutex.withLock {
|
||||||
|
try {
|
||||||
|
val file = getContextFile(contextId)
|
||||||
|
if (file.exists() && !file.delete()) {
|
||||||
|
throw MessageOperationException(
|
||||||
|
"Failed to delete messages for context $contextId"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is MessageOperationException) throw e
|
||||||
|
throw MessageOperationException(
|
||||||
|
"Failed to delete messages for context $contextId",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun replaceByContext(contextId: String, messages: List<Message>) {
|
||||||
|
mutex.withLock {
|
||||||
|
try {
|
||||||
|
// Valideer dat alle messages de juiste contextId hebben
|
||||||
|
if (messages.any { it.contextId != contextId }) {
|
||||||
|
throw MessageOperationException(
|
||||||
|
"All messages must have contextId '$contextId'"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val file = getContextFile(contextId)
|
||||||
|
val jsonContent = json.encodeToString(messages)
|
||||||
|
file.writeText(jsonContent)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
if (e is MessageOperationException) throw e
|
||||||
|
throw MessageOperationException(
|
||||||
|
"Failed to replace messages for context $contextId",
|
||||||
|
e
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility: Clear all stored messages across all contexts
|
||||||
|
*/
|
||||||
|
suspend fun clearAll() {
|
||||||
|
mutex.withLock {
|
||||||
|
File(storageDir).listFiles()?.forEach { it.delete() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility: Get list of all context IDs that have stored messages
|
||||||
|
*/
|
||||||
|
fun getAllContextIds(): List<String> {
|
||||||
|
return File(storageDir).listFiles()
|
||||||
|
?.filter { it.extension == "json" }
|
||||||
|
?.map { it.nameWithoutExtension }
|
||||||
|
?: emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user