Skip to content

Commit 74fd243

Browse files
authored
feat(agents): add streamlined lk agent init command (#696)
* feat(agents): add streamlined lk agent init command * chore(agents): support agent-starter-node init option * chore: bump version
1 parent e0e8548 commit 74fd243

File tree

8 files changed

+290
-145
lines changed

8 files changed

+290
-145
lines changed

cmd/lk/agent.go

Lines changed: 155 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -32,17 +32,14 @@ import (
3232

3333
livekitcli "github.com/livekit/livekit-cli/v2"
3434
"github.com/livekit/livekit-cli/v2/pkg/agentfs"
35+
"github.com/livekit/livekit-cli/v2/pkg/bootstrap"
3536
"github.com/livekit/livekit-cli/v2/pkg/config"
3637
"github.com/livekit/livekit-cli/v2/pkg/util"
3738
lkproto "github.com/livekit/protocol/livekit"
3839
"github.com/livekit/protocol/logger"
3940
lksdk "github.com/livekit/server-sdk-go/v2"
4041
)
4142

42-
const (
43-
cloudAgentsBetaSignupURL = "https://forms.gle/GkGNNTiMt2qyfnu78"
44-
)
45-
4643
var (
4744
idFlag = func(required bool) *cli.StringFlag {
4845
return &cli.StringFlag{
@@ -97,6 +94,47 @@ var (
9794
Aliases: []string{"a"},
9895
Usage: "Manage LiveKit Cloud Agents",
9996
Commands: []*cli.Command{
97+
{
98+
Name: "init",
99+
Usage: "Initialize a new LiveKit Cloud agent project",
100+
Before: createAgentClient,
101+
Action: initAgent,
102+
MutuallyExclusiveFlags: []cli.MutuallyExclusiveFlags{{
103+
Flags: [][]cli.Flag{{
104+
&cli.StringFlag{
105+
Name: "lang",
106+
Usage: "`LANGUAGE` of the project, one of \"node\", \"python\"",
107+
Action: func(ctx context.Context, cmd *cli.Command, l string) error {
108+
if l == "" {
109+
return nil
110+
}
111+
if !slices.Contains([]string{"node", "python"}, l) {
112+
return fmt.Errorf("unsupported language: %s", l)
113+
}
114+
return nil
115+
},
116+
Hidden: true,
117+
},
118+
&cli.BoolFlag{
119+
Name: "deploy",
120+
Usage: "If set, automatically deploys the agent to LiveKit Cloud after initialization.",
121+
Value: false,
122+
},
123+
templateFlag,
124+
templateURLFlag,
125+
}, {
126+
sandboxFlag,
127+
&cli.BoolFlag{
128+
Name: "no-sandbox",
129+
Usage: "If set, will not create a sandbox for the project. ",
130+
Value: false,
131+
},
132+
}},
133+
}},
134+
Flags: []cli.Flag{},
135+
ArgsUsage: "[AGENT-NAME]",
136+
DisableSliceFlagSeparator: true,
137+
},
100138
{
101139
Name: "create",
102140
Usage: "Create a new LiveKit Cloud Agent",
@@ -330,6 +368,91 @@ func createAgentClient(ctx context.Context, cmd *cli.Command) (context.Context,
330368
return ctx, nil
331369
}
332370

371+
func initAgent(ctx context.Context, cmd *cli.Command) error {
372+
// TODO: (@rektdeckard) move compatibility flag into template index,
373+
// then show template picker containing only compatible templates
374+
if !(cmd.IsSet("lang") || cmd.IsSet("template") || cmd.IsSet("template-url")) {
375+
var lang string
376+
// Prompt for language
377+
if err := huh.NewSelect[string]().
378+
Title("Select the language for your agent project").
379+
Options(
380+
huh.NewOption("Python", "python"),
381+
huh.NewOption("Node.js", "node"),
382+
).
383+
Value(&lang).
384+
WithTheme(util.Theme).
385+
Run(); err != nil {
386+
return err
387+
}
388+
389+
switch lang {
390+
case "node":
391+
templateURL = "https://github.com/livekit-examples/agent-starter-node"
392+
case "python":
393+
templateURL = "https://github.com/livekit-examples/agent-starter-python"
394+
default:
395+
return fmt.Errorf("unsupported language: %s", lang)
396+
}
397+
}
398+
399+
logger.Debugw("Initializing agent project", "working-dir", workingDir)
400+
401+
// Create sandbox
402+
if !cmd.Bool("no-sandbox") || sandboxID == "" {
403+
if err := util.Await("Creating sandbox app...", ctx, func(ctx context.Context) error {
404+
token, err := requireToken(ctx, cmd)
405+
if err != nil {
406+
return err
407+
}
408+
409+
appName = cmd.Args().First()
410+
if appName == "" {
411+
appName = project.Name
412+
}
413+
// We set agent name in env for use in template tasks
414+
os.Setenv("LIVEKIT_AGENT_NAME", appName)
415+
416+
// TODO: (@rektdeckard) figure out why AccessKeyProvider does not immediately
417+
// have access to newly-created API keys, then remove this sleep
418+
time.Sleep(4 * time.Second)
419+
sandboxID, err = bootstrap.CreateSandbox(
420+
ctx,
421+
appName,
422+
// NOTE: we may want to support embed sandbox in the future
423+
"https://github.com/livekit-examples/agent-starter-react",
424+
token,
425+
serverURL,
426+
)
427+
return err
428+
}); err != nil {
429+
return fmt.Errorf("failed to create sandbox: %w", err)
430+
} else {
431+
fmt.Println("Creating sandbox app...")
432+
fmt.Printf("Created sandbox app [%s]\n", util.Accented(sandboxID))
433+
}
434+
435+
}
436+
437+
// Run template bootstrap
438+
shouldDeploy := cmd.Bool("deploy")
439+
if shouldDeploy {
440+
cmd.Set("install", "true")
441+
}
442+
if err := setupTemplate(ctx, cmd); err != nil {
443+
return err
444+
}
445+
// Deploy if requested
446+
if shouldDeploy {
447+
fmt.Println("Deploying agent...")
448+
if err := createAgent(ctx, cmd); err != nil {
449+
return fmt.Errorf("failed to deploy agent: %w", err)
450+
}
451+
}
452+
453+
return nil
454+
}
455+
333456
func createAgent(ctx context.Context, cmd *cli.Command) error {
334457
subdomainMatches := subdomainPattern.FindStringSubmatch(project.URL)
335458
if len(subdomainMatches) < 2 {
@@ -424,11 +547,9 @@ func createAgent(ctx context.Context, cmd *cli.Command) error {
424547
resp, err := agentsClient.CreateAgent(ctx, req)
425548
if err != nil {
426549
if twerr, ok := err.(twirp.Error); ok {
427-
if twerr.Code() == twirp.PermissionDenied {
428-
return fmt.Errorf("agent hosting is disabled for this project -- join the beta program here [%s]", cloudAgentsBetaSignupURL)
429-
}
550+
return fmt.Errorf("unable to create agent: %s", twerr.Msg())
430551
}
431-
return err
552+
return fmt.Errorf("unable to create agent: %w", err)
432553
}
433554

434555
lkConfig.Agent.ID = resp.AgentId
@@ -512,11 +633,9 @@ func createAgentConfig(ctx context.Context, cmd *cli.Command) error {
512633
})
513634
if err != nil {
514635
if twerr, ok := err.(twirp.Error); ok {
515-
if twerr.Code() == twirp.PermissionDenied {
516-
return fmt.Errorf("agent hosting is disabled for this project -- join the beta program here [%s]", cloudAgentsBetaSignupURL)
517-
}
636+
return fmt.Errorf("unable to list agents: %s", twerr.Msg())
518637
}
519-
return err
638+
return fmt.Errorf("unable to list agents: %w", err)
520639
}
521640
if len(response.Agents) == 0 {
522641
return fmt.Errorf("agent not found")
@@ -581,11 +700,9 @@ func deployAgent(ctx context.Context, cmd *cli.Command) error {
581700
resp, err := agentsClient.DeployAgent(ctx, req)
582701
if err != nil {
583702
if twerr, ok := err.(twirp.Error); ok {
584-
if twerr.Code() == twirp.PermissionDenied {
585-
return fmt.Errorf("agent hosting is disabled for this project -- join the beta program here [%s]", cloudAgentsBetaSignupURL)
586-
}
703+
return fmt.Errorf("unable to deploy agent: %s", twerr.Msg())
587704
}
588-
return err
705+
return fmt.Errorf("unable to deploy agent: %w", err)
589706
}
590707

591708
if !resp.Success {
@@ -619,11 +736,9 @@ func getAgentStatus(ctx context.Context, cmd *cli.Command) error {
619736
})
620737
if err != nil {
621738
if twerr, ok := err.(twirp.Error); ok {
622-
if twerr.Code() == twirp.PermissionDenied {
623-
return fmt.Errorf("agent hosting is disabled for this project -- join the beta program here [%s]", cloudAgentsBetaSignupURL)
624-
}
739+
return fmt.Errorf("unable to list agents: %s", twerr.Msg())
625740
}
626-
return err
741+
return fmt.Errorf("unable to list agents: %w", err)
627742
}
628743

629744
if len(res.Agents) == 0 {
@@ -721,11 +836,9 @@ func updateAgent(ctx context.Context, cmd *cli.Command) error {
721836
})
722837
if err != nil {
723838
if twerr, ok := err.(twirp.Error); ok {
724-
if twerr.Code() == twirp.PermissionDenied {
725-
return fmt.Errorf("agent hosting is disabled for this project -- join the beta program here [%s]", cloudAgentsBetaSignupURL)
726-
}
839+
return fmt.Errorf("unable to update agent: %s", twerr.Msg())
727840
}
728-
return err
841+
return fmt.Errorf("unable to update agent: %w", err)
729842
}
730843

731844
if resp.Success {
@@ -755,11 +868,9 @@ func rollbackAgent(ctx context.Context, cmd *cli.Command) error {
755868

756869
if err != nil {
757870
if twerr, ok := err.(twirp.Error); ok {
758-
if twerr.Code() == twirp.PermissionDenied {
759-
return fmt.Errorf("agent hosting is disabled for this project -- join the beta program here [%s]", cloudAgentsBetaSignupURL)
760-
}
871+
return fmt.Errorf("unable to rollback agent: %s", twerr.Msg())
761872
}
762-
return err
873+
return fmt.Errorf("unable to rollback agent: %w", err)
763874
}
764875

765876
if !resp.Success {
@@ -818,11 +929,9 @@ func deleteAgent(ctx context.Context, cmd *cli.Command) error {
818929

819930
if err != nil {
820931
if twerr, ok := err.(twirp.Error); ok {
821-
if twerr.Code() == twirp.PermissionDenied {
822-
return fmt.Errorf("agent hosting is disabled for this project -- join the beta program here [%s]", cloudAgentsBetaSignupURL)
823-
}
932+
return fmt.Errorf("unable to delete agent: %s", twerr.Msg())
824933
}
825-
return err
934+
return fmt.Errorf("unable to delete agent: %w", err)
826935
}
827936

828937
if !res.Success {
@@ -846,11 +955,9 @@ func listAgentVersions(ctx context.Context, cmd *cli.Command) error {
846955
versions, err := agentsClient.ListAgentVersions(ctx, req)
847956
if err != nil {
848957
if twerr, ok := err.(twirp.Error); ok {
849-
if twerr.Code() == twirp.PermissionDenied {
850-
return fmt.Errorf("agent hosting is disabled for this project -- join the beta program here [%s]", cloudAgentsBetaSignupURL)
851-
}
958+
return fmt.Errorf("unable to list agent versions: %s", twerr.Msg())
852959
}
853-
return err
960+
return fmt.Errorf("unable to list agent versions: %w", err)
854961
}
855962

856963
table := util.CreateTable().
@@ -885,23 +992,19 @@ func listAgents(ctx context.Context, cmd *cli.Command) error {
885992
})
886993
if err != nil {
887994
if twerr, ok := err.(twirp.Error); ok {
888-
if twerr.Code() == twirp.PermissionDenied {
889-
return fmt.Errorf("agent hosting is disabled for this project -- join the beta program here [%s]", cloudAgentsBetaSignupURL)
890-
}
995+
return fmt.Errorf("unable to list agents: %s", twerr.Msg())
891996
}
892-
return err
997+
return fmt.Errorf("unable to list agents: %w", err)
893998
}
894999
items = append(items, res.Agents...)
8951000
}
8961001
} else {
8971002
agents, err := agentsClient.ListAgents(ctx, &lkproto.ListAgentsRequest{})
8981003
if err != nil {
8991004
if twerr, ok := err.(twirp.Error); ok {
900-
if twerr.Code() == twirp.PermissionDenied {
901-
return fmt.Errorf("agent hosting is disabled for this project -- join the beta program here [%s]", cloudAgentsBetaSignupURL)
902-
}
1005+
return fmt.Errorf("unable to list agents: %s", twerr.Msg())
9031006
}
904-
return err
1007+
return fmt.Errorf("unable to list agents: %w", err)
9051008
}
9061009
items = agents.Agents
9071010
}
@@ -950,11 +1053,9 @@ func listAgentSecrets(ctx context.Context, cmd *cli.Command) error {
9501053
secrets, err := agentsClient.ListAgentSecrets(ctx, req)
9511054
if err != nil {
9521055
if twerr, ok := err.(twirp.Error); ok {
953-
if twerr.Code() == twirp.PermissionDenied {
954-
return fmt.Errorf("agent hosting is disabled for this project -- join the beta program here [%s]", cloudAgentsBetaSignupURL)
955-
}
1056+
return fmt.Errorf("unable to list agent secrets: %s", twerr.Msg())
9561057
}
957-
return err
1058+
return fmt.Errorf("unable to list agent secrets: %w", err)
9581059
}
9591060

9601061
table := util.CreateTable().
@@ -1011,11 +1112,9 @@ func updateAgentSecrets(ctx context.Context, cmd *cli.Command) error {
10111112
resp, err := agentsClient.UpdateAgentSecrets(ctx, req)
10121113
if err != nil {
10131114
if twerr, ok := err.(twirp.Error); ok {
1014-
if twerr.Code() == twirp.PermissionDenied {
1015-
return fmt.Errorf("agent hosting is disabled for this project -- join the beta program here [%s]", cloudAgentsBetaSignupURL)
1016-
}
1115+
return fmt.Errorf("unable to update agent secrets: %s", twerr.Msg())
10171116
}
1018-
return err
1117+
return fmt.Errorf("unable to update agent secrets: %w", err)
10191118
}
10201119

10211120
if resp.Success {
@@ -1067,11 +1166,9 @@ func selectAgent(ctx context.Context, _ *cli.Command, excludeEmptyVersion bool)
10671166
})
10681167
if err != nil {
10691168
if twerr, ok := err.(twirp.Error); ok {
1070-
if twerr.Code() == twirp.PermissionDenied {
1071-
return "", fmt.Errorf("agent hosting is disabled for this project -- join the beta program here [%s]", cloudAgentsBetaSignupURL)
1072-
}
1169+
return "", fmt.Errorf("unable to list agents: %s", twerr.Msg())
10731170
}
1074-
return "", err
1171+
return "", fmt.Errorf("unable to list agents: %w", err)
10751172
}
10761173

10771174
if len(agents.Agents) == 0 {
@@ -1213,11 +1310,9 @@ func getClientSettings(ctx context.Context, silent bool) (map[string]string, err
12131310

12141311
if err != nil {
12151312
if twerr, ok := err.(twirp.Error); ok {
1216-
if twerr.Code() == twirp.PermissionDenied {
1217-
return nil, fmt.Errorf("agent hosting is disabled for this project -- join the beta program here [%s]", cloudAgentsBetaSignupURL)
1218-
}
1313+
return nil, fmt.Errorf("unable to get client settings: %s", twerr.Msg())
12191314
}
1220-
return nil, err
1315+
return nil, fmt.Errorf("unable to get client settings: %w", err)
12211316
}
12221317

12231318
if clientSettingsResponse == nil {

0 commit comments

Comments
 (0)