diff --git a/pom.xml b/pom.xml
index 054d520..91b9924 100644
--- a/pom.xml
+++ b/pom.xml
@@ -3,15 +3,19 @@
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
4.0.0
-
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.5.3
+
+
nl.trivion
SoftwareFabric
1.0-SNAPSHOT
- UTF-8
- official
- 21
+ 21
+ 2.2.10
@@ -21,80 +25,134 @@
-
- src/main/kotlin
- src/test/kotlin
-
-
- org.jetbrains.kotlin
- kotlin-maven-plugin
- 2.2.10
-
-
- compile
- compile
-
- compile
-
-
-
- test-compile
- test-compile
-
- test-compile
-
-
-
-
-
- maven-surefire-plugin
- 2.22.2
-
-
- maven-failsafe-plugin
- 2.22.2
-
-
- org.codehaus.mojo
- exec-maven-plugin
- 1.6.0
-
- MainKt
-
-
-
-
+
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-bom
+ 1.10.2
+ pom
+ import
+
+
+ org.jetbrains.kotlinx
+ kotlinx-serialization-bom
+ 1.8.1
+ pom
+ import
+
+
+
+
- org.jetbrains.kotlin
- kotlin-test-junit5
- 2.2.10
- test
+ org.springframework.boot
+ spring-boot-starter
- org.junit.jupiter
- junit-jupiter
- 5.10.0
- test
+ org.jetbrains.kotlin
+ kotlin-reflect
org.jetbrains.kotlin
kotlin-stdlib
- 2.2.10
+
ai.koog
koog-agents-jvm
- 0.4.2
+ 0.5.0
-
+
+ ai.koog
+ prompt-cache-redis-jvm
+ 0.5.0
+
+
+ ai.koog
+ prompt-executor-model-jvm
+ 0.5.0
+
+
+
org.eclipse.jgit
org.eclipse.jgit
7.3.0.202506031305-r
+
+
+ org.slf4j
+ slf4j-api
+
+
+
+
+ com.squareup.okhttp3
+ okhttp
+ 4.12.0
+
+
+ com.google.code.gson
+ gson
+ 2.10.1
+
+
+ org.projectlombok
+ lombok
+ 1.18.30
+ provided
+
+
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-core
+ 1.10.2
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ org.jetbrains.kotlin
+ kotlin-test-junit5
+ test
+
+ ${project.basedir}/src/main/kotlin
+ ${project.basedir}/src/test/kotlin
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-plugin
+
+
+ -Xjsr305=strict
+
+
+ spring
+
+
+
+
+ org.jetbrains.kotlin
+ kotlin-maven-allopen
+ ${kotlin.version}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/kotlin/nl/trivion/softwarefabric/Main.kt b/src/main/kotlin/nl/trivion/softwarefabric/Main.kt
deleted file mode 100644
index 7946459..0000000
--- a/src/main/kotlin/nl/trivion/softwarefabric/Main.kt
+++ /dev/null
@@ -1,142 +0,0 @@
-package org.example.nl.trivion.softwarefabric
-
-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.nodeExecuteTool
-import ai.koog.agents.core.dsl.extension.nodeLLMRequest
-import ai.koog.agents.core.dsl.extension.nodeLLMSendToolResult
-import ai.koog.agents.core.dsl.extension.onAssistantMessage
-import ai.koog.agents.core.dsl.extension.onToolCall
-import ai.koog.agents.core.feature.handler.AgentFinishedContext
-import ai.koog.agents.core.feature.handler.AgentStartContext
-import ai.koog.agents.core.tools.ToolRegistry
-import ai.koog.agents.core.tools.annotations.LLMDescription
-import ai.koog.agents.core.tools.annotations.Tool
-import ai.koog.agents.core.tools.reflect.ToolSet
-import ai.koog.agents.core.tools.reflect.asTools
-import ai.koog.agents.features.eventHandler.feature.EventHandler
-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
-
-// Use the OpenAI executor with an API key from an environment variable
-val promptExecutor = simpleOllamaAIExecutor("http://localhost:11434")
-
-val customModel: LLModel = LLModel(
- provider = LLMProvider.Ollama,
- id = "gpt-oss:20b",
- capabilities = listOf(
- LLMCapability.Temperature,
- LLMCapability.Tools
- ),
- contextLength = 2048
-)
-
-// Create a simple strategy
-val agentStrategy = strategy("Simple calculator") {
- // Define nodes for the strategy
- val nodeSendInput by nodeLLMRequest()
- val nodeExecuteTool by nodeExecuteTool()
- val nodeSendToolResult by nodeLLMSendToolResult()
-
- // Define edges between nodes
- // Start -> Send input
- edge(nodeStart forwardTo nodeSendInput)
-
- // Send input -> Finish
- edge(
- (nodeSendInput forwardTo nodeFinish)
- transformed { it }
- onAssistantMessage { true }
- )
-
- // Send input -> Execute tool
- edge(
- (nodeSendInput forwardTo nodeExecuteTool)
- onToolCall { true }
- )
-
- // Execute tool -> Send the tool result
- edge(nodeExecuteTool forwardTo nodeSendToolResult)
-
- // Send the tool result -> finish
- edge(
- (nodeSendToolResult forwardTo nodeFinish)
- transformed { it }
- onAssistantMessage { true }
- )
-}
-
-// Configure the agent
-val agentConfig = AIAgentConfig(
- prompt = Prompt.build("simple-calculator") {
- system(
- """
- You are a simple calculator assistant.
- You can add two numbers together using the calculator tool.
- When the user provides input, extract the numbers they want to add.
- The input might be in various formats like "add 5 and 7", "5 + 7", or just "5 7".
- Extract the two numbers and use the calculator tool to add them.
- Always respond with a clear, friendly message showing the calculation and result.
- """.trimIndent()
- )
- },
- model = customModel,
- maxAgentIterations = 10
-)
-
-// Implement a simple calculator tool that can add two numbers
-@LLMDescription("Tools for performing basic arithmetic operations")
-class CalculatorTools : ToolSet {
- @Tool
- @LLMDescription("Add two numbers together and return their sum")
- fun add(
- @LLMDescription("First number to add (integer value)")
- num1: Int,
-
- @LLMDescription("Second number to add (integer value)")
- num2: Int
- ): String {
- val sum = num1 + num2
- return "The sum of $num1 and $num2 is: $sum"
- }
-}
-
-// Add the tool to the tool registry
-val toolRegistry = ToolRegistry {
- tools(CalculatorTools().asTools())
-}
-
-// Create the agent
-val agent = AIAgent(
- promptExecutor = promptExecutor,
- toolRegistry = toolRegistry,
- strategy = agentStrategy,
- agentConfig = agentConfig,
- installFeatures = {
- install(EventHandler) {
- onBeforeAgentStarted { eventContext: AgentStartContext<*> ->
- println("Starting strategy: ${eventContext.strategy.name}")
- }
- onAgentFinished { eventContext: AgentFinishedContext ->
- println("Result: ${eventContext.result}")
- }
- }
- }
-)
-
-fun main() {
- runBlocking {
- println("Enter two numbers to add (e.g., 'add 5 and 7' or '5 + 7'):")
-
- // Read the user input and send it to the agent
- val userInput = readlnOrNull() ?: ""
- val agentResult = agent.run(userInput)
- println("The agent returned: $agentResult")
- }
-}
\ No newline at end of file
diff --git a/src/main/kotlin/nl/trivion/softwarefabric/SoftwareFabricApplication.kt b/src/main/kotlin/nl/trivion/softwarefabric/SoftwareFabricApplication.kt
new file mode 100644
index 0000000..9eca39a
--- /dev/null
+++ b/src/main/kotlin/nl/trivion/softwarefabric/SoftwareFabricApplication.kt
@@ -0,0 +1,13 @@
+package nl.trivion.softwarefabric
+
+import org.springframework.boot.autoconfigure.SpringBootApplication
+import org.springframework.boot.runApplication
+import org.springframework.scheduling.annotation.EnableScheduling
+
+@SpringBootApplication
+@EnableScheduling
+class SoftwareFabricApplication
+
+fun main(args: Array) {
+ runApplication(*args)
+}
\ No newline at end of file
diff --git a/src/main/kotlin/nl/trivion/softwarefabric/agents/DeveloperAgent.kt b/src/main/kotlin/nl/trivion/softwarefabric/agents/DeveloperAgent.kt
new file mode 100644
index 0000000..5ef91f8
--- /dev/null
+++ b/src/main/kotlin/nl/trivion/softwarefabric/agents/DeveloperAgent.kt
@@ -0,0 +1,184 @@
+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.nodeExecuteTool
+import ai.koog.agents.core.dsl.extension.nodeLLMRequest
+import ai.koog.agents.core.dsl.extension.nodeLLMSendToolResult
+import ai.koog.agents.core.dsl.extension.onAssistantMessage
+import ai.koog.agents.core.dsl.extension.onToolCall
+import ai.koog.agents.core.tools.ToolRegistry
+import ai.koog.agents.core.tools.reflect.asTools
+import ai.koog.agents.features.eventHandler.feature.EventHandler
+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.GlobalScope
+import kotlinx.coroutines.launch
+import lombok.extern.slf4j.Slf4j
+import nl.trivion.softwarefabric.service.GiteaService
+import nl.trivion.softwarefabric.service.Issue
+import org.example.nl.trivion.softwarefabric.tools.GitTools
+import org.slf4j.LoggerFactory
+import org.springframework.scheduling.annotation.Scheduled
+import org.springframework.stereotype.Service
+import java.util.concurrent.atomic.AtomicBoolean
+
+@Service
+@Slf4j
+class DeveloperAgent(private val giteaService: GiteaService) {
+
+ companion object {
+ private val log = LoggerFactory.getLogger(DeveloperAgent::class.java)
+ }
+
+ // Flag to track if agent is busy
+ private val isAgentBusy = AtomicBoolean(false)
+
+ val promptExecutor = simpleOllamaAIExecutor("http://localhost:11434")
+
+ val customModel: LLModel = LLModel(
+ provider = LLMProvider.Ollama,
+ id = "gpt-oss:20b",
+ capabilities = listOf(
+ LLMCapability.Temperature,
+ LLMCapability.Tools
+ ),
+ contextLength = 2048
+ )
+
+ // Create a simple strategy
+ val agentStrategy = strategy("Developer") {
+ // Define nodes for the strategy
+ val nodeSendInput by nodeLLMRequest()
+ val nodeExecuteTool by nodeExecuteTool()
+ val nodeSendToolResult by nodeLLMSendToolResult()
+
+ // Define edges between nodes
+ // Start -> Send input
+ edge(nodeStart forwardTo nodeSendInput)
+
+ // Send input -> Finish
+ edge(
+ (nodeSendInput forwardTo nodeFinish)
+ transformed { it }
+ onAssistantMessage { true }
+ )
+
+ // Send input -> Execute tool
+ edge(
+ (nodeSendInput forwardTo nodeExecuteTool)
+ onToolCall { true }
+ )
+
+ // Execute tool -> Send the tool result
+ edge(nodeExecuteTool forwardTo nodeSendToolResult)
+
+ // Send the tool result -> finish
+ edge(
+ (nodeSendToolResult forwardTo nodeFinish)
+ transformed { it }
+ onAssistantMessage { true }
+ )
+ }
+
+ // Configure the agent
+ val agentConfig = AIAgentConfig(
+ prompt = Prompt.build("developer") {
+ system(
+ """
+ You are a git repository managing agent. You have the git tool to clone a remote git repo.
+ The remote url you can use is "http://dixienas:3001/Trivion/tasklist.git".
+ The project name is "tasklist".
+ """.trimIndent()
+ )
+ },
+ model = customModel,
+ maxAgentIterations = 10
+ )
+
+ // Add the tool to the tool registry
+ val toolRegistry = ToolRegistry {
+ tools(GitTools().asTools()
+ )
+ }
+
+ // Create the agent
+// val agent = AIAgent(
+// promptExecutor = promptExecutor,
+// toolRegistry = toolRegistry,
+// strategy = agentStrategy,
+// agentConfig = agentConfig,
+// installFeatures = {
+// install(EventHandler) {
+// onAgentCompleted { it ->
+// log.info("Result: ${it.result}")
+// }
+// }
+// }
+// )
+
+ fun createAgent(): AIAgent {
+ return AIAgent(
+ promptExecutor = promptExecutor,
+ toolRegistry = toolRegistry,
+ strategy = agentStrategy,
+ agentConfig = agentConfig,
+ installFeatures = {
+ install(EventHandler) {
+ onAgentCompleted { it ->
+ log.info("Result: ${it.result}")
+ isAgentBusy.set(false)
+ }
+ }
+ }
+ )
+ }
+
+ @Scheduled(fixedRate = 5000)
+ fun checkIssueToDo() {
+ // Check if agent is already busy
+ if (isAgentBusy.get()) {
+ log.info("Agent is still busy processing previous issue, skipping this cycle")
+ return
+ }
+
+ val issues = giteaService.getIssues().filter { it: Issue ->
+ it.labels.contains("Backlog")
+ && it.assigned.isNullOrEmpty()
+ }
+
+ if (issues.isEmpty()) {
+ log.info("No issues found")
+ return
+ }
+
+ val issue = issues.first()
+ log.info("Processing issue ${issue.number}: ${issue.title}")
+
+ // Set agent as busy before starting
+ if (!isAgentBusy.compareAndSet(false, true)) {
+ log.info("Agent became busy while checking, skipping")
+ return
+ }
+
+ GlobalScope.launch {
+ try {
+ giteaService.updateIssueLabels(issue.number, listOf("Build"))
+ val agent = createAgent()
+ val result = agent.run(issue.body)
+ log.info("Agent completed with result: $result")
+ giteaService.updateIssueLabels(issue.number, listOf("Verify"))
+ } catch (e: Exception) {
+ log.error("Error processing issue: ${e.message}", e)
+ } finally {
+ // Always reset the busy flag
+ isAgentBusy.set(false)
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/kotlin/nl/trivion/softwarefabric/service/GiteaService.kt b/src/main/kotlin/nl/trivion/softwarefabric/service/GiteaService.kt
new file mode 100644
index 0000000..76a97f1
--- /dev/null
+++ b/src/main/kotlin/nl/trivion/softwarefabric/service/GiteaService.kt
@@ -0,0 +1,261 @@
+package nl.trivion.softwarefabric.service
+
+import ai.koog.agents.core.tools.annotations.LLMDescription
+import ai.koog.agents.core.tools.annotations.Tool
+import com.google.gson.Gson
+import com.google.gson.annotations.SerializedName
+import lombok.extern.slf4j.Slf4j
+import nl.trivion.softwarefabric.agents.DeveloperAgent
+import okhttp3.*
+import okhttp3.MediaType.Companion.toMediaType
+import okhttp3.RequestBody.Companion.toRequestBody
+import org.slf4j.LoggerFactory
+import org.springframework.beans.factory.annotation.Value
+import org.springframework.stereotype.Component
+import org.springframework.stereotype.Service
+
+// Model classes
+data class Issue(
+ val number: Int,
+ val title: String,
+ val body: String,
+ val assigned: String?,
+ val labels: List
+)
+
+// Response classes voor parsing
+data class GiteaIssueResponse(
+ val id: Int,
+ val number: Int,
+ val title: String,
+ val body: String,
+ val assignee: Assignee?,
+ val labels: List