DesignerAgent toegevoegd

This commit is contained in:
2025-10-11 21:48:24 +02:00
parent e2c19dd5af
commit 43ba49a1aa
7 changed files with 522 additions and 6 deletions

View File

@@ -0,0 +1,200 @@
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.agents.snapshot.feature.Persistence
import ai.koog.agents.snapshot.feature.persistence
import ai.koog.agents.snapshot.providers.InMemoryPersistenceStorageProvider
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.GlobalScope
import kotlinx.coroutines.launch
import lombok.extern.slf4j.Slf4j
import nl.trivion.softwarefabric.service.GiteaService
import nl.trivion.softwarefabric.service.Issue
import nl.trivion.softwarefabric.tools.FileTools
import nl.trivion.softwarefabric.tools.ProjectTool
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.io.File
import java.nio.file.Files
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.io.path.Path
@Service
@Slf4j
class DesignerAgent(private val giteaService: GiteaService) {
companion object {
private val log = LoggerFactory.getLogger(DesignerAgent::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("Designer") {
// 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 (when assistant responds with text)
edge(
(nodeSendInput forwardTo nodeFinish)
transformed { it }
onAssistantMessage { true }
)
// Send input -> Execute tool (when assistant makes a tool call)
edge(
(nodeSendInput forwardTo nodeExecuteTool)
onToolCall { true }
)
// Execute tool -> Send the tool result
edge(nodeExecuteTool forwardTo nodeSendToolResult)
// Send the tool result -> finish (when assistant responds with text)
edge(
(nodeSendToolResult forwardTo nodeFinish)
transformed { it }
onAssistantMessage { true }
)
// NIEUWE EDGE: Send the tool result -> Execute another tool (when assistant makes another tool call)
edge(
(nodeSendToolResult forwardTo nodeExecuteTool)
onToolCall { true }
)
}
val systemPrompt = this::class.java.getResource("/prompts/designer-system-prompt.txt")
?.readText()
?: error("System prompt not found")
// Configure the agent
val agentConfig = AIAgentConfig(
prompt = Prompt.build("designer") {
system(systemPrompt
)
},
model = customModel,
maxAgentIterations = 10
)
// Add the tool to the tool registry
val toolRegistry = ToolRegistry {
tools(GitTools().asTools())
tools(FileTools().asTools())
tools(ProjectTool(giteaService).asTools())
}
fun createAgent(agentId: String): AIAgent<String, String> {
val provider = JVMFilePersistenceStorageProvider(Path("/Users/erik/projecten/ai-persistent"))
return AIAgent(
promptExecutor = promptExecutor,
toolRegistry = toolRegistry,
strategy = agentStrategy,
agentConfig = agentConfig,
id = agentId,
installFeatures = {
install(EventHandler) {
onAgentCompleted { it ->
log.info("Result: ${it.result}")
isAgentBusy.set(false)
}
}
install(Persistence) {
// Use in-memory storage for snapshots
storage = provider
// Enable automatic persistence
enableAutomaticPersistence = true
}
}
)
}
@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.any() { it in listOf("New", "Answered")}
&& 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
}
val taskMessage = if (issue.labels.contains("New"))
"taskNumber: ${issue.number} \nTitle: ${issue.title}\n\n${issue.body}"
else {
val comments = giteaService.getComments(issue.number)
val comment = comments.last()
"answer: ${comment.body}"
}
GlobalScope.launch {
try {
giteaService.updateIssueLabels(issue.number, listOf("Design"))
val agent = createAgent("developer-agent-issue-tasklist-${issue.number}")
val result = agent.run(taskMessage)
log.info("Agent completed with result: $result")
giteaService.updateIssueLabels(issue.number, listOf("Ready"))
} catch (e: Exception) {
log.error("Error processing issue: ${e.message}", e)
} finally {
// Always reset the busy flag
isAgentBusy.set(false)
}
}
}
}

View File

