Working code with Tensorzero through Supabase proxy
This commit is contained in:
73
agent.go
73
agent.go
@@ -55,10 +55,11 @@ type LinuxDiagnosticAgent struct {
|
|||||||
|
|
||||||
// NewLinuxDiagnosticAgent creates a new diagnostic agent
|
// NewLinuxDiagnosticAgent creates a new diagnostic agent
|
||||||
func NewLinuxDiagnosticAgent() *LinuxDiagnosticAgent {
|
func NewLinuxDiagnosticAgent() *LinuxDiagnosticAgent {
|
||||||
endpoint := os.Getenv("NANNYAPI_ENDPOINT")
|
// Get Supabase project URL for TensorZero proxy
|
||||||
if endpoint == "" {
|
supabaseURL := os.Getenv("SUPABASE_PROJECT_URL")
|
||||||
// Default endpoint - OpenAI SDK will append /chat/completions automatically
|
if supabaseURL == "" {
|
||||||
endpoint = "http://tensorzero.netcup.internal:3000/openai/v1"
|
fmt.Printf("Warning: SUPABASE_PROJECT_URL not set, TensorZero integration will not work\n")
|
||||||
|
supabaseURL = "https://gpqzsricripnvbrpsyws.supabase.co" // fallback
|
||||||
}
|
}
|
||||||
|
|
||||||
model := os.Getenv("NANNYAPI_MODEL")
|
model := os.Getenv("NANNYAPI_MODEL")
|
||||||
@@ -67,14 +68,9 @@ func NewLinuxDiagnosticAgent() *LinuxDiagnosticAgent {
|
|||||||
fmt.Printf("Warning: Using default model '%s'. Set NANNYAPI_MODEL environment variable for your specific function.\n", model)
|
fmt.Printf("Warning: Using default model '%s'. Set NANNYAPI_MODEL environment variable for your specific function.\n", model)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create OpenAI client with custom base URL
|
// Note: We don't use the OpenAI client anymore, we use direct HTTP to Supabase proxy
|
||||||
// Note: The OpenAI SDK automatically appends "/chat/completions" to the base URL
|
|
||||||
config := openai.DefaultConfig("")
|
|
||||||
config.BaseURL = endpoint
|
|
||||||
client := openai.NewClientWithConfig(config)
|
|
||||||
|
|
||||||
agent := &LinuxDiagnosticAgent{
|
agent := &LinuxDiagnosticAgent{
|
||||||
client: client,
|
client: nil, // Not used anymore
|
||||||
model: model,
|
model: model,
|
||||||
executor: NewCommandExecutor(10 * time.Second), // 10 second timeout for commands
|
executor: NewCommandExecutor(10 * time.Second), // 10 second timeout for commands
|
||||||
}
|
}
|
||||||
@@ -195,7 +191,7 @@ type TensorZeroResponse struct {
|
|||||||
EpisodeID string `json:"episode_id"`
|
EpisodeID string `json:"episode_id"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendRequest sends a request to the TensorZero API with tensorzero::episode_id support
|
// sendRequest sends a request to the TensorZero API via Supabase proxy with JWT authentication
|
||||||
func (a *LinuxDiagnosticAgent) sendRequest(messages []openai.ChatCompletionMessage) (*openai.ChatCompletionResponse, error) {
|
func (a *LinuxDiagnosticAgent) sendRequest(messages []openai.ChatCompletionMessage) (*openai.ChatCompletionResponse, error) {
|
||||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
@@ -223,17 +219,14 @@ func (a *LinuxDiagnosticAgent) sendRequest(messages []openai.ChatCompletionMessa
|
|||||||
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
return nil, fmt.Errorf("failed to marshal request: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create HTTP request
|
// Get Supabase project URL and build TensorZero proxy endpoint
|
||||||
endpoint := os.Getenv("NANNYAPI_ENDPOINT")
|
supabaseURL := os.Getenv("SUPABASE_PROJECT_URL")
|
||||||
if endpoint == "" {
|
if supabaseURL == "" {
|
||||||
endpoint = "http://tensorzero.netcup.internal:3000/openai/v1"
|
supabaseURL = "https://gpqzsricripnvbrpsyws.supabase.co"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure the endpoint ends with /chat/completions
|
// Build Supabase function URL with OpenAI v1 compatible path
|
||||||
if endpoint[len(endpoint)-1] != '/' {
|
endpoint := supabaseURL + "/functions/v1/tensorzero-proxy/openai/v1/chat/completions"
|
||||||
endpoint += "/"
|
|
||||||
}
|
|
||||||
endpoint += "chat/completions"
|
|
||||||
|
|
||||||
req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewBuffer(requestBody))
|
req, err := http.NewRequestWithContext(ctx, "POST", endpoint, bytes.NewBuffer(requestBody))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -242,6 +235,14 @@ func (a *LinuxDiagnosticAgent) sendRequest(messages []openai.ChatCompletionMessa
|
|||||||
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
// Add JWT authentication header
|
||||||
|
accessToken, err := a.getAccessToken()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get access token: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||||
|
|
||||||
// Make the request
|
// Make the request
|
||||||
client := &http.Client{Timeout: 30 * time.Second}
|
client := &http.Client{Timeout: 30 * time.Second}
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
@@ -257,7 +258,7 @@ func (a *LinuxDiagnosticAgent) sendRequest(messages []openai.ChatCompletionMessa
|
|||||||
}
|
}
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
if resp.StatusCode != http.StatusOK {
|
||||||
return nil, fmt.Errorf("API request failed with status %d: %s", resp.StatusCode, string(body))
|
return nil, fmt.Errorf("TensorZero API request failed with status %d: %s", resp.StatusCode, string(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Parse TensorZero response
|
// Parse TensorZero response
|
||||||
@@ -274,3 +275,31 @@ func (a *LinuxDiagnosticAgent) sendRequest(messages []openai.ChatCompletionMessa
|
|||||||
|
|
||||||
return &tzResponse.ChatCompletionResponse, nil
|
return &tzResponse.ChatCompletionResponse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// getAccessToken retrieves the current access token for authentication
|
||||||
|
func (a *LinuxDiagnosticAgent) getAccessToken() (string, error) {
|
||||||
|
// Read token from the standard token file location
|
||||||
|
tokenPath := os.Getenv("TOKEN_PATH")
|
||||||
|
if tokenPath == "" {
|
||||||
|
tokenPath = "/var/lib/nannyagent/token.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenData, err := os.ReadFile(tokenPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("failed to read token file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tokenInfo struct {
|
||||||
|
AccessToken string `json:"access_token"`
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := json.Unmarshal(tokenData, &tokenInfo); err != nil {
|
||||||
|
return "", fmt.Errorf("failed to parse token file: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenInfo.AccessToken == "" {
|
||||||
|
return "", fmt.Errorf("access token is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokenInfo.AccessToken, nil
|
||||||
|
}
|
||||||
|
|||||||
163
main.go
163
main.go
@@ -1,8 +1,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"nannyagentv2/internal/auth"
|
"nannyagentv2/internal/auth"
|
||||||
@@ -13,9 +19,135 @@ import (
|
|||||||
|
|
||||||
const Version = "v2.0.0"
|
const Version = "v2.0.0"
|
||||||
|
|
||||||
|
// checkRootPrivileges ensures the program is running as root
|
||||||
|
func checkRootPrivileges() {
|
||||||
|
if os.Geteuid() != 0 {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ ERROR: This program must be run as root for eBPF functionality.\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "Please run with: sudo %s\n", os.Args[0])
|
||||||
|
fmt.Fprintf(os.Stderr, "Reason: eBPF programs require root privileges to:\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " - Load programs into the kernel\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " - Attach to kernel functions and tracepoints\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " - Access kernel memory maps\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkKernelVersionCompatibility ensures kernel version is 4.4 or higher
|
||||||
|
func checkKernelVersionCompatibility() {
|
||||||
|
output, err := exec.Command("uname", "-r").Output()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ ERROR: Cannot determine kernel version: %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
kernelVersion := strings.TrimSpace(string(output))
|
||||||
|
|
||||||
|
// Parse version (e.g., "5.15.0-56-generic" -> major=5, minor=15)
|
||||||
|
parts := strings.Split(kernelVersion, ".")
|
||||||
|
if len(parts) < 2 {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ ERROR: Cannot parse kernel version: %s\n", kernelVersion)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
major, err := strconv.Atoi(parts[0])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ ERROR: Cannot parse major kernel version: %s\n", parts[0])
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
minor, err := strconv.Atoi(parts[1])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ ERROR: Cannot parse minor kernel version: %s\n", parts[1])
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if kernel is 4.4 or higher
|
||||||
|
if major < 4 || (major == 4 && minor < 4) {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ ERROR: Kernel version %s is too old for eBPF.\n", kernelVersion)
|
||||||
|
fmt.Fprintf(os.Stderr, "Required: Linux kernel 4.4 or higher\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "Current: %s\n", kernelVersion)
|
||||||
|
fmt.Fprintf(os.Stderr, "Reason: eBPF requires kernel features introduced in 4.4+:\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " - BPF system call support\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " - eBPF program types (kprobe, tracepoint)\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " - BPF maps and helper functions\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✅ Kernel version %s is compatible with eBPF\n", kernelVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkEBPFSupport validates eBPF subsystem availability
|
||||||
|
func checkEBPFSupport() {
|
||||||
|
// Check if /sys/kernel/debug/tracing exists (debugfs mounted)
|
||||||
|
if _, err := os.Stat("/sys/kernel/debug/tracing"); os.IsNotExist(err) {
|
||||||
|
fmt.Fprintf(os.Stderr, "⚠️ WARNING: debugfs not mounted. Some eBPF features may not work.\n")
|
||||||
|
fmt.Fprintf(os.Stderr, "To fix: sudo mount -t debugfs debugfs /sys/kernel/debug\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we can access BPF syscall
|
||||||
|
fd, _, errno := syscall.Syscall(321, 0, 0, 0) // BPF syscall number on x86_64
|
||||||
|
if errno != 0 && errno != syscall.EINVAL {
|
||||||
|
fmt.Fprintf(os.Stderr, "❌ ERROR: BPF syscall not available (errno: %v)\n", errno)
|
||||||
|
fmt.Fprintf(os.Stderr, "This may indicate:\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " - Kernel compiled without BPF support\n")
|
||||||
|
fmt.Fprintf(os.Stderr, " - BPF syscall disabled in kernel config\n")
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
if fd > 0 {
|
||||||
|
syscall.Close(int(fd))
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Printf("✅ eBPF syscall is available\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// runInteractiveDiagnostics starts the interactive diagnostic session
|
||||||
|
func runInteractiveDiagnostics(agent *LinuxDiagnosticAgent) {
|
||||||
|
fmt.Println("")
|
||||||
|
fmt.Println("🔍 Linux eBPF-Enhanced Diagnostic Agent")
|
||||||
|
fmt.Println("=======================================")
|
||||||
|
fmt.Println("Linux Diagnostic Agent Started")
|
||||||
|
fmt.Println("Enter a system issue description (or 'quit' to exit):")
|
||||||
|
|
||||||
|
scanner := bufio.NewScanner(os.Stdin)
|
||||||
|
for {
|
||||||
|
fmt.Print("> ")
|
||||||
|
if !scanner.Scan() {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
input := strings.TrimSpace(scanner.Text())
|
||||||
|
if input == "quit" || input == "exit" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if input == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process the issue with AI capabilities via TensorZero
|
||||||
|
if err := agent.DiagnoseIssue(input); err != nil {
|
||||||
|
fmt.Printf("Error: %v\n", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Goodbye!")
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
fmt.Printf("🚀 NannyAgent v%s starting...\n", Version)
|
fmt.Printf("🚀 NannyAgent v%s starting...\n", Version)
|
||||||
|
|
||||||
|
// Perform system compatibility checks first
|
||||||
|
fmt.Println("Performing system compatibility checks...")
|
||||||
|
checkRootPrivileges()
|
||||||
|
checkKernelVersionCompatibility()
|
||||||
|
checkEBPFSupport()
|
||||||
|
fmt.Println("✅ All system checks passed")
|
||||||
|
fmt.Println("")
|
||||||
|
|
||||||
// Load configuration
|
// Load configuration
|
||||||
cfg, err := config.LoadConfig()
|
cfg, err := config.LoadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -36,21 +168,23 @@ func main() {
|
|||||||
|
|
||||||
fmt.Println("✅ Authentication successful!")
|
fmt.Println("✅ Authentication successful!")
|
||||||
|
|
||||||
// Start metrics collection and heartbeat loop
|
// Initialize the diagnostic agent
|
||||||
fmt.Println("❤️ Starting metrics collection and heartbeat...")
|
agent := NewLinuxDiagnosticAgent()
|
||||||
|
|
||||||
ticker := time.NewTicker(time.Duration(cfg.MetricsInterval) * time.Second)
|
// Start background metrics collection in a goroutine
|
||||||
defer ticker.Stop()
|
go func() {
|
||||||
|
fmt.Println("❤️ Starting background metrics collection and heartbeat...")
|
||||||
|
|
||||||
// Send initial heartbeat
|
ticker := time.NewTicker(time.Duration(cfg.MetricsInterval) * time.Second)
|
||||||
if err := sendHeartbeat(cfg, token, metricsCollector); err != nil {
|
defer ticker.Stop()
|
||||||
log.Printf("⚠️ Initial heartbeat failed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Main heartbeat loop
|
// Send initial heartbeat
|
||||||
for {
|
if err := sendHeartbeat(cfg, token, metricsCollector); err != nil {
|
||||||
select {
|
log.Printf("⚠️ Initial heartbeat failed: %v", err)
|
||||||
case <-ticker.C:
|
}
|
||||||
|
|
||||||
|
// Main heartbeat loop
|
||||||
|
for range ticker.C {
|
||||||
// Check if token needs refresh
|
// Check if token needs refresh
|
||||||
if authManager.IsTokenExpired(token) {
|
if authManager.IsTokenExpired(token) {
|
||||||
fmt.Println("🔄 Token expiring soon, refreshing...")
|
fmt.Println("🔄 Token expiring soon, refreshing...")
|
||||||
@@ -85,7 +219,10 @@ func main() {
|
|||||||
}
|
}
|
||||||
// No logging for successful heartbeats - they should be silent
|
// No logging for successful heartbeats - they should be silent
|
||||||
}
|
}
|
||||||
}
|
}()
|
||||||
|
|
||||||
|
// Start the interactive diagnostic session (blocking)
|
||||||
|
runInteractiveDiagnostics(agent)
|
||||||
}
|
}
|
||||||
|
|
||||||
// sendHeartbeat collects metrics and sends heartbeat to the server
|
// sendHeartbeat collects metrics and sends heartbeat to the server
|
||||||
|
|||||||
Reference in New Issue
Block a user