- Add RoutingWorkflow: generic state machine executor for WorkflowSpec - Add LLM Router: natural language → WorkflowSpec generation - Add RetrieveMemoryActivity: query poimen-memory for context - Add activities: AnalyzeCode, SecurityScan, GenerateReport, Notify, etc. - Add agent-prompts/router: LLM prompt documentation - Extend starter with --route flag for routing workflows - Remove orchestrator job (trigger via API/message instead) - Clean up: move docs to Desktop, add .gitignore for *.md
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
package action
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NotifyStatusInput is input for NotifyStatusActivity
|
||||
type NotifyStatusInput struct {
|
||||
Channel string `json:"channel"` // slack, email, webhook
|
||||
Status string `json:"status"` // success, failure, warning
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
// NotifyStatusOutput is output from NotifyStatusActivity
|
||||
type NotifyStatusOutput struct {
|
||||
NotificationID string `json:"notificationId"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
|
||||
// NotifyStatusActivity sends notifications
|
||||
func NotifyStatusActivity(ctx context.Context, in NotifyStatusInput) (NotifyStatusOutput, error) {
|
||||
logger := newActivityLogger(ctx)
|
||||
logger.Info("NotifyStatusActivity started", "channel", in.Channel, "status", in.Status)
|
||||
|
||||
timestamp := time.Now().Format(time.RFC3339)
|
||||
notificationID := fmt.Sprintf("notify-%d", time.Now().UnixNano())
|
||||
|
||||
switch in.Channel {
|
||||
case "slack":
|
||||
if err := sendSlackNotification(ctx, in); err != nil {
|
||||
logger.Warn("Slack notification failed", "error", err)
|
||||
// Don't fail the activity, just log
|
||||
}
|
||||
case "webhook":
|
||||
if err := sendWebhookNotification(ctx, in); err != nil {
|
||||
logger.Warn("Webhook notification failed", "error", err)
|
||||
}
|
||||
case "email":
|
||||
// Email would require SMTP setup - log for now
|
||||
logger.Info("Email notification (logged)", "message", in.Message)
|
||||
default:
|
||||
logger.Info("Notification logged", "channel", in.Channel, "message", in.Message)
|
||||
}
|
||||
|
||||
logger.Info("NotifyStatusActivity completed", "notificationId", notificationID)
|
||||
return NotifyStatusOutput{
|
||||
NotificationID: notificationID,
|
||||
Timestamp: timestamp,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func sendSlackNotification(ctx context.Context, in NotifyStatusInput) error {
|
||||
webhookURL := os.Getenv("SLACK_WEBHOOK_URL")
|
||||
if webhookURL == "" {
|
||||
return fmt.Errorf("SLACK_WEBHOOK_URL not set")
|
||||
}
|
||||
|
||||
// Map status to emoji
|
||||
emoji := "ℹ️"
|
||||
switch in.Status {
|
||||
case "success":
|
||||
emoji = "✅"
|
||||
case "failure":
|
||||
emoji = "❌"
|
||||
case "warning":
|
||||
emoji = "⚠️"
|
||||
}
|
||||
|
||||
payload := map[string]string{
|
||||
"text": fmt.Sprintf("%s *%s*: %s", emoji, in.Status, in.Message),
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(payload)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", webhookURL, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return fmt.Errorf("slack returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func sendWebhookNotification(ctx context.Context, in NotifyStatusInput) error {
|
||||
webhookURL := os.Getenv("NOTIFICATION_WEBHOOK_URL")
|
||||
if webhookURL == "" {
|
||||
return fmt.Errorf("NOTIFICATION_WEBHOOK_URL not set")
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"channel": in.Channel,
|
||||
"status": in.Status,
|
||||
"message": in.Message,
|
||||
"timestamp": time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
body, _ := json.Marshal(payload)
|
||||
req, err := http.NewRequestWithContext(ctx, "POST", webhookURL, bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode >= 400 {
|
||||
return fmt.Errorf("webhook returned status %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ArchiveResultsInput is input for ArchiveResultsActivity
|
||||
type ArchiveResultsInput struct {
|
||||
ReportPath string `json:"reportPath"`
|
||||
Destination string `json:"destination"` // s3://bucket/path or local path
|
||||
Metadata map[string]interface{} `json:"metadata,omitempty"`
|
||||
}
|
||||
|
||||
// ArchiveResultsOutput is output from ArchiveResultsActivity
|
||||
type ArchiveResultsOutput struct {
|
||||
ArchiveURL string `json:"archiveUrl"`
|
||||
ArchiveSize int64 `json:"archiveSize"`
|
||||
}
|
||||
|
||||
// ArchiveResultsActivity archives results to storage
|
||||
func ArchiveResultsActivity(ctx context.Context, in ArchiveResultsInput) (ArchiveResultsOutput, error) {
|
||||
logger := newActivityLogger(ctx)
|
||||
logger.Info("ArchiveResultsActivity started", "reportPath", in.ReportPath, "destination", in.Destination)
|
||||
|
||||
// Check if source exists
|
||||
info, err := os.Stat(in.ReportPath)
|
||||
if err != nil {
|
||||
return ArchiveResultsOutput{}, fmt.Errorf("report not found: %s", in.ReportPath)
|
||||
}
|
||||
|
||||
// For now, just copy to local destination or log for cloud
|
||||
var archiveURL string
|
||||
var archiveSize int64
|
||||
|
||||
if len(in.Destination) > 5 && in.Destination[:5] == "s3://" {
|
||||
// Would use AWS SDK - for now just log
|
||||
logger.Info("Would upload to S3", "destination", in.Destination)
|
||||
archiveURL = in.Destination
|
||||
archiveSize = info.Size()
|
||||
} else {
|
||||
// Copy to local destination
|
||||
content, err := os.ReadFile(in.ReportPath)
|
||||
if err != nil {
|
||||
return ArchiveResultsOutput{}, fmt.Errorf("failed to read report: %w", err)
|
||||
}
|
||||
|
||||
destPath := in.Destination
|
||||
if destPath == "" {
|
||||
destPath = fmt.Sprintf("/tmp/archive-%d", time.Now().UnixNano())
|
||||
}
|
||||
|
||||
if err := os.WriteFile(destPath, content, 0644); err != nil {
|
||||
return ArchiveResultsOutput{}, fmt.Errorf("failed to write archive: %w", err)
|
||||
}
|
||||
|
||||
archiveURL = destPath
|
||||
archiveSize = int64(len(content))
|
||||
}
|
||||
|
||||
logger.Info("ArchiveResultsActivity completed", "archiveUrl", archiveURL, "size", archiveSize)
|
||||
return ArchiveResultsOutput{
|
||||
ArchiveURL: archiveURL,
|
||||
ArchiveSize: archiveSize,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeploymentPreCheckInput is input for DeploymentPreCheckActivity
|
||||
type DeploymentPreCheckInput struct {
|
||||
Path string `json:"path"`
|
||||
CheckType string `json:"checkType,omitempty"` // lint, test, build, all
|
||||
}
|
||||
|
||||
// DeploymentPreCheckOutput is output from DeploymentPreCheckActivity
|
||||
type DeploymentPreCheckOutput struct {
|
||||
Passed bool `json:"passed"`
|
||||
Failures []string `json:"failures"`
|
||||
Warnings []string `json:"warnings"`
|
||||
}
|
||||
|
||||
// DeploymentPreCheckActivity validates deployment readiness
|
||||
func DeploymentPreCheckActivity(ctx context.Context, in DeploymentPreCheckInput) (DeploymentPreCheckOutput, error) {
|
||||
logger := newActivityLogger(ctx)
|
||||
logger.Info("DeploymentPreCheckActivity started", "path", in.Path, "checkType", in.CheckType)
|
||||
|
||||
output := DeploymentPreCheckOutput{
|
||||
Passed: true,
|
||||
Failures: []string{},
|
||||
Warnings: []string{},
|
||||
}
|
||||
|
||||
checkType := in.CheckType
|
||||
if checkType == "" {
|
||||
checkType = "all"
|
||||
}
|
||||
|
||||
// Check if path exists
|
||||
if _, err := os.Stat(in.Path); os.IsNotExist(err) {
|
||||
output.Passed = false
|
||||
output.Failures = append(output.Failures, fmt.Sprintf("path does not exist: %s", in.Path))
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// Check for Go project
|
||||
isGo := false
|
||||
if _, err := os.Stat(fmt.Sprintf("%s/go.mod", in.Path)); err == nil {
|
||||
isGo = true
|
||||
}
|
||||
|
||||
if isGo && (checkType == "all" || checkType == "build") {
|
||||
// Try go build
|
||||
cmd := exec.CommandContext(ctx, "go", "build", "./...")
|
||||
cmd.Dir = in.Path
|
||||
if buildOut, err := cmd.CombinedOutput(); err != nil {
|
||||
output.Passed = false
|
||||
output.Failures = append(output.Failures, fmt.Sprintf("build failed: %s", string(buildOut)))
|
||||
}
|
||||
}
|
||||
|
||||
if isGo && (checkType == "all" || checkType == "test") {
|
||||
// Try go test
|
||||
cmd := exec.CommandContext(ctx, "go", "test", "-short", "./...")
|
||||
cmd.Dir = in.Path
|
||||
if testOut, err := cmd.CombinedOutput(); err != nil {
|
||||
output.Passed = false
|
||||
output.Failures = append(output.Failures, fmt.Sprintf("tests failed: %s", string(testOut)))
|
||||
}
|
||||
}
|
||||
|
||||
if isGo && (checkType == "all" || checkType == "lint") {
|
||||
// Try go vet
|
||||
cmd := exec.CommandContext(ctx, "go", "vet", "./...")
|
||||
cmd.Dir = in.Path
|
||||
if vetOut, err := cmd.CombinedOutput(); err != nil {
|
||||
output.Warnings = append(output.Warnings, fmt.Sprintf("vet issues: %s", string(vetOut)))
|
||||
}
|
||||
}
|
||||
|
||||
logger.Info("DeploymentPreCheckActivity completed", "passed", output.Passed, "failures", len(output.Failures))
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// ApproveWorkflowInput is input for ApproveWorkflowActivity
|
||||
type ApproveWorkflowInput struct {
|
||||
WorkflowID string `json:"workflowId"`
|
||||
RequiredApprovals int `json:"requiredApprovals,omitempty"`
|
||||
TimeoutMinutes int `json:"timeoutMinutes,omitempty"`
|
||||
}
|
||||
|
||||
// ApproveWorkflowOutput is output from ApproveWorkflowActivity
|
||||
type ApproveWorkflowOutput struct {
|
||||
Approved bool `json:"approved"`
|
||||
Approver string `json:"approver,omitempty"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
|
||||
// ApproveWorkflowActivity handles approval workflow (auto-approves for now)
|
||||
func ApproveWorkflowActivity(ctx context.Context, in ApproveWorkflowInput) (ApproveWorkflowOutput, error) {
|
||||
logger := newActivityLogger(ctx)
|
||||
logger.Info("ApproveWorkflowActivity started", "workflowId", in.WorkflowID)
|
||||
|
||||
// For now, auto-approve
|
||||
// In production, this would wait for human approval via signal or external system
|
||||
|
||||
timestamp := time.Now().Format(time.RFC3339)
|
||||
|
||||
logger.Info("ApproveWorkflowActivity completed (auto-approved)")
|
||||
return ApproveWorkflowOutput{
|
||||
Approved: true,
|
||||
Approver: "system-auto",
|
||||
Timestamp: timestamp,
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user