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
36 changes: 27 additions & 9 deletions pkg/cmd/agents/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type CreateOptions struct {
PrintFlags *cmdutil.PrintFlags

JSON string
File string
Name string
Instructions string
}
Expand All @@ -47,23 +48,36 @@ func NewCreateCmd(f *cmdutil.Factory) *cobra.Command {
# Create from a JSON object
$ algolia agentstudio agents create --json '{"name":"support-bot","instructions":"You are a helpful assistant."}'

# Create from a JSON file
$ algolia agentstudio agents create --file agent.json

# Create from stdin
$ cat agent.json | algolia agentstudio agents create --file -

# Create with a provider and model so the agent can be published
$ algolia agentstudio agents create --json '{"name":"support-bot","instructions":"You are a helpful assistant.","providerId":"22222222-2222-4222-8222-222222222222","model":"gpt-4o"}'
`),
RunE: func(cmd *cobra.Command, args []string) error {
if opts.JSON != "" {
if opts.Name != "" || opts.Instructions != "" {
return cmdutil.FlagErrorf("--json cannot be combined with --name or --instructions")
}
} else if opts.Name == "" || opts.Instructions == "" {
return cmdutil.FlagErrorf("`--json` or both `--name` and `--instructions` are required")
hasPayload := opts.JSON != "" || opts.File != ""
hasFlags := opts.Name != "" || opts.Instructions != ""
if hasPayload && hasFlags {
return cmdutil.FlagErrorf(
"--json and --file cannot be combined with --name or --instructions",
)
}
if !hasPayload && (opts.Name == "" || opts.Instructions == "") {
return cmdutil.FlagErrorf(
"`--json`, `--file`, or both `--name` and `--instructions` are required",
)
}

return runCreateCmd(opts)
},
}

cmd.Flags().StringVar(&opts.JSON, "json", "", "Agent JSON object (include providerId and model to publish later)")
cmd.Flags().
StringVar(&opts.JSON, "json", "", "Agent JSON object (include providerId and model to publish later)")
cmd.Flags().StringVarP(&opts.File, "file", "f", "", "JSON file path (use - for stdin)")
cmd.Flags().StringVar(&opts.Name, "name", "", "Agent name")
cmd.Flags().StringVar(&opts.Instructions, "instructions", "", "Agent prompt")

Expand All @@ -72,14 +86,18 @@ func NewCreateCmd(f *cmdutil.Factory) *cobra.Command {
}

func buildAgent(opts *CreateOptions) (*agentStudio.AgentConfigCreate, error) {
if opts.JSON == "" {
raw, err := cmdutil.JSONPayload(opts.JSON, opts.File, opts.IO.In)
if err != nil {
return nil, err
}
if raw == "" {
return agentStudio.NewEmptyAgentConfigCreate().
SetName(opts.Name).
SetInstructions(opts.Instructions), nil
}

var body agentStudio.AgentConfigCreate
if err := json.Unmarshal([]byte(opts.JSON), &body); err != nil {
if err := json.Unmarshal([]byte(raw), &body); err != nil {
return nil, fmt.Errorf("parsing agent JSON: %w", err)
}
if body.Name == "" || body.Instructions == "" {
Expand Down
126 changes: 113 additions & 13 deletions pkg/cmd/agents/create/create_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package create_test
import (
"io"
"net/http"
"os"
"path/filepath"
"strings"
"testing"

Expand Down Expand Up @@ -30,28 +32,42 @@ const createdAgentBody = `{
func TestCreateAgent_FromFlags(t *testing.T) {
r := &httpmock.Registry{}
var gotBody []byte
r.Register(httpmock.REST("POST", "agent-studio/1/agents"), func(req *http.Request) (*http.Response, error) {
gotBody, _ = io.ReadAll(req.Body)
return httpmock.StringResponse(createdAgentBody)(req)
})
r.Register(
httpmock.REST("POST", "agent-studio/1/agents"),
func(req *http.Request) (*http.Response, error) {
gotBody, _ = io.ReadAll(req.Body)
return httpmock.StringResponse(createdAgentBody)(req)
},
)

f, out := test.NewFactory(false, r, nil, "")
cmd := create.NewCreateCmd(f)
_, err := test.Execute(cmd, `--name cli-test-agent --instructions "You are a helpful assistant."`, out)
_, err := test.Execute(
cmd,
`--name cli-test-agent --instructions "You are a helpful assistant."`,
out,
)
require.NoError(t, err)

assert.JSONEq(t, createdAgentBody, strings.TrimSpace(out.String()))
assert.JSONEq(t, `{"name":"cli-test-agent","instructions":"You are a helpful assistant."}`, string(gotBody))
assert.JSONEq(
t,
`{"name":"cli-test-agent","instructions":"You are a helpful assistant."}`,
string(gotBody),
)
r.Verify(t)
}

func TestCreateAgent_FromJSON(t *testing.T) {
r := &httpmock.Registry{}
var gotBody []byte
r.Register(httpmock.REST("POST", "agent-studio/1/agents"), func(req *http.Request) (*http.Response, error) {
gotBody, _ = io.ReadAll(req.Body)
return httpmock.StringResponse(createdAgentBody)(req)
})
r.Register(
httpmock.REST("POST", "agent-studio/1/agents"),
func(req *http.Request) (*http.Response, error) {
gotBody, _ = io.ReadAll(req.Body)
return httpmock.StringResponse(createdAgentBody)(req)
},
)

f, out := test.NewFactory(false, r, nil, "")
cmd := create.NewCreateCmd(f)
Expand All @@ -71,22 +87,106 @@ func TestCreateAgent_FromJSON(t *testing.T) {
r.Verify(t)
}

func TestCreateAgent_FromFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "agent.json")
require.NoError(
t,
os.WriteFile(
path,
[]byte(
`{"name":"cli-test-agent","instructions":"You are a helpful assistant.","model":"gpt-4o"}`,
),
0o600,
),
)

r := &httpmock.Registry{}
var gotBody []byte
r.Register(
httpmock.REST("POST", "agent-studio/1/agents"),
func(req *http.Request) (*http.Response, error) {
gotBody, _ = io.ReadAll(req.Body)
return httpmock.StringResponse(createdAgentBody)(req)
},
)

f, out := test.NewFactory(false, r, nil, "")
cmd := create.NewCreateCmd(f)
_, err := test.Execute(cmd, "--file "+path, out)
require.NoError(t, err)

assert.JSONEq(t, createdAgentBody, strings.TrimSpace(out.String()))
assert.JSONEq(
t,
`{"name":"cli-test-agent","instructions":"You are a helpful assistant.","model":"gpt-4o"}`,
string(gotBody),
)
r.Verify(t)
}

func TestCreateAgent_FromStdin(t *testing.T) {
r := &httpmock.Registry{}
var gotBody []byte
r.Register(
httpmock.REST("POST", "agent-studio/1/agents"),
func(req *http.Request) (*http.Response, error) {
gotBody, _ = io.ReadAll(req.Body)
return httpmock.StringResponse(createdAgentBody)(req)
},
)

f, out := test.NewFactory(
false,
r,
nil,
`{"name":"cli-test-agent","instructions":"You are a helpful assistant."}`,
)
cmd := create.NewCreateCmd(f)
_, err := test.Execute(cmd, "--file -", out)
require.NoError(t, err)

assert.JSONEq(t, createdAgentBody, strings.TrimSpace(out.String()))
assert.JSONEq(
t,
`{"name":"cli-test-agent","instructions":"You are a helpful assistant."}`,
string(gotBody),
)
r.Verify(t)
}

func TestCreateAgent_MissingInput(t *testing.T) {
r := &httpmock.Registry{}
f, out := test.NewFactory(false, r, nil, "")
cmd := create.NewCreateCmd(f)
_, err := test.Execute(cmd, "", out)
require.Error(t, err)
assert.Contains(t, err.Error(), "`--json` or both `--name` and `--instructions` are required")
assert.Contains(
t,
err.Error(),
"`--json`, `--file`, or both `--name` and `--instructions` are required",
)
}

func TestCreateAgent_JSONAndFlagsConflict(t *testing.T) {
r := &httpmock.Registry{}
f, out := test.NewFactory(false, r, nil, "")
cmd := create.NewCreateCmd(f)
_, err := test.Execute(cmd, `--json '{"name":"x","instructions":"y"}' --name x --instructions y`, out)
_, err := test.Execute(
cmd,
`--json '{"name":"x","instructions":"y"}' --name x --instructions y`,
out,
)
require.Error(t, err)
assert.Contains(t, err.Error(), "cannot be combined with --name or --instructions")
}

func TestCreateAgent_JSONAndFileConflict(t *testing.T) {
r := &httpmock.Registry{}
f, out := test.NewFactory(false, r, nil, "")
cmd := create.NewCreateCmd(f)
_, err := test.Execute(cmd, `--json '{"name":"x","instructions":"y"}' --file agent.json`, out)
require.Error(t, err)
assert.Contains(t, err.Error(), "--json cannot be combined")
assert.Contains(t, err.Error(), "--json cannot be combined with --file")
}

func TestCreateAgent_InvalidJSON(t *testing.T) {
Expand Down
35 changes: 26 additions & 9 deletions pkg/cmd/agents/update/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ type UpdateOptions struct {

AgentID string
JSON string
File string
Name string
Instructions string
}
Expand All @@ -39,7 +40,10 @@ func NewUpdateCmd(f *cmdutil.Factory) *cobra.Command {
cmd := &cobra.Command{
Use: "update <agent-id>",
Short: "Update an Agent Studio agent",
Args: validators.ExactArgsWithMsg(1, "agentstudio agents update requires a <agent-id> argument."),
Args: validators.ExactArgsWithMsg(
1,
"agentstudio agents update requires a <agent-id> argument.",
),
Annotations: map[string]string{
"acls": "editSettings",
},
Expand All @@ -49,23 +53,32 @@ func NewUpdateCmd(f *cmdutil.Factory) *cobra.Command {

# Update from a JSON object
$ algolia agentstudio agents update 11111111-1111-4111-8111-111111111111 --json '{"name":"renamed-bot","instructions":"Be brief."}'

# Update from a JSON file
$ algolia agentstudio agents update 11111111-1111-4111-8111-111111111111 --file patch.json
`),
RunE: func(cmd *cobra.Command, args []string) error {
opts.AgentID = args[0]

if opts.JSON != "" {
if opts.Name != "" || opts.Instructions != "" {
return cmdutil.FlagErrorf("--json cannot be combined with --name or --instructions")
}
} else if opts.Name == "" && opts.Instructions == "" {
return cmdutil.FlagErrorf("`--json` or at least one of `--name` or `--instructions` is required")
hasPayload := opts.JSON != "" || opts.File != ""
hasFlags := opts.Name != "" || opts.Instructions != ""
if hasPayload && hasFlags {
return cmdutil.FlagErrorf(
"--json and --file cannot be combined with --name or --instructions",
)
}
if !hasPayload && !hasFlags {
return cmdutil.FlagErrorf(
"`--json`, `--file`, or at least one of `--name` or `--instructions` is required",
)
}

return runUpdateCmd(opts)
},
}

cmd.Flags().StringVar(&opts.JSON, "json", "", "Agent JSON patch object")
cmd.Flags().StringVarP(&opts.File, "file", "f", "", "JSON file path (use - for stdin)")
cmd.Flags().StringVar(&opts.Name, "name", "", "Agent name")
cmd.Flags().StringVar(&opts.Instructions, "instructions", "", "Agent prompt")

Expand All @@ -74,7 +87,11 @@ func NewUpdateCmd(f *cmdutil.Factory) *cobra.Command {
}

func buildPatch(opts *UpdateOptions) (*agentStudio.AgentConfigUpdate, error) {
if opts.JSON == "" {
raw, err := cmdutil.JSONPayload(opts.JSON, opts.File, opts.IO.In)
if err != nil {
return nil, err
}
if raw == "" {
body := agentStudio.NewEmptyAgentConfigUpdate()
if opts.Name != "" {
body.SetName(opts.Name)
Expand All @@ -86,7 +103,7 @@ func buildPatch(opts *UpdateOptions) (*agentStudio.AgentConfigUpdate, error) {
}

var body agentStudio.AgentConfigUpdate
if err := json.Unmarshal([]byte(opts.JSON), &body); err != nil {
if err := json.Unmarshal([]byte(raw), &body); err != nil {
return nil, fmt.Errorf("parsing agent JSON: %w", err)
}

Expand Down
Loading
Loading