package main import ( "bytes" "context" "encoding/json" "fmt" "log" "net/http" "os" "strings" "time" "chorus.services/bzzz/executor" "chorus.services/bzzz/logging" "chorus.services/bzzz/pkg/types" "chorus.services/bzzz/sandbox" "github.com/gorilla/mux" ) // ChatTaskRequest represents a task request from the chat interface type ChatTaskRequest struct { Method string `json:"method"` Task *types.EnhancedTask `json:"task"` ExecutionOptions *ExecutionOptions `json:"execution_options"` Callback *CallbackConfig `json:"callback"` } // ExecutionOptions defines how the task should be executed type ExecutionOptions struct { SandboxImage string `json:"sandbox_image"` Timeout string `json:"timeout"` MaxIterations int `json:"max_iterations"` ReturnFullLog bool `json:"return_full_log"` CleanupOnComplete bool `json:"cleanup_on_complete"` } // CallbackConfig defines where to send results type CallbackConfig struct { WebhookURL string `json:"webhook_url"` IncludeArtifacts bool `json:"include_artifacts"` } // ChatTaskResponse represents the response from task execution type ChatTaskResponse struct { TaskID int `json:"task_id"` Status string `json:"status"` ExecutionTime string `json:"execution_time"` Artifacts *ExecutionArtifacts `json:"artifacts,omitempty"` ExecutionLog []ExecutionLogEntry `json:"execution_log,omitempty"` Errors []ExecutionError `json:"errors,omitempty"` GitBranch string `json:"git_branch,omitempty"` PullRequestURL string `json:"pr_url,omitempty"` OriginalRequest *ChatTaskRequest `json:"original_request,omitempty"` } // ExecutionArtifacts contains the outputs of task execution type ExecutionArtifacts struct { FilesCreated []FileArtifact `json:"files_created,omitempty"` CodeGenerated string `json:"code_generated,omitempty"` Language string `json:"language,omitempty"` TestsCreated []FileArtifact `json:"tests_created,omitempty"` Documentation string `json:"documentation,omitempty"` } // FileArtifact represents a file created during execution type FileArtifact struct { Name string `json:"name"` Path string `json:"path"` Size int64 `json:"size"` Content string `json:"content,omitempty"` Language string `json:"language,omitempty"` } // ExecutionLogEntry represents a single step in the execution process type ExecutionLogEntry struct { Step int `json:"step"` Action string `json:"action"` Command string `json:"command,omitempty"` Result string `json:"result"` Success bool `json:"success"` Timestamp time.Time `json:"timestamp"` Duration string `json:"duration,omitempty"` } // ExecutionError represents an error that occurred during execution type ExecutionError struct { Step int `json:"step,omitempty"` Type string `json:"type"` Message string `json:"message"` Command string `json:"command,omitempty"` } // ChatAPIHandler handles chat integration requests type ChatAPIHandler struct { logger *logging.HypercoreLog } // NewChatAPIHandler creates a new chat API handler func NewChatAPIHandler() *ChatAPIHandler { // Note: HypercoreLog expects a peer.ID, but for testing we use nil // In production, this should be integrated with the actual P2P peer ID return &ChatAPIHandler{ logger: nil, // Will be set up when P2P integration is available } } // ExecuteTaskHandler handles task execution requests from N8N chat workflow func (h *ChatAPIHandler) ExecuteTaskHandler(w http.ResponseWriter, r *http.Request) { ctx := r.Context() // Parse request var req ChatTaskRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { h.sendError(w, http.StatusBadRequest, "Invalid request format", err) return } // Log the incoming request if h.logger != nil { h.logger.Append(logging.TaskProgress, map[string]interface{}{ "task_id": req.Task.Number, "method": req.Method, "source": "chat_api", "status": "received", }) } // Validate request if req.Task == nil { h.sendError(w, http.StatusBadRequest, "Task is required", nil) return } // Send immediate response to N8N response := map[string]interface{}{ "task_id": req.Task.Number, "status": "accepted", "message": "Task accepted for execution", } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(response) // Execute task asynchronously go h.executeTaskAsync(ctx, &req) } // executeTaskAsync executes the task in a separate goroutine func (h *ChatAPIHandler) executeTaskAsync(ctx context.Context, req *ChatTaskRequest) { startTime := time.Now() var response ChatTaskResponse response.TaskID = req.Task.Number response.OriginalRequest = req // Create execution log var executionLog []ExecutionLogEntry var artifacts ExecutionArtifacts var errors []ExecutionError defer func() { response.ExecutionTime = time.Since(startTime).String() response.ExecutionLog = executionLog response.Artifacts = &artifacts response.Errors = errors // Send callback to N8N if req.Callback != nil && req.Callback.WebhookURL != "" { h.sendCallback(req.Callback.WebhookURL, &response) } }() // Log start of execution executionLog = append(executionLog, ExecutionLogEntry{ Step: 1, Action: "Starting task execution", Result: fmt.Sprintf("Task: %s", req.Task.Title), Success: true, Timestamp: time.Now(), }) // Create sandbox sb, err := sandbox.CreateSandbox(ctx, req.ExecutionOptions.SandboxImage) if err != nil { response.Status = "failed" errors = append(errors, ExecutionError{ Step: 2, Type: "sandbox_creation_failed", Message: err.Error(), }) return } // Ensure cleanup defer func() { if req.ExecutionOptions.CleanupOnComplete { sb.DestroySandbox() } }() executionLog = append(executionLog, ExecutionLogEntry{ Step: 2, Action: "Created sandbox", Result: fmt.Sprintf("Sandbox ID: %s", sb.ID[:12]), Success: true, Timestamp: time.Now(), }) // Clone repository if specified if req.Task.GitURL != "" { cloneCmd := fmt.Sprintf("git clone %s .", req.Task.GitURL) result, err := sb.RunCommand(cloneCmd) success := err == nil executionLog = append(executionLog, ExecutionLogEntry{ Step: 3, Action: "Clone repository", Command: cloneCmd, Result: fmt.Sprintf("Exit: %d, Output: %s", result.ExitCode, result.StdOut), Success: success, Timestamp: time.Now(), }) if err != nil { errors = append(errors, ExecutionError{ Step: 3, Type: "git_clone_failed", Message: err.Error(), Command: cloneCmd, }) } } // Execute the task using the existing executor result, err := executor.ExecuteTask(ctx, req.Task, h.logger) if err != nil { response.Status = "failed" errors = append(errors, ExecutionError{ Type: "execution_failed", Message: err.Error(), }) return } // Collect artifacts from sandbox h.collectArtifacts(sb, &artifacts) // Set success status response.Status = "success" if result.BranchName != "" { response.GitBranch = result.BranchName } executionLog = append(executionLog, ExecutionLogEntry{ Step: len(executionLog) + 1, Action: "Task completed successfully", Result: fmt.Sprintf("Files created: %d", len(artifacts.FilesCreated)), Success: true, Timestamp: time.Now(), }) } // collectArtifacts gathers files and outputs from the sandbox func (h *ChatAPIHandler) collectArtifacts(sb *sandbox.Sandbox, artifacts *ExecutionArtifacts) { // List files created in workspace result, err := sb.RunCommand("find . -type f -name '*.py' -o -name '*.js' -o -name '*.go' -o -name '*.java' -o -name '*.cpp' -o -name '*.rs' | head -20") if err == nil && result.StdOut != "" { files := strings.Split(strings.TrimSpace(result.StdOut), "\n") var validFiles []string for _, line := range files { if strings.TrimSpace(line) != "" { validFiles = append(validFiles, strings.TrimSpace(line)) } } files = validFiles for _, file := range files { // Get file content content, err := sb.ReadFile(file) if err == nil && len(content) < 10000 { // Limit content size stat, _ := sb.RunCommand(fmt.Sprintf("stat -c '%%s' %s", file)) size := int64(0) if stat.ExitCode == 0 { fmt.Sscanf(stat.StdOut, "%d", &size) } artifact := FileArtifact{ Name: file, Path: file, Size: size, Content: string(content), Language: h.detectLanguage(file), } artifacts.FilesCreated = append(artifacts.FilesCreated, artifact) // If this looks like the main generated code, set it if artifacts.CodeGenerated == "" && size > 0 { artifacts.CodeGenerated = string(content) artifacts.Language = artifact.Language } } } } } // detectLanguage detects programming language from file extension func (h *ChatAPIHandler) detectLanguage(filename string) string { extensions := map[string]string{ ".py": "python", ".js": "javascript", ".ts": "typescript", ".go": "go", ".java": "java", ".cpp": "cpp", ".c": "c", ".rs": "rust", ".rb": "ruby", ".php": "php", } for ext, lang := range extensions { if len(filename) > len(ext) && filename[len(filename)-len(ext):] == ext { return lang } } return "text" } // sendCallback sends the execution results back to N8N webhook func (h *ChatAPIHandler) sendCallback(webhookURL string, response *ChatTaskResponse) { jsonData, err := json.Marshal(response) if err != nil { log.Printf("Failed to marshal callback response: %v", err) return } client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Post(webhookURL, "application/json", bytes.NewBuffer(jsonData)) if err != nil { log.Printf("Failed to send callback to %s: %v", webhookURL, err) return } defer resp.Body.Close() if resp.StatusCode != http.StatusOK { log.Printf("Callback webhook returned status %d", resp.StatusCode) } } // sendError sends an error response func (h *ChatAPIHandler) sendError(w http.ResponseWriter, statusCode int, message string, err error) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(statusCode) errorResponse := map[string]interface{}{ "error": message, "status": statusCode, } if err != nil { errorResponse["details"] = err.Error() } json.NewEncoder(w).Encode(errorResponse) } // HealthHandler provides a health check endpoint func (h *ChatAPIHandler) HealthHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(map[string]interface{}{ "status": "healthy", "service": "bzzz-chat-api", "timestamp": time.Now().Format(time.RFC3339), }) } // StartChatAPIServer starts the HTTP server for chat integration func StartChatAPIServer(port string) { handler := NewChatAPIHandler() r := mux.NewRouter() // API routes api := r.PathPrefix("/bzzz/api").Subrouter() api.HandleFunc("/execute-task", handler.ExecuteTaskHandler).Methods("POST") api.HandleFunc("/health", handler.HealthHandler).Methods("GET") // Add CORS middleware r.Use(func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Access-Control-Allow-Origin", "*") w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } next.ServeHTTP(w, r) }) }) log.Printf("🚀 Starting Bzzz Chat API server on port %s", port) log.Printf("📡 Endpoints:") log.Printf(" POST /bzzz/api/execute-task - Execute task in sandbox") log.Printf(" GET /bzzz/api/health - Health check") if err := http.ListenAndServe(":"+port, r); err != nil { log.Fatalf("Failed to start server: %v", err) } } func main() { port := "8080" if len(os.Args) > 1 { port = os.Args[1] } StartChatAPIServer(port) }