Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cmd/sync/attachments.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,8 @@ func createInput(cmd *cobra.Command) *synccreate.Input {
return &text
}
input := synccreate.Input{
Content: value(contentFlag), JSONSchema: value(jsonSchemaFlag), Name: value(nameFlag), Description: value(descriptionFlag),
Content: value(contentFlag), JSONSchema: value(jsonSchemaFlag), Name: value(nameFlag),
Description: value(descriptionFlag), ModelConfigKey: value(modelConfigKeyFlag),
}
if !set {
return nil
Expand Down
24 changes: 18 additions & 6 deletions cmd/sync/prompt.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,28 +153,40 @@ func newWatchCmd(client resources.Client, scope syncdomain.Scope, help targetFla
func newAddCmd(client resources.Client) *cobra.Command {
cmd := &cobra.Command{
Use: "add [project-key/config-key/variation-key...]",
Short: "Add prompt variations from LaunchDarkly",
Short: "Add prompt variations from LaunchDarkly, or create a variation",
Long: "Add prompt variations from LaunchDarkly, or create a variation. " +
"Without --content, add starts syncing variations that exist in LaunchDarkly. " +
"With --content, add creates the variation in an existing config. For an agent config, the content is the instructions. " +
"For a completion config, the content is role blocks such as <system>...</system>, or plain text for one system message. " +
"The project-key/config-key/variation-key form still works.",
Example: ` # Choose variations interactively
ldcli sync prompts add

# Choose variations in one config
ldcli sync prompts add --project production --config-key support

# Add variations by key without prompting
ldcli sync prompts add --project production --config-key support --key default --key concise --no-input`,
ldcli sync prompts add --project production --config-key support --key default --key concise --no-input

# Create an agent variation without prompting
ldcli sync prompts add --project production --config-key support --key escalation \
--model-config-key claude \
--content "Hand the conversation to a person when the customer asks for one." \
--yes --no-input`,
Args: validatedArgs(cobra.MinimumNArgs(0)),
RunE: runCommand(client, syncdomain.ScopePrompts, func(cmd *cobra.Command, args []string) (syncprompt.CommandAction, error) {
variations, err := parseVariationSelectors(args)
if err != nil {
return nil, err
}
dryRun, _ := cmd.Flags().GetBool(dryRunFlag)
return syncprompt.AddAction{Resources: variations, Filter: targetFilter(cmd), DryRun: dryRun}, nil
return syncprompt.AddAction{Resources: variations, Filter: targetFilter(cmd), DryRun: dryRun, Create: createInput(cmd)}, nil
}),
}
addTargetFlags(cmd, questionFlagHelp)
cmd.Flags().String(contentFlag, "", "Prompt of a variation that add creates")
cmd.Flags().String(modelConfigKeyFlag, "", "Model config of a variation that add creates")
cmd.Flags().String(nameFlag, "", "Name of a variation that add creates. The default comes from the key.")
cmd.Flags().Bool(dryRunFlag, false, "Preview local files without creating them")
cmd.Flags().Bool(diffFlag, false, "Show full diffs and file content in the plan")
cmd.Flags().Bool(yesFlag, false, "Create without confirmation")
cmd.Flags().Bool(noInputFlag, false, "Fail instead of prompting for missing input")
return cmd
}
Expand Down
20 changes: 15 additions & 5 deletions cmd/sync/scope_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ import (
// syncCommandFlags is the exact flag set of every sync command that runs. A
// new command, or a new question without a flag, must change this list.
var syncCommandFlags = map[string][]string{
"sync all": {"conflict", "diff", "dry-run", "no-input", "project", "yes"},
"sync all watch": {"conflict", "diff", "no-input", "project", "yes"},
"sync prompts": {"config-key", "conflict", "diff", "dry-run", "key", "no-input", "project", "resolve", "yes"},
"sync prompts watch": {"config-key", "conflict", "diff", "key", "no-input", "project", "resolve", "yes"},
"sync prompts add": {"config-key", "diff", "dry-run", "key", "no-input", "project"},
"sync all": {"conflict", "diff", "dry-run", "no-input", "project", "yes"},
"sync all watch": {"conflict", "diff", "no-input", "project", "yes"},
"sync prompts": {"config-key", "conflict", "diff", "dry-run", "key", "no-input", "project", "resolve", "yes"},
"sync prompts watch": {"config-key", "conflict", "diff", "key", "no-input", "project", "resolve", "yes"},
"sync prompts add": {
"config-key", "content", "diff", "dry-run", "key", "model-config-key", "name", "no-input", "project", "yes",
},
"sync prompts attach tool": {"conflict", "diff", "no-input", "project", "resolve", "to", "yes"},
"sync prompts attach skill": {"conflict", "diff", "no-input", "project", "resolve", "to", "yes"},
"sync prompts detach": {"archive", "config-key", "key", "no-input", "project", "yes"},
Expand Down Expand Up @@ -102,6 +104,14 @@ func TestAddReadsTheCreateInputFromTheFlagsThatAreSet(t *testing.T) {
assert.Equal(t, "", *input.Description)
assert.Nil(t, input.Name)
assert.Nil(t, input.JSONSchema)

prompts, _, err := root.Find([]string{"prompts", "add"})
require.NoError(t, err)
require.NoError(t, prompts.ParseFlags([]string{"--content", "Escalate.", "--model-config-key", "claude"}))
input = createInput(prompts)
require.NotNil(t, input)
assert.Equal(t, "claude", *input.ModelConfigKey)
assert.Nil(t, input.Name)
}

func TestSyncCommandScopeNamesTheKindOfTheCommand(t *testing.T) {
Expand Down
3 changes: 2 additions & 1 deletion internal/sync/bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,8 @@ func selectedVariations(options Options) ([]synclocal.VariationFile, error) {
})
if index < 0 {
return nil, fmt.Errorf(
"variation %q does not exist in config %q in project %q", variationKey, configKey, selection.ProjectKey,
"variation %q does not exist in config %q in project %q. To create it, run %s with --model-config-key and --content.",
variationKey, configKey, selection.ProjectKey, AddCommand(selection),
)
}

Expand Down
13 changes: 13 additions & 0 deletions internal/sync/bootstrap/bootstrap_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,19 @@ func TestRunUsesExplicitVariationSelectorsWithoutTerminal(t *testing.T) {
assert.Contains(t, output.String(), " .launchdarkly/project/configs/config/variation.prompt.md +9")
}

func TestRunNamesTheCreateFlagsOfAMissingVariation(t *testing.T) {
catalog := &fakeCatalog{config: syncapi.Config{Key: "support", Mode: syncdomain.VariationModeAgent}}

err := Run(Options{
Catalog: catalog, Store: synclocal.NewStore(t.TempDir()), Output: &bytes.Buffer{},
Selections: []syncdomain.ResourceID{syncdomain.VariationID("demo", "support", "escalation")},
})

require.EqualError(t, err, `variation "escalation" does not exist in config "support" in project "demo". `+
"To create it, run ldcli sync prompts add --project demo --config-key support --key escalation "+
"with --model-config-key and --content.")
}

func TestFinishSelectionDryRunDoesNotCreateFiles(t *testing.T) {
root := t.TempDir()
var output bytes.Buffer
Expand Down
27 changes: 27 additions & 0 deletions internal/sync/prompt/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,33 @@ func TestCreateVariationRemovesTheFileWhenLaunchDarklyRejectsTheCreate(t *testin
assert.NoDirExists(t, filepath.Join(root, syncdomain.RootDir))
}

func TestPromptsAddCreatesAVariationFromItsFlags(t *testing.T) {
root := initRepository(t)
api := &directAPI{modelConfigs: []syncapi.ModelConfig{claude}}

_, stderr, err := runPrompt(t, root, api, "add", "--project", "production", "--config-key", "support", "--key", "escalation",
"--model-config-key", "claude", "--name", "Escalate to a person", "--content", "Hand the conversation to a person.",
"--yes", "--no-input")

require.NoError(t, err)
assert.Contains(t, stderr, "Note: Created .launchdarkly/production/"+escalationFile+".\n")
require.NotNil(t, api.variation)
assert.Equal(t, "Escalate to a person", api.variation.Name)
assert.Equal(t, "Hand the conversation to a person.", api.variation.Instructions)
}

func TestPromptsAddRefusesASelectorWithCreateFlags(t *testing.T) {
root := initRepository(t)
api := &directAPI{}

_, _, err := runPrompt(t, root, api, "add", "production/support/escalation",
"--model-config-key", "claude", "--content", "Escalate.", "--yes", "--no-input")

require.ErrorContains(t, err, "a variation that add creates comes from --project, --config-key, and --key, "+
"not from project-key/config-key/variation-key arguments")
assert.Empty(t, api.requests)
}

// createVariationAction is the add that creates the variation key in the
// config "support" of the project "production".
func createVariationAction(key string, input synccreate.Input) syncprompt.AddAction {
Expand Down
Loading