mirror of
https://github.com/amir20/dozzle.git
synced 2026-06-23 04:10:12 +00:00
fix: remove host_id from cloud tool parameters to prevent LLM hallucination
The LLM was fabricating Swarm-style host IDs instead of using actual host IDs, causing tool calls to fail. Since container IDs are unique across hosts, we now resolve the host automatically via ListAllContainers. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -128,9 +128,13 @@ func TestHandleRequest_CallTool_RestartContainer(t *testing.T) {
|
||||
mockClient := &MockClientService{}
|
||||
mockClient.On("ContainerAction", mock.Anything, mock.Anything, container.Restart).Return(nil)
|
||||
|
||||
cs := container_support.NewContainerService(mockClient, container.Container{ID: "abc123"})
|
||||
cs := container_support.NewContainerService(mockClient, container.Container{ID: "abc123", Host: "local"})
|
||||
|
||||
mockHost := &MockHostService{}
|
||||
mockHost.On("ListAllContainers", container.ContainerLabels(nil)).Return([]container.Container{
|
||||
{ID: "abc123", Name: "nginx", Host: "local", State: "running"},
|
||||
}, nil)
|
||||
mockHost.On("Hosts").Return([]container.Host{{ID: "local", Name: "my-server"}})
|
||||
mockHost.On("FindContainer", "local", "abc123", container.ContainerLabels(nil)).Return(cs, nil)
|
||||
|
||||
client := &Client{
|
||||
@@ -143,7 +147,7 @@ func TestHandleRequest_CallTool_RestartContainer(t *testing.T) {
|
||||
Type: &pb.ToolRequest_CallTool{
|
||||
CallTool: &pb.CallToolRequest{
|
||||
Name: "restart_container",
|
||||
ArgumentsJson: `{"container_id": "abc123", "host_id": "local"}`,
|
||||
ArgumentsJson: `{"container_id": "abc123"}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
+9
-10
@@ -31,7 +31,7 @@ func AvailableTools(enableActions bool) []*pb.ToolDefinition {
|
||||
},
|
||||
{
|
||||
Name: "find_containers",
|
||||
Description: "Search for Docker containers by name, state, or health status. All parameters are optional. Returns container ID, name, image, state, health, and host. Use this before start/stop/restart actions to get the container ID and host.",
|
||||
Description: "Search for Docker containers by name, state, or health status. All parameters are optional. Returns container ID, name, image, state, health, and host. Use this before other container tools to get the container ID.",
|
||||
ParametersJson: findContainerParams,
|
||||
},
|
||||
{
|
||||
@@ -51,40 +51,39 @@ func AvailableTools(enableActions bool) []*pb.ToolDefinition {
|
||||
},
|
||||
{
|
||||
Name: "fetch_container_logs",
|
||||
Description: "Fetch raw logs from a running Docker container. Requires container_id and host from find_containers. Optionally filter by time range, log level, text search, or regex pattern. Returns up to 100 matching log lines.",
|
||||
ParametersJson: `{"type":"object","properties":{"container_id":{"type":"string","description":"The container ID (from find_containers)"},"host_id":{"type":"string","description":"The host ID where the container is running (from find_containers)"},"start":{"type":"string","description":"Optional ISO 8601 start time for log range"},"end":{"type":"string","description":"Optional ISO 8601 end time for log range"},"level":{"type":"string","description":"Optional log level filter (e.g. error, warn, info)"},"query":{"type":"string","description":"Optional text search query (case-insensitive substring match)"},"regex":{"type":"string","description":"Optional regex pattern to match against log messages"}},"required":["container_id","host_id"]}`,
|
||||
Description: "Fetch raw logs from a Docker container. Requires container_id from find_containers. Optionally filter by time range, log level, text search, or regex pattern. Returns up to 100 matching log lines.",
|
||||
ParametersJson: `{"type":"object","properties":{"container_id":{"type":"string","description":"The container ID (from find_containers)"},"start":{"type":"string","description":"Optional ISO 8601 start time for log range"},"end":{"type":"string","description":"Optional ISO 8601 end time for log range"},"level":{"type":"string","description":"Optional log level filter (e.g. error, warn, info)"},"query":{"type":"string","description":"Optional text search query (case-insensitive substring match)"},"regex":{"type":"string","description":"Optional regex pattern to match against log messages"}},"required":["container_id"]}`,
|
||||
},
|
||||
}
|
||||
|
||||
inspectParams := `{"type":"object","properties":{"container_id":{"type":"string","description":"The container ID (from find_containers)"},"host_id":{"type":"string","description":"The host ID where the container is running (from find_containers)"}},"required":["container_id","host_id"]}`
|
||||
containerIDParams := `{"type":"object","properties":{"container_id":{"type":"string","description":"The container ID (from find_containers)"}},"required":["container_id"]}`
|
||||
tools = append(tools, &pb.ToolDefinition{
|
||||
Name: "inspect_container",
|
||||
Description: "Get detailed configuration of a Docker container including environment variables, port mappings, mounts, restart policy, network mode, labels, and resource limits.",
|
||||
ParametersJson: inspectParams,
|
||||
ParametersJson: containerIDParams,
|
||||
})
|
||||
|
||||
if enableActions {
|
||||
actionParams := `{"type":"object","properties":{"container_id":{"type":"string","description":"The container ID (from find_containers)"},"host_id":{"type":"string","description":"The host ID where the container is running (from find_containers)"}},"required":["container_id","host_id"]}`
|
||||
tools = append(tools,
|
||||
&pb.ToolDefinition{
|
||||
Name: "start_container",
|
||||
Description: "Start a stopped Docker container",
|
||||
ParametersJson: actionParams,
|
||||
ParametersJson: containerIDParams,
|
||||
},
|
||||
&pb.ToolDefinition{
|
||||
Name: "stop_container",
|
||||
Description: "Stop a running Docker container",
|
||||
ParametersJson: actionParams,
|
||||
ParametersJson: containerIDParams,
|
||||
},
|
||||
&pb.ToolDefinition{
|
||||
Name: "restart_container",
|
||||
Description: "Restart a Docker container",
|
||||
ParametersJson: actionParams,
|
||||
ParametersJson: containerIDParams,
|
||||
},
|
||||
&pb.ToolDefinition{
|
||||
Name: "update_container",
|
||||
Description: "Update a Docker container by pulling the latest version of its image and recreating it with the same configuration. If the image is already up to date, no recreation occurs. For swarm service containers, updates the service instead.",
|
||||
ParametersJson: actionParams,
|
||||
ParametersJson: containerIDParams,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
|
||||
type containerActionArgs struct {
|
||||
ContainerID string `json:"container_id"`
|
||||
Host string `json:"host_id"`
|
||||
}
|
||||
|
||||
func executeContainerAction(ctx context.Context, name string, argsJSON string, hostService ToolHostService, labels container.ContainerLabels) (*pb.CallToolResponse, error) {
|
||||
@@ -28,13 +27,10 @@ func executeContainerAction(ctx context.Context, name string, argsJSON string, h
|
||||
if args.ContainerID == "" {
|
||||
return nil, fmt.Errorf("container_id is required")
|
||||
}
|
||||
if args.Host == "" {
|
||||
return nil, fmt.Errorf("host is required")
|
||||
}
|
||||
|
||||
cs, err := hostService.FindContainer(args.Host, args.ContainerID, labels)
|
||||
cs, err := findContainerByID(args.ContainerID, hostService, labels)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("container not found: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := cs.Action(ctx, action); err != nil {
|
||||
@@ -60,13 +56,10 @@ func executeUpdateContainer(ctx context.Context, argsJSON string, hostService To
|
||||
if args.ContainerID == "" {
|
||||
return nil, fmt.Errorf("container_id is required")
|
||||
}
|
||||
if args.Host == "" {
|
||||
return nil, fmt.Errorf("host is required")
|
||||
}
|
||||
|
||||
cs, err := hostService.FindContainer(args.Host, args.ContainerID, labels)
|
||||
cs, err := findContainerByID(args.ContainerID, hostService, labels)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("container not found: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
progressCh := make(chan container.UpdateProgress, 100)
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
|
||||
type inspectContainerArgs struct {
|
||||
ContainerID string `json:"container_id"`
|
||||
Host string `json:"host_id"`
|
||||
}
|
||||
|
||||
type findContainersArgs struct {
|
||||
@@ -156,13 +155,13 @@ func executeInspectContainer(argsJSON string, hostService ToolHostService, label
|
||||
if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse arguments: %w", err)
|
||||
}
|
||||
if args.ContainerID == "" || args.Host == "" {
|
||||
return nil, fmt.Errorf("container_id and host are required")
|
||||
if args.ContainerID == "" {
|
||||
return nil, fmt.Errorf("container_id is required")
|
||||
}
|
||||
|
||||
cs, err := hostService.FindContainer(args.Host, args.ContainerID, labels)
|
||||
cs, err := findContainerByID(args.ContainerID, hostService, labels)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("container not found: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c := cs.Container
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
package cloud
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/amir20/dozzle/internal/container"
|
||||
container_support "github.com/amir20/dozzle/internal/support/container"
|
||||
pb "github.com/amir20/dozzle/proto/cloud"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
@@ -62,3 +64,16 @@ func logHostErrors(errs []error) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// findContainerByID searches across all hosts to find a container by ID and returns its ContainerService.
|
||||
func findContainerByID(containerID string, hostService ToolHostService, labels container.ContainerLabels) (*container_support.ContainerService, error) {
|
||||
containers, errs := hostService.ListAllContainers(labels)
|
||||
logHostErrors(errs)
|
||||
|
||||
for _, c := range containers {
|
||||
if c.ID == containerID {
|
||||
return hostService.FindContainer(c.Host, c.ID, labels)
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("container %s not found on any host", containerID)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ import (
|
||||
|
||||
type fetchLogsArgs struct {
|
||||
ContainerID string `json:"container_id"`
|
||||
Host string `json:"host_id"`
|
||||
Start string `json:"start"`
|
||||
End string `json:"end"`
|
||||
Level string `json:"level"`
|
||||
@@ -27,13 +26,13 @@ func executeFetchContainerLogs(ctx context.Context, argsJSON string, hostService
|
||||
if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse arguments: %w", err)
|
||||
}
|
||||
if args.ContainerID == "" || args.Host == "" {
|
||||
return nil, fmt.Errorf("container_id and host are required")
|
||||
if args.ContainerID == "" {
|
||||
return nil, fmt.Errorf("container_id is required")
|
||||
}
|
||||
|
||||
cs, err := hostService.FindContainer(args.Host, args.ContainerID, labels)
|
||||
cs, err := findContainerByID(args.ContainerID, hostService, labels)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("container not found: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
start := time.Now().Add(-1 * time.Hour)
|
||||
|
||||
@@ -174,12 +174,16 @@ func TestExecuteTool_RestartContainer(t *testing.T) {
|
||||
mockClient := &MockClientService{}
|
||||
mockClient.On("ContainerAction", mock.Anything, mock.Anything, container.Restart).Return(nil)
|
||||
|
||||
cs := container_support.NewContainerService(mockClient, container.Container{ID: "abc123"})
|
||||
cs := container_support.NewContainerService(mockClient, container.Container{ID: "abc123", Host: "local"})
|
||||
|
||||
mockHost := &MockHostService{}
|
||||
mockHost.On("ListAllContainers", container.ContainerLabels(nil)).Return([]container.Container{
|
||||
{ID: "abc123", Name: "nginx", Host: "local", State: "running"},
|
||||
}, nil)
|
||||
mockHost.On("Hosts").Return([]container.Host{{ID: "local", Name: "my-server"}})
|
||||
mockHost.On("FindContainer", "local", "abc123", container.ContainerLabels(nil)).Return(cs, nil)
|
||||
|
||||
argsJSON := `{"container_id": "abc123", "host_id": "local"}`
|
||||
argsJSON := `{"container_id": "abc123"}`
|
||||
resp := ExecuteTool(context.Background(), "restart_container", argsJSON, true, mockHost, nil)
|
||||
assert.True(t, resp.Success)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user