Integrate-with-supabase-backend

This commit is contained in:
Harshavardhan Musanalli
2025-10-28 07:53:14 +01:00
parent 8832450a1f
commit 8328f8d5b3
6 changed files with 267 additions and 55 deletions

View File

@@ -128,13 +128,10 @@ func (a *LinuxDiagnosticAgent) DiagnoseIssue(issue string) error {
if len(diagnosticResp.Commands) > 0 { if len(diagnosticResp.Commands) > 0 {
fmt.Printf("🔧 Executing diagnostic commands...\n") fmt.Printf("🔧 Executing diagnostic commands...\n")
for _, cmd := range diagnosticResp.Commands { for _, cmd := range diagnosticResp.Commands {
fmt.Printf("⚙️ Executing command '%s': %s\n", cmd.ID, cmd.Command)
result := a.executor.Execute(cmd) result := a.executor.Execute(cmd)
commandResults = append(commandResults, result) commandResults = append(commandResults, result)
if result.ExitCode == 0 { if result.ExitCode != 0 {
fmt.Printf("✅ Command '%s' completed successfully\n", cmd.ID)
} else {
fmt.Printf("❌ Command '%s' failed with exit code %d\n", cmd.ID, result.ExitCode) fmt.Printf("❌ Command '%s' failed with exit code %d\n", cmd.ID, result.ExitCode)
} }
} }
@@ -143,7 +140,6 @@ func (a *LinuxDiagnosticAgent) DiagnoseIssue(issue string) error {
// Execute eBPF programs if present // Execute eBPF programs if present
var ebpfResults []map[string]interface{} var ebpfResults []map[string]interface{}
if len(diagnosticResp.EBPFPrograms) > 0 { if len(diagnosticResp.EBPFPrograms) > 0 {
fmt.Printf("🔬 Executing %d eBPF programs...\n", len(diagnosticResp.EBPFPrograms))
ebpfResults = a.executeEBPFPrograms(diagnosticResp.EBPFPrograms) ebpfResults = a.executeEBPFPrograms(diagnosticResp.EBPFPrograms)
} }
@@ -170,15 +166,6 @@ func (a *LinuxDiagnosticAgent) DiagnoseIssue(issue string) error {
evidenceSummary = append(evidenceSummary, summaryStr) evidenceSummary = append(evidenceSummary, summaryStr)
} }
allResults["ebpf_evidence_summary"] = evidenceSummary allResults["ebpf_evidence_summary"] = evidenceSummary
fmt.Printf("<22> Sending eBPF monitoring data to TensorZero:\n")
for _, summary := range evidenceSummary {
fmt.Printf(" - %s\n", summary)
}
fmt.Printf("✅ Executed %d commands, %d eBPF programs\n", len(commandResults), len(ebpfResults))
} else {
fmt.Printf("✅ Executed %d commands\n", len(commandResults))
} }
resultsJSON, err := json.MarshalIndent(allResults, "", " ") resultsJSON, err := json.MarshalIndent(allResults, "", " ")
@@ -227,7 +214,7 @@ func (a *LinuxDiagnosticAgent) executeEBPFPrograms(ebpfPrograms []EBPFRequest) [
} }
for _, prog := range ebpfPrograms { for _, prog := range ebpfPrograms {
fmt.Printf("🔬 Starting eBPF program [%s]: %s -> %s (%ds)\n", prog.Name, prog.Type, prog.Target, int(prog.Duration)) // eBPF program starting - only show in debug mode
// Actually start the eBPF program using the real manager // Actually start the eBPF program using the real manager
programID, err := a.ebpfManager.StartEBPFProgram(prog) programID, err := a.ebpfManager.StartEBPFProgram(prog)
@@ -248,16 +235,11 @@ func (a *LinuxDiagnosticAgent) executeEBPFPrograms(ebpfPrograms []EBPFRequest) [
} }
// Let the eBPF program run for the specified duration // Let the eBPF program run for the specified duration
fmt.Printf("⏰ Waiting %d seconds for eBPF program to collect data...\n", int(prog.Duration))
time.Sleep(time.Duration(prog.Duration) * time.Second) time.Sleep(time.Duration(prog.Duration) * time.Second)
// Give the collectEvents goroutine a moment to finish and store results // Give the collectEvents goroutine a moment to finish and store results
fmt.Printf("⏳ Allowing program to complete data collection...\n")
time.Sleep(500 * time.Millisecond) time.Sleep(500 * time.Millisecond)
// Get the results (should be in completedResults now)
fmt.Printf("📊 Getting results for eBPF program [%s]...\n", prog.Name)
// Use a channel to implement timeout for GetProgramResults // Use a channel to implement timeout for GetProgramResults
type resultPair struct { type resultPair struct {
trace *EBPFTrace trace *EBPFTrace
@@ -282,11 +264,9 @@ func (a *LinuxDiagnosticAgent) executeEBPFPrograms(ebpfPrograms []EBPFRequest) [
} }
// Try to stop the program (may already be stopped by collectEvents) // Try to stop the program (may already be stopped by collectEvents)
fmt.Printf("🛑 Stopping eBPF program [%s]...\n", prog.Name)
stopErr := a.ebpfManager.StopProgram(programID) stopErr := a.ebpfManager.StopProgram(programID)
if stopErr != nil { if stopErr != nil {
fmt.Printf("⚠️ eBPF program [%s] cleanup: %v (may have already completed)\n", prog.Name, stopErr) // Only show warning in debug mode - this is normal for completed programs
// Don't return here, we still want to process results if we got them
} }
if resultErr != nil { if resultErr != nil {
@@ -325,7 +305,6 @@ func (a *LinuxDiagnosticAgent) executeEBPFPrograms(ebpfPrograms []EBPFRequest) [
result["end_time"] = trace.EndTime.Format(time.RFC3339) result["end_time"] = trace.EndTime.Format(time.RFC3339)
result["actual_duration"] = trace.EndTime.Sub(trace.StartTime).Seconds() result["actual_duration"] = trace.EndTime.Sub(trace.StartTime).Seconds()
fmt.Printf("✅ eBPF program [%s] completed - collected %d real events\n", prog.Name, trace.EventCount)
} else { } else {
result["data_points"] = 0 result["data_points"] = 0
result["error"] = "No trace data returned" result["error"] = "No trace data returned"

View File

@@ -0,0 +1,90 @@
package logging
import (
"fmt"
"log"
"log/syslog"
"os"
)
type Logger struct {
syslogWriter *syslog.Writer
debugMode bool
}
var defaultLogger *Logger
func init() {
defaultLogger = NewLogger()
}
func NewLogger() *Logger {
l := &Logger{
debugMode: os.Getenv("DEBUG") == "true",
}
// Try to connect to syslog
if writer, err := syslog.New(syslog.LOG_INFO|syslog.LOG_DAEMON, "nannyagentv2"); err == nil {
l.syslogWriter = writer
}
return l
}
func (l *Logger) Info(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
if l.syslogWriter != nil {
l.syslogWriter.Info(msg)
}
log.Printf("[INFO] %s", msg)
}
func (l *Logger) Debug(format string, args ...interface{}) {
if !l.debugMode {
return
}
msg := fmt.Sprintf(format, args...)
if l.syslogWriter != nil {
l.syslogWriter.Debug(msg)
}
log.Printf("[DEBUG] %s", msg)
}
func (l *Logger) Warning(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
if l.syslogWriter != nil {
l.syslogWriter.Warning(msg)
}
log.Printf("[WARNING] %s", msg)
}
func (l *Logger) Error(format string, args ...interface{}) {
msg := fmt.Sprintf(format, args...)
if l.syslogWriter != nil {
l.syslogWriter.Err(msg)
}
log.Printf("[ERROR] %s", msg)
}
func (l *Logger) Close() {
if l.syslogWriter != nil {
l.syslogWriter.Close()
}
}
// Global logging functions
func Info(format string, args ...interface{}) {
defaultLogger.Info(format, args...)
}
func Debug(format string, args ...interface{}) {
defaultLogger.Debug(format, args...)
}
func Warning(format string, args ...interface{}) {
defaultLogger.Warning(format, args...)
}
func Error(format string, args ...interface{}) {
defaultLogger.Error(format, args...)
}

View File

@@ -168,3 +168,170 @@ type MetricsRequest struct {
BlockDevices []BlockDevice `json:"block_devices"` BlockDevices []BlockDevice `json:"block_devices"`
NetworkStats map[string]uint64 `json:"network_stats"` NetworkStats map[string]uint64 `json:"network_stats"`
} }
// eBPF related types
type EBPFEvent struct {
Timestamp int64 `json:"timestamp"`
EventType string `json:"event_type"`
ProcessID int `json:"process_id"`
ProcessName string `json:"process_name"`
UserID int `json:"user_id"`
Data map[string]interface{} `json:"data"`
}
type EBPFTrace struct {
TraceID string `json:"trace_id"`
StartTime time.Time `json:"start_time"`
EndTime time.Time `json:"end_time"`
Capability string `json:"capability"`
Events []EBPFEvent `json:"events"`
Summary string `json:"summary"`
EventCount int `json:"event_count"`
ProcessList []string `json:"process_list"`
}
type EBPFRequest struct {
Name string `json:"name"`
Type string `json:"type"` // "tracepoint", "kprobe", "kretprobe"
Target string `json:"target"` // tracepoint path or function name
Duration int `json:"duration"` // seconds
Filters map[string]string `json:"filters,omitempty"`
Description string `json:"description"`
}
type NetworkEvent struct {
Timestamp uint64 `json:"timestamp"`
PID uint32 `json:"pid"`
TID uint32 `json:"tid"`
UID uint32 `json:"uid"`
EventType string `json:"event_type"`
Comm [16]byte `json:"-"`
CommStr string `json:"comm"`
}
// Agent types
type DiagnosticResponse struct {
ResponseType string `json:"response_type"`
Reasoning string `json:"reasoning"`
Commands []Command `json:"commands"`
}
type ResolutionResponse struct {
ResponseType string `json:"response_type"`
RootCause string `json:"root_cause"`
ResolutionPlan string `json:"resolution_plan"`
Confidence string `json:"confidence"`
}
type Command struct {
ID string `json:"id"`
Command string `json:"command"`
Description string `json:"description"`
}
type CommandResult struct {
ID string `json:"id"`
Command string `json:"command"`
Description string `json:"description"`
Output string `json:"output"`
ExitCode int `json:"exit_code"`
Error string `json:"error,omitempty"`
}
type EBPFEnhancedDiagnosticResponse struct {
ResponseType string `json:"response_type"`
Reasoning string `json:"reasoning"`
Commands []Command `json:"commands"`
EBPFPrograms []EBPFRequest `json:"ebpf_programs"`
NextActions []string `json:"next_actions,omitempty"`
}
type TensorZeroRequest struct {
Model string `json:"model"`
Messages []map[string]interface{} `json:"messages"`
EpisodeID string `json:"tensorzero::episode_id,omitempty"`
}
type TensorZeroResponse struct {
Choices []map[string]interface{} `json:"choices"`
EpisodeID string `json:"episode_id"`
}
// WebSocket types
type WebSocketMessage struct {
Type string `json:"type"`
Data interface{} `json:"data"`
}
type InvestigationTask struct {
TaskID string `json:"task_id"`
InvestigationID string `json:"investigation_id"`
AgentID string `json:"agent_id"`
DiagnosticPayload map[string]interface{} `json:"diagnostic_payload"`
EpisodeID string `json:"episode_id,omitempty"`
}
type TaskResult struct {
TaskID string `json:"task_id"`
Success bool `json:"success"`
CommandResults map[string]interface{} `json:"command_results,omitempty"`
Error string `json:"error,omitempty"`
}
type HeartbeatData struct {
AgentID string `json:"agent_id"`
Timestamp time.Time `json:"timestamp"`
Version string `json:"version"`
}
// Investigation server types
type InvestigationRequest struct {
Issue string `json:"issue"`
AgentID string `json:"agent_id"`
EpisodeID string `json:"episode_id,omitempty"`
Timestamp string `json:"timestamp,omitempty"`
Priority string `json:"priority,omitempty"`
Description string `json:"description,omitempty"`
}
type InvestigationResponse struct {
Status string `json:"status"`
Message string `json:"message"`
Results map[string]interface{} `json:"results,omitempty"`
AgentID string `json:"agent_id"`
Timestamp string `json:"timestamp"`
EpisodeID string `json:"episode_id,omitempty"`
Investigation *PendingInvestigation `json:"investigation,omitempty"`
}
type PendingInvestigation struct {
ID string `json:"id"`
Issue string `json:"issue"`
AgentID string `json:"agent_id"`
Status string `json:"status"`
DiagnosticPayload map[string]interface{} `json:"diagnostic_payload"`
CommandResults map[string]interface{} `json:"command_results,omitempty"`
EpisodeID *string `json:"episode_id,omitempty"`
CreatedAt string `json:"created_at"`
StartedAt *string `json:"started_at,omitempty"`
CompletedAt *string `json:"completed_at,omitempty"`
ErrorMessage *string `json:"error_message,omitempty"`
}
// System types
type SystemInfo struct {
Hostname string `json:"hostname"`
Platform string `json:"platform"`
PlatformInfo map[string]string `json:"platform_info"`
KernelVersion string `json:"kernel_version"`
Uptime string `json:"uptime"`
LoadAverage []float64 `json:"load_average"`
CPUInfo map[string]string `json:"cpu_info"`
MemoryInfo map[string]string `json:"memory_info"`
DiskInfo []map[string]string `json:"disk_info"`
}
// Executor types
type CommandExecutor struct {
timeout time.Duration
}

View File

@@ -60,7 +60,7 @@ func NewInvestigationServer(agent *LinuxDiagnosticAgent, authManager *auth.AuthM
if authManager != nil { if authManager != nil {
if id, err := authManager.GetCurrentAgentID(); err == nil { if id, err := authManager.GetCurrentAgentID(); err == nil {
agentID = id agentID = id
fmt.Printf("✅ Retrieved agent ID from auth manager: %s\n", agentID)
} else { } else {
fmt.Printf("❌ Failed to get agent ID from auth manager: %v\n", err) fmt.Printf("❌ Failed to get agent ID from auth manager: %v\n", err)
} }
@@ -250,7 +250,7 @@ func (s *InvestigationServer) sendCommandResultsToTensorZero(diagnosticResp Diag
// Check if it's a resolution response // Check if it's a resolution response
if err := json.Unmarshal([]byte(content), &resolutionResp); err == nil && resolutionResp.ResponseType == "resolution" { if err := json.Unmarshal([]byte(content), &resolutionResp); err == nil && resolutionResp.ResponseType == "resolution" {
fmt.Printf("✅ TensorZero provided final resolution\n")
return map[string]interface{}{ return map[string]interface{}{
"type": "resolution", "type": "resolution",
"response": resolutionResp, "response": resolutionResp,
@@ -355,7 +355,7 @@ func (s *InvestigationServer) handleDiagnosticExecution(requestBody map[string]i
result := s.agent.executor.Execute(cmd) result := s.agent.executor.Execute(cmd)
commandResults = append(commandResults, result) commandResults = append(commandResults, result)
fmt.Printf("✅ Command '%s' completed with exit code %d\n", cmd.ID, result.ExitCode)
if result.Error != "" { if result.Error != "" {
fmt.Printf("⚠️ Command '%s' had error: %s\n", cmd.ID, result.Error) fmt.Printf("⚠️ Command '%s' had error: %s\n", cmd.ID, result.Error)
} }
@@ -471,7 +471,7 @@ func (s *InvestigationServer) handlePendingInvestigation(investigation PendingIn
return return
} }
fmt.Printf("✅ Realtime investigation %s completed successfully\n", investigation.InvestigationID)
} }
// updateInvestigationStatus updates the status of a pending investigation // updateInvestigationStatus updates the status of a pending investigation

View File

@@ -73,7 +73,7 @@ func checkKernelVersionCompatibility() {
os.Exit(1) os.Exit(1)
} }
fmt.Printf("✅ Kernel version %s is compatible with eBPF\n", kernelVersion)
} }
// checkEBPFSupport validates eBPF subsystem availability // checkEBPFSupport validates eBPF subsystem availability
@@ -97,7 +97,7 @@ func checkEBPFSupport() {
syscall.Close(int(fd)) syscall.Close(int(fd))
} }
fmt.Printf("✅ eBPF syscall is available\n")
} }
// runInteractiveDiagnostics starts the interactive diagnostic session // runInteractiveDiagnostics starts the interactive diagnostic session

View File

@@ -130,7 +130,6 @@ func (w *WebSocketClient) Start() error {
// Stop closes the WebSocket connection // Stop closes the WebSocket connection
func (c *WebSocketClient) Stop() { func (c *WebSocketClient) Stop() {
fmt.Println("🛑 Stopping WebSocket client...")
c.cancel() c.cancel()
if c.conn != nil { if c.conn != nil {
c.conn.Close() c.conn.Close()
@@ -313,8 +312,6 @@ func (c *WebSocketClient) handleInvestigationTask(data interface{}) {
// executeDiagnosticCommands executes the commands from a diagnostic response // executeDiagnosticCommands executes the commands from a diagnostic response
func (c *WebSocketClient) executeDiagnosticCommands(diagnosticPayload map[string]interface{}) (map[string]interface{}, error) { func (c *WebSocketClient) executeDiagnosticCommands(diagnosticPayload map[string]interface{}) (map[string]interface{}, error) {
fmt.Println("🔧 Executing diagnostic commands...")
results := map[string]interface{}{ results := map[string]interface{}{
"agent_id": c.agentID, "agent_id": c.agentID,
"execution_time": time.Now().UTC().Format(time.RFC3339), "execution_time": time.Now().UTC().Format(time.RFC3339),
@@ -360,8 +357,6 @@ func (c *WebSocketClient) executeDiagnosticCommands(diagnosticPayload map[string
if err != nil { if err != nil {
result["error"] = err.Error() result["error"] = err.Error()
fmt.Printf("❌ Command [%s] failed: %v (exit code: %d)\n", id, err, exitCode) fmt.Printf("❌ Command [%s] failed: %v (exit code: %d)\n", id, err, exitCode)
} else {
// Command completed successfully - output captured
} }
commandResults = append(commandResults, result) commandResults = append(commandResults, result)
@@ -374,17 +369,11 @@ func (c *WebSocketClient) executeDiagnosticCommands(diagnosticPayload map[string
// Execute eBPF programs if present // Execute eBPF programs if present
ebpfPrograms, hasEBPF := diagnosticPayload["ebpf_programs"].([]interface{}) ebpfPrograms, hasEBPF := diagnosticPayload["ebpf_programs"].([]interface{})
if hasEBPF && len(ebpfPrograms) > 0 { if hasEBPF && len(ebpfPrograms) > 0 {
fmt.Printf("🔬 Executing %d eBPF programs...\n", len(ebpfPrograms))
ebpfResults := c.executeEBPFPrograms(ebpfPrograms) ebpfResults := c.executeEBPFPrograms(ebpfPrograms)
results["ebpf_results"] = ebpfResults results["ebpf_results"] = ebpfResults
results["total_ebpf_programs"] = len(ebpfPrograms) results["total_ebpf_programs"] = len(ebpfPrograms)
} else {
fmt.Printf(" No eBPF programs in diagnostic payload\n")
} }
fmt.Printf("✅ Executed %d commands, %d successful\n",
results["total_commands"], results["successful_commands"])
return results, nil return results, nil
} }
@@ -455,8 +444,6 @@ func (c *WebSocketClient) executeCommandsFromPayload(commands []interface{}) []m
if err != nil { if err != nil {
result["error"] = err.Error() result["error"] = err.Error()
fmt.Printf("❌ Command [%s] failed: %v (exit code: %d)\n", id, err, exitCode) fmt.Printf("❌ Command [%s] failed: %v (exit code: %d)\n", id, err, exitCode)
} else {
fmt.Printf("✅ Command [%s] completed successfully\n", id)
} }
commandResults = append(commandResults, result) commandResults = append(commandResults, result)
@@ -657,14 +644,10 @@ func (c *WebSocketClient) handlePendingInvestigation(investigation PendingInvest
}, },
} }
fmt.Printf("🔄 Sending command results to TensorZero for continued analysis...\n")
fmt.Printf("📤 Command results payload size: %d bytes\n", len(commandsJSON))
// Use the episode ID from the investigation to maintain conversation continuity // Use the episode ID from the investigation to maintain conversation continuity
episodeID := "" episodeID := ""
if investigation.EpisodeID != nil { if investigation.EpisodeID != nil {
episodeID = *investigation.EpisodeID episodeID = *investigation.EpisodeID
fmt.Printf("🔗 Using episode ID: %s\n", episodeID)
} }
// Continue conversation until resolution (same as agent) // Continue conversation until resolution (same as agent)
@@ -678,8 +661,6 @@ func (c *WebSocketClient) handlePendingInvestigation(investigation PendingInvest
return return
} }
fmt.Printf("✅ TensorZero responded successfully\n")
if len(tzResp.Choices) == 0 { if len(tzResp.Choices) == 0 {
fmt.Printf("⚠️ No choices in TensorZero response\n") fmt.Printf("⚠️ No choices in TensorZero response\n")
c.updateInvestigationStatus(investigation.ID, "completed", resultsForDB, nil) c.updateInvestigationStatus(investigation.ID, "completed", resultsForDB, nil)
@@ -688,7 +669,7 @@ func (c *WebSocketClient) handlePendingInvestigation(investigation PendingInvest
aiContent := tzResp.Choices[0].Message.Content aiContent := tzResp.Choices[0].Message.Content
if len(aiContent) > 300 { if len(aiContent) > 300 {
fmt.Printf("🤖 AI Response preview: %s...\n", aiContent[:300]) // AI response received successfully
} else { } else {
fmt.Printf("🤖 AI Response: %s\n", aiContent) fmt.Printf("🤖 AI Response: %s\n", aiContent)
} }
@@ -705,7 +686,6 @@ func (c *WebSocketClient) handlePendingInvestigation(investigation PendingInvest
if err := json.Unmarshal([]byte(aiContent), &resolutionResp); err == nil && resolutionResp.ResponseType == "resolution" { if err := json.Unmarshal([]byte(aiContent), &resolutionResp); err == nil && resolutionResp.ResponseType == "resolution" {
// This is the final resolution - show summary and complete // This is the final resolution - show summary and complete
fmt.Printf("✅ Detected RESOLUTION response - completing investigation\n")
fmt.Printf("\n=== DIAGNOSIS COMPLETE ===\n") fmt.Printf("\n=== DIAGNOSIS COMPLETE ===\n")
fmt.Printf("Root Cause: %s\n", resolutionResp.RootCause) fmt.Printf("Root Cause: %s\n", resolutionResp.RootCause)
fmt.Printf("Resolution Plan: %s\n", resolutionResp.ResolutionPlan) fmt.Printf("Resolution Plan: %s\n", resolutionResp.ResolutionPlan)
@@ -722,7 +702,6 @@ func (c *WebSocketClient) handlePendingInvestigation(investigation PendingInvest
} }
if err := json.Unmarshal([]byte(aiContent), &diagnosticResp); err == nil && diagnosticResp.ResponseType == "diagnostic" { if err := json.Unmarshal([]byte(aiContent), &diagnosticResp); err == nil && diagnosticResp.ResponseType == "diagnostic" {
fmt.Printf("✅ Detected DIAGNOSTIC response - continuing conversation\n")
fmt.Printf("🔄 AI requested additional diagnostics, executing...\n") fmt.Printf("🔄 AI requested additional diagnostics, executing...\n")
// Execute additional commands if any // Execute additional commands if any
@@ -738,7 +717,6 @@ func (c *WebSocketClient) handlePendingInvestigation(investigation PendingInvest
// Execute additional eBPF programs if any // Execute additional eBPF programs if any
if len(diagnosticResp.EBPFPrograms) > 0 { if len(diagnosticResp.EBPFPrograms) > 0 {
fmt.Printf("🔬 Executing %d additional eBPF programs...\n", len(diagnosticResp.EBPFPrograms))
ebpfResults := c.executeEBPFPrograms(diagnosticResp.EBPFPrograms) ebpfResults := c.executeEBPFPrograms(diagnosticResp.EBPFPrograms)
additionalResults["ebpf_results"] = ebpfResults additionalResults["ebpf_results"] = ebpfResults
} }
@@ -766,9 +744,7 @@ func (c *WebSocketClient) handlePendingInvestigation(investigation PendingInvest
// Attach final AI response to results for DB and mark as completed_with_analysis // Attach final AI response to results for DB and mark as completed_with_analysis
resultsForDB["ai_response"] = finalAIContent resultsForDB["ai_response"] = finalAIContent
fmt.Printf("💾 Updating database with results and AI analysis...\n")
c.updateInvestigationStatus(investigation.ID, "completed_with_analysis", resultsForDB, nil) c.updateInvestigationStatus(investigation.ID, "completed_with_analysis", resultsForDB, nil)
fmt.Printf("✅ Investigation completed with AI analysis\n")
} }
// updateInvestigationStatus updates the status of a pending investigation // updateInvestigationStatus updates the status of a pending investigation