From afa50610ccc618feac3135180590b889a6c03359 Mon Sep 17 00:00:00 2001 From: Rick Blalock Date: Thu, 11 Sep 2025 21:12:30 -0400 Subject: [PATCH 1/3] add prompts.yaml to bundle --- internal/bundler/bundler.go | 48 ++++++++++++++++++++++++++++++++++++- 1 file changed, 47 insertions(+), 1 deletion(-) 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 { From 5006fe67f81d81dcd17fd80e84c4eb636946477c Mon Sep 17 00:00:00 2001 From: Rick Blalock Date: Fri, 12 Sep 2025 08:47:05 -0400 Subject: [PATCH 2/3] process prompts on deployment --- cmd/cloud.go | 7 ++ internal/prompts/prompts.go | 137 ++++++++++++++++++++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 internal/prompts/prompts.go 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/prompts/prompts.go b/internal/prompts/prompts.go new file mode 100644 index 00000000..b61fa7a1 --- /dev/null +++ b/internal/prompts/prompts.go @@ -0,0 +1,137 @@ +package prompts + +import ( + "context" + "crypto/sha256" + "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"` + ContentHash string `json:"content_hash"` +} + +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) + } + 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) + } + + // Calculate content hash for change detection + contentHash := calculateContentHash(content) + + apiRequest.Prompts = append(apiRequest.Prompts, PromptRequest{ + Slug: prompt.ID, + Content: content, + ContentHash: contentHash, + }) + + logger.Debug("processing prompt: %s (hash: %s)", prompt.ID, contentHash) + } + + // 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 +} + +// calculateContentHash generates a SHA256 hash of the prompt content for change detection +func calculateContentHash(content map[string]interface{}) string { + // Sort keys for consistent hashing + contentBytes, _ := json.Marshal(content) + hash := sha256.Sum256(contentBytes) + return fmt.Sprintf("%x", hash) +} From cde0ff14d65e4a9026abab5e32d1584c12bc4de7 Mon Sep 17 00:00:00 2001 From: Rick Blalock Date: Fri, 12 Sep 2025 21:52:52 -0400 Subject: [PATCH 3/3] Remove hash check since we're moving it to the API --- internal/prompts/prompts.go | 25 ++++++------------------- 1 file changed, 6 insertions(+), 19 deletions(-) diff --git a/internal/prompts/prompts.go b/internal/prompts/prompts.go index b61fa7a1..5957e7cb 100644 --- a/internal/prompts/prompts.go +++ b/internal/prompts/prompts.go @@ -2,7 +2,6 @@ package prompts import ( "context" - "crypto/sha256" "encoding/json" "fmt" "io" @@ -27,9 +26,8 @@ type PromptsFile struct { } type PromptRequest struct { - Slug string `json:"slug"` - Content map[string]interface{} `json:"content"` - ContentHash string `json:"content_hash"` + Slug string `json:"slug"` + Content map[string]interface{} `json:"content"` } type PromptsAPIRequest struct { @@ -63,6 +61,7 @@ func ProcessPrompts(ctx context.Context, logger logger.Logger, client *util.APIC 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) } @@ -85,16 +84,12 @@ func ProcessPrompts(ctx context.Context, logger logger.Logger, client *util.APIC return fmt.Errorf("failed to unmarshal prompt %s content: %w", prompt.ID, err) } - // Calculate content hash for change detection - contentHash := calculateContentHash(content) - apiRequest.Prompts = append(apiRequest.Prompts, PromptRequest{ - Slug: prompt.ID, - Content: content, - ContentHash: contentHash, + Slug: prompt.ID, + Content: content, }) - logger.Debug("processing prompt: %s (hash: %s)", prompt.ID, contentHash) + logger.Debug("processing prompt: %s", prompt.ID) } // Send to API @@ -127,11 +122,3 @@ func parsePromptsFile(filename string) ([]Prompt, error) { return promptsFile.Prompts, nil } - -// calculateContentHash generates a SHA256 hash of the prompt content for change detection -func calculateContentHash(content map[string]interface{}) string { - // Sort keys for consistent hashing - contentBytes, _ := json.Marshal(content) - hash := sha256.Sum256(contentBytes) - return fmt.Sprintf("%x", hash) -}