init code

This commit is contained in:
Christoph K.
2026-02-25 07:30:06 +01:00
parent 270eb56ac8
commit 797657c56b
7 changed files with 403 additions and 0 deletions

7
PRD.md Normal file
View File

@@ -0,0 +1,7 @@
# Mein Projekt
## Tasks
- [ ] Erstelle eine Datei hello.go mit einem Hello World Programm
- [ ] Erstelle eine Datei README.md mit einer kurzen Projektbeschreibung
- [ ] Projektstruktur anlegen

159
agent/loop.go Normal file
View File

@@ -0,0 +1,159 @@
package agent
import (
"context"
"fmt"
"strings"
"time"
"github.com/openai/openai-go"
oaioption "github.com/openai/openai-go/option"
"llm-agent/prd"
)
const (
baseURL = "http://127.0.0.1:12434/v1"
maxRetries = 3
maxTurns = 10 // Sicherheitslimit pro Task
)
var systemPrompt = `Du bist ein autonomer Coding-Agent. Du bekommst einen Task und erledigst ihn vollständig.
Du hast folgende Tools zur Verfügung:
- TOOL:READ_FILE:pfad → Datei lesen
- TOOL:WRITE_FILE:pfad:inhalt → Datei schreiben
- TOOL:LIST_FILES:pfad → Verzeichnis auflisten
Regeln:
1. Analysiere den Task zuerst
2. Nutze die Tools um Dateien zu lesen/schreiben
3. Wenn der Task vollständig erledigt ist, schreibe am Ende: TASK_COMPLETE
4. Bei Fehlern beschreibe das Problem klar`
type AgentLoop struct {
client *openai.Client
model string
workDir string
prdFile string
}
func NewAgentLoop(model, workDir, prdFile string) *AgentLoop {
client := openai.NewClient(
oaioption.WithBaseURL(baseURL),
oaioption.WithAPIKey("ollama"),
)
return &AgentLoop{
client: &client,
model: model,
workDir: workDir,
prdFile: prdFile,
}
}
func (a *AgentLoop) Run() error {
tasks, err := prd.ParseTasks(a.prdFile)
if err != nil {
return fmt.Errorf("PRD lesen fehlgeschlagen: %w", err)
}
pending := 0
for _, t := range tasks {
if !t.Completed {
pending++
}
}
fmt.Printf("📋 %d Tasks gefunden, %d offen\n\n", len(tasks), pending)
for _, task := range tasks {
if task.Completed {
fmt.Printf("✅ Überspringe (bereits erledigt): %s\n", task.Title)
continue
}
fmt.Printf("\n🔄 Starte Task: %s\n", task.Title)
fmt.Println(strings.Repeat("─", 50))
success := false
for attempt := 1; attempt <= maxRetries; attempt++ {
if attempt > 1 {
fmt.Printf("🔁 Retry %d/%d...\n", attempt, maxRetries)
time.Sleep(time.Duration(attempt) * 2 * time.Second) // Backoff
}
err := a.runTask(task)
if err == nil {
success = true
break
}
fmt.Printf("⚠️ Fehler: %v\n", err)
}
if success {
prd.MarkTaskComplete(a.prdFile, task.Title)
fmt.Printf("✅ Task abgeschlossen: %s\n", task.Title)
} else {
fmt.Printf("❌ Task fehlgeschlagen nach %d Versuchen: %s\n", maxRetries, task.Title)
}
}
fmt.Println("\n🎉 Alle Tasks abgearbeitet!")
return nil
}
func (a *AgentLoop) runTask(task prd.Task) error {
// FRISCHER Kontext für jeden Task
messages := []openai.ChatCompletionMessageParamUnion{
openai.SystemMessage(systemPrompt),
openai.UserMessage(fmt.Sprintf("Task: %s\nArbeitsverzeichnis: %s", task.Title, a.workDir)),
}
for turn := 0; turn < maxTurns; turn++ {
fmt.Printf(" 💭 Turn %d...\n", turn+1)
resp, err := a.client.Chat.Completions.New(
context.Background(),
openai.ChatCompletionNewParams{
Model: a.model,
Messages: messages,
},
)
if err != nil {
return fmt.Errorf("API-Fehler: %w", err)
}
response := resp.Choices[0].Message.Content
fmt.Printf(" 🤖 %s\n", truncate(response, 200))
// Antwort zur History hinzufügen
messages = append(messages, openai.AssistantMessage(response))
// Completion Detection: Layer 1 - Signal Token
if strings.Contains(response, "TASK_COMPLETE") {
return nil // Erfolg!
}
// Tool Execution
toolOutput, hadTools := ExecuteTools(response, a.workDir)
if hadTools {
fmt.Printf(" 🔧 Tool-Output: %s\n", truncate(toolOutput, 150))
// Tool-Ergebnis zurück ans LLM
messages = append(messages, openai.UserMessage(toolOutput))
continue
}
// Kein Tool, kein TASK_COMPLETE → LLM anstupsen
messages = append(messages, openai.UserMessage(
"Bitte fahre fort. Wenn der Task erledigt ist, schreibe TASK_COMPLETE.",
))
}
return fmt.Errorf("maximale Turns (%d) erreicht ohne TASK_COMPLETE", maxTurns)
}
func truncate(s string, max int) string {
if len(s) <= max {
return s
}
return s[:max] + "..."
}

88
agent/tools.go Normal file
View File

@@ -0,0 +1,88 @@
package agent
import (
"fmt"
"os"
"path/filepath"
"strings"
)
// Tool-Ergebnis das dem LLM zurückgegeben wird
type ToolResult struct {
Success bool
Output string
}
// Parst Tool-Calls aus der LLM-Antwort
// Erwartetes Format:
// TOOL:READ_FILE:path/to/file
// TOOL:WRITE_FILE:path/to/file:<<<content>>>
// TOOL:LIST_FILES:.
func ExecuteTools(response string, workDir string) (string, bool) {
lines := strings.Split(response, "\n")
var toolOutputs []string
hasToolCall := false
for _, line := range lines {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, "TOOL:") {
continue
}
hasToolCall = true
result := executeTool(line, workDir)
toolOutputs = append(toolOutputs, result)
}
return strings.Join(toolOutputs, "\n"), hasToolCall
}
func executeTool(toolCall string, workDir string) string {
parts := strings.SplitN(toolCall, ":", 4)
if len(parts) < 3 {
return "ERROR: Ungültiger Tool-Call"
}
toolName := parts[1]
arg1 := parts[2]
switch toolName {
case "READ_FILE":
path := filepath.Join(workDir, arg1)
content, err := os.ReadFile(path)
if err != nil {
return fmt.Sprintf("READ_FILE ERROR: %v", err)
}
return fmt.Sprintf("READ_FILE %s:\n%s", arg1, string(content))
case "WRITE_FILE":
if len(parts) < 4 {
return "ERROR: WRITE_FILE braucht Inhalt"
}
content := parts[3]
path := filepath.Join(workDir, arg1)
// Verzeichnis anlegen falls nötig
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
return fmt.Sprintf("WRITE_FILE ERROR: %v", err)
}
if err := os.WriteFile(path, []byte(content), 0644); err != nil {
return fmt.Sprintf("WRITE_FILE ERROR: %v", err)
}
return fmt.Sprintf("WRITE_FILE OK: %s geschrieben", arg1)
case "LIST_FILES":
path := filepath.Join(workDir, arg1)
entries, err := os.ReadDir(path)
if err != nil {
return fmt.Sprintf("LIST_FILES ERROR: %v", err)
}
var files []string
for _, e := range entries {
files = append(files, e.Name())
}
return fmt.Sprintf("LIST_FILES %s:\n%s", arg1, strings.Join(files, "\n"))
}
return fmt.Sprintf("ERROR: Unbekanntes Tool: %s", toolName)
}

11
go.mod Normal file
View File

@@ -0,0 +1,11 @@
module llm-agent
go 1.25.7
require (
github.com/openai/openai-go v1.12.0 // indirect
github.com/tidwall/gjson v1.14.4 // indirect
github.com/tidwall/match v1.1.1 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
)

12
go.sum Normal file
View File

@@ -0,0 +1,12 @@
github.com/openai/openai-go v1.12.0 h1:NBQCnXzqOTv5wsgNC36PrFEiskGfO5wccfCWDo9S1U0=
github.com/openai/openai-go v1.12.0/go.mod h1:g461MYGXEXBVdV5SaR/5tNzNbSfwTBBefwc+LlDCK0Y=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM=
github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=

67
main.go Normal file
View File

@@ -0,0 +1,67 @@
package main
import (
"bufio"
"context"
"fmt"
"log"
"os"
"strconv"
"strings"
"github.com/openai/openai-go"
oaioption "github.com/openai/openai-go/option"
"llm-agent/agent"
)
const baseURL = "http://127.0.0.1:12434/v1"
func selectModel(client *openai.Client) string {
modelsPage, err := client.Models.List(context.Background())
if err != nil {
log.Fatalf("Fehler beim Abrufen der Modelle: %v", err)
}
models := modelsPage.Data
if len(models) == 0 {
log.Fatal("Keine Modelle verfügbar!")
}
fmt.Println("\n📦 Verfügbare Modelle:")
fmt.Println(strings.Repeat("─", 50))
for i, m := range models {
fmt.Printf(" [%d] %s\n", i+1, m.ID)
}
fmt.Println(strings.Repeat("─", 50))
scanner := bufio.NewScanner(os.Stdin)
for {
fmt.Printf("Wähle ein Modell (1-%d): ", len(models))
if !scanner.Scan() {
log.Fatal("Eingabe fehlgeschlagen")
}
choice, err := strconv.Atoi(strings.TrimSpace(scanner.Text()))
if err != nil || choice < 1 || choice > len(models) {
fmt.Printf("❌ Ungültige Eingabe.\n")
continue
}
selected := models[choice-1].ID
fmt.Printf("✅ Modell gewählt: %s\n", selected)
return selected
}
}
func main() {
client := openai.NewClient(
oaioption.WithBaseURL(baseURL),
oaioption.WithAPIKey("ollama"),
)
fmt.Println("🤖 LLM Agent")
model := selectModel(&client)
loop := agent.NewAgentLoop(model, ".", "PRD.md")
if err := loop.Run(); err != nil {
log.Fatalf("Agent fehlgeschlagen: %v", err)
}
}

59
prd/parser.go Normal file
View File

@@ -0,0 +1,59 @@
package prd
import (
"bufio"
"os"
"strings"
)
type Task struct {
Title string
Completed bool
Index int
}
func ParseTasks(filepath string) ([]Task, error) {
file, err := os.Open(filepath)
if err != nil {
return nil, err
}
defer file.Close()
var tasks []Task
index := 0
scanner := bufio.NewScanner(file)
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
if strings.HasPrefix(line, "- [ ] ") {
tasks = append(tasks, Task{
Title: strings.TrimPrefix(line, "- [ ] "),
Completed: false,
Index: index,
})
index++
} else if strings.HasPrefix(line, "- [x] ") {
tasks = append(tasks, Task{
Title: strings.TrimPrefix(line, "- [x] "),
Completed: true,
Index: index,
})
index++
}
}
return tasks, scanner.Err()
}
func MarkTaskComplete(filepath string, taskTitle string) error {
content, err := os.ReadFile(filepath)
if err != nil {
return err
}
updated := strings.ReplaceAll(
string(content),
"- [ ] "+taskTitle,
"- [x] "+taskTitle,
)
return os.WriteFile(filepath, []byte(updated), 0644)
}