diff --git a/cmd/cloud.go b/cmd/cloud.go index 00beab5d..d86ea54a 100644 --- a/cmd/cloud.go +++ b/cmd/cloud.go @@ -21,6 +21,7 @@ import ( "github.com/agentuity/cli/internal/errsystem" "github.com/agentuity/cli/internal/ignore" "github.com/agentuity/cli/internal/project" + "github.com/agentuity/cli/internal/prompts" "github.com/agentuity/cli/internal/util" "github.com/agentuity/go-common/crypto" "github.com/agentuity/go-common/env" @@ -450,6 +451,12 @@ Examples: orgSecret = *startResponse.Data.OrgSecret } + // Process prompts.yaml if present + if err := prompts.ProcessPrompts(ctx, logger, client, dir, startResponse.Data.DeploymentId); err != nil { + errsystem.New(errsystem.ErrDeployProject, err, + errsystem.WithContextMessage("Error processing prompts")).ShowErrorAndExit() + } + var saveProject bool // remove any agents that were deleted from the project diff --git a/internal/bundler/bundler.go b/internal/bundler/bundler.go index 9fe579b3..78a21f8b 100644 --- a/internal/bundler/bundler.go +++ b/internal/bundler/bundler.go @@ -376,6 +376,10 @@ func bundleJavascript(ctx BundleContext, dir string, outdir string, theproject * return nil // This line will never be reached due to os.Exit } + if err := copyPromptsFromSrc(ctx.Logger, dir, outdir); err != nil { + return fmt.Errorf("copy prompts.yaml: %w", err) + } + if err := validateDiskRequest(ctx, outdir); err != nil { return err } @@ -469,7 +473,49 @@ func bundlePython(ctx BundleContext, dir string, outdir string, theproject *proj } config["app"] = app } - return os.WriteFile(filepath.Join(outdir, "config.json"), []byte(cstr.JSONStringify(config)), 0644) + if err := os.WriteFile(filepath.Join(outdir, "config.json"), []byte(cstr.JSONStringify(config)), 0644); err != nil { + return err + } + + if err := copyPromptsFromSrc(ctx.Logger, dir, outdir); err != nil { + return fmt.Errorf("copy prompts.yaml: %w", err) + } + + return nil +} + +func copyPromptsFromSrc(logger logger.Logger, projectDir, outdir string) error { + srcRoot := filepath.Join(projectDir, "src") + if !util.Exists(srcRoot) { + return nil // nothing to do + } + + return filepath.Walk(srcRoot, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + if info.IsDir() { + return nil + } + name := strings.ToLower(info.Name()) + if name != "prompts.yaml" && name != "prompts.yml" { + return nil + } + + rel, err := filepath.Rel(projectDir, path) + if err != nil { + return err + } + destPath := filepath.Join(outdir, rel) + + if err := os.MkdirAll(filepath.Dir(destPath), 0o755); err != nil { + return err + } + + logger.Debug("copying prompts file: %s -> %s", path, destPath) + _, err = util.CopyFile(path, destPath) + return err + }) } func getAgents(theproject *project.Project, filename string) []AgentConfig { diff --git a/internal/prompts/prompts.go b/internal/prompts/prompts.go new file mode 100644 index 00000000..5957e7cb --- /dev/null +++ b/internal/prompts/prompts.go @@ -0,0 +1,124 @@ +package prompts + +import ( + "context" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + + "github.com/agentuity/cli/internal/util" + "github.com/agentuity/go-common/logger" + "gopkg.in/yaml.v3" +) + +type Prompt struct { + ID string `yaml:"id" json:"id"` + Name string `yaml:"name" json:"name"` + Description string `yaml:"description,omitempty" json:"description,omitempty"` + System string `yaml:"system,omitempty" json:"system,omitempty"` + Prompt string `yaml:"prompt,omitempty" json:"prompt,omitempty"` +} + +type PromptsFile struct { + Prompts []Prompt `yaml:"prompts" json:"prompts"` +} + +type PromptRequest struct { + Slug string `json:"slug"` + Content map[string]interface{} `json:"content"` +} + +type PromptsAPIRequest struct { + Prompts []PromptRequest `json:"prompts"` +} + +// ProcessPrompts reads the single prompts.yaml file from .agentuity bundle and sends it to the API +func ProcessPrompts(ctx context.Context, logger logger.Logger, client *util.APIClient, projectDir string, deploymentId string) error { + // Look for the single prompts.yaml file that was copied by bundler + promptsFile := filepath.Join(projectDir, ".agentuity", "src", "prompts.yaml") + if !util.Exists(promptsFile) { + logger.Debug("no prompts.yaml found in bundle, skipping prompts processing") + return nil + } + + prompts, err := parsePromptsFile(promptsFile) + if err != nil { + return fmt.Errorf("failed to parse prompts.yaml: %w", err) + } + + if len(prompts) == 0 { + logger.Debug("no prompts found in prompts.yaml, skipping prompts processing") + return nil + } + + // Validate prompts have required fields + for _, prompt := range prompts { + if prompt.ID == "" { + return fmt.Errorf("prompt missing required 'id' field") + } + if prompt.Name == "" { + return fmt.Errorf("prompt '%s' missing required 'name' field", prompt.ID) + } + // Either / or + if prompt.System == "" { + return fmt.Errorf("prompt '%s' missing required 'system' field", prompt.ID) + } + if prompt.Prompt == "" { + return fmt.Errorf("prompt '%s' missing required 'prompt' field", prompt.ID) + } + } + + // Convert to API request format + var apiRequest PromptsAPIRequest + for _, prompt := range prompts { + // Convert prompt to map for JSON serialization + contentBytes, err := json.Marshal(prompt) + if err != nil { + return fmt.Errorf("failed to marshal prompt %s: %w", prompt.ID, err) + } + + var content map[string]interface{} + if err := json.Unmarshal(contentBytes, &content); err != nil { + return fmt.Errorf("failed to unmarshal prompt %s content: %w", prompt.ID, err) + } + + apiRequest.Prompts = append(apiRequest.Prompts, PromptRequest{ + Slug: prompt.ID, + Content: content, + }) + + logger.Debug("processing prompt: %s", prompt.ID) + } + + // Send to API + endpoint := fmt.Sprintf("/cli/deploy/%s/prompts", deploymentId) + if err := client.Do("PUT", endpoint, apiRequest, nil); err != nil { + return fmt.Errorf("failed to process prompts via API: %w", err) + } + + logger.Info("processed %d prompts successfully", len(prompts)) + return nil +} + +// parsePromptsFile parses a single prompts.yaml file +func parsePromptsFile(filename string) ([]Prompt, error) { + file, err := os.Open(filename) + if err != nil { + return nil, err + } + defer file.Close() + + content, err := io.ReadAll(file) + if err != nil { + return nil, err + } + + var promptsFile PromptsFile + if err := yaml.Unmarshal(content, &promptsFile); err != nil { + return nil, fmt.Errorf("failed to parse YAML: %w", err) + } + + return promptsFile.Prompts, nil +}