From 4238e2100eb2388890f3e378b9c38d20bf5a9bf2 Mon Sep 17 00:00:00 2001 From: Kunal Kushwaha Date: Wed, 28 Jan 2026 14:05:08 +0900 Subject: [PATCH] feat(trace): enhance trace viewer UI and workflow template - Improve trace viewer with split-pane layout (tree left, details right) - Add user-friendly span names (e.g. "Sequential Workflow", step names) - Fix ANSI escape code handling in text alignment - Add UI hints for navigation - Update workflow template to use proper sequential workflow construction - Update default Anthropic model to claude-sonnet-4-20250514 --- internal/templates/engine.go | 253 ------------------ internal/tui/span_tree.go | 64 +++++ internal/tui/styles.go | 11 + internal/tui/trace_viewer.go | 90 +++++-- pkg/scaffold/engine.go | 98 +++++++ pkg/scaffold/service.go | 5 +- pkg/scaffold/template_loader.go | 12 +- pkg/scaffold/template_registry.go | 34 ++- .../templates/workflow/agents.go.tmpl | 24 ++ .../templates/workflow/factory.go.tmpl | 44 +++ pkg/scaffold/templates/workflow/main.go.tmpl | 208 ++++++++------ .../templates/workflow/workflow.go.tmpl | 29 ++ 12 files changed, 500 insertions(+), 372 deletions(-) delete mode 100644 internal/templates/engine.go create mode 100644 pkg/scaffold/engine.go create mode 100644 pkg/scaffold/templates/workflow/agents.go.tmpl create mode 100644 pkg/scaffold/templates/workflow/factory.go.tmpl create mode 100644 pkg/scaffold/templates/workflow/workflow.go.tmpl diff --git a/internal/templates/engine.go b/internal/templates/engine.go deleted file mode 100644 index 8afe652..0000000 --- a/internal/templates/engine.go +++ /dev/null @@ -1,253 +0,0 @@ -// Package templates provides template generation and rendering functionality. -package templates - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "unicode" - - "github.com/agenticgokit/agk/internal/config" -) - -// capitalize converts the first letter to uppercase -func capitalize(s string) string { - if len(s) == 0 { - return s - } - r := []rune(s) - r[0] = unicode.ToUpper(r[0]) - return string(r) -} - -// Engine handles template rendering -type Engine struct{} - -// NewEngine creates a new template engine -func NewEngine() *Engine { - return &Engine{} -} - -// RenderWorkflow generates the main workflow Go files -func (e *Engine) RenderWorkflow(projectPath string, cfg *config.ProjectConfig) error { - workflowDir := filepath.Join(projectPath, "workflow") - - // Generate workflow.go - workflowContent := e.generateWorkflowContent(cfg) - if err := os.WriteFile(filepath.Join(workflowDir, "workflow.go"), []byte(workflowContent), 0600); err != nil { - return fmt.Errorf("failed to write workflow.go: %w", err) - } - - // Generate agents.go - agentsContent := e.generateAgentsContent(cfg) - if err := os.WriteFile(filepath.Join(workflowDir, "agents.go"), []byte(agentsContent), 0600); err != nil { - return fmt.Errorf("failed to write agents.go: %w", err) - } - - // Generate factory.go - factoryContent := e.generateFactoryContent(cfg) - if err := os.WriteFile(filepath.Join(workflowDir, "factory.go"), []byte(factoryContent), 0600); err != nil { - return fmt.Errorf("failed to write factory.go: %w", err) - } - - return nil -} - -// RenderREADME generates the README.md file -func (e *Engine) RenderREADME(projectPath string, cfg *config.ProjectConfig) error { - readmePath := filepath.Join(projectPath, "README.md") - - content := e.generateREADMEContent(cfg) - - if err := os.WriteFile(readmePath, []byte(content), 0600); err != nil { - return fmt.Errorf("failed to write README: %w", err) - } - - return nil -} - -func (e *Engine) generateWorkflowContent(cfg *config.ProjectConfig) string { - packageName := strings.ToLower(cfg.Name) - packageName = strings.ReplaceAll(packageName, "-", "_") - workflowName := capitalize(packageName) - - return `package workflow - -import ( - "context" - "fmt" - - "github.com/agenticgokit/agenticgokit/v1beta/core" -) - -// ` + workflowName + `Workflow represents the main workflow -type ` + workflowName + `Workflow struct { - agents map[string]core.Agent -} - -// New` + workflowName + `Workflow creates a new workflow instance -func New` + workflowName + `Workflow() (*` + workflowName + `Workflow, error) { - // TODO: Initialize agents - agents := make(map[string]core.Agent) - - return &` + workflowName + `Workflow{ - agents: agents, - }, nil -} - -// Execute runs the workflow -func (w *` + workflowName + `Workflow) Execute(ctx context.Context, input string) (string, error) { - // TODO: Implement workflow logic - return fmt.Sprintf("Processing: %s", input), nil -} -` -} - -func (e *Engine) generateAgentsContent(cfg *config.ProjectConfig) string { - return `package workflow - -import ( - "github.com/agenticgokit/agenticgokit/v1beta/core" -) - -// CreateAgents initializes all agents for the workflow -func CreateAgents(llmProvider, model string) (map[string]core.Agent, error) { - agents := make(map[string]core.Agent) - - // TODO: Create agents based on your workflow requirements - // Example: - // mainAgent, err := core.NewAgent(core.AgentConfig{ - // Name: "main-agent", - // LLMProvider: llmProvider, - // Model: model, - // }) - // if err != nil { - // return nil, err - // } - // agents["main"] = mainAgent - - return agents, nil -} -` -} - -func (e *Engine) generateFactoryContent(cfg *config.ProjectConfig) string { - return `package workflow - -import ( - "fmt" - - "github.com/agenticgokit/agenticgokit/v1beta/core" -) - -// Factory handles workflow component creation -type Factory struct { - llmProvider string - model string -} - -// NewFactory creates a new workflow factory -func NewFactory(llmProvider, model string) *Factory { - return &Factory{ - llmProvider: llmProvider, - model: model, - } -} - -// CreateAgent creates an agent with the given configuration -func (f *Factory) CreateAgent(name string, agentType string) (core.Agent, error) { - // TODO: Implement agent creation based on type - return nil, fmt.Errorf("agent type %s not implemented", agentType) -} - -// CreateWorkflow creates a new workflow -func (f *Factory) CreateWorkflow() (*Workflow, error) { - workflowName := capitalize(strings.ToLower(strings.ReplaceAll("workflow", "-", "_"))) - _ = workflowName - // TODO: Implement workflow creation - return nil, fmt.Errorf("workflow creation not yet implemented") -} -` -} - -func (e *Engine) generateREADMEContent(cfg *config.ProjectConfig) string { - content := `# ` + cfg.Name + ` - -` + cfg.Description + ` - -## Getting Started - -### Prerequisites - -- Go 1.21 or later -- AgenticGoKit CLI -- ` + cfg.LLMProvider + ` API key - -### Setup - -1. Install dependencies: - ` + "`" + `bash - go mod tidy - ` + "`" + ` - -2. Configure your environment: - ` + "`" + `bash - export OPENAI_API_KEY=your-key-here - ` + "`" + ` - -3. Run the project: - ` + "`" + `bash - agk workflow run --workflow workflow/main.yaml - ` + "`" + ` - -## Project Structure - -` + "`" + ` -. -├── agk.toml # Project configuration -├── go.mod # Go module definition -├── README.md # This file -├── workflow/ # Workflow definitions -│ └── main.yaml # Main workflow -├── agents/ # Agent implementations -├── internal/ # Internal packages -├── pkg/ # Public packages -├── test/ # Tests and fixtures -└── docs/ # Documentation -` + "`" + ` - -## Configuration - -Edit ` + "`" + `agk.toml` + "`" + ` to configure: -- LLM provider and model -- Agent types and count -- Workflow execution options -- Server settings - -## Development - -### Running Locally - -` + "`" + `bash -agk serve --port 8080 -` + "`" + ` - -### Running Tests - -` + "`" + `bash -go test ./... -` + "`" + ` - -## Resources - -- [AgenticGoKit Documentation](https://github.com/agenticgokit/agenticgokit) -- [Workflow Examples](https://github.com/agenticgokit/agenticgokit/tree/main/examples) -- [API Reference](https://pkg.go.dev/github.com/agenticgokit/agenticgokit) - -## License - -MIT License - See LICENSE file for details -` - return content -} diff --git a/internal/tui/span_tree.go b/internal/tui/span_tree.go index a1425b5..12a0124 100644 --- a/internal/tui/span_tree.go +++ b/internal/tui/span_tree.go @@ -2,6 +2,7 @@ package tui import ( "encoding/json" + "fmt" "strings" "time" ) @@ -253,3 +254,66 @@ func (s *Span) GetSpanType() string { return "other" } } + +// GetFriendlyName returns a user-friendly display name for the span +func (s *Span) GetFriendlyName() string { + attrs := s.GetAllAttributes() + name := strings.ToLower(s.Name) + + // For workflow steps, use the step name as primary label + if strings.Contains(name, "workflow.step") { + if stepName, ok := attrs["agk.workflow.step_name"]; ok { + return fmt.Sprintf("🔹 %v", stepName) + } + } + + // For workflow root spans, show workflow mode + if strings.Contains(name, "agk.workflow.sequential") { + return "📋 Sequential Workflow" + } + if strings.Contains(name, "agk.workflow.parallel") { + return "⚡ Parallel Workflow" + } + if strings.Contains(name, "agk.workflow.dag") { + return "🔀 DAG Workflow" + } + if strings.Contains(name, "agk.workflow.loop") { + return "🔄 Loop Workflow" + } + + // For LLM spans, show provider and model + if strings.Contains(name, "llm") { + if model, ok := attrs["agk.llm.model"]; ok { + provider := "llm" + if p, ok := attrs["agk.llm.provider"]; ok { + provider = fmt.Sprintf("%v", p) + } + return fmt.Sprintf("🤖 %s [%v]", provider, model) + } + } + + // For agent spans, simplify + if strings.Contains(name, "agk.agent.run") { + if model, ok := attrs["agk.llm.model"]; ok { + return fmt.Sprintf("🤖 Agent [%v]", model) + } + return "🤖 Agent" + } + + // Default: return original name + return s.Name +} + +// IsWorkflowStep returns true if this span represents a workflow step +func (s *Span) IsWorkflowStep() bool { + return strings.Contains(strings.ToLower(s.Name), "workflow.step") +} + +// IsInternalSpan returns true if this span should be hidden by default (detail level) +func (s *Span) IsInternalSpan() bool { + name := strings.ToLower(s.Name) + // These are internal implementation details, show only when parent expanded + return strings.Contains(name, "agk.agent.run.stream") || + strings.Contains(name, "agk.agent.run.execute") || + strings.Contains(name, "transform") +} diff --git a/internal/tui/styles.go b/internal/tui/styles.go index 95d9287..f94a38b 100644 --- a/internal/tui/styles.go +++ b/internal/tui/styles.go @@ -112,6 +112,17 @@ var ( Bold(true) ) +// Pane styles for split layout +var ( + LeftPaneStyle = lipgloss.NewStyle(). + Border(lipgloss.NormalBorder(), false, true, false, false). + BorderForeground(mutedColor). + Padding(0, 1) + + RightPaneStyle = lipgloss.NewStyle(). + Padding(0, 1) +) + // GetSpanStyle returns the appropriate style based on span name func GetSpanStyle(spanName string) lipgloss.Style { switch { diff --git a/internal/tui/trace_viewer.go b/internal/tui/trace_viewer.go index 830fb84..ba974d4 100644 --- a/internal/tui/trace_viewer.go +++ b/internal/tui/trace_viewer.go @@ -9,6 +9,7 @@ import ( "github.com/charmbracelet/bubbles/viewport" tea "github.com/charmbracelet/bubbletea" + "github.com/charmbracelet/lipgloss" ) // tickMsg is sent periodically to check for file updates @@ -662,14 +663,28 @@ func (m Model) renderTreeView() string { } // Run Details Header (Specific to this view) - // We keep this as "sub-header" - header := m.renderRunSummary() // Renamed from renderHeader to avoid confusion + header := m.renderRunSummary() b.WriteString(header) b.WriteString("\n") - // Span tree + // Split-pane layout using lipgloss.JoinHorizontal + leftWidth := (m.width - 10) * 55 / 100 // 55% for tree + rightWidth := (m.width - 10) - leftWidth // Rest for details + + // Render left pane (tree) treeContent := m.renderSpanTree() - b.WriteString(treeContent) + leftPane := LeftPaneStyle.Width(leftWidth).Render(treeContent) + + // Render right pane (details) + var rightContent string + if m.cursor < len(m.visibleNodes) { + rightContent = m.renderQuickDetails(m.visibleNodes[m.cursor]) + } + rightPane := RightPaneStyle.Width(rightWidth).Render(rightContent) + + // Join panes horizontally + splitView := lipgloss.JoinHorizontal(lipgloss.Top, leftPane, rightPane) + b.WriteString(splitView) // Help bar help := m.renderHelpBar() @@ -679,6 +694,48 @@ func (m Model) renderTreeView() string { return b.String() } +// renderQuickDetails renders a compact detail view for the right panel +func (m Model) renderQuickDetails(node *SpanNode) string { + var b strings.Builder + + // Header with friendly name and hint + b.WriteString(HeaderStyle.Render("📋 Details")) + b.WriteString(" " + MutedStyle.Render("[d] Full Details")) + b.WriteString("\n") + b.WriteString(strings.Repeat("─", 30)) + b.WriteString("\n\n") + + // Span name + b.WriteString(fmt.Sprintf("Name: %s\n", node.Span.GetFriendlyName())) + b.WriteString(fmt.Sprintf("Duration: %dms\n", node.DurationMs)) + b.WriteString("\n") + + // Key attributes + attrs := node.Span.GetImportantAttributes() + if len(attrs) > 0 { + b.WriteString(MutedStyle.Render("Attributes:")) + b.WriteString("\n") + for k, v := range attrs { + // Clean up key names for display + shortKey := strings.TrimPrefix(k, "agk.") + shortKey = strings.TrimPrefix(shortKey, "llm.") + shortKey = strings.TrimPrefix(shortKey, "workflow.") + b.WriteString(fmt.Sprintf(" %s: %v\n", shortKey, v)) + } + } + + // Status + if node.Span.Status.Code != "" && node.Span.Status.Code != StatusUnset { + if node.Span.Status.Code == "Ok" { + b.WriteString("\n" + SuccessStyle.Render("✓ Success")) + } else { + b.WriteString("\n" + ErrorStyle.Render("✗ Error: "+node.Span.Status.Description)) + } + } + + return b.String() +} + func (m Model) renderRunSummary() string { // Previously renderHeader var lines []string @@ -781,28 +838,19 @@ func (m Model) renderSpanLine(node *SpanNode, selected bool) string { prefix = " " } - // Span name with styling + // Use friendly name for cleaner display + friendlyName := node.Span.GetFriendlyName() spanStyle := GetSpanStyle(node.Span.Name) - name := spanStyle.Render(node.Span.Name) + name := spanStyle.Render(friendlyName) - // Get additional context from attributes + // Get additional context from attributes (only if not already in friendly name) var context string attrs := node.Span.GetAllAttributes() - // For workflow steps, show the step name - if stepName, ok := attrs["agk.workflow.step_name"]; ok { - context = MutedStyle.Render(fmt.Sprintf(" [%v]", stepName)) - } - - // For LLM spans, show the model - if model, ok := attrs["agk.llm.model"]; ok { - context = MutedStyle.Render(fmt.Sprintf(" [%v]", model)) - } - - // For agent spans, show provider info - if provider, ok := attrs["agk.llm.provider"]; ok { - if context == "" { - context = MutedStyle.Render(fmt.Sprintf(" [%v]", provider)) + // For workflow steps, show model info as context + if node.Span.IsWorkflowStep() { + if model, ok := attrs["agk.llm.model"]; ok { + context = MutedStyle.Render(fmt.Sprintf(" [%v]", model)) } } diff --git a/pkg/scaffold/engine.go b/pkg/scaffold/engine.go new file mode 100644 index 0000000..a584003 --- /dev/null +++ b/pkg/scaffold/engine.go @@ -0,0 +1,98 @@ +// Package scaffold provides project scaffolding and template generation functionality. +// This file contains the template Engine for rendering workflow files. +package scaffold + +import ( + "os" + "path/filepath" + "strings" + "unicode" + + "github.com/agenticgokit/agk/internal/config" +) + +// capitalize converts the first letter to uppercase +func capitalize(s string) string { + if len(s) == 0 { + return s + } + r := []rune(s) + r[0] = unicode.ToUpper(r[0]) + return string(r) +} + +// Engine handles template rendering for scaffolding +type Engine struct{} + +// NewEngine creates a new template engine +func NewEngine() *Engine { + return &Engine{} +} + +// RenderWorkflow generates the main workflow Go files +func (e *Engine) RenderWorkflow(projectPath string, cfg *config.ProjectConfig) error { + workflowDir := filepath.Join(projectPath, "workflow") + + // Prepare template data + packageName := strings.ToLower(cfg.Name) + packageName = strings.ReplaceAll(packageName, "-", "_") + workflowName := capitalize(packageName) + + data := TemplateData{ + ProjectName: cfg.Name, + WorkflowName: workflowName, + Description: cfg.Description, + LLMProvider: cfg.LLMProvider, + } + + // Generate workflow.go + workflowContent, err := RenderTemplate("templates/workflow/workflow.go.tmpl", data) + if err != nil { + return err + } + if err := os.WriteFile(filepath.Join(workflowDir, "workflow.go"), []byte(workflowContent), 0600); err != nil { + return err + } + + // Generate agents.go + agentsContent, err := RenderTemplate("templates/workflow/agents.go.tmpl", data) + if err != nil { + return err + } + if err := os.WriteFile(filepath.Join(workflowDir, "agents.go"), []byte(agentsContent), 0600); err != nil { + return err + } + + // Generate factory.go + factoryContent, err := RenderTemplate("templates/workflow/factory.go.tmpl", data) + if err != nil { + return err + } + if err := os.WriteFile(filepath.Join(workflowDir, "factory.go"), []byte(factoryContent), 0600); err != nil { + return err + } + + return nil +} + +// RenderREADME generates the README.md file +func (e *Engine) RenderREADME(projectPath string, cfg *config.ProjectConfig) error { + readmePath := filepath.Join(projectPath, "README.md") + + data := TemplateData{ + ProjectName: cfg.Name, + Description: cfg.Description, + LLMProvider: cfg.LLMProvider, + } + + content, err := RenderTemplate("templates/workflow/README.md.tmpl", data) + if err != nil { + return err + } + + if err := os.WriteFile(readmePath, []byte(content), 0600); err != nil { + return err + } + + return nil +} diff --git a/pkg/scaffold/service.go b/pkg/scaffold/service.go index 52c8f80..e25680f 100644 --- a/pkg/scaffold/service.go +++ b/pkg/scaffold/service.go @@ -11,7 +11,6 @@ import ( "github.com/rs/zerolog" "github.com/agenticgokit/agk/internal/config" - "github.com/agenticgokit/agk/internal/templates" ) // GenerateOptions contains options for project generation @@ -29,7 +28,7 @@ type GenerateOptions struct { // Service handles project scaffolding and generation type Service struct { logger *zerolog.Logger - templateEngine *templates.Engine + templateEngine *Engine configGenerator *config.Generator generator *Generator } @@ -38,7 +37,7 @@ type Service struct { func NewService(logger *zerolog.Logger) *Service { return &Service{ logger: logger, - templateEngine: templates.NewEngine(), + templateEngine: NewEngine(), configGenerator: config.NewGenerator(), generator: NewGenerator(), } diff --git a/pkg/scaffold/template_loader.go b/pkg/scaffold/template_loader.go index ffce098..e229102 100644 --- a/pkg/scaffold/template_loader.go +++ b/pkg/scaffold/template_loader.go @@ -13,11 +13,13 @@ var templateFS embed.FS // TemplateData holds data for template rendering type TemplateData struct { - ProjectName string - LLMModel string - LLMProvider string - Description string - AgentType string + ProjectName string + WorkflowName string + LLMModel string + LLMProvider string + Description string + AgentType string + APIKeyEnv string } // RenderTemplate renders a template file with the provided data diff --git a/pkg/scaffold/template_registry.go b/pkg/scaffold/template_registry.go index e4f3e0b..5c7cbe7 100644 --- a/pkg/scaffold/template_registry.go +++ b/pkg/scaffold/template_registry.go @@ -9,6 +9,11 @@ import ( const ( DefaultGpt4Turbo = "gpt-4-turbo" + + ProviderAnthropic = "anthropic" + ProviderOllama = "ollama" + ProviderOpenAI = "openai" + ProviderAzure = "azure" ) // GetTemplateGenerator returns the appropriate template generator for the given template type @@ -149,9 +154,9 @@ func (g *SingleAgentGenerator) Generate(ctx context.Context, opts GenerateOption // Determine LLM model based on provider llmModel := DefaultGpt4Turbo - if opts.LLMProvider == "anthropic" { + if opts.LLMProvider == ProviderAnthropic { llmModel = "claude-3-sonnet-20240229" - } else if opts.LLMProvider == "ollama" { + } else if opts.LLMProvider == ProviderOllama { llmModel = "llama3.2" } @@ -280,6 +285,7 @@ func generateTemplateFiles(opts GenerateOptions, files map[string]string) error LLMProvider: opts.LLMProvider, Description: opts.Description, AgentType: opts.AgentType, + APIKeyEnv: getAPIKeyEnv(opts.LLMProvider), } for fileName, templatePath := range files { @@ -335,13 +341,29 @@ func (g *WorkflowGenerator) Generate(ctx context.Context, opts GenerateOptions) // Helper to get default model for provider func getLLMModel(provider string) string { switch provider { - case "anthropic": - return "claude-3-sonnet-20240229" - case "ollama": + case ProviderAnthropic: + return "claude-sonnet-4-20250514" + case ProviderOllama: return "llama3.2" - case "openai": + case ProviderOpenAI: return "gpt-4-turbo" default: return "gpt-4-turbo" } } + +// Helper to get the API key environment variable name for provider +func getAPIKeyEnv(provider string) string { + switch provider { + case ProviderAnthropic: + return "ANTHROPIC_API_KEY" + case ProviderOllama: + return "OLLAMA_HOST" // Ollama doesn't need API key, but uses host + case ProviderOpenAI: + return "OPENAI_API_KEY" + case ProviderAzure: + return "AZURE_OPENAI_API_KEY" + default: + return "OPENAI_API_KEY" + } +} diff --git a/pkg/scaffold/templates/workflow/agents.go.tmpl b/pkg/scaffold/templates/workflow/agents.go.tmpl new file mode 100644 index 0000000..afa875c --- /dev/null +++ b/pkg/scaffold/templates/workflow/agents.go.tmpl @@ -0,0 +1,24 @@ +package workflow + +import ( + "github.com/agenticgokit/agenticgokit/v1beta/core" +) + +// CreateAgents initializes all agents for the workflow +func CreateAgents(llmProvider, model string) (map[string]core.Agent, error) { + agents := make(map[string]core.Agent) + + // TODO: Create agents based on your workflow requirements + // Example: + // mainAgent, err := core.NewAgent(core.AgentConfig{ + // Name: "main-agent", + // LLMProvider: llmProvider, + // Model: model, + // }) + // if err != nil { + // return nil, err + // } + // agents["main"] = mainAgent + + return agents, nil +} diff --git a/pkg/scaffold/templates/workflow/factory.go.tmpl b/pkg/scaffold/templates/workflow/factory.go.tmpl new file mode 100644 index 0000000..447beea --- /dev/null +++ b/pkg/scaffold/templates/workflow/factory.go.tmpl @@ -0,0 +1,44 @@ +package workflow + +import ( + "fmt" + "strings" + + "github.com/agenticgokit/agenticgokit/v1beta/core" +) + +// Factory handles workflow component creation +type Factory struct { + llmProvider string + model string +} + +// NewFactory creates a new workflow factory +func NewFactory(llmProvider, model string) *Factory { + return &Factory{ + llmProvider: llmProvider, + model: model, + } +} + +// CreateAgent creates an agent with the given configuration +func (f *Factory) CreateAgent(name string, agentType string) (core.Agent, error) { + // TODO: Implement agent creation based on type + return nil, fmt.Errorf("agent type %s not implemented", agentType) +} + +// capitalize converts the first letter to uppercase +func capitalize(s string) string { + if len(s) == 0 { + return s + } + return strings.ToUpper(s[:1]) + s[1:] +} + +// CreateWorkflow creates a new workflow +func (f *Factory) CreateWorkflow() (*Workflow, error) { + workflowName := capitalize(strings.ToLower(strings.ReplaceAll("workflow", "-", "_"))) + _ = workflowName + // TODO: Implement workflow creation + return nil, fmt.Errorf("workflow creation not yet implemented") +} diff --git a/pkg/scaffold/templates/workflow/main.go.tmpl b/pkg/scaffold/templates/workflow/main.go.tmpl index 68bf114..537ee73 100644 --- a/pkg/scaffold/templates/workflow/main.go.tmpl +++ b/pkg/scaffold/templates/workflow/main.go.tmpl @@ -8,124 +8,164 @@ import ( "time" agk "github.com/agenticgokit/agenticgokit/v1beta" + _ "github.com/agenticgokit/agenticgokit/plugins/llm/{{.LLMProvider}}" ) func main() { - ctx, cancel := context.WithTimeout(context.Background(), 180*time.Second) + ctx, cancel := context.WithTimeout(context.Background(), 300*time.Second) defer cancel() - // Get service version from environment or use default - serviceVersion := os.Getenv("SERVICE_VERSION") - if serviceVersion == "" { - serviceVersion = "0.1.0" + fmt.Println("🚀 Creating {{.ProjectName}} Workflow...") + fmt.Println("=====================================") + + // Create a sequential workflow with multiple agents + // Tracing is handled at the workflow level (set AGK_TRACE=true) + workflow, err := agk.NewSequentialWorkflow(&agk.WorkflowConfig{ + Mode: agk.Sequential, + Timeout: 180 * time.Second, + }) + if err != nil { + log.Fatalf("Failed to create workflow: %v", err) + } + + // Create agents for each step + // Note: Observability/tracing is handled by the workflow - no need to add WithObservability to agents + researcherConfig := &agk.Config{ + Name: "researcher", + SystemPrompt: "You are a research assistant. When given a topic, provide detailed, factual information about it. Include key concepts, important details, and relevant context.", + Timeout: 60 * time.Second, + LLM: agk.LLMConfig{ + Provider: "{{.LLMProvider}}", + Model: "{{.LLMModel}}", + Temperature: 0.7, + MaxTokens: 2000, + APIKey: os.Getenv("{{.APIKeyEnv}}"), + }, } - // Create a streaming workflow with multiple steps - // Each step is an agent that processes and transforms the input - // Steps run sequentially, with each step's output feeding into the next - - fmt.Println("Creating Streaming Workflow...") - fmt.Println("================================") - - // Create workflow using the builder pattern - workflow, err := agk.NewWorkflow("{{.ProjectName}}"). - WithObservability("{{.ProjectName}}", serviceVersion). - // Step 1: Research - gather information - AddStep("research", agk.NewBuilder("researcher"). - - WithConfig(&agk.Config{ - SystemPrompt: "You are a research assistant. When given a topic, provide detailed, factual information about it. Include key concepts, important details, and relevant context.", - LLM: agk.LLMConfig{ - Provider: "{{.LLMProvider}}", - Model: "{{.LLMModel}}", - Temperature: 0.7, - MaxTokens: 2000, - }, - }).MustBuild()). - // Step 2: Summarize - condense the research - AddStep("summarize", agk.NewBuilder("summarizer"). - - WithConfig(&agk.Config{ - SystemPrompt: "You are a summarization expert. Take the provided content and create a clear, concise summary that captures the key points. Use bullet points for clarity.", - LLM: agk.LLMConfig{ - Provider: "{{.LLMProvider}}", - Model: "{{.LLMModel}}", - Temperature: 0.5, - MaxTokens: 1000, - }, - }).MustBuild()). - // Step 3: Format - create final output - AddStep("format", agk.NewBuilder("formatter"). - - WithConfig(&agk.Config{ - SystemPrompt: "You are a content formatter. Take the provided summary and format it as a professional report with sections, headers, and clear structure.", - LLM: agk.LLMConfig{ - Provider: "{{.LLMProvider}}", - Model: "{{.LLMModel}}", - Temperature: 0.3, - MaxTokens: 1500, - }, - }).MustBuild()). + researcher, err := agk.NewBuilder("researcher"). + WithConfig(researcherConfig). Build() + if err != nil { + log.Fatalf("Failed to create researcher agent: %v", err) + } + + summarizerConfig := &agk.Config{ + Name: "summarizer", + SystemPrompt: "You are a summarization expert. Take the provided content and create a clear, concise summary that captures the key points. Use bullet points for clarity.", + Timeout: 60 * time.Second, + LLM: agk.LLMConfig{ + Provider: "{{.LLMProvider}}", + Model: "{{.LLMModel}}", + Temperature: 0.5, + MaxTokens: 1000, + APIKey: os.Getenv("{{.APIKeyEnv}}"), + }, + } + summarizer, err := agk.NewBuilder("summarizer"). + WithConfig(summarizerConfig). + Build() if err != nil { - log.Fatalf("Failed to create workflow: %v", err) + log.Fatalf("Failed to create summarizer agent: %v", err) + } + + formatterConfig := &agk.Config{ + Name: "formatter", + SystemPrompt: "You are a content formatter. Take the provided summary and format it as a professional report with sections, headers, and clear structure.", + Timeout: 60 * time.Second, + LLM: agk.LLMConfig{ + Provider: "{{.LLMProvider}}", + Model: "{{.LLMModel}}", + Temperature: 0.3, + MaxTokens: 1500, + APIKey: os.Getenv("{{.APIKeyEnv}}"), + }, + } + + formatter, err := agk.NewBuilder("formatter"). + WithConfig(formatterConfig). + Build() + if err != nil { + log.Fatalf("Failed to create formatter agent: %v", err) } - defer workflow.Cleanup(ctx) + + // Add steps to the workflow + // Step 1: Research - gather information + if err := workflow.AddStep(agk.WorkflowStep{ + Name: "research", + Agent: researcher, + }); err != nil { + log.Fatalf("Failed to add research step: %v", err) + } + + // Step 2: Summarize - condense the research + if err := workflow.AddStep(agk.WorkflowStep{ + Name: "summarize", + Agent: summarizer, + }); err != nil { + log.Fatalf("Failed to add summarize step: %v", err) + } + + // Step 3: Format - create final output + if err := workflow.AddStep(agk.WorkflowStep{ + Name: "format", + Agent: formatter, + }); err != nil { + log.Fatalf("Failed to add format step: %v", err) + } + + // Initialize the workflow + if err := workflow.Initialize(ctx); err != nil { + log.Fatalf("Failed to initialize workflow: %v", err) + } + defer workflow.Shutdown(ctx) // Run the workflow with streaming topic := "artificial intelligence in healthcare" - fmt.Printf("\nTopic: %s\n\n", topic) + fmt.Printf("\n📋 Topic: %s\n\n", topic) + + // Show observability hint + fmt.Println("💡 Tip: Enable tracing with: AGK_TRACE=true AGK_TRACE_EXPORTER=file") + fmt.Println("📡 Streaming workflow execution:") + fmt.Println("─────────────────────────────────") - // Stream the workflow execution stream, err := workflow.RunStream(ctx, topic) if err != nil { log.Fatalf("Failed to start workflow: %v", err) } // Track progress through steps - currentStep := "" - totalChunks := 0 - stepOutputs := make(map[string]int) - for chunk := range stream.Chunks() { if chunk.Error != nil { - fmt.Printf("\nError: %v\n", chunk.Error) + fmt.Printf("\n❌ Error: %v\n", chunk.Error) break } switch chunk.Type { - case agk.ChunkTypeStepStart: - currentStep = chunk.StepName - fmt.Printf("\n[Step: %s]\n", currentStep) - fmt.Println(string(make([]byte, 40))) - + case agk.ChunkTypeAgentStart: + if stepName, ok := chunk.Metadata["step_name"].(string); ok { + fmt.Printf("\n\n🔹 [Step: %s]\n", stepName) + fmt.Println("────────────────────") + } case agk.ChunkTypeDelta: fmt.Print(chunk.Delta) - totalChunks++ - stepOutputs[currentStep]++ - - case agk.ChunkTypeStepComplete: - fmt.Printf("\n\nStep '%s' completed\n", chunk.StepName) - - case agk.ChunkTypeDone: - fmt.Println("\n") - fmt.Println("================================") - fmt.Println("WORKFLOW COMPLETED") - fmt.Println("================================") - fmt.Printf("Total Chunks: %d\n", totalChunks) - fmt.Println("\nChunks per Step:") - for step, count := range stepOutputs { - fmt.Printf(" - %s: %d\n", step, count) + case agk.ChunkTypeAgentComplete: + if stepName, ok := chunk.Metadata["step_name"].(string); ok { + fmt.Printf("\n✅ Step '%s' completed\n", stepName) } + case agk.ChunkTypeDone: + fmt.Println("\n\n=====================================") + fmt.Println("🎉 WORKFLOW COMPLETED") + fmt.Println("=====================================") } } // Get final result - result := stream.Result() - if result.Error != nil { - log.Fatalf("Workflow failed: %v", result.Error) + result, err := stream.Wait() + if err != nil { + log.Fatalf("Workflow failed: %v", err) } - fmt.Printf("\nFinal Output Length: %d characters\n", len(result.Output)) + fmt.Printf("\n📊 Duration: %v | Success: %v\n", result.Duration, result.Success) } diff --git a/pkg/scaffold/templates/workflow/workflow.go.tmpl b/pkg/scaffold/templates/workflow/workflow.go.tmpl new file mode 100644 index 0000000..9ccad77 --- /dev/null +++ b/pkg/scaffold/templates/workflow/workflow.go.tmpl @@ -0,0 +1,29 @@ +package workflow + +import ( + "context" + "fmt" + + "github.com/agenticgokit/agenticgokit/v1beta/core" +) + +// {{.WorkflowName}}Workflow represents the main workflow +type {{.WorkflowName}}Workflow struct { + agents map[string]core.Agent +} + +// New{{.WorkflowName}}Workflow creates a new workflow instance +func New{{.WorkflowName}}Workflow() (*{{.WorkflowName}}Workflow, error) { + // TODO: Initialize agents + agents := make(map[string]core.Agent) + + return &{{.WorkflowName}}Workflow{ + agents: agents, + }, nil +} + +// Execute runs the workflow +func (w *{{.WorkflowName}}Workflow) Execute(ctx context.Context, input string) (string, error) { + // TODO: Implement workflow logic + return fmt.Sprintf("Processing: %s", input), nil +}