@@ -139,7 +139,7 @@ class DeveloperAgent(private val giteaService: GiteaService) {
)
}
@Scheduled(fixedRate = 5000)
// @Scheduled(fixedRate = 5000)
fun checkIssueToDo() {
// Check if agent is already busy
if (isAgentBusy.get()) {

View File

@@ -39,6 +39,12 @@ data class Assignee(
val fullName: String
)
data class User(
val login: String,
@SerializedName("full_name")
val fullName: String
)
data class Label(
val id: Int,
val name: String,
@@ -53,6 +59,34 @@ data class CreateIssueRequest(
val labels: List<Int>? = null
)
data class Comment(
val body: String,
val user: String
)
data class GiteaAddCommentRequest(
val body: String
)
data class GiteaCommentResponse(
val body: String,
val user: User
)
data class CreateReactionRequest(
val content: String
)
data class GiteaReactionResponse(
val content: String,
val user: User
)
data class Reaction(
val content: String,
val user: String
)
data class UpdateLabelsRequest(
val labels: List<Int>
)
@@ -125,7 +159,6 @@ class GiteaService {
}
private fun mapToIssue(giteaIssue: GiteaIssueResponse): Issue {
log.info("Gitea issue: {}", giteaIssue.toString())
return Issue(
number = giteaIssue.number,
title = giteaIssue.title,
@@ -137,6 +170,20 @@ class GiteaService {
)
}
private fun mapToReaction(giteaReaction: GiteaReactionResponse): Reaction {
return Reaction(
content = giteaReaction.content,
user = giteaReaction.user.login
)
}
private fun mapToComment(giteaComment: GiteaCommentResponse): Comment {
return Comment(
body = giteaComment.body,
user = giteaComment.user.login
)
}
private fun getIssueById(issueNumber: Int): Issue? {
val url = "$baseUrl/issues/$issueNumber"
val request = buildGetRequest(url)
@@ -191,6 +238,22 @@ class GiteaService {
}
}
fun getComments(issueNumber: Int): List<Comment> {
val url = "$baseUrl/issues/$issueNumber/comments"
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 giteaComments = gson.fromJson(json, Array<GiteaCommentResponse>::class.java)
return giteaComments.map { mapToComment(it) }
}
}
private fun createIssueLabelIds(
title: String,
body: String,
@@ -258,4 +321,47 @@ class GiteaService {
val labelIds = getLabelIdsByNames(labelNames)
return updateIssueLabelsIds(issueNumber, labelIds)
}
fun createReaction(
issueNumber: Int,
content: String
): Reaction? {
val createRequest = CreateReactionRequest(content)
val url = "$baseUrl/issues/$issueNumber/reactions"
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 giteaReaction = gson.fromJson(json, GiteaReactionResponse::class.java)
return mapToReaction(giteaReaction)
}
}
fun addComment(
issueNumber: Int,
body: String
): Comment? {
val createRequest = GiteaAddCommentRequest(body)
val url = "$baseUrl/issues/$issueNumber/comments"
val request = buildPostRequest(url, createRequest)
executeRequest(request).use { response ->
if (!response.isSuccessful) {
println("Failed to add comment: $response")
println("Response body: ${response.body?.string()}")
return null
}
val json = response.body?.string() ?: return null
val giteaComment = gson.fromJson(json, GiteaCommentResponse::class.java)
return mapToComment(giteaComment)
}
}
}

View File

@@ -0,0 +1,46 @@
package nl.trivion.softwarefabric.tools
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 org.slf4j.LoggerFactory
import java.nio.file.Files
import java.nio.file.Paths
import java.nio.file.StandardOpenOption
@LLMDescription("Tools for performing basic file operations")
class FileTools : ToolSet {
companion object {
private val log = LoggerFactory.getLogger(FileTools::class.java)
}
val workspace = "/Users/erik/projecten/workspace"
@Tool
@LLMDescription("Write a file to the relative path")
fun writeFile(
@LLMDescription("The relative path where the file should be created")
relativePath: String,
@LLMDescription("The name of the file to create")
filename: String,
@LLMDescription("The content to write to the file")
fileContent: String
) {
log.info("Write '{}' to '{}'", filename, relativePath)
// Combineer workspace, relatieve pad en bestandsnaam
val fullPath = Paths.get(workspace, relativePath, filename)
// Maak de directory structuur aan als deze nog niet bestaat
Files.createDirectories(fullPath.parent)
// Schrijf de inhoud naar het bestand
Files.writeString(
fullPath,
fileContent,
StandardOpenOption.CREATE,
StandardOpenOption.TRUNCATE_EXISTING
)
}
}

View File

@@ -6,7 +6,6 @@ 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")
@@ -34,9 +33,10 @@ class GitTools : ToolSet {
projectName: String): String {
val localPath = workspace + File.separatorChar + projectName
log.info("localPath: {}", localPath)
log.info("Cloning repository {} to {}", remoteUrl, localPath)
if (File(localPath).exists()) {
log.info("$projectName already cloned")
return "\"$projectName\" already cloned"
}
@@ -58,9 +58,9 @@ class GitTools : ToolSet {
@LLMDescription("The local path from the local git repository")
projectName: String,
@LLMDescription("The branch to pull")
branch: String) {
branch: String): String {
val localPath = workspace + File.pathSeparator + projectName
val localPath = workspace + File.separatorChar + projectName
Git.open(File(localPath)).use { git ->
git.pull()
.setRemote("origin")
@@ -68,5 +68,41 @@ class GitTools : ToolSet {
.setCredentialsProvider(UsernamePasswordCredentialsProvider(username, password))
.call()
}
return "Project $projectName successfully pulled"
}
@Tool
@LLMDescription("Commit changed to repository")
fun commit(
@LLMDescription("The local path from the local git repository")
projectName: String,
@LLMDescription("Commit message")
message: String): String {
val localPath = workspace + File.separatorChar + projectName
Git.open(File(localPath)).use { git ->
git.add().addFilepattern(".").call()
git.commit()
.setMessage(message)
.setCredentialsProvider(UsernamePasswordCredentialsProvider(username, password))
.call()
}
return "Project $projectName successfully commit changes"
}
@Tool
@LLMDescription("Push to remote repository")
fun push(
@LLMDescription("The local path from the local git repository")
projectName: String): String {
val localPath = workspace + File.separatorChar + projectName
Git.open(File(localPath)).use { git ->
git.push()
.setRemote("origin")
.setCredentialsProvider(UsernamePasswordCredentialsProvider(username, password))
.call()
}
return "Project $projectName successfully pushed to remote repository"
}
}

View File

@@ -0,0 +1,34 @@
package nl.trivion.softwarefabric.tools
import ai.koog.agents.core.agent.context.AIAgentContext
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.snapshot.feature.persistence
import nl.trivion.softwarefabric.service.GiteaService
import org.slf4j.LoggerFactory
import kotlin.reflect.KType
import kotlin.reflect.typeOf
@LLMDescription("Tools for project management and team communication. Use these tools when you need to ask questions to team members about the project.")
class ProjectTool(private val giteaService: GiteaService): ToolSet {
companion object {
private val log = LoggerFactory.getLogger(ProjectTool::class.java)
}
@Tool
@LLMDescription("Ask a question to a specific team member. Use this when you need clarification, information, or input from someone in the team. The question will be routed to the appropriate team member based on their role.")
fun askQuestion(
@LLMDescription("The complete question you want to ask. Be specific and provide context so the team member understands what you need. For example: 'What should be the maximum file size for uploads?' or 'Should we use dark mode as default?'")
question: String,
@LLMDescription("The task number.")
taskNumber: Int,
@LLMDescription("The role of the team member who should answer this question. Options: 'user' (for product owner/stakeholder questions about requirements), 'developer' (for technical implementation questions), 'tester' (for testing and quality assurance questions), 'designer' (for UI/UX and design questions).")
role: String): String {
log.info("role: $role question: $question")
giteaService.addComment(taskNumber, question)
return "Question is send to $role. Do not ask any more questions until this question is answered!"
}
}

View File

@@ -0,0 +1,94 @@
You are an expert software architect and designer specializing in Kotlin-based microservices that run in Docker containers.
Your role is to create comprehensive technical designs for developers who will implement Kotlin components. Focus on the WHAT and WHY, not the HOW.
## Available Tools
You have access to the following tools:
1. **GIT Tool**: Use this to clone the project repository and push changes
- Clone the repository using the project name
- After creating the design.md file, commit and push it to the repository
2. **Question Tool**: Use this to ask clarification questions to the user when needed
## Workflow
1. Ask for the project name if not provided
2. Use the GIT tool to clone the repository
3. Analyze existing code and structure if available
4. Ask clarification questions if needed using the question tool
5. Create the comprehensive design
6. Save the design as design.md in the project root
7. Use the GIT tool to commit and push the design.md file to the repository
## Input Requirements
The user must provide the **project name**. You will use this project name to:
1. Retrieve the corresponding GIT repository
2. Analyze existing code and structure if available
3. Save the design document in the correct project location
If the project name is not provided, ask the user for it before proceeding.
## Your Responsibilities
1. **Design Complete Interfaces**: Define detailed interfaces with clear method signatures, parameters, return types, and documentation
2. **Identify Services**: Determine which services are needed and describe their responsibilities
3. **Define Data Contracts**: Specify data models, DTOs, and domain objects with all properties
4. **Describe Component Interactions**: Explain how services communicate and depend on each other
5. **Document API Contracts**: Define REST endpoints, message formats, or event structures
6. **Specify External Dependencies**: Identify databases, message queues, external APIs, and other infrastructure needs
7. **Define Configuration Requirements**: List environment variables, configuration properties, and deployment parameters
## Output Requirements
Your final design MUST be saved as a file named **design.md** in the root directory of the project.
The design.md file should contain all the sections described in the "Output Format" section below.
## Asking Questions
If anything is unclear or you need more information to create a complete design, you MUST ask the user for clarification. Use the available tool to ask questions to the user.
Do not make assumptions about critical design decisions. It's better to ask than to design something that doesn't meet the requirements.
Examples of when to ask questions:
- Unclear business requirements or use cases
- Missing information about external systems or APIs
- Ambiguous performance or scalability requirements
- Uncertainty about authentication/authorization needs
- Questions about data retention, privacy, or compliance requirements
## Design Principles
- Use clear, descriptive names for interfaces, services, and methods
- Document the purpose and responsibility of each component
- Specify input validation requirements and error handling expectations
- Define data flow between components
- Consider scalability, reliability, and maintainability
- Think about observability: logging, metrics, and health checks
- Design for Docker deployment: consider 12-factor app principles
## What NOT to Do
- Do NOT provide implementation details or actual code
- Do NOT decide on specific frameworks or libraries (unless critical to the design)
- Do NOT specify project structure, package names, or file organization
- Do NOT write actual Kotlin code (only interface signatures)
## Output Format
For each design request, provide:
1. **Overview**: Brief description of what needs to be built
2. **Services**: List of services with their responsibilities
3. **Interfaces**: Detailed interface definitions with method signatures
4. **Data Models**: Domain objects and DTOs with all properties
5. **Service Communication**: How services interact (REST, events, etc.)
6. **External Dependencies**: Databases, message brokers, third-party APIs
7. **Configuration**: Required environment variables and settings
8. **Docker Considerations**: Ports, volumes, health checks, and container-specific requirements
9. **Non-Functional Requirements**: Performance, security, and operational concerns
Remember: Your designs should be complete enough that a developer can implement them without making major architectural decisions, but flexible enough to allow technical creativity in the implementation.