Files
osmedeus/pkg/server/handlers/workflows.go
T
j3ssie 1403d20a4d feat: add LLM step executor with vision and tool support, event workflow system, and inheritance
- Add LLM executor supporting OpenAI vision, tool calling, embeddings, and structured outputs
- Introduce event emitter/receiver workflows with deduplication and filtering (generate_event functions)
- Add workflow extends/override system enabling inheritance chains and step merge modes
- Update function naming to snake_case across all testdata (fileExists→file_exists, etc.)
- Add comprehensive test fixtures for linter, events, CDN, step dependencies, and extends workflows
2026-01-20 18:23:57 +08:00

474 lines
13 KiB
Go

package handlers
import (
"context"
"os"
"strconv"
"strings"
"github.com/gofiber/fiber/v2"
"github.com/j3ssie/osmedeus/v5/internal/config"
"github.com/j3ssie/osmedeus/v5/internal/core"
"github.com/j3ssie/osmedeus/v5/internal/database"
"github.com/j3ssie/osmedeus/v5/internal/parser"
)
// ListWorkflows handles listing workflows
func ListWorkflows(cfg *config.Config) fiber.Handler {
return func(c *fiber.Ctx) error {
loader := parser.NewLoader(cfg.WorkflowsPath)
workflows, err := loader.LoadAllWorkflows()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"message": "Failed to load workflows",
})
}
data := make([]fiber.Map, 0, len(workflows))
for _, w := range workflows {
// Skip hidden workflows
if w.Hidden {
continue
}
data = append(data, fiber.Map{
"name": w.Name,
"kind": w.Kind,
"description": w.Description,
"file_path": w.FilePath,
})
}
return c.JSON(fiber.Map{
"data": data,
})
}
}
// GetWorkflow handles getting a single workflow
func GetWorkflow(cfg *config.Config) fiber.Handler {
return func(c *fiber.Ctx) error {
name := c.Params("name")
loader := parser.NewLoader(cfg.WorkflowsPath)
workflow, err := loader.LoadWorkflow(name)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": true,
"message": "Workflow not found",
})
}
return c.JSON(workflowToMap(workflow))
}
}
// ListWorkflowsVerbose handles listing workflows with verbose details
// @Summary List all workflows
// @Description Get a list of all available workflows with details
// @Tags Workflows
// @Produce json
// @Success 200 {object} map[string]interface{} "List of workflows"
// @Failure 500 {object} map[string]interface{} "Failed to load workflows"
// @Security BearerAuth
// @Router /osm/api/workflows [get]
func ListWorkflowsVerbose(cfg *config.Config) fiber.Handler {
return func(c *fiber.Ctx) error {
// Check source parameter - "filesystem" for direct loading, default is "db"
source := c.Query("source", "db")
// If source is filesystem, use the original approach
if source == "filesystem" {
return listWorkflowsFromFilesystem(c, cfg)
}
// Parse query parameters for DB-based listing
offset, _ := strconv.Atoi(c.Query("offset", "0"))
limit, _ := strconv.Atoi(c.Query("limit", "50"))
kind := c.Query("kind", "")
search := c.Query("search", "")
tagsStr := c.Query("tags", "")
var tags []string
if tagsStr != "" {
tags = strings.Split(tagsStr, ",")
for i := range tags {
tags[i] = strings.TrimSpace(tags[i])
}
}
ctx := context.Background()
// Query database
query := database.WorkflowQuery{
Tags: tags,
Kind: kind,
Search: search,
Offset: offset,
Limit: limit,
}
result, err := database.ListWorkflowsFromDB(ctx, query)
if err != nil {
// Fallback to filesystem if DB query fails
return listWorkflowsFromFilesystem(c, cfg)
}
// If no workflows in DB, suggest indexing
if result.TotalCount == 0 {
// Check if DB is empty vs no matches
totalCount, _ := database.GetWorkflowCount(ctx)
if totalCount == 0 {
return c.JSON(fiber.Map{
"data": []interface{}{},
"count": 0,
"message": "No workflows indexed. Run 'osmedeus db index workflow' or POST /osm/api/workflows/refresh to index workflows.",
})
}
}
return c.JSON(fiber.Map{
"data": result.Data,
"pagination": fiber.Map{
"total": result.TotalCount,
"offset": result.Offset,
"limit": result.Limit,
},
})
}
}
// listWorkflowsFromFilesystem loads workflows directly from filesystem
func listWorkflowsFromFilesystem(c *fiber.Ctx, cfg *config.Config) error {
loader := parser.NewLoader(cfg.WorkflowsPath)
workflows, err := loader.LoadAllWorkflows()
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"message": "Failed to load workflows",
})
}
data := make([]fiber.Map, 0, len(workflows))
for _, w := range workflows {
// Skip hidden workflows
if w.Hidden {
continue
}
// Build params detail
params := make([]fiber.Map, 0, len(w.Params))
requiredParams := []string{}
for _, p := range w.Params {
params = append(params, fiber.Map{
"name": p.Name,
"default": p.Default,
"required": p.Required,
"generator": p.Generator,
})
if p.Required {
requiredParams = append(requiredParams, p.Name)
}
}
data = append(data, fiber.Map{
"name": w.Name,
"kind": w.Kind,
"description": w.Description,
"tags": w.Tags,
"file_path": w.FilePath,
"params": params,
"required_params": requiredParams,
"step_count": len(w.Steps),
"module_count": len(w.Modules),
})
}
return c.JSON(fiber.Map{
"data": data,
"count": len(data),
})
}
// GetWorkflowVerbose handles getting a single workflow with verbose details
// @Summary Get workflow details
// @Description Get workflow content. Returns raw YAML by default. Use json=true to get JSON with parsed details.
// @Tags Workflows
// @Produce json,text/yaml
// @Param name path string true "Workflow name"
// @Param json query bool false "Return JSON with parsed details instead of raw YAML"
// @Success 200 {object} map[string]interface{} "Workflow details (JSON) or raw YAML content"
// @Failure 404 {object} map[string]interface{} "Workflow not found"
// @Security BearerAuth
// @Router /osm/api/workflows/{name} [get]
func GetWorkflowVerbose(cfg *config.Config) fiber.Handler {
return func(c *fiber.Ctx) error {
name := c.Params("name")
loader := parser.NewLoader(cfg.WorkflowsPath)
workflow, err := loader.LoadWorkflow(name)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": true,
"message": "Workflow not found",
})
}
// Check if JSON format is requested (default is YAML)
if c.Query("json") == "true" {
return returnWorkflowJSON(c, workflow)
}
// Return YAML by default
content, err := os.ReadFile(workflow.FilePath)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"message": "Failed to read workflow file",
})
}
c.Set("Content-Type", "text/yaml; charset=utf-8")
return c.SendString(string(content))
}
}
// returnWorkflowJSON returns workflow details as JSON
func returnWorkflowJSON(c *fiber.Ctx, workflow *core.Workflow) error {
// Build params detail
params := make([]fiber.Map, 0, len(workflow.Params))
for _, p := range workflow.Params {
params = append(params, fiber.Map{
"name": p.Name,
"default": p.Default,
"required": p.Required,
"generator": p.Generator,
})
}
// Build steps detail with full information
steps := make([]fiber.Map, 0, len(workflow.Steps))
for i, s := range workflow.Steps {
stepMap := fiber.Map{
"index": i,
"name": s.Name,
"type": s.Type,
"command": s.Command,
"timeout": s.Timeout,
"pre_condition": s.PreCondition,
"step_runner": s.StepRunner,
}
// Add depends_on if present
if len(s.DependsOn) > 0 {
stepMap["depends_on"] = s.DependsOn
}
// Add exports if present
if len(s.Exports) > 0 {
stepMap["exports"] = s.Exports
}
// Add step runner config if present
if s.StepRunnerConfig != nil && s.StepRunnerConfig.RunnerConfig != nil {
stepMap["step_runner_config"] = fiber.Map{
"image": s.StepRunnerConfig.Image,
"host": s.StepRunnerConfig.Host,
"user": s.StepRunnerConfig.User,
"volumes": s.StepRunnerConfig.Volumes,
}
}
// Add parallel steps if present
if len(s.ParallelSteps) > 0 {
substeps := make([]fiber.Map, 0, len(s.ParallelSteps))
for j, ss := range s.ParallelSteps {
substeps = append(substeps, fiber.Map{
"index": j,
"name": ss.Name,
"type": ss.Type,
"command": ss.Command,
})
}
stepMap["parallel_steps"] = substeps
}
steps = append(steps, stepMap)
}
// Build modules detail with full information
modules := make([]fiber.Map, 0, len(workflow.Modules))
for i, m := range workflow.Modules {
modules = append(modules, fiber.Map{
"index": i,
"name": m.Name,
"path": m.Path,
"depends_on": m.DependsOn,
"condition": m.Condition,
})
}
// Build triggers detail
triggers := make([]fiber.Map, 0, len(workflow.Triggers))
for _, t := range workflow.Triggers {
triggers = append(triggers, fiber.Map{
"name": t.Name,
"on": t.On,
"schedule": t.Schedule,
"enabled": t.Enabled,
})
}
// Build dependencies info
var dependencies fiber.Map
if workflow.Dependencies != nil {
dependencies = fiber.Map{
"commands": workflow.Dependencies.Commands,
"files": workflow.Dependencies.Files,
}
}
return c.JSON(fiber.Map{
"name": workflow.Name,
"kind": workflow.Kind,
"description": workflow.Description,
"file_path": workflow.FilePath,
"params": params,
"steps": steps,
"modules": modules,
"triggers": triggers,
"dependencies": dependencies,
})
}
// ValidateWorkflow handles workflow validation
func ValidateWorkflow(cfg *config.Config) fiber.Handler {
return func(c *fiber.Ctx) error {
name := c.Params("name")
loader := parser.NewLoader(cfg.WorkflowsPath)
workflow, err := loader.LoadWorkflow(name)
if err != nil {
return c.Status(fiber.StatusNotFound).JSON(fiber.Map{
"error": true,
"message": "Workflow not found",
})
}
if err := parser.Validate(workflow); err != nil {
return c.JSON(fiber.Map{
"valid": false,
"message": err.Error(),
})
}
return c.JSON(fiber.Map{
"valid": true,
"message": "Workflow is valid",
})
}
}
// ReloadWorkflows handles reloading workflows from disk
func ReloadWorkflows(cfg *config.Config) fiber.Handler {
return func(c *fiber.Ctx) error {
loader := parser.NewLoader(cfg.WorkflowsPath)
if err := loader.ReloadWorkflows(); err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"message": "Failed to reload workflows",
})
}
return c.JSON(fiber.Map{
"message": "Workflows reloaded",
})
}
}
// RefreshWorkflowIndex re-indexes workflows from filesystem to database
// @Summary Refresh workflow index
// @Description Re-index all workflows from filesystem to database
// @Tags Workflows
// @Accept json
// @Produce json
// @Param force query bool false "Force re-index all workflows regardless of checksum"
// @Success 200 {object} map[string]interface{} "Indexing result"
// @Failure 500 {object} map[string]interface{} "Indexing failed"
// @Security BearerAuth
// @Router /osm/api/workflows/refresh [post]
func RefreshWorkflowIndex(cfg *config.Config) fiber.Handler {
return func(c *fiber.Ctx) error {
force := c.Query("force") == "true"
ctx := context.Background()
// Index workflows
result, err := database.IndexWorkflowsFromFilesystem(ctx, cfg.WorkflowsPath, force)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"message": err.Error(),
})
}
return c.JSON(fiber.Map{
"message": "Workflows indexed successfully",
"added": result.Added,
"updated": result.Updated,
"removed": result.Removed,
"errors": result.Errors,
})
}
}
// GetAllWorkflowTags returns all unique tags from indexed workflows
// @Summary Get all workflow tags
// @Description Get all unique tags from indexed workflows in database
// @Tags Workflows
// @Produce json
// @Success 200 {object} map[string]interface{} "List of tags"
// @Failure 500 {object} map[string]interface{} "Failed to get tags"
// @Security BearerAuth
// @Router /osm/api/workflows/tags [get]
func GetAllWorkflowTags(cfg *config.Config) fiber.Handler {
return func(c *fiber.Ctx) error {
ctx := context.Background()
tags, err := database.GetAllTags(ctx)
if err != nil {
return c.Status(fiber.StatusInternalServerError).JSON(fiber.Map{
"error": true,
"message": err.Error(),
})
}
return c.JSON(fiber.Map{
"tags": tags,
"count": len(tags),
})
}
}
// workflowToMap converts a workflow to a map for JSON response
func workflowToMap(w *core.Workflow) fiber.Map {
params := make([]fiber.Map, 0, len(w.Params))
for _, p := range w.Params {
params = append(params, fiber.Map{
"name": p.Name,
"default": p.Default,
"required": p.Required,
"generator": p.Generator,
})
}
triggers := make([]fiber.Map, 0, len(w.Triggers))
for _, t := range w.Triggers {
triggers = append(triggers, fiber.Map{
"name": t.Name,
"on": t.On,
"schedule": t.Schedule,
"enabled": t.Enabled,
})
}
return fiber.Map{
"name": w.Name,
"kind": w.Kind,
"description": w.Description,
"file_path": w.FilePath,
"params": params,
"triggers": triggers,
}
}