Pulse/internal/ai/tools/tools_knowledge.go
rcourtman a75393d1c5 refactor(ai): consolidate tool implementations into domain-specific files
- Merge tools_infrastructure.go, tools_intelligence.go, tools_patrol.go,
  tools_profiles.go into their respective domain tools
- Expand tools_control.go with command execution logic
- Expand tools_discovery.go with resource discovery handlers
- Expand tools_storage.go with storage-related operations
- Expand tools_metrics.go with metrics functionality
- Update tests to match new structure

This consolidation reduces file count and groups related functionality together.
2026-01-28 21:21:28 +00:00

357 lines
12 KiB
Go

package tools
import (
"context"
"fmt"
"time"
)
// IncidentRecorderProvider provides access to incident recording data
type IncidentRecorderProvider interface {
GetWindowsForResource(resourceID string, limit int) []*IncidentWindow
GetWindow(windowID string) *IncidentWindow
}
// IncidentWindow represents a high-frequency recording window during an incident
type IncidentWindow struct {
ID string `json:"id"`
ResourceID string `json:"resource_id"`
ResourceName string `json:"resource_name,omitempty"`
ResourceType string `json:"resource_type,omitempty"`
TriggerType string `json:"trigger_type"`
TriggerID string `json:"trigger_id,omitempty"`
StartTime time.Time `json:"start_time"`
EndTime *time.Time `json:"end_time,omitempty"`
Status string `json:"status"`
DataPoints []IncidentDataPoint `json:"data_points"`
Summary *IncidentSummary `json:"summary,omitempty"`
}
// IncidentDataPoint represents a single data point in an incident window
type IncidentDataPoint struct {
Timestamp time.Time `json:"timestamp"`
Metrics map[string]float64 `json:"metrics"`
}
// IncidentSummary provides computed statistics about an incident window
type IncidentSummary struct {
Duration time.Duration `json:"duration_ms"`
DataPoints int `json:"data_points"`
Peaks map[string]float64 `json:"peaks"`
Lows map[string]float64 `json:"lows"`
Averages map[string]float64 `json:"averages"`
Changes map[string]float64 `json:"changes"`
}
// EventCorrelatorProvider provides access to correlated events
type EventCorrelatorProvider interface {
GetCorrelationsForResource(resourceID string, window time.Duration) []EventCorrelation
}
// EventCorrelation represents a correlated event
type EventCorrelation struct {
EventType string `json:"event_type"`
Timestamp time.Time `json:"timestamp"`
ResourceID string `json:"resource_id"`
ResourceName string `json:"resource_name,omitempty"`
Description string `json:"description"`
Severity string `json:"severity,omitempty"`
Metadata map[string]interface{} `json:"metadata,omitempty"`
}
// TopologyProvider provides access to resource relationships
type TopologyProvider interface {
GetRelatedResources(resourceID string, depth int) []RelatedResource
}
// RelatedResource represents a resource related to another resource
type RelatedResource struct {
ResourceID string `json:"resource_id"`
ResourceName string `json:"resource_name"`
ResourceType string `json:"resource_type"`
Relationship string `json:"relationship"`
}
// KnowledgeStoreProvider provides access to stored knowledge/notes
type KnowledgeStoreProvider interface {
SaveNote(resourceID, note, category string) error
GetKnowledge(resourceID string, category string) []KnowledgeEntry
}
// KnowledgeEntry represents a stored note about a resource
type KnowledgeEntry struct {
ID string `json:"id"`
ResourceID string `json:"resource_id"`
Note string `json:"note"`
Category string `json:"category,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
}
// registerKnowledgeTools registers the pulse_knowledge tool
func (e *PulseToolExecutor) registerKnowledgeTools() {
e.registry.Register(RegisteredTool{
Definition: Tool{
Name: "pulse_knowledge",
Description: `Manage AI knowledge, notes, and incident analysis.
Actions:
- remember: Save a note about a resource for future reference
- recall: Retrieve saved notes about a resource
- incidents: Get high-resolution incident recording data
- correlate: Get correlated events around a timestamp
- relationships: Get resource dependency graph
Examples:
- Save note: action="remember", resource_id="101", note="Production database server", category="purpose"
- Recall: action="recall", resource_id="101"
- Get incidents: action="incidents", resource_id="101"
- Correlate events: action="correlate", resource_id="101", window_minutes=30
- Get relationships: action="relationships", resource_id="101"`,
InputSchema: InputSchema{
Type: "object",
Properties: map[string]PropertySchema{
"action": {
Type: "string",
Description: "Knowledge action to perform",
Enum: []string{"remember", "recall", "incidents", "correlate", "relationships"},
},
"resource_id": {
Type: "string",
Description: "Resource ID to operate on",
},
"note": {
Type: "string",
Description: "For remember: the note to save",
},
"category": {
Type: "string",
Description: "For remember/recall: note category (purpose, owner, maintenance, issue)",
},
"window_id": {
Type: "string",
Description: "For incidents: specific incident window ID",
},
"timestamp": {
Type: "string",
Description: "For correlate: ISO timestamp to center search around (default: now)",
},
"window_minutes": {
Type: "integer",
Description: "For correlate: time window in minutes (default: 15)",
},
"depth": {
Type: "integer",
Description: "For relationships: levels to traverse (default: 1, max: 3)",
},
"limit": {
Type: "integer",
Description: "For incidents: max windows to return (default: 5)",
},
},
Required: []string{"action", "resource_id"},
},
},
Handler: func(ctx context.Context, exec *PulseToolExecutor, args map[string]interface{}) (CallToolResult, error) {
return exec.executeKnowledge(ctx, args)
},
})
}
// executeKnowledge routes to the appropriate knowledge handler based on action
func (e *PulseToolExecutor) executeKnowledge(ctx context.Context, args map[string]interface{}) (CallToolResult, error) {
action, _ := args["action"].(string)
switch action {
case "remember":
return e.executeRemember(ctx, args)
case "recall":
return e.executeRecall(ctx, args)
case "incidents":
return e.executeGetIncidentWindow(ctx, args)
case "correlate":
return e.executeCorrelateEvents(ctx, args)
case "relationships":
return e.executeGetRelationshipGraph(ctx, args)
default:
return NewErrorResult(fmt.Errorf("unknown action: %s. Use: remember, recall, incidents, correlate, relationships", action)), nil
}
}
// Tool handler implementations
func (e *PulseToolExecutor) executeGetIncidentWindow(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
resourceID, _ := args["resource_id"].(string)
windowID, _ := args["window_id"].(string)
limit := intArg(args, "limit", 5)
if resourceID == "" {
return NewErrorResult(fmt.Errorf("resource_id is required")), nil
}
if e.incidentRecorderProvider == nil {
return NewTextResult("Incident recording data not available. The incident recorder may not be enabled."), nil
}
// If a specific window ID is requested
if windowID != "" {
window := e.incidentRecorderProvider.GetWindow(windowID)
if window == nil {
return NewTextResult(fmt.Sprintf("Incident window '%s' not found.", windowID)), nil
}
return NewJSONResult(map[string]interface{}{
"window": window,
}), nil
}
// Get windows for the resource
windows := e.incidentRecorderProvider.GetWindowsForResource(resourceID, limit)
if len(windows) == 0 {
return NewTextResult(fmt.Sprintf("No incident recording data found for resource '%s'. Incident data is captured when alerts fire.", resourceID)), nil
}
return NewJSONResult(map[string]interface{}{
"resource_id": resourceID,
"windows": windows,
"count": len(windows),
}), nil
}
func (e *PulseToolExecutor) executeCorrelateEvents(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
resourceID, _ := args["resource_id"].(string)
timestampStr, _ := args["timestamp"].(string)
windowMinutes := intArg(args, "window_minutes", 15)
if resourceID == "" {
return NewErrorResult(fmt.Errorf("resource_id is required")), nil
}
if e.eventCorrelatorProvider == nil {
return NewTextResult("Event correlation not available. The event correlator may not be enabled."), nil
}
// Parse timestamp or use now
var timestamp time.Time
if timestampStr != "" {
var err error
timestamp, err = time.Parse(time.RFC3339, timestampStr)
if err != nil {
return NewErrorResult(fmt.Errorf("invalid timestamp format: %w", err)), nil
}
} else {
timestamp = time.Now()
}
window := time.Duration(windowMinutes) * time.Minute
correlations := e.eventCorrelatorProvider.GetCorrelationsForResource(resourceID, window)
if len(correlations) == 0 {
return NewTextResult(fmt.Sprintf("No correlated events found for resource '%s' within %d minutes of %s.",
resourceID, windowMinutes, timestamp.Format(time.RFC3339))), nil
}
return NewJSONResult(map[string]interface{}{
"resource_id": resourceID,
"timestamp": timestamp.Format(time.RFC3339),
"window_minutes": windowMinutes,
"events": correlations,
"count": len(correlations),
}), nil
}
func (e *PulseToolExecutor) executeGetRelationshipGraph(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
resourceID, _ := args["resource_id"].(string)
depth := intArg(args, "depth", 1)
if resourceID == "" {
return NewErrorResult(fmt.Errorf("resource_id is required")), nil
}
// Cap depth to prevent excessive traversal
if depth < 1 {
depth = 1
}
if depth > 3 {
depth = 3
}
if e.topologyProvider == nil {
return NewTextResult("Topology information not available."), nil
}
related := e.topologyProvider.GetRelatedResources(resourceID, depth)
if len(related) == 0 {
return NewTextResult(fmt.Sprintf("No relationships found for resource '%s'.", resourceID)), nil
}
return NewJSONResult(map[string]interface{}{
"resource_id": resourceID,
"depth": depth,
"related_resources": related,
"count": len(related),
}), nil
}
func (e *PulseToolExecutor) executeRemember(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
resourceID, _ := args["resource_id"].(string)
note, _ := args["note"].(string)
category, _ := args["category"].(string)
if resourceID == "" {
return NewErrorResult(fmt.Errorf("resource_id is required")), nil
}
if note == "" {
return NewErrorResult(fmt.Errorf("note is required")), nil
}
if e.knowledgeStoreProvider == nil {
return NewTextResult("Knowledge storage not available."), nil
}
if err := e.knowledgeStoreProvider.SaveNote(resourceID, note, category); err != nil {
return NewErrorResult(fmt.Errorf("failed to save note: %w", err)), nil
}
response := map[string]interface{}{
"success": true,
"resource_id": resourceID,
"note": note,
"message": "Note saved successfully",
}
if category != "" {
response["category"] = category
}
return NewJSONResult(response), nil
}
func (e *PulseToolExecutor) executeRecall(_ context.Context, args map[string]interface{}) (CallToolResult, error) {
resourceID, _ := args["resource_id"].(string)
category, _ := args["category"].(string)
if resourceID == "" {
return NewErrorResult(fmt.Errorf("resource_id is required")), nil
}
if e.knowledgeStoreProvider == nil {
return NewTextResult("Knowledge storage not available."), nil
}
entries := e.knowledgeStoreProvider.GetKnowledge(resourceID, category)
if len(entries) == 0 {
if category != "" {
return NewTextResult(fmt.Sprintf("No notes found for resource '%s' in category '%s'.", resourceID, category)), nil
}
return NewTextResult(fmt.Sprintf("No notes found for resource '%s'.", resourceID)), nil
}
response := map[string]interface{}{
"resource_id": resourceID,
"notes": entries,
"count": len(entries),
}
if category != "" {
response["category"] = category
}
return NewJSONResult(response), nil
}