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 debug console.log statements from frontend routes
|
||||||
- [x] Remove legacy fallback code from route components (Docker, Hosts, Dashboard)
|
- [x] Remove legacy fallback code from route components (Docker, Hosts, Dashboard)
|
||||||
- [x] Simplify route components to use centralized `useResourcesAsLegacy()` hook
|
- [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 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
|
prompt += cfg.CustomContext
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add connected infrastructure info
|
// Add connected infrastructure info via unified resource model
|
||||||
// Try unified resource context first (Phase 2), fall back to legacy
|
|
||||||
s.mu.RLock()
|
s.mu.RLock()
|
||||||
hasResourceProvider := s.resourceProvider != nil
|
hasResourceProvider := s.resourceProvider != nil
|
||||||
s.mu.RUnlock()
|
s.mu.RUnlock()
|
||||||
|
|
||||||
if hasResourceProvider {
|
if hasResourceProvider {
|
||||||
// Use the new unified resource model for cleaner, deduplicated context
|
|
||||||
prompt += s.buildUnifiedResourceContext()
|
prompt += s.buildUnifiedResourceContext()
|
||||||
} else {
|
} else {
|
||||||
// Fall back to legacy state-based context
|
log.Warn().Msg("AI context: resource provider not available, infrastructure context will be limited")
|
||||||
prompt += s.buildInfrastructureContext()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add user annotations from all resources (global context)
|
// Add user annotations from all resources (global context)
|
||||||
|
|
@ -1920,294 +1917,6 @@ func formatContextKey(key string) string {
|
||||||
return key
|
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
|
// buildUserAnnotationsContext gathers all user annotations from guests and docker containers
|
||||||
// These provide infrastructure context that the AI should know about for any query
|
// These provide infrastructure context that the AI should know about for any query
|
||||||
func (s *Service) buildUserAnnotationsContext() string {
|
func (s *Service) buildUserAnnotationsContext() string {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue