Skip to content

Commit 827fd07

Browse files
committed
feat(agents): add streamlined lk agent init command
1 parent e0e8548 commit 827fd07

File tree

7 files changed

+287
-144
lines changed

7 files changed

+287
-144
lines changed

cmd/lk/agent.go

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

434553
lkConfig.Agent.ID = resp.AgentId
@@ -512,11 +631,9 @@ func createAgentConfig(ctx context.Context, cmd *cli.Command) error {
512631
})
513632
if err != nil {
514633
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-
}
634+
return fmt.Errorf("unable to list agents: %s", twerr.Msg())
518635
}
519-
return err
636+
return fmt.Errorf("unable to list agents: %w", err)
520637
}
521638
if len(response.Agents) == 0 {
522639
return fmt.Errorf("agent not found")
@@ -581,11 +698,9 @@ func deployAgent(ctx context.Context, cmd *cli.Command) error {
581698
resp, err := agentsClient.DeployAgent(ctx, req)
582699
if err != nil {
583700
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-
}
701+
return fmt.Errorf("unable to deploy agent: %s", twerr.Msg())
587702
}
588-
return err
703+
return fmt.Errorf("unable to deploy agent: %w", err)
589704
}
590705

591706
if !resp.Success {
@@ -619,11 +734,9 @@ func getAgentStatus(ctx context.Context, cmd *cli.Command) error {
619734
})
620735
if err != nil {
621736
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-
}
737+
return fmt.Errorf("unable to list agents: %s", twerr.Msg())
625738
}
626-
return err
739+
return fmt.Errorf("unable to list agents: %w", err)
627740
}
628741

629742
if len(res.Agents) == 0 {
@@ -721,11 +834,9 @@ func updateAgent(ctx context.Context, cmd *cli.Command) error {
721834
})
722835
if err != nil {
723836
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-
}
837+
return fmt.Errorf("unable to update agent: %s", twerr.Msg())
727838
}
728-
return err
839+
return fmt.Errorf("unable to update agent: %w", err)
729840
}
730841

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

756867
if err != nil {
757868
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-
}
869+
return fmt.Errorf("unable to rollback agent: %s", twerr.Msg())
761870
}
762-
return err
871+
return fmt.Errorf("unable to rollback agent: %w", err)
763872
}
764873

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

819928
if err != nil {
820929
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-
}
930+
return fmt.Errorf("unable to delete agent: %s", twerr.Msg())
824931
}
825-
return err
932+
return fmt.Errorf("unable to delete agent: %w", err)
826933
}
827934

828935
if !res.Success {
@@ -846,11 +953,9 @@ func listAgentVersions(ctx context.Context, cmd *cli.Command) error {
846953
versions, err := agentsClient.ListAgentVersions(ctx, req)
847954
if err != nil {
848955
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-
}
956+
return fmt.Errorf("unable to list agent versions: %s", twerr.Msg())
852957
}
853-
return err
958+
return fmt.Errorf("unable to list agent versions: %w", err)
854959
}
855960

856961
table := util.CreateTable().
@@ -885,23 +990,19 @@ func listAgents(ctx context.Context, cmd *cli.Command) error {
885990
})
886991
if err != nil {
887992
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-
}
993+
return fmt.Errorf("unable to list agents: %s", twerr.Msg())
891994
}
892-
return err
995+
return fmt.Errorf("unable to list agents: %w", err)
893996
}
894997
items = append(items, res.Agents...)
895998
}
896999
} else {
8971000
agents, err := agentsClient.ListAgents(ctx, &lkproto.ListAgentsRequest{})
8981001
if err != nil {
8991002
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-
}
1003+
return fmt.Errorf("unable to list agents: %s", twerr.Msg())
9031004
}
904-
return err
1005+
return fmt.Errorf("unable to list agents: %w", err)
9051006
}
9061007
items = agents.Agents
9071008
}
@@ -950,11 +1051,9 @@ func listAgentSecrets(ctx context.Context, cmd *cli.Command) error {
9501051
secrets, err := agentsClient.ListAgentSecrets(ctx, req)
9511052
if err != nil {
9521053
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-
}
1054+
return fmt.Errorf("unable to list agent secrets: %s", twerr.Msg())
9561055
}
957-
return err
1056+
return fmt.Errorf("unable to list agent secrets: %w", err)
9581057
}
9591058

9601059
table := util.CreateTable().
@@ -1011,11 +1110,9 @@ func updateAgentSecrets(ctx context.Context, cmd *cli.Command) error {
10111110
resp, err := agentsClient.UpdateAgentSecrets(ctx, req)
10121111
if err != nil {
10131112
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-
}
1113+
return fmt.Errorf("unable to update agent secrets: %s", twerr.Msg())
10171114
}
1018-
return err
1115+
return fmt.Errorf("unable to update agent secrets: %w", err)
10191116
}
10201117

10211118
if resp.Success {
@@ -1067,11 +1164,9 @@ func selectAgent(ctx context.Context, _ *cli.Command, excludeEmptyVersion bool)
10671164
})
10681165
if err != nil {
10691166
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-
}
1167+
return "", fmt.Errorf("unable to list agents: %s", twerr.Msg())
10731168
}
1074-
return "", err
1169+
return "", fmt.Errorf("unable to list agents: %w", err)
10751170
}
10761171

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

12141309
if err != nil {
12151310
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-
}
1311+
return nil, fmt.Errorf("unable to get client settings: %s", twerr.Msg())
12191312
}
1220-
return nil, err
1313+
return nil, fmt.Errorf("unable to get client settings: %w", err)
12211314
}
12221315

12231316
if clientSettingsResponse == nil {

0 commit comments

Comments
 (0)