diff --git a/.gitignore b/.gitignore index dfb4d13..92a217d 100644 --- a/.gitignore +++ b/.gitignore @@ -3,7 +3,6 @@ *.json *.mp4 *.jpg -*.md .mcpregistry_github_token .mcpregistry_registry_token .env diff --git a/BUILD.bazel b/BUILD.bazel index a579583..e5d0c25 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -18,6 +18,7 @@ go_library( deps = [ "//cmd", "//cmd/activity", + "//cmd/agent", "//cmd/caption", "//cmd/channel", "//cmd/channelBanner", diff --git a/cmd/BUILD.bazel b/cmd/BUILD.bazel index 42cb5e7..10d77d2 100644 --- a/cmd/BUILD.bazel +++ b/cmd/BUILD.bazel @@ -3,13 +3,11 @@ load("@rules_go//go:def.bzl", "go_library") go_library( name = "cmd", srcs = [ - "agent.go", "auth.go", "mcp.go", "root.go", "version.go", ], - embedsrcs = ["INSTRUCTION.md"], importpath = "github.com/eat-pray-ai/yutu/cmd", visibility = ["//visibility:public"], x_defs = { @@ -27,13 +25,5 @@ go_library( "@com_github_savioxavier_termlink//:termlink", "@com_github_spf13_cobra//:cobra", "@com_github_spf13_viper//:viper", - "@org_golang_google_adk//agent", - "@org_golang_google_adk//agent/llmagent", - "@org_golang_google_adk//cmd/launcher", - "@org_golang_google_adk//cmd/launcher/full", - "@org_golang_google_adk//model/gemini", - "@org_golang_google_adk//tool", - "@org_golang_google_adk//tool/mcptoolset", - "@org_golang_google_genai//:genai", ], ) diff --git a/cmd/agent/BUILD.bazel b/cmd/agent/BUILD.bazel new file mode 100644 index 0000000..7cb5360 --- /dev/null +++ b/cmd/agent/BUILD.bazel @@ -0,0 +1,26 @@ +load("@rules_go//go:def.bzl", "go_library") + +go_library( + name = "agent", + srcs = [ + "agent.go", + "console.go", + "web.go", + ], + embedsrcs = ["INSTRUCTION.md"], + importpath = "github.com/eat-pray-ai/yutu/cmd/agent", + visibility = ["//visibility:public"], + deps = [ + "//cmd", + "@com_github_modelcontextprotocol_go_sdk//mcp", + "@com_github_spf13_cobra//:cobra", + "@org_golang_google_adk//agent", + "@org_golang_google_adk//agent/llmagent", + "@org_golang_google_adk//cmd/launcher", + "@org_golang_google_adk//cmd/launcher/full", + "@org_golang_google_adk//model/gemini", + "@org_golang_google_adk//tool", + "@org_golang_google_adk//tool/mcptoolset", + "@org_golang_google_genai//:genai", + ], +) diff --git a/cmd/agent/agent.go b/cmd/agent/agent.go index bbd48ab..ec16568 100644 --- a/cmd/agent/agent.go +++ b/cmd/agent/agent.go @@ -4,9 +4,11 @@ package agent import ( + "context" _ "embed" "log/slog" "os" + "time" rootCmd "github.com/eat-pray-ai/yutu/cmd" "github.com/modelcontextprotocol/go-sdk/mcp" @@ -29,74 +31,88 @@ const ( var ( //go:embed INSTRUCTION.md instruction string + + streamingMode string + idleTimeout time.Duration + port int + readTimeout time.Duration + writeTimeout time.Duration + sublaunchers []string + sseWriteTimeout time.Duration + webuiAddress string + a2aAgentURL string + apiServerAddress string ) var agentCmd = &cobra.Command{ Use: "agent", Short: agentShort, Long: agentLong, - PreRun: func(cmd *cobra.Command, args []string) { + PersistentPreRun: func(cmd *cobra.Command, args []string) { envInstruction, ok := os.LookupEnv("YUTU_AGENT_INSTRUCTION") if ok && envInstruction != "" { instruction = envInstruction } }, Run: func(cmd *cobra.Command, args []string) { - ctx := cmd.Context() - m, err := gemini.NewModel( - ctx, os.Getenv("GEMINI_MODEL"), &genai.ClientConfig{ - APIKey: os.Getenv("GEMINI_API_KEY"), - Backend: genai.BackendGeminiAPI, - }, - ) - if err != nil { - slog.ErrorContext(ctx, "failed to create Gemini model", "error", err) - os.Exit(1) - } - - clientTransport, serverTransport := mcp.NewInMemoryTransports() - _, err = rootCmd.Server.Connect(ctx, serverTransport, nil) - if err != nil { - slog.ErrorContext(ctx, "failed to connect to MCP server", "error", err) - os.Exit(1) - } - - mcpToolSet, err := mcptoolset.New( - mcptoolset.Config{ - Transport: clientTransport, - }, - ) - if err != nil { - slog.ErrorContext(ctx, "failed to create MCP tool set", "error", err) - } - - a, err := llmagent.New( - llmagent.Config{ - Model: m, - Name: "YouTube Copilot", - Instruction: instruction, - Toolsets: []tool.Toolset{mcpToolSet}, - }, - ) - if err != nil { - slog.ErrorContext(ctx, "failed to create LLM agent", "error", err) - os.Exit(1) - } - - config := &launcher.Config{ - AgentLoader: agent.NewSingleLoader(a), - } - l := full.NewLauncher() - if err := l.Execute(ctx, config, os.Args[2:]); err != nil { - slog.ErrorContext( - ctx, "failed to launch agent", - "launch", l.CommandLineSyntax(), "error", err, - ) - os.Exit(1) - } + _ = cmd.Help() }, } func init() { rootCmd.RootCmd.AddCommand(agentCmd) } + +func launch(ctx context.Context, args []string) { + m, err := gemini.NewModel( + ctx, os.Getenv("GEMINI_MODEL"), &genai.ClientConfig{ + APIKey: os.Getenv("GEMINI_API_KEY"), + Backend: genai.BackendGeminiAPI, + }, + ) + if err != nil { + slog.ErrorContext(ctx, "failed to create Gemini model", "error", err) + os.Exit(1) + } + + clientTransport, serverTransport := mcp.NewInMemoryTransports() + _, err = rootCmd.Server.Connect(ctx, serverTransport, nil) + if err != nil { + slog.ErrorContext(ctx, "failed to connect to MCP server", "error", err) + os.Exit(1) + } + + mcpToolSet, err := mcptoolset.New( + mcptoolset.Config{ + Transport: clientTransport, + }, + ) + if err != nil { + slog.ErrorContext(ctx, "failed to create MCP tool set", "error", err) + } + + a, err := llmagent.New( + llmagent.Config{ + Model: m, + Name: "YouTube Copilot", + Instruction: instruction, + Toolsets: []tool.Toolset{mcpToolSet}, + }, + ) + if err != nil { + slog.ErrorContext(ctx, "failed to create LLM agent", "error", err) + os.Exit(1) + } + + config := &launcher.Config{ + AgentLoader: agent.NewSingleLoader(a), + } + l := full.NewLauncher() + if err := l.Execute(ctx, config, args); err != nil { + slog.ErrorContext( + ctx, "failed to launch agent", + "launch", l.CommandLineSyntax(), "error", err, + ) + os.Exit(1) + } +} diff --git a/cmd/agent/console.go b/cmd/agent/console.go new file mode 100644 index 0000000..dd43f8d --- /dev/null +++ b/cmd/agent/console.go @@ -0,0 +1,31 @@ +// Copyright 2026 eat-pray-ai & OpenWaygate +// SPDX-License-Identifier: Apache-2.0 + +package agent + +import ( + "github.com/spf13/cobra" +) + +const ( + consoleShort = "Start agent in console mode" + consoleLong = "Start agent in console mode" +) + +var consoleCmd = &cobra.Command{ + Use: "console", + Short: consoleShort, + Long: consoleLong, + Run: func(cmd *cobra.Command, args []string) { + launcherArgs := []string{"console"} + if streamingMode != "" { + launcherArgs = append(launcherArgs, "-streaming_mode", streamingMode) + } + launch(cmd.Context(), launcherArgs) + }, +} + +func init() { + agentCmd.AddCommand(consoleCmd) + consoleCmd.Flags().StringVarP(&streamingMode, "streamingMode", "m", "sse", "defines streaming mode: none|sse") +} diff --git a/cmd/agent/web.go b/cmd/agent/web.go new file mode 100644 index 0000000..14f65b3 --- /dev/null +++ b/cmd/agent/web.go @@ -0,0 +1,63 @@ +// Copyright 2026 eat-pray-ai & OpenWaygate +// SPDX-License-Identifier: Apache-2.0 + +package agent + +import ( + "slices" + "strconv" + "time" + + "github.com/spf13/cobra" +) + +var webCmd = &cobra.Command{ + Use: "web", + Short: "Start agent in web mode", + Long: "start agent in web mode with additional sub-servers specified by sublaunchers", + Run: func(cmd *cobra.Command, args []string) { + launcherArgs := []string{"web"} + + launcherArgs = append(launcherArgs, "-idle-timeout", idleTimeout.String()) + launcherArgs = append(launcherArgs, "-port", strconv.Itoa(port)) + launcherArgs = append(launcherArgs, "-read-timeout", readTimeout.String()) + launcherArgs = append(launcherArgs, "-write-timeout", writeTimeout.String()) + + if slices.Contains(sublaunchers, "api") { + launcherArgs = append( + launcherArgs, "api", + "-sse-write-timeout", sseWriteTimeout.String(), + "-webui_address", webuiAddress, + ) + } + if slices.Contains(sublaunchers, "a2a") { + launcherArgs = append(launcherArgs, "a2a", "-a2a_agent_url", a2aAgentURL) + } + if slices.Contains(sublaunchers, "webui") { + launcherArgs = append(launcherArgs, "webui", "-api_server_address", apiServerAddress) + } + + launch(cmd.Context(), launcherArgs) + }, +} + +func init() { + agentCmd.AddCommand(webCmd) + + // web flags + webCmd.Flags().DurationVarP(&idleTimeout, "idleTimeout", "i", time.Minute, "Server idle timeout") + webCmd.Flags().IntVarP(&port, "port", "p", 8080, "Localhost port for the server") + webCmd.Flags().DurationVarP(&readTimeout, "readTimeout", "r", 15*time.Second, "Server read timeout") + webCmd.Flags().DurationVarP(&writeTimeout, "writeTimeout", "w", 15*time.Second, "Server write timeout") + webCmd.Flags().StringSliceVarP(&sublaunchers, "sublaunchers", "l", []string{"api"}, "One or more sublaunchers: api, a2a, and webui") + + // api flags + webCmd.Flags().DurationVarP(&sseWriteTimeout, "sseWriteTimeout", "s", 2*time.Minute, "SSE server write timeout") + webCmd.Flags().StringVarP(&webuiAddress, "webuiAddress", "W", "localhost:8080", "ADK WebUI address") + + // a2a flags + webCmd.Flags().StringVarP(&a2aAgentURL, "a2aAgentUrl", "u", "http://localhost:8080", "A2A host URL") + + // webui flags + webCmd.Flags().StringVarP(&apiServerAddress, "apiServerAddress", "U", "http://localhost:8080/api", "ADK REST API server address") +}