From f7eed7b05270643bd3dedad4f5a2e0cef72828b2 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 8 Dec 2025 09:21:03 +0000 Subject: [PATCH] cleanup: remove legacy AI context fallback (buildInfrastructureContext) The AI service now uses only buildUnifiedResourceContext() for infrastructure context, since the resourceProvider is always set during router initialization. Removed: - buildInfrastructureContext() function (~288 lines of dead code) - Legacy fallback path in buildSystemPrompt() The unified resource context provides a cleaner, deduplicated view of infrastructure that includes: - All resources grouped by platform and type - Top CPU/Memory/Disk consumers - Active alerts on resources - Infrastructure summary statistics This completes the AI service migration to unified resources. --- .gemini/docs/unified-resource-architecture.md | 2 +- internal/ai/service.go | 295 +----------------- 2 files changed, 3 insertions(+), 294 deletions(-) diff --git a/.gemini/docs/unified-resource-architecture.md b/.gemini/docs/unified-resource-architecture.md index 3c820ee..dd51e00 100644 --- a/.gemini/docs/unified-resource-architecture.md +++ b/.gemini/docs/unified-resource-architecture.md @@ -548,8 +548,8 @@ The dedicated `/resources` unified view was abandoned in favor of migrating exis - [x] Remove debug console.log statements from frontend routes - [x] Remove legacy fallback code from route components (Docker, Hosts, Dashboard) - [x] Simplify route components to use centralized `useResourcesAsLegacy()` hook + - [x] Remove legacy AI context fallback (`buildInfrastructureContext` removed, 288 lines) - [ ] Remove unused legacy arrays from backend StateFrontend (optional - still broadcast for API compatibility) - - [ ] Remove legacy AI context fallback (optional - verify AI uses unified model first) --- diff --git a/internal/ai/service.go b/internal/ai/service.go index 4d5ff7b..3611fbb 100644 --- a/internal/ai/service.go +++ b/internal/ai/service.go @@ -1760,18 +1760,15 @@ Pulse manages LXC containers agentlessly from the PVE host. prompt += cfg.CustomContext } - // Add connected infrastructure info - // Try unified resource context first (Phase 2), fall back to legacy + // Add connected infrastructure info via unified resource model s.mu.RLock() hasResourceProvider := s.resourceProvider != nil s.mu.RUnlock() if hasResourceProvider { - // Use the new unified resource model for cleaner, deduplicated context prompt += s.buildUnifiedResourceContext() } else { - // Fall back to legacy state-based context - prompt += s.buildInfrastructureContext() + log.Warn().Msg("AI context: resource provider not available, infrastructure context will be limited") } // Add user annotations from all resources (global context) @@ -1920,294 +1917,6 @@ func formatContextKey(key string) string { return key } -// buildInfrastructureContext returns detailed info about what Pulse can see and control -// This enables the AI to reason about cross-system troubleshooting paths -func (s *Service) buildInfrastructureContext() string { - var sections []string - - // Load nodes config for infrastructure info - nodesConfig, err := s.persistence.LoadNodesConfig() - if err != nil { - log.Debug().Err(err).Msg("Failed to load nodes config for AI context") - return "" - } - - // Build agent lookup map for matching - agentsByHostname := make(map[string]string) // hostname -> agentID - var connectedAgents []string - if s.agentServer != nil { - agents := s.agentServer.GetConnectedAgents() - for _, agent := range agents { - agentsByHostname[strings.ToLower(agent.Hostname)] = agent.AgentID - connectedAgents = append(connectedAgents, agent.Hostname) - } - } - - // List PVE instances with agent status and cluster info - if nodesConfig != nil && len(nodesConfig.PVEInstances) > 0 { - sections = append(sections, "### Proxmox VE Nodes") - - // Group nodes by cluster for better context - clusterNodes := make(map[string][]string) // clusterName -> list of node names - nodeHasAgent := make(map[string]bool) - - for _, pve := range nodesConfig.PVEInstances { - hasAgent := false - for _, hostname := range connectedAgents { - // Check if agent hostname matches or contains PVE name - if strings.Contains(strings.ToLower(hostname), strings.ToLower(pve.Name)) || - strings.Contains(strings.ToLower(pve.Name), strings.ToLower(hostname)) { - hasAgent = true - break - } - } - nodeHasAgent[pve.Name] = hasAgent - - agentStatus := "NO AGENT" - if hasAgent { - agentStatus = "HAS AGENT" - } - - // Build cluster membership info - clusterInfo := "" - if pve.IsCluster && pve.ClusterName != "" { - clusterInfo = fmt.Sprintf(" [cluster: %s]", pve.ClusterName) - clusterNodes[pve.ClusterName] = append(clusterNodes[pve.ClusterName], pve.Name) - - // Also add cluster endpoints as members - for _, ep := range pve.ClusterEndpoints { - if ep.NodeName != pve.Name { - clusterNodes[pve.ClusterName] = append(clusterNodes[pve.ClusterName], ep.NodeName) - } - } - } - - sections = append(sections, fmt.Sprintf("- **%s** (%s): %s%s", pve.Name, pve.Host, agentStatus, clusterInfo)) - } - - // Add cluster-aware execution hints - for clusterName, nodes := range clusterNodes { - // Check if any node in the cluster has an agent - var agentNodes []string - for _, node := range nodes { - if nodeHasAgent[node] { - agentNodes = append(agentNodes, node) - } - } - if len(agentNodes) > 0 { - // Remove duplicates from nodes list - uniqueNodes := make(map[string]bool) - for _, n := range nodes { - uniqueNodes[n] = true - } - var nodeList []string - for n := range uniqueNodes { - nodeList = append(nodeList, n) - } - sections = append(sections, fmt.Sprintf("\n**Cluster %s**: Nodes %v share storage and can manage each other's VMs/containers. Agent on %v can run pct/qm commands for guests on ANY node in this cluster.", - clusterName, nodeList, agentNodes)) - } - } - } - - // List PBS instances - if nodesConfig != nil && len(nodesConfig.PBSInstances) > 0 { - sections = append(sections, "\n### Proxmox Backup Servers (PBS)") - sections = append(sections, "Pulse automatically fetches backup data from these PBS servers. Backup status in guest context comes from here.") - for _, pbs := range nodesConfig.PBSInstances { - sections = append(sections, fmt.Sprintf("- **%s** (%s)", pbs.Name, pbs.Host)) - } - } else { - sections = append(sections, "\n### Proxmox Backup Servers: NONE") - sections = append(sections, "No PBS connected - backup_status will show 'NEVER' for all guests") - } - - // List connected agents with their capabilities - if len(connectedAgents) > 0 { - sections = append(sections, "\n### Connected Agents (Command Execution)") - sections = append(sections, "These hosts have the pulse-agent installed. You can run commands on them.") - for _, hostname := range connectedAgents { - // Check if this agent is a Docker host by looking at docker metadata - dockerMeta, _ := s.persistence.LoadDockerMetadata() - hasDocker := false - if dockerMeta != nil { - for id := range dockerMeta { - if strings.Contains(strings.ToLower(id), strings.ToLower(hostname)) { - hasDocker = true - break - } - } - } - - capabilities := "shell commands, file reads" - if hasDocker { - capabilities = "shell commands, file reads, Docker (docker ps, docker exec, docker logs)" - } - sections = append(sections, fmt.Sprintf("- **%s**: %s", hostname, capabilities)) - } - } else { - sections = append(sections, "\n### Connected Agents: NONE") - sections = append(sections, "No agents connected - cannot execute commands on any host") - } - - // Add full guest inventory if state provider is available - s.mu.RLock() - stateProvider := s.stateProvider - s.mu.RUnlock() - - if stateProvider != nil { - state := stateProvider.GetState() - - // Group guests by node for better readability - guestsByNode := make(map[string][]string) - - // Add containers - for _, ct := range state.Containers { - if ct.Template { - continue // Skip templates - } - ips := "" - if len(ct.IPAddresses) > 0 { - ips = " - " + strings.Join(ct.IPAddresses, ", ") - } - entry := fmt.Sprintf(" - **%s** (LXC %d)%s [%s]", ct.Name, ct.VMID, ips, ct.Status) - guestsByNode[ct.Node] = append(guestsByNode[ct.Node], entry) - } - - // Add VMs - for _, vm := range state.VMs { - if vm.Template { - continue // Skip templates - } - ips := "" - if len(vm.IPAddresses) > 0 { - ips = " - " + strings.Join(vm.IPAddresses, ", ") - } - entry := fmt.Sprintf(" - **%s** (VM %d)%s [%s]", vm.Name, vm.VMID, ips, vm.Status) - guestsByNode[vm.Node] = append(guestsByNode[vm.Node], entry) - } - - if len(guestsByNode) > 0 { - sections = append(sections, "\n### All Guests (VMs & Containers)") - sections = append(sections, "Complete list of guests Pulse knows about. Use VMID with pct/qm commands.") - for node, guests := range guestsByNode { - sections = append(sections, fmt.Sprintf("\n**Node %s:**", node)) - for _, guest := range guests { - sections = append(sections, guest) - } - } - } - - // Add standalone hosts (Linux/Windows servers with host agents) - if len(state.Hosts) > 0 { - sections = append(sections, "\n### Standalone Hosts") - sections = append(sections, "Linux/Windows servers monitored via Pulse host agent. Commands can be run directly on these.") - for _, host := range state.Hosts { - ips := "" - for _, iface := range host.NetworkInterfaces { - if len(iface.Addresses) > 0 { - ips = " - " + strings.Join(iface.Addresses, ", ") - break - } - } - osInfo := "" - if host.OSName != "" { - osInfo = fmt.Sprintf(" (%s)", host.OSName) - } - cpuMem := "" - if host.CPUCount > 0 { - cpuMem = fmt.Sprintf(", %d CPU, %.0f%% mem", host.CPUCount, host.Memory.Usage) - } - entry := fmt.Sprintf(" - **%s**%s%s%s [%s]", host.Hostname, osInfo, ips, cpuMem, host.Status) - sections = append(sections, entry) - } - } - - // Add Docker hosts and their containers - if len(state.DockerHosts) > 0 { - sections = append(sections, "\n### Docker Hosts") - sections = append(sections, "Hosts running Docker/Podman. Use docker ps, docker exec, docker logs to manage containers.") - for _, dh := range state.DockerHosts { - if dh.Hidden || dh.PendingUninstall { - continue // Skip hidden or pending uninstall hosts - } - ips := "" - for _, iface := range dh.NetworkInterfaces { - if len(iface.Addresses) > 0 { - ips = " - " + strings.Join(iface.Addresses, ", ") - break - } - } - runtime := dh.Runtime - if runtime == "" { - runtime = "Docker" - } - containerCount := len(dh.Containers) - runningCount := 0 - for _, c := range dh.Containers { - if c.State == "running" { - runningCount++ - } - } - hostEntry := fmt.Sprintf("\n**%s** (%s, %d/%d containers running)%s [%s]", - dh.DisplayName, runtime, runningCount, containerCount, ips, dh.Status) - sections = append(sections, hostEntry) - - // List containers on this host - for _, c := range dh.Containers { - // Build port info - portInfo := "" - if len(c.Ports) > 0 { - var ports []string - for _, p := range c.Ports { - if p.PublicPort > 0 { - ports = append(ports, fmt.Sprintf("%d->%d", p.PublicPort, p.PrivatePort)) - } - } - if len(ports) > 0 { - portInfo = " ports:" + strings.Join(ports, ",") - } - } - // Truncate image name for brevity - image := c.Image - if idx := strings.LastIndex(image, "/"); idx > 0 { - image = image[idx+1:] - } - if len(image) > 30 { - image = image[:27] + "..." - } - healthInfo := "" - if c.Health != "" && c.Health != "none" { - healthInfo = fmt.Sprintf(" (%s)", c.Health) - } - entry := fmt.Sprintf(" - **%s** [%s]%s - %s%s", c.Name, c.State, healthInfo, image, portInfo) - sections = append(sections, entry) - } - } - } - } - - if len(sections) == 0 { - return "" - } - - result := "\n\n## Infrastructure Map\n" + strings.Join(sections, "\n") - - // Limit context size to prevent overwhelming the AI (max ~50KB of infrastructure context) - const maxContextSize = 50000 - if len(result) > maxContextSize { - log.Warn(). - Int("original_size", len(result)). - Int("max_size", maxContextSize). - Msg("Infrastructure context truncated - too many resources") - result = result[:maxContextSize] + "\n\n[... Infrastructure context truncated due to size ...]" - } - - log.Debug().Int("infrastructure_context_size", len(result)).Msg("Built infrastructure context") - - return result -} - // buildUserAnnotationsContext gathers all user annotations from guests and docker containers // These provide infrastructure context that the AI should know about for any query func (s *Service) buildUserAnnotationsContext() string {