tools update als fallback zu xml

This commit is contained in:
Christoph K.
2026-03-04 07:45:12 +01:00
parent c26ccce817
commit 076ab0d0c0

View File

@@ -2,6 +2,7 @@ package agent
import ( import (
"context" "context"
"encoding/json"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@@ -100,7 +101,6 @@ func (a *AgentLoop) Run() error {
func (a *AgentLoop) runTask(task prd.Task) error { func (a *AgentLoop) runTask(task prd.Task) error {
executor := NewToolExecutor(a.workDir) executor := NewToolExecutor(a.workDir)
// Frischer Kontext pro Task
messages := []openai.ChatCompletionMessageParamUnion{ messages := []openai.ChatCompletionMessageParamUnion{
openai.SystemMessage(systemPrompt), openai.SystemMessage(systemPrompt),
openai.UserMessage(fmt.Sprintf( openai.UserMessage(fmt.Sprintf(
@@ -133,7 +133,7 @@ func (a *AgentLoop) runTask(task prd.Task) error {
openai.ChatCompletionNewParams{ openai.ChatCompletionNewParams{
Model: a.model, Model: a.model,
Messages: messages, Messages: messages,
Tools: Tools, // ← Tool Calling Tools: Tools,
}, },
) )
cancel() cancel()
@@ -147,12 +147,19 @@ func (a *AgentLoop) runTask(task prd.Task) error {
} }
choice := resp.Choices[0] choice := resp.Choices[0]
// Antwort zur History hinzufügen
messages = append(messages, choice.Message.ToParam()) messages = append(messages, choice.Message.ToParam())
// Echte Tool-Calls vom SDK
toolCalls := choice.Message.ToolCalls
// Fallback: XML-Format parsen wenn Modell kein natives Tool Calling nutzt
if len(toolCalls) == 0 && strings.Contains(choice.Message.Content, "<function=") {
a.log.Debug("XML-Fallback: Parse Tool-Calls aus Content")
toolCalls = parseXMLToolCalls(choice.Message.Content)
}
// Kein Tool-Call → LLM hat nur Text geantwortet // Kein Tool-Call → LLM hat nur Text geantwortet
if len(choice.Message.ToolCalls) == 0 { if len(toolCalls) == 0 {
a.log.ChatMessage("assistant", choice.Message.Content) a.log.ChatMessage("assistant", choice.Message.Content)
nudge := "Nutze die bereitgestellten Tools. Rufe task_complete auf wenn du fertig bist." nudge := "Nutze die bereitgestellten Tools. Rufe task_complete auf wenn du fertig bist."
a.log.ChatMessage("user", nudge) a.log.ChatMessage("user", nudge)
@@ -161,9 +168,11 @@ func (a *AgentLoop) runTask(task prd.Task) error {
} }
// Tool-Calls ausführen // Tool-Calls ausführen
for _, toolCall := range choice.Message.ToolCalls { for _, toolCall := range toolCalls {
a.log.Info(" 🔧 %s(%s)", toolCall.Function.Name, a.log.Info(" 🔧 %s(%s)",
truncate(toolCall.Function.Arguments, 80)) toolCall.Function.Name,
truncate(toolCall.Function.Arguments, 80),
)
result, done := executor.Execute(toolCall) result, done := executor.Execute(toolCall)
a.log.ChatMessage("tool", a.log.ChatMessage("tool",
@@ -173,7 +182,7 @@ func (a *AgentLoop) runTask(task prd.Task) error {
messages = append(messages, openai.ToolMessage(result, toolCall.ID)) messages = append(messages, openai.ToolMessage(result, toolCall.ID))
if done { if done {
return nil // task_complete aufgerufen → Erfolg return nil // task_complete → Erfolg
} }
} }
} }
@@ -181,6 +190,98 @@ func (a *AgentLoop) runTask(task prd.Task) error {
return fmt.Errorf("maximale Turns (%d) erreicht", maxTurns) return fmt.Errorf("maximale Turns (%d) erreicht", maxTurns)
} }
// ─── XML Fallback Parser ──────────────────────────────────
// parseXMLToolCalls parst Tool-Calls im XML-Format das manche Modelle nutzen:
//
// <function=write_file>
// <parameter=path>hello.go</parameter>
// <parameter=content>package main...</parameter>
// </function>
func parseXMLToolCalls(content string) []openai.ChatCompletionMessageToolCall {
var calls []openai.ChatCompletionMessageToolCall
remaining := content
callID := 0
for {
// Funktionsname extrahieren
start := strings.Index(remaining, "<function=")
if start == -1 {
break
}
nameStart := start + len("<function=")
nameEnd := strings.Index(remaining[nameStart:], ">")
if nameEnd == -1 {
break
}
funcName := strings.TrimSpace(remaining[nameStart : nameStart+nameEnd])
// Block bis </function> extrahieren
blockEnd := strings.Index(remaining, "</function>")
if blockEnd == -1 {
break
}
block := remaining[start : blockEnd+len("</function>")]
// Parameter extrahieren und als JSON serialisieren
params := extractXMLParams(block)
argsJSON, err := json.Marshal(params)
if err != nil {
remaining = remaining[blockEnd+len("</function>"):]
continue
}
callID++
calls = append(calls, openai.ChatCompletionMessageToolCall{
ID: fmt.Sprintf("xml-call-%d", callID),
Type: "function",
Function: openai.ChatCompletionMessageToolCallFunction{
Name: funcName,
Arguments: string(argsJSON),
},
})
remaining = remaining[blockEnd+len("</function>"):]
}
return calls
}
// extractXMLParams extrahiert alle <parameter=key>value</parameter> aus einem Block
func extractXMLParams(block string) map[string]string {
params := make(map[string]string)
remaining := block
for {
start := strings.Index(remaining, "<parameter=")
if start == -1 {
break
}
// Key extrahieren
keyStart := start + len("<parameter=")
keyEnd := strings.Index(remaining[keyStart:], ">")
if keyEnd == -1 {
break
}
key := strings.TrimSpace(remaining[keyStart : keyStart+keyEnd])
// Value extrahieren
valueStart := keyStart + keyEnd + 1
closeTag := "</parameter>"
valueEnd := strings.Index(remaining[valueStart:], closeTag)
if valueEnd == -1 {
break
}
value := strings.TrimSpace(remaining[valueStart : valueStart+valueEnd])
params[key] = value
remaining = remaining[valueStart+valueEnd+len(closeTag):]
}
return params
}
// ─── Hilfsfunktionen ───────────────────────────────────── // ─── Hilfsfunktionen ─────────────────────────────────────
func truncate(s string, max int) string { func truncate(s string, max int) string {
@@ -225,7 +326,6 @@ func formatResponse(resp *openai.ChatCompletion, elapsed time.Duration) string {
resp.Usage.TotalTokens, resp.Usage.TotalTokens,
)) ))
// Tool-Calls anzeigen
if len(resp.Choices[0].Message.ToolCalls) > 0 { if len(resp.Choices[0].Message.ToolCalls) > 0 {
sb.WriteString(" Tool-Calls :\n") sb.WriteString(" Tool-Calls :\n")
for _, tc := range resp.Choices[0].Message.ToolCalls { for _, tc := range resp.Choices[0].Message.ToolCalls {
@@ -234,12 +334,12 @@ func formatResponse(resp *openai.ChatCompletion, elapsed time.Duration) string {
truncate(tc.Function.Arguments, 100), truncate(tc.Function.Arguments, 100),
)) ))
} }
} else { } else if content := resp.Choices[0].Message.Content; content != "" {
content := resp.Choices[0].Message.Content
sb.WriteString(" Content :\n") sb.WriteString(" Content :\n")
for _, line := range strings.Split(content, "\n") { for _, line := range strings.Split(content, "\n") {
sb.WriteString(fmt.Sprintf(" %s\n", line)) sb.WriteString(fmt.Sprintf(" %s\n", line))
} }
} }
return sb.String() return sb.String()
} }