mirror of
https://github.com/j3ssie/osmedeus.git
synced 2026-08-31 12:19:50 +02:00
- 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
474 lines
13 KiB
Go
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,
|
|
}
|
|
}
|