eerste check in
This commit is contained in:
174
pom.xml
174
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">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-parent</artifactId>
|
||||
<version>3.5.3</version>
|
||||
<relativePath/> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<groupId>nl.trivion</groupId>
|
||||
<artifactId>SoftwareFabric</artifactId>
|
||||
<version>1.0-SNAPSHOT</version>
|
||||
|
||||
<properties>
|
||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||
<kotlin.code.style>official</kotlin.code.style>
|
||||
<kotlin.compiler.jvmTarget>21</kotlin.compiler.jvmTarget>
|
||||
<java.version>21</java.version>
|
||||
<kotlin.version>2.2.10</kotlin.version>
|
||||
</properties>
|
||||
|
||||
<repositories>
|
||||
@@ -21,80 +25,134 @@
|
||||
</repository>
|
||||
</repositories>
|
||||
|
||||
<build>
|
||||
<sourceDirectory>src/main/kotlin</sourceDirectory>
|
||||
<testSourceDirectory>src/test/kotlin</testSourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<version>2.2.10</version>
|
||||
<executions>
|
||||
<execution>
|
||||
<id>compile</id>
|
||||
<phase>compile</phase>
|
||||
<goals>
|
||||
<goal>compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
<execution>
|
||||
<id>test-compile</id>
|
||||
<phase>test-compile</phase>
|
||||
<goals>
|
||||
<goal>test-compile</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-surefire-plugin</artifactId>
|
||||
<version>2.22.2</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<artifactId>maven-failsafe-plugin</artifactId>
|
||||
<version>2.22.2</version>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.codehaus.mojo</groupId>
|
||||
<artifactId>exec-maven-plugin</artifactId>
|
||||
<version>1.6.0</version>
|
||||
<configuration>
|
||||
<mainClass>MainKt</mainClass>
|
||||
</configuration>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlinx</groupId>
|
||||
<artifactId>kotlinx-coroutines-bom</artifactId>
|
||||
<version>1.10.2</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlinx</groupId>
|
||||
<artifactId>kotlinx-serialization-bom</artifactId>
|
||||
<version>1.8.1</version> <!-- Of nieuwer -->
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</dependencyManagement>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring & Kotlin base -->
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-test-junit5</artifactId>
|
||||
<version>2.2.10</version>
|
||||
<scope>test</scope>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.jupiter</groupId>
|
||||
<artifactId>junit-jupiter</artifactId>
|
||||
<version>5.10.0</version>
|
||||
<scope>test</scope>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-reflect</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-stdlib</artifactId>
|
||||
<version>2.2.10</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Koog AI dependencies -->
|
||||
<dependency>
|
||||
<groupId>ai.koog</groupId>
|
||||
<artifactId>koog-agents-jvm</artifactId>
|
||||
<version>0.4.2</version>
|
||||
<version>0.5.0</version>
|
||||
</dependency>
|
||||
<!-- https://mvnrepository.com/artifact/org.eclipse.jgit/org.eclipse.jgit -->
|
||||
<dependency>
|
||||
<groupId>ai.koog</groupId>
|
||||
<artifactId>prompt-cache-redis-jvm</artifactId>
|
||||
<version>0.5.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>ai.koog</groupId>
|
||||
<artifactId>prompt-executor-model-jvm</artifactId>
|
||||
<version>0.5.0</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Utilities -->
|
||||
<dependency>
|
||||
<groupId>org.eclipse.jgit</groupId>
|
||||
<artifactId>org.eclipse.jgit</artifactId>
|
||||
<version>7.3.0.202506031305-r</version>
|
||||
<exclusions>
|
||||
<exclusion>
|
||||
<groupId>org.slf4j</groupId>
|
||||
<artifactId>slf4j-api</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.squareup.okhttp3</groupId>
|
||||
<artifactId>okhttp</artifactId>
|
||||
<version>4.12.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.google.code.gson</groupId>
|
||||
<artifactId>gson</artifactId>
|
||||
<version>2.10.1</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.projectlombok</groupId>
|
||||
<artifactId>lombok</artifactId>
|
||||
<version>1.18.30</version>
|
||||
<scope>provided</scope>
|
||||
</dependency>
|
||||
|
||||
<!-- Coroutines version lock -->
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlinx</groupId>
|
||||
<artifactId>kotlinx-coroutines-core</artifactId>
|
||||
<version>1.10.2</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Test dependencies -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-test-junit5</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<sourceDirectory>${project.basedir}/src/main/kotlin</sourceDirectory>
|
||||
<testSourceDirectory>${project.basedir}/src/test/kotlin</testSourceDirectory>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-plugin</artifactId>
|
||||
<configuration>
|
||||
<args>
|
||||
<arg>-Xjsr305=strict</arg>
|
||||
</args>
|
||||
<compilerPlugins>
|
||||
<plugin>spring</plugin>
|
||||
</compilerPlugins>
|
||||
</configuration>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.jetbrains.kotlin</groupId>
|
||||
<artifactId>kotlin-maven-allopen</artifactId>
|
||||
<version>${kotlin.version}</version>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
|
||||
</project>
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
@@ -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<String>) {
|
||||
runApplication<SoftwareFabricApplication>(*args)
|
||||
}
|
||||
@@ -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<String, String> {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<String>
|
||||
)
|
||||
|
||||
// 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<Label>
|
||||
)
|
||||
|
||||
data class Assignee(
|
||||
val login: String,
|
||||
@SerializedName("full_name")
|
||||
val fullName: String
|
||||
)
|
||||
|
||||
data class Label(
|
||||
val id: Int,
|
||||
val name: String,
|
||||
val color: String
|
||||
)
|
||||
|
||||
// Request classes
|
||||
data class CreateIssueRequest(
|
||||
val title: String,
|
||||
val body: String,
|
||||
val assignee: String? = null,
|
||||
val labels: List<Int>? = null
|
||||
)
|
||||
|
||||
data class UpdateLabelsRequest(
|
||||
val labels: List<Int>
|
||||
)
|
||||
|
||||
@Service
|
||||
@Slf4j
|
||||
class GiteaService {
|
||||
@Value("\${gitea.username}")
|
||||
private lateinit var username: String
|
||||
|
||||
@Value("\${gitea.password}")
|
||||
private lateinit var password: String
|
||||
|
||||
@Value("\${gitea.owner}")
|
||||
private lateinit var owner: String
|
||||
|
||||
@Value("\${gitea.repo}")
|
||||
private lateinit var repo: String
|
||||
|
||||
@Value("\${gitea.base-url}")
|
||||
private lateinit var apiBaseUrl: String
|
||||
|
||||
private val baseUrl: String
|
||||
get() = "$apiBaseUrl/repos/$owner/$repo"
|
||||
|
||||
private val gson = Gson()
|
||||
private val client = OkHttpClient()
|
||||
private val jsonMediaType = "application/json; charset=utf-8".toMediaType()
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(GiteaService::class.java)
|
||||
}
|
||||
|
||||
// Generic private functions
|
||||
private fun createAuthHeader(): String {
|
||||
return Credentials.basic(username, password)
|
||||
}
|
||||
|
||||
private fun executeRequest(request: Request): Response {
|
||||
return client.newCall(request).execute()
|
||||
}
|
||||
|
||||
private fun buildGetRequest(url: String): Request {
|
||||
return Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("Authorization", createAuthHeader())
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun buildPostRequest(url: String, body: Any): Request {
|
||||
val jsonBody = gson.toJson(body)
|
||||
val requestBody = jsonBody.toRequestBody(jsonMediaType)
|
||||
|
||||
return Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("Authorization", createAuthHeader())
|
||||
.post(requestBody)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun buildPutRequest(url: String, body: Any): Request {
|
||||
val jsonBody = gson.toJson(body)
|
||||
val requestBody = jsonBody.toRequestBody(jsonMediaType)
|
||||
|
||||
return Request.Builder()
|
||||
.url(url)
|
||||
.addHeader("Authorization", createAuthHeader())
|
||||
.put(requestBody)
|
||||
.build()
|
||||
}
|
||||
|
||||
private fun mapToIssue(giteaIssue: GiteaIssueResponse): Issue {
|
||||
log.info("Gitea issue: {}", giteaIssue.toString())
|
||||
return Issue(
|
||||
number = giteaIssue.number,
|
||||
title = giteaIssue.title,
|
||||
body = giteaIssue.body,
|
||||
assigned = giteaIssue.assignee?.let {
|
||||
if (it.fullName.isNotBlank()) it.fullName else it.login
|
||||
},
|
||||
labels = giteaIssue.labels.map { it.name }
|
||||
)
|
||||
}
|
||||
|
||||
private fun getIssueById(issueNumber: Int): Issue? {
|
||||
val url = "$baseUrl/issues/$issueNumber"
|
||||
val request = buildGetRequest(url)
|
||||
|
||||
executeRequest(request).use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
println("Failed to get issue: $response")
|
||||
return null
|
||||
}
|
||||
|
||||
val json = response.body?.string() ?: return null
|
||||
val giteaIssue = gson.fromJson(json, GiteaIssueResponse::class.java)
|
||||
return mapToIssue(giteaIssue)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAvailableLabels(): List<Label> {
|
||||
val url = "$baseUrl/labels"
|
||||
val request = buildGetRequest(url)
|
||||
|
||||
executeRequest(request).use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
println("Failed to get labels: $response")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val json = response.body?.string() ?: return emptyList()
|
||||
return gson.fromJson(json, Array<Label>::class.java).toList()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getLabelIdsByNames(labelNames: List<String>): List<Int> {
|
||||
val availableLabels = getAvailableLabels()
|
||||
return labelNames.mapNotNull { name ->
|
||||
availableLabels.find { it.name.equals(name, ignoreCase = true) }?.id
|
||||
}
|
||||
}
|
||||
|
||||
fun getIssues(): List<Issue> {
|
||||
val url = "$baseUrl/issues"
|
||||
val request = buildGetRequest(url)
|
||||
|
||||
executeRequest(request).use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
println("Unexpected code $response")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val json = response.body?.string() ?: return emptyList()
|
||||
val giteaIssues = gson.fromJson(json, Array<GiteaIssueResponse>::class.java)
|
||||
return giteaIssues.map { mapToIssue(it) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun createIssueLabelIds(
|
||||
title: String,
|
||||
body: String,
|
||||
assignee: String? = null,
|
||||
labelIds: List<Int>? = null
|
||||
): Issue? {
|
||||
val url = "$baseUrl/issues"
|
||||
val createRequest = CreateIssueRequest(
|
||||
title = title,
|
||||
body = body,
|
||||
assignee = assignee,
|
||||
labels = labelIds
|
||||
)
|
||||
|
||||
val request = buildPostRequest(url, createRequest)
|
||||
|
||||
executeRequest(request).use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
println("Failed to create issue: $response")
|
||||
println("Response body: ${response.body?.string()}")
|
||||
return null
|
||||
}
|
||||
|
||||
val json = response.body?.string() ?: return null
|
||||
val giteaIssue = gson.fromJson(json, GiteaIssueResponse::class.java)
|
||||
return mapToIssue(giteaIssue)
|
||||
}
|
||||
}
|
||||
|
||||
fun createIssue(
|
||||
title: String,
|
||||
body: String,
|
||||
assignee: String? = null,
|
||||
labelNames: List<String>? = null
|
||||
): Issue? {
|
||||
val labelIds = labelNames?.let { getLabelIdsByNames(it) }
|
||||
return createIssueLabelIds(title, body, assignee, labelIds)
|
||||
}
|
||||
|
||||
private fun updateIssueLabelsIds(
|
||||
issueNumber: Int,
|
||||
labelIds: List<Int>
|
||||
): Issue? {
|
||||
val url = "$baseUrl/issues/$issueNumber/labels"
|
||||
log.info("url: {}", url)
|
||||
val updateRequest = UpdateLabelsRequest(labels = labelIds)
|
||||
|
||||
val request = buildPutRequest(url, updateRequest)
|
||||
|
||||
executeRequest(request).use { response ->
|
||||
if (!response.isSuccessful) {
|
||||
println("Failed to update labels: $response")
|
||||
println("Response body: ${response.body?.string()}")
|
||||
return null
|
||||
}
|
||||
|
||||
return getIssueById(issueNumber)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateIssueLabels(
|
||||
issueNumber: Int,
|
||||
labelNames: List<String>
|
||||
): Issue? {
|
||||
val labelIds = getLabelIdsByNames(labelNames)
|
||||
return updateIssueLabelsIds(issueNumber, labelIds)
|
||||
}
|
||||
}
|
||||
@@ -5,37 +5,62 @@ import ai.koog.agents.core.tools.annotations.Tool
|
||||
import ai.koog.agents.core.tools.reflect.ToolSet
|
||||
import org.eclipse.jgit.api.Git
|
||||
import org.eclipse.jgit.transport.UsernamePasswordCredentialsProvider
|
||||
import org.slf4j.LoggerFactory
|
||||
import org.springframework.beans.factory.annotation.Value
|
||||
import java.io.File
|
||||
|
||||
val username: String? = System.getenv("GIT_USERNAME")
|
||||
val password: String? = System.getenv("GIT_PASSWORD")
|
||||
//val username: String? = System.getenv("GIT_USERNAME")
|
||||
//val password: String? = System.getenv("GIT_PASSWORD")
|
||||
//val workspace: String? = System.getenv("SF_WORKSPACE")
|
||||
|
||||
// Implement a simple calculator tool that can add two numbers
|
||||
@LLMDescription("Tools for performing basic git operations")
|
||||
class GitTools : ToolSet {
|
||||
|
||||
companion object {
|
||||
private val log = LoggerFactory.getLogger(GitTools::class.java)
|
||||
}
|
||||
|
||||
val username = "developer-ai"
|
||||
val password = "XCO1bsNyhyIzk2"
|
||||
val workspace = "/Users/erik/projecten/workspace"
|
||||
|
||||
@Tool
|
||||
@LLMDescription("Clone een git repository")
|
||||
fun cloneRepository(
|
||||
@LLMDescription("The remote url from the git repository")
|
||||
remoteUrl: String,
|
||||
@LLMDescription("The local path to clone the repository to")
|
||||
localPath: File) {
|
||||
@LLMDescription("The name of the project to clone")
|
||||
projectName: String): String {
|
||||
|
||||
Git.cloneRepository()
|
||||
.setURI(remoteUrl)
|
||||
.setDirectory(localPath)
|
||||
.call()
|
||||
}
|
||||
val localPath = workspace + File.separatorChar + projectName
|
||||
log.info("localPath: {}", localPath)
|
||||
|
||||
if (File(localPath).exists()) {
|
||||
return "\"$projectName\" already cloned"
|
||||
}
|
||||
|
||||
try {
|
||||
Git.cloneRepository()
|
||||
.setURI(remoteUrl)
|
||||
.setDirectory(File(localPath))
|
||||
.call()
|
||||
} catch (e: Exception) {
|
||||
log.error("Error during clone $projectName", e)
|
||||
return "Error found: ${e.message}"
|
||||
}
|
||||
return "Repository $projectName successfully cloned"
|
||||
}
|
||||
|
||||
@Tool
|
||||
@LLMDescription("Pull a git repository")
|
||||
fun pull(
|
||||
@LLMDescription("The local path from the local git repository")
|
||||
localPath: String,
|
||||
projectName: String,
|
||||
@LLMDescription("The branch to pull")
|
||||
branch: String) {
|
||||
|
||||
val localPath = workspace + File.pathSeparator + projectName
|
||||
Git.open(File(localPath)).use { git ->
|
||||
git.pull()
|
||||
.setRemote("origin")
|
||||
|
||||
5
src/main/resources/application-test.properties
Normal file
5
src/main/resources/application-test.properties
Normal file
@@ -0,0 +1,5 @@
|
||||
gitea.username=developer-ai
|
||||
gitea.password=XCO1bsNyhyIzk2
|
||||
gitea.owner=Trivion
|
||||
gitea.repo=tasklist
|
||||
gitea.base-url=http://dixienas:3001/api/v1
|
||||
7
src/main/resources/application.properties
Normal file
7
src/main/resources/application.properties
Normal file
@@ -0,0 +1,7 @@
|
||||
gitea.username=developer-ai
|
||||
gitea.password=XCO1bsNyhyIzk2
|
||||
gitea.owner=Trivion
|
||||
gitea.repo=tasklist
|
||||
gitea.base-url=http://dixienas:3001/api/v1
|
||||
|
||||
workspace=/Users/erik/projecten/workspace
|
||||
Reference in New Issue
Block a user