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.
This commit is contained in:
parent
7e8611e0ae
commit
f7eed7b052
2 changed files with 3 additions and 294 deletions
|
|
@ -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)
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue