Skip to content

Commit a586408

Browse files
Agent Engine support (#749)
* Agent Engine support * Test fix * Fixes * Fixes * Error handling fix * Fixes * Fixes in encode * Fixed encode * sseTimeout fix * reflection fix * sseWriteTime fix * fix * encode fixes * linter fixes * various fixes * encode tests * linter fixes * fix * linter fixes * Fixes in encode
1 parent 4093a8a commit a586408

22 files changed

Lines changed: 2107 additions & 14 deletions

File tree

cmd/adkgo/internal/deploy/agentengine/agentengine.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package agentengine
1818

1919
import (
2020
"context"
21+
"encoding/json"
2122
"fmt"
2223
"os"
2324
"os/exec"
@@ -34,6 +35,7 @@ import (
3435

3536
"google.golang.org/adk/cmd/adkgo/internal/deploy"
3637
"google.golang.org/adk/internal/cli/util"
38+
"google.golang.org/adk/server/agentengine"
3739
)
3840

3941
type gCloudFlags struct {
@@ -45,7 +47,6 @@ type agentEngineServiceFlags struct {
4547
name string
4648
displayName string
4749
serverPort int
48-
api bool // enable api or not
4950
}
5051

5152
type buildFlags struct {
@@ -93,7 +94,6 @@ func init() {
9394
agentEngineCmd.PersistentFlags().IntVar(&flags.agentEngine.serverPort, "server_port", 8080, "agentEngine server port")
9495
agentEngineCmd.PersistentFlags().StringVarP(&flags.source.entryPointPath, "entry_point_path", "e", "", "Path to an entry point (go 'main')")
9596
agentEngineCmd.PersistentFlags().StringVarP(&flags.source.sourceDir, "source_dir", "d", "", "Directory to archive, defaults to current working directory")
96-
agentEngineCmd.PersistentFlags().BoolVar(&flags.agentEngine.api, "api", true, "Enable API")
9797
}
9898

9999
// computeFlags uses command line arguments to create a full config
@@ -168,7 +168,7 @@ func (f *deployAgentEngineFlags) prepareDockerfile() error {
168168
FROM golang:1.25 as builder
169169
WORKDIR /app
170170
COPY . .
171-
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o ` + f.build.execFile + ` ` + f.source.origEntryPointPath + `
171+
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags "-s -w" -o ` + f.build.execFile + ` ` + f.source.origEntryPointPath + `
172172
173173
FROM gcr.io/distroless/static-debian11
174174
@@ -177,9 +177,7 @@ EXPOSE ` + strconv.Itoa(flags.agentEngine.serverPort) + `
177177
# Command to run the executable when the container starts
178178
CMD ["/app/` + f.build.execFile + `", "web", "-port", "` + strconv.Itoa(flags.agentEngine.serverPort) + `"`)
179179

180-
if flags.agentEngine.api {
181-
b.WriteString(`, "api"`)
182-
}
180+
b.WriteString(`, "agentengine"`)
183181

184182
b.WriteString(`]`)
185183
return os.WriteFile(f.build.dockerfileBuildPath, []byte(b.String()), 0o600)
@@ -228,6 +226,16 @@ func (f *deployAgentEngineFlags) gcloudDeployToAgentEngine() error {
228226
return fmt.Errorf("cannot read archive file: %w", err)
229227
}
230228

229+
methods, err := agentengine.ListClassMethods()
230+
if err != nil {
231+
return fmt.Errorf("cannot list class methods: %w", err)
232+
}
233+
methodsJSON, err := json.Marshal(methods)
234+
if err != nil {
235+
return fmt.Errorf("cannot marshal methods: %w", err)
236+
}
237+
p("Methods:", string(methodsJSON))
238+
231239
req := &aiplatformpb.CreateReasoningEngineRequest{
232240
Parent: parent,
233241
ReasoningEngine: &aiplatformpb.ReasoningEngine{
@@ -257,6 +265,7 @@ func (f *deployAgentEngineFlags) gcloudDeployToAgentEngine() error {
257265
{Name: "GOOGLE_API_KEY", SecretRef: &aiplatformpb.SecretRef{Secret: "GOOGLE_API_KEY", Version: "latest"}},
258266
},
259267
},
268+
ClassMethods: methods,
260269
},
261270
},
262271
}

cmd/adkgo/internal/deploy/cloudrun/cloudrun.go

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -219,17 +219,17 @@ CMD ["/app/` + f.build.execFile + `", "web", "-port", "` + strconv.Itoa(flags.cl
219219
}
220220
if flags.cloudRun.pubsub {
221221
b.WriteString(`, "pubsub"`)
222-
b.WriteString(fmt.Sprintf(`, "--trigger_max_retries", "%d"`, flags.cloudRun.pubsubTrigger.maxRetries))
223-
b.WriteString(fmt.Sprintf(`, "--trigger_base_delay", "%s"`, flags.cloudRun.pubsubTrigger.baseDelay.String()))
224-
b.WriteString(fmt.Sprintf(`, "--trigger_max_delay", "%s"`, flags.cloudRun.pubsubTrigger.maxDelay.String()))
225-
b.WriteString(fmt.Sprintf(`, "--trigger_max_concurrent_runs", "%d"`, flags.cloudRun.pubsubTrigger.maxRuns))
222+
fmt.Fprintf(&b, `, "--trigger_max_retries", "%d"`, flags.cloudRun.pubsubTrigger.maxRetries)
223+
fmt.Fprintf(&b, `, "--trigger_base_delay", "%s"`, flags.cloudRun.pubsubTrigger.baseDelay.String())
224+
fmt.Fprintf(&b, `, "--trigger_max_delay", "%s"`, flags.cloudRun.pubsubTrigger.maxDelay.String())
225+
fmt.Fprintf(&b, `, "--trigger_max_concurrent_runs", "%d"`, flags.cloudRun.pubsubTrigger.maxRuns)
226226
}
227227
if flags.cloudRun.eventarc {
228228
b.WriteString(`, "eventarc"`)
229-
b.WriteString(fmt.Sprintf(`, "--trigger_max_retries", "%d"`, flags.cloudRun.eventarcTrigger.maxRetries))
230-
b.WriteString(fmt.Sprintf(`, "--trigger_base_delay", "%s"`, flags.cloudRun.eventarcTrigger.baseDelay.String()))
231-
b.WriteString(fmt.Sprintf(`, "--trigger_max_delay", "%s"`, flags.cloudRun.eventarcTrigger.maxDelay.String()))
232-
b.WriteString(fmt.Sprintf(`, "--trigger_max_concurrent_runs", "%d"`, flags.cloudRun.eventarcTrigger.maxRuns))
229+
fmt.Fprintf(&b, `, "--trigger_max_retries", "%d"`, flags.cloudRun.eventarcTrigger.maxRetries)
230+
fmt.Fprintf(&b, `, "--trigger_base_delay", "%s"`, flags.cloudRun.eventarcTrigger.baseDelay.String())
231+
fmt.Fprintf(&b, `, "--trigger_max_delay", "%s"`, flags.cloudRun.eventarcTrigger.maxDelay.String())
232+
fmt.Fprintf(&b, `, "--trigger_max_concurrent_runs", "%d"`, flags.cloudRun.eventarcTrigger.maxRuns)
233233
}
234234
b.WriteString(`]`)
235235
return os.WriteFile(f.build.dockerfileBuildPath, []byte(b.String()), 0o600)
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package agentengine provides easy way to deploy to AgentEngine.
16+
package agentengine
17+
18+
import (
19+
"google.golang.org/adk/cmd/launcher"
20+
"google.golang.org/adk/cmd/launcher/universal"
21+
"google.golang.org/adk/cmd/launcher/web"
22+
webagentengine "google.golang.org/adk/cmd/launcher/web/agentengine"
23+
)
24+
25+
// NewLauncher returns a launcher capable of serving queries from AgentEngine.
26+
func NewLauncher(agentEngineId string) launcher.Launcher {
27+
return universal.NewLauncher(web.NewLauncher(webagentengine.NewLauncher(agentEngineId)))
28+
}
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package agentengine provides a sublauncher that provides web interface as required by Agent Engine
16+
package agentengine
17+
18+
import (
19+
"flag"
20+
"fmt"
21+
"net/http"
22+
"strings"
23+
"time"
24+
25+
"github.com/gorilla/mux"
26+
27+
"google.golang.org/adk/cmd/launcher"
28+
weblauncher "google.golang.org/adk/cmd/launcher/web"
29+
"google.golang.org/adk/internal/cli/util"
30+
"google.golang.org/adk/server/agentengine"
31+
)
32+
33+
// agentEngineConfig contains parameters for launching ADK Agent Engine server
34+
type agentEngineConfig struct {
35+
pathPrefix string
36+
agentEngineID string
37+
maxPayloadSize int64
38+
sseWriteTimeout time.Duration
39+
}
40+
41+
type agentEngineLauncher struct {
42+
flags *flag.FlagSet // flags are used to parse command-line arguments
43+
config *agentEngineConfig
44+
}
45+
46+
// NewLauncher creates new api launcher. It extends Web launcher
47+
func NewLauncher(agentEngineId string) weblauncher.Sublauncher {
48+
config := &agentEngineConfig{}
49+
50+
fs := flag.NewFlagSet("web", flag.ContinueOnError)
51+
fs.StringVar(&config.pathPrefix, "path_prefix", "/api", "ADK Agent Engine API path prefix. Default is '/api'.")
52+
fs.Int64Var(&config.maxPayloadSize, "max_payload_size", 10*1024*1024, "The payload will be truncated after this amount of bytes")
53+
fs.DurationVar(&config.sseWriteTimeout, "sse-write-timeout", 120*time.Second, "SSE server write timeout (i.e. '10s', '2m' - see time.ParseDuration for details) - for writing the SSE response after reading the headers & body")
54+
55+
config.agentEngineID = agentEngineId
56+
57+
return &agentEngineLauncher{
58+
config: config,
59+
flags: fs,
60+
}
61+
}
62+
63+
// CommandLineSyntax implements web.Sublauncher. Returns the command-line syntax for the agentEngine launcher.
64+
func (a *agentEngineLauncher) CommandLineSyntax() string {
65+
return util.FormatFlagUsage(a.flags)
66+
}
67+
68+
// SimpleDescription implements web.Sublauncher
69+
func (a *agentEngineLauncher) SimpleDescription() string {
70+
return "starts AgentEngine server which serves reasoning engine API while deployed to Agent Engine"
71+
}
72+
73+
// UserMessage implements web.Sublauncher.
74+
func (a *agentEngineLauncher) UserMessage(webUrl string, printer func(v ...any)) {
75+
// TODO(kdroste) description
76+
printer(fmt.Sprintf(" agentEngine: you can access this server locally by %s%s", webUrl, "/api/reasoning_engine"))
77+
printer(fmt.Sprintf(" %s%s", webUrl, "/api/stream_reasoning_engine"))
78+
printer(" to access it while deployed to Agent Engine, you should use")
79+
printer(" https://${LOCATION_ID}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${LOCATION_ID}/reasoningEngines/${RESOURCE_ID}:query")
80+
printer(" or https://${LOCATION_ID}-aiplatform.googleapis.com/v1/projects/${PROJECT_ID}/locations/${LOCATION_ID}/reasoningEngines/${RESOURCE_ID}:streamQuery")
81+
}
82+
83+
// SetupSubrouters adds the API router to the parent router.
84+
func (a *agentEngineLauncher) SetupSubrouters(router *mux.Router, config *launcher.Config) error {
85+
// Create the ADK AgentEngine API handler
86+
apiHandler, err := agentengine.NewHandler(config, a.config.sseWriteTimeout, a.config.maxPayloadSize, a.config.agentEngineID)
87+
if err != nil {
88+
return fmt.Errorf("agentengine.NewHandler failed: %v", err)
89+
}
90+
91+
router.Methods("POST").
92+
PathPrefix(a.config.pathPrefix).
93+
Handler(http.StripPrefix(a.config.pathPrefix, apiHandler))
94+
95+
return nil
96+
}
97+
98+
// Keyword implements web.Sublauncher. Returns the command-line keyword for A2A launcher.
99+
func (a *agentEngineLauncher) Keyword() string {
100+
return "agentengine"
101+
}
102+
103+
var _ weblauncher.Sublauncher = &agentEngineLauncher{}
104+
105+
// Parse parses the command-line arguments for the API launcher.
106+
func (a *agentEngineLauncher) Parse(args []string) ([]string, error) {
107+
err := a.flags.Parse(args)
108+
if err != nil || !a.flags.Parsed() {
109+
return nil, fmt.Errorf("failed to parse agent engine flags: %v", err)
110+
}
111+
p := a.config.pathPrefix
112+
if !strings.HasPrefix(p, "/") {
113+
p = "/" + p
114+
}
115+
a.config.pathPrefix = strings.TrimSuffix(p, "/")
116+
117+
restArgs := a.flags.Args()
118+
return restArgs, nil
119+
}

examples/agentengine/main.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
// Copyright 2025 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
// Package provides a quickstart for Agent Engine deployment
16+
package main
17+
18+
import (
19+
"context"
20+
"log"
21+
"math/rand/v2"
22+
"os"
23+
24+
"google.golang.org/genai"
25+
26+
"google.golang.org/adk/agent"
27+
"google.golang.org/adk/agent/llmagent"
28+
"google.golang.org/adk/cmd/launcher"
29+
"google.golang.org/adk/cmd/launcher/agentengine"
30+
"google.golang.org/adk/model/gemini"
31+
"google.golang.org/adk/session/vertexai"
32+
"google.golang.org/adk/tool"
33+
"google.golang.org/adk/tool/functiontool"
34+
)
35+
36+
func main() {
37+
ctx := context.Background()
38+
39+
// those values are provided by AgentEngine, visible after the deployment to the container
40+
// for tesing, simply set those to your GCP project
41+
projectID := os.Getenv("GOOGLE_CLOUD_PROJECT")
42+
location := os.Getenv("GOOGLE_CLOUD_AGENT_ENGINE_LOCATION")
43+
agentEngineID := os.Getenv("GOOGLE_CLOUD_AGENT_ENGINE_ID")
44+
45+
model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{
46+
Backend: genai.BackendVertexAI,
47+
Project: projectID,
48+
Location: location,
49+
})
50+
if err != nil {
51+
log.Fatalf("Failed to create model: %v", err)
52+
}
53+
54+
type Input struct {
55+
Min int `json:"min"`
56+
Max int `json:"max"`
57+
}
58+
type Output struct {
59+
Result int `json:"result"`
60+
}
61+
handler := func(ctx tool.Context, input Input) (Output, error) {
62+
return Output{
63+
Result: input.Min + rand.IntN(input.Max-input.Min+1),
64+
}, nil
65+
}
66+
randomTool, err := functiontool.New(functiontool.Config{
67+
Name: "random",
68+
Description: "Returns a random number between min and max",
69+
}, handler)
70+
if err != nil {
71+
log.Fatalf("Failed to create tool: %v", err)
72+
}
73+
74+
a, err := llmagent.New(llmagent.Config{
75+
Name: "ae_agent",
76+
Model: model,
77+
Description: "General helpful agent",
78+
Instruction: "You are a helpful agent, you should answer any questions you are given. Use 'random' tool to provide random numbers.",
79+
Tools: []tool.Tool{
80+
randomTool,
81+
},
82+
})
83+
if err != nil {
84+
log.Fatalf("Failed to create agent: %v", err)
85+
}
86+
87+
sessionService, err := vertexai.NewSessionService(
88+
ctx, vertexai.VertexAIServiceConfig{
89+
ProjectID: projectID,
90+
Location: location,
91+
ReasoningEngine: agentEngineID,
92+
})
93+
if err != nil {
94+
log.Fatalf("Failed to create session service: %v", err)
95+
}
96+
97+
config := &launcher.Config{
98+
SessionService: sessionService,
99+
AgentLoader: agent.NewSingleLoader(a),
100+
}
101+
102+
l := agentengine.NewLauncher(agentEngineID)
103+
if err = l.Execute(ctx, config, os.Args[1:]); err != nil {
104+
log.Fatalf("Run failed: %v\n\n%s", err, l.CommandLineSyntax())
105+
}
106+
}

0 commit comments

Comments
 (0)