test
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -38,3 +38,5 @@ build/
|
||||
### Mac OS ###
|
||||
.DS_Store
|
||||
/.idea/
|
||||
/agent-persistence-test/
|
||||
/a2a-messages/
|
||||
|
||||
31
pom.xml
31
pom.xml
@@ -16,6 +16,7 @@
|
||||
<properties>
|
||||
<java.version>21</java.version>
|
||||
<kotlin.version>2.2.10</kotlin.version>
|
||||
<koog.version>0.5.3</koog.version>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
@@ -50,6 +51,10 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-web</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-reflect</artifactId>
|
||||
@@ -63,17 +68,37 @@
|
||||
<dependency>
|
||||
<groupId>ai.koog</groupId>
|
||||
<artifactId>koog-agents-jvm</artifactId>
|
||||
<version>0.5.0</version>
|
||||
<version>${koog.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.koog</groupId>
|
||||
<artifactId>prompt-cache-redis-jvm</artifactId>
|
||||
<version>0.5.0</version>
|
||||
<version>${koog.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.koog</groupId>
|
||||
<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>
|
||||
|
||||
<!-- 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)
|
||||
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")
|
||||
|
||||
@@ -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