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