mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-29 03:50:18 +00:00
- 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.
357 lines
12 KiB
Go
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
|
|
}
|