From 1588fbfd734f06a4e3ce6f5e58cda2155011582d Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 1 Oct 2025 10:12:32 -0400 Subject: [PATCH 001/125] Added google_search snippet --- docs/tools/built-in-tools.md | 6 ++ .../tools/built-in-tools/google_search.go | 86 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 examples/go/snippets/tools/built-in-tools/google_search.go diff --git a/docs/tools/built-in-tools.md b/docs/tools/built-in-tools.md index 8c6ed257..26958220 100644 --- a/docs/tools/built-in-tools.md +++ b/docs/tools/built-in-tools.md @@ -43,6 +43,12 @@ The `google_search` tool allows the agent to perform web searches using Google S --8<-- "examples/java/snippets/src/main/java/tools/GoogleSearchAgentApp.java:full_code" ``` +=== "Golang" + + ```go + --8<-- "examples/go/snippets/tools/built-in-tools/google_search.go" + ``` + ### Code Execution
diff --git a/examples/go/snippets/tools/built-in-tools/google_search.go b/examples/go/snippets/tools/built-in-tools/google_search.go new file mode 100644 index 00000000..2e2cc097 --- /dev/null +++ b/examples/go/snippets/tools/built-in-tools/google_search.go @@ -0,0 +1,86 @@ +package main + +import ( + "context" + "fmt" + "log" + + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/sessionservice" + "google.golang.org/adk/tool" + "google.golang.org/adk/tool/geminitool" + "google.golang.org/genai" +) + +func createSearchAgent(ctx context.Context) (agent.Agent, error) { + model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{}) + if err != nil { + return nil, fmt.Errorf("failed to create model: %v", err) + } + + return llmagent.New(llmagent.Config{ + Name: "basic_search_agent", + Model: model, + Description: "Agent to answer questions using Google Search.", + Instruction: "I can answer your questions by searching the web. Just ask me anything!", + Tools: []tool.Tool{geminitool.GoogleSearch{}}, + }) +} + +const ( + userID = "user1234" + appName = "Google Search_agent" +) + +func callAgent(ctx context.Context, a agent.Agent, prompt string) { + sessionService := sessionservice.Mem() + session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ + AppName: appName, + UserID: userID, + }) + if err != nil { + log.Fatalf("failed to create the session service: %v", err) + } + + config := &runner.Config{ + AppName: appName, + Agent: a, + SessionService: sessionService, + } + r, err := runner.New(config) + if err != nil { + log.Fatalf("failed to create the runner: %v", err) + } + + sessionID := session.Session.ID().SessionID + userMsg := &genai.Content{ + Parts: []*genai.Part{{Text: prompt}}, + Role: string(genai.RoleUser), + } + + for event, err := range r.Run(ctx, userID, sessionID, userMsg, &runner.RunConfig{ + StreamingMode: runner.StreamingModeSSE, + }) { + if err != nil { + fmt.Printf("\nAGENT_ERROR: %v\n", err) + } else { + for _, p := range event.LLMResponse.Content.Parts { + fmt.Print(p.Text) + } + } + } +} + +func main() { + agent, err := createSearchAgent(context.Background()) + if err != nil { + panic(err) + } + fmt.Println("Agent created:", agent.Name()) + fmt.Printf("\nPrompt: %s\nResponse: ", "what's the latest ai news?") + callAgent(context.Background(), agent, "what's the latest ai news?") + fmt.Println("\n---") +} From b6b43d366af330ea438aa6e4fb75ca7fe9dd35a1 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 1 Oct 2025 11:00:26 -0400 Subject: [PATCH 002/125] Fix: Address Go style guide issues in google_search.go --- .../tools/built-in-tools/google_search.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/examples/go/snippets/tools/built-in-tools/google_search.go b/examples/go/snippets/tools/built-in-tools/google_search.go index 2e2cc097..6f258530 100644 --- a/examples/go/snippets/tools/built-in-tools/google_search.go +++ b/examples/go/snippets/tools/built-in-tools/google_search.go @@ -35,14 +35,14 @@ const ( appName = "Google Search_agent" ) -func callAgent(ctx context.Context, a agent.Agent, prompt string) { +func callAgent(ctx context.Context, a agent.Agent, prompt string) error { sessionService := sessionservice.Mem() session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ AppName: appName, UserID: userID, }) if err != nil { - log.Fatalf("failed to create the session service: %v", err) + return fmt.Errorf("failed to create the session service: %v", err) } config := &runner.Config{ @@ -52,7 +52,7 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) { } r, err := runner.New(config) if err != nil { - log.Fatalf("failed to create the runner: %v", err) + return fmt.Errorf("failed to create the runner: %v", err) } sessionID := session.Session.ID().SessionID @@ -61,6 +61,8 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) { Role: string(genai.RoleUser), } + // The r.Run method streams events and errors. + // The loop iterates over the results, handling them as they arrive. for event, err := range r.Run(ctx, userID, sessionID, userMsg, &runner.RunConfig{ StreamingMode: runner.StreamingModeSSE, }) { @@ -72,15 +74,19 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) { } } } + return nil } func main() { agent, err := createSearchAgent(context.Background()) if err != nil { - panic(err) + log.Fatalf("Failed to create agent: %v", err) } fmt.Println("Agent created:", agent.Name()) - fmt.Printf("\nPrompt: %s\nResponse: ", "what's the latest ai news?") - callAgent(context.Background(), agent, "what's the latest ai news?") + prompt := "what's the latest ai news?" + fmt.Printf("\nPrompt: %s\nResponse: ", prompt) + if err := callAgent(context.Background(), agent, prompt); err != nil { + log.Fatalf("Error calling agent: %v", err) + } fmt.Println("\n---") } From fe6232134d535dd5ac60bbe853e0e7adc9f7dc1e Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Thu, 9 Oct 2025 20:33:39 -0400 Subject: [PATCH 003/125] Migrated to new API --- .../tools/built-in-tools/google_search.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/go/snippets/tools/built-in-tools/google_search.go b/examples/go/snippets/tools/built-in-tools/google_search.go index 6f258530..99ed259e 100644 --- a/examples/go/snippets/tools/built-in-tools/google_search.go +++ b/examples/go/snippets/tools/built-in-tools/google_search.go @@ -7,9 +7,9 @@ import ( "google.golang.org/adk/agent" "google.golang.org/adk/agent/llmagent" - "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/model/gemini" "google.golang.org/adk/runner" - "google.golang.org/adk/sessionservice" + "google.golang.org/adk/session" "google.golang.org/adk/tool" "google.golang.org/adk/tool/geminitool" "google.golang.org/genai" @@ -36,8 +36,8 @@ const ( ) func callAgent(ctx context.Context, a agent.Agent, prompt string) error { - sessionService := sessionservice.Mem() - session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ + sessionService := session.InMemoryService() + session, err := sessionService.Create(ctx, &session.CreateRequest{ AppName: appName, UserID: userID, }) @@ -45,7 +45,7 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) error { return fmt.Errorf("failed to create the session service: %v", err) } - config := &runner.Config{ + config := runner.Config{ AppName: appName, Agent: a, SessionService: sessionService, @@ -55,7 +55,7 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) error { return fmt.Errorf("failed to create the runner: %v", err) } - sessionID := session.Session.ID().SessionID + sessionID := session.Session.ID() userMsg := &genai.Content{ Parts: []*genai.Part{{Text: prompt}}, Role: string(genai.RoleUser), @@ -63,8 +63,8 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) error { // The r.Run method streams events and errors. // The loop iterates over the results, handling them as they arrive. - for event, err := range r.Run(ctx, userID, sessionID, userMsg, &runner.RunConfig{ - StreamingMode: runner.StreamingModeSSE, + for event, err := range r.Run(ctx, userID, sessionID, userMsg, &agent.RunConfig{ + StreamingMode: agent.StreamingModeSSE, }) { if err != nil { fmt.Printf("\nAGENT_ERROR: %v\n", err) From de0e165697fdd4ffac9b5cecf85beea0d3e992e9 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Thu, 9 Oct 2025 20:39:10 -0400 Subject: [PATCH 004/125] Fixed tool info in md file --- docs/tools/built-in-tools.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/tools/built-in-tools.md b/docs/tools/built-in-tools.md index 26958220..1b9ac18a 100644 --- a/docs/tools/built-in-tools.md +++ b/docs/tools/built-in-tools.md @@ -7,7 +7,7 @@ agent that needs to retrieve information from the web can directly use the ## How to Use -1. **Import:** Import the desired tool from the tools module. This is `agents.tools` in Python or `com.google.adk.tools` in Java. +1. **Import:** Import the desired tool from the tools module. This is `agents.tools` in Python, `com.google.adk.tools` in Java, or `google.golang.org/adk/tool/geminitool` in Go. 2. **Configure:** Initialize the tool, providing required parameters if any. 3. **Register:** Add the initialized tool to the **tools** list of your Agent. @@ -18,6 +18,7 @@ tool when the agent calls it. Important: check the ***Limitations*** section of ## Available Built-in tools Note: Java only supports Google Search and Code Execution tools currently. +Note: Go only supports the Google Search tool currently. ### Google Search From e1bc449e1e76da36e95e8ea4cad24bf7f4ebe1dd Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 10 Oct 2025 09:37:53 -0400 Subject: [PATCH 005/125] Added partial check --- examples/go/snippets/tools/built-in-tools/google_search.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/go/snippets/tools/built-in-tools/google_search.go b/examples/go/snippets/tools/built-in-tools/google_search.go index 99ed259e..bad89c34 100644 --- a/examples/go/snippets/tools/built-in-tools/google_search.go +++ b/examples/go/snippets/tools/built-in-tools/google_search.go @@ -68,7 +68,7 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) error { }) { if err != nil { fmt.Printf("\nAGENT_ERROR: %v\n", err) - } else { + } else if event.Partial { for _, p := range event.LLMResponse.Content.Parts { fmt.Print(p.Text) } From 13cb8358e4c7910c170f98f7486f7f09f3a16e2e Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Tue, 30 Sep 2025 13:18:57 -0400 Subject: [PATCH 006/125] Added function-tools sample --- docs/tools/function-tools.md | 14 ++ .../tools/function-tools/func_tool.go | 167 ++++++++++++++++++ 2 files changed, 181 insertions(+) create mode 100644 examples/go/snippets/tools/function-tools/func_tool.go diff --git a/docs/tools/function-tools.md b/docs/tools/function-tools.md index 5243a20b..0404632c 100644 --- a/docs/tools/function-tools.md +++ b/docs/tools/function-tools.md @@ -148,6 +148,20 @@ A tool can write data to a `temp:` variable, and a subsequent tool can read it. For input `GOOG`: {"symbol": "GOOG", "price": "1.0"} ``` + === "Go" + + This tool retrieves the mocked value of a stock price. + + ```go + --8<-- "examples/go/snippets/tools/function-tools/func_tool.go" + ``` + + The return value from this tool will be a `map[string]any` marshalled into a JSON object. + + ```json + For input `{"symbol": "GOOG"}`: {"price":1,"symbol":"GOOG"} + ``` + ### Best Practices While you have considerable flexibility in defining your function, remember that simplicity enhances usability for the LLM. Consider these guidelines: diff --git a/examples/go/snippets/tools/function-tools/func_tool.go b/examples/go/snippets/tools/function-tools/func_tool.go new file mode 100644 index 00000000..cf642b08 --- /dev/null +++ b/examples/go/snippets/tools/function-tools/func_tool.go @@ -0,0 +1,167 @@ +package main + +import ( + "context" + "fmt" + "log" + "strings" + + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/sessionservice" + "google.golang.org/adk/tool" + + "google.golang.org/genai" +) + +// mockStockPrices provides a simple in-memory database of stock prices +// to simulate a real-world stock data API. This allows the example to +// demonstrate tool functionality without making external network calls. +var mockStockPrices = map[string]float64{ + "GOOG": 600.6, + "AAPL": 123.4, + "MSFT": 234.5, +} + +// getStockPriceArgs defines the schema for the arguments passed to the getStockPrice tool. +// Using a struct is the recommended approach in the Go ADK as it provides strong +// typing and clear validation for the expected inputs. +type getStockPriceArgs struct { + Symbol string +} + +// getStockPrice is a tool that retrieves the stock price for a given ticker symbol +// from the mockStockPrices map. It demonstrates how a function can be used as a +// tool by an agent. If the symbol is found, it returns a map containing the +// symbol and its price. Otherwise, it returns an error message. +func getStockPrice(ctx context.Context, input getStockPriceArgs) map[string]any { + symbolUpper := strings.ToUpper(input.Symbol) + if price, ok := mockStockPrices[symbolUpper]; ok { + fmt.Printf("Tool: Found price for %s: %f\n", input.Symbol, price) + return map[string]any{"symbol": input.Symbol, "price": price} + } + return map[string]any{"symbol": input.Symbol, "error": "No data found for symbol"} +} + +// createStockAgent initializes and configures an LlmAgent. +// This agent is equipped with the getStockPrice tool and is instructed +// on how to respond to user queries about stock prices. It uses the +// Gemini model to understand user intent and decide when to use its tools. +func createStockAgent(ctx context.Context) (agent.Agent, error) { + stockPriceTool, err := tool.NewFunctionTool( + tool.FunctionToolConfig{ + Name: "get_stock_price", + Description: "Retrieves the current stock price for a given symbol.", + }, + getStockPrice) + if err != nil { + return nil, err + } + + model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{}) + + if err != nil { + log.Fatalf("Failed to create model: %v", err) + } + + + return llmagent.New(llmagent.Config{ + Name: "stock_agent", + Model: model, + Instruction: "You are an agent who retrieves stock prices. If a ticker symbol is provided, fetch the current price. If only a company name is given, first perform a Google search to find the correct ticker symbol before retrieving the stock price. If the provided ticker symbol is invalid or data cannot be retrieved, inform the user that the stock price could not be found.", + Description: "This agent specializes in retrieving real-time stock prices. Given a stock ticker symbol (e.g., AAPL, GOOG, MSFT) or the stock name, use the tools and reliable data sources to provide the most up-to-date price.", + Tools: []tool.Tool{ + stockPriceTool, + }, + }) +} + +// userID and appName are constants used to identify the user and application +// throughout the session. These values are important for logging, tracking, +// and managing state across different agent interactions. +const ( + userID = "example_user_id" + appName = "example_app" +) + +// callAgent orchestrates the execution of the agent for a given prompt. +// It sets up the necessary services, creates a session, and uses a runner +// to manage the agent's lifecycle. It streams the agent's responses and +// prints them to the console, handling any potential errors during the run. +func callAgent(ctx context.Context, agent agent.Agent, prompt string) { + + sessionService := sessionservice.Mem() + + // Create a new session for the agent interactions. + session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ + AppName: appName, + UserID: userID, + }) + if err != nil { + log.Fatalf("Failed to create the session service: %v", err) + } + + config := &runner.Config{ + AppName: appName, + Agent: agent, + SessionService: sessionService, + } + + // Create the runner to manage the agent execution. + r, err := runner.New(config) + + if err != nil { + log.Fatalf("Failed to create the runner: %v", err) + } + + sessionID := session.Session.ID().SessionID + + userMsg := &genai.Content{ + Parts: []*genai.Part{ + genai.NewPartFromText(prompt), + }, + Role: string(genai.RoleUser), + } + + for event, err := range r.Run(ctx, userID, sessionID, userMsg, &runner.RunConfig{ + StreamingMode: runner.StreamingModeSSE, + }) { + if err != nil { + fmt.Printf("\nAGENT_ERROR: %v\n", err) + } else { + for _, p := range event.LLMResponse.Content.Parts { + fmt.Print(p.Text) + } + } + } +} + +// RunAgentSimulation serves as the entry point for this example. +// It creates the stock agent and then simulates a series of user interactions +// by sending different prompts to the agent. This function showcases how the +// agent responds to various queries, including both successful and unsuccessful +// attempts to retrieve stock prices. +func RunAgentSimulation() { + // Create the stock agent + agent, err := createStockAgent(context.Background()) + if err != nil { + panic(err) + } + + fmt.Println("Agent created:", agent.Name()) + + prompts := []string{ + "stock price of GOOG", + "What's the price of MSFT?", + "Can you find the stock price for an unknown company XYZ?", + } + + // Simulate running the agent with different prompts + for _, prompt := range prompts { + fmt.Printf("\nPrompt: %s\nResponse: ", prompt) + callAgent(context.Background(), agent, prompt) + fmt.Println("\n---") + } +} From 38d5e3303dc42032d1364dba535f067c0074cf3c Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Thu, 9 Oct 2025 21:01:04 -0400 Subject: [PATCH 007/125] Migrated to new API --- .../tools/function-tools/func_tool.go | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/examples/go/snippets/tools/function-tools/func_tool.go b/examples/go/snippets/tools/function-tools/func_tool.go index cf642b08..7fedec07 100644 --- a/examples/go/snippets/tools/function-tools/func_tool.go +++ b/examples/go/snippets/tools/function-tools/func_tool.go @@ -8,9 +8,9 @@ import ( "google.golang.org/adk/agent" "google.golang.org/adk/agent/llmagent" - "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/model/gemini" "google.golang.org/adk/runner" - "google.golang.org/adk/sessionservice" + "google.golang.org/adk/session" "google.golang.org/adk/tool" "google.golang.org/genai" @@ -32,17 +32,24 @@ type getStockPriceArgs struct { Symbol string } +// getStockPriceResults defines the output schema for the getStockPrice tool. +type getStockPriceResults struct { + Symbol string `json:"symbol"` + Price float64 `json:"price,omitempty"` + Error string `json:"error,omitempty"` +} + // getStockPrice is a tool that retrieves the stock price for a given ticker symbol // from the mockStockPrices map. It demonstrates how a function can be used as a -// tool by an agent. If the symbol is found, it returns a map containing the -// symbol and its price. Otherwise, it returns an error message. -func getStockPrice(ctx context.Context, input getStockPriceArgs) map[string]any { +// tool by an agent. If the symbol is found, it returns a struct containing the +// symbol and its price. Otherwise, it returns a struct with an error message. +func getStockPrice(ctx tool.Context, input getStockPriceArgs) getStockPriceResults { symbolUpper := strings.ToUpper(input.Symbol) if price, ok := mockStockPrices[symbolUpper]; ok { fmt.Printf("Tool: Found price for %s: %f\n", input.Symbol, price) - return map[string]any{"symbol": input.Symbol, "price": price} + return getStockPriceResults{Symbol: input.Symbol, Price: price} } - return map[string]any{"symbol": input.Symbol, "error": "No data found for symbol"} + return getStockPriceResults{Symbol: input.Symbol, Error: "No data found for symbol"} } // createStockAgent initializes and configures an LlmAgent. @@ -90,22 +97,19 @@ const ( // It sets up the necessary services, creates a session, and uses a runner // to manage the agent's lifecycle. It streams the agent's responses and // prints them to the console, handling any potential errors during the run. -func callAgent(ctx context.Context, agent agent.Agent, prompt string) { - - sessionService := sessionservice.Mem() - +func callAgent(ctx context.Context, a agent.Agent, prompt string) { + sessionService := session.InMemoryService() // Create a new session for the agent interactions. - session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ + session, err := sessionService.Create(ctx, &session.CreateRequest{ AppName: appName, UserID: userID, }) if err != nil { log.Fatalf("Failed to create the session service: %v", err) } - - config := &runner.Config{ + config := runner.Config{ AppName: appName, - Agent: agent, + Agent: a, SessionService: sessionService, } @@ -116,7 +120,7 @@ func callAgent(ctx context.Context, agent agent.Agent, prompt string) { log.Fatalf("Failed to create the runner: %v", err) } - sessionID := session.Session.ID().SessionID + sessionID := session.Session.ID() userMsg := &genai.Content{ Parts: []*genai.Part{ @@ -125,8 +129,8 @@ func callAgent(ctx context.Context, agent agent.Agent, prompt string) { Role: string(genai.RoleUser), } - for event, err := range r.Run(ctx, userID, sessionID, userMsg, &runner.RunConfig{ - StreamingMode: runner.StreamingModeSSE, + for event, err := range r.Run(ctx, userID, sessionID, userMsg, &agent.RunConfig{ + StreamingMode: agent.StreamingModeSSE, }) { if err != nil { fmt.Printf("\nAGENT_ERROR: %v\n", err) @@ -165,3 +169,8 @@ func RunAgentSimulation() { fmt.Println("\n---") } } + +func main() { + fmt.Println("Attempting to run the agent simulation...") + RunAgentSimulation() +} From 8ce06d1c2712f8ec32bc8c96f5ac5e5b1c260615 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 10 Oct 2025 09:35:25 -0400 Subject: [PATCH 008/125] Added partial check --- examples/go/snippets/tools/function-tools/func_tool.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/go/snippets/tools/function-tools/func_tool.go b/examples/go/snippets/tools/function-tools/func_tool.go index 7fedec07..faa5148a 100644 --- a/examples/go/snippets/tools/function-tools/func_tool.go +++ b/examples/go/snippets/tools/function-tools/func_tool.go @@ -134,7 +134,7 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) { }) { if err != nil { fmt.Printf("\nAGENT_ERROR: %v\n", err) - } else { + } else if event.Partial { for _, p := range event.LLMResponse.Content.Parts { fmt.Print(p.Text) } From 7647d91a789ab201acadaf6b8d2e6c0efec42431 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 10 Oct 2025 09:41:15 -0400 Subject: [PATCH 009/125] Ran linter --- examples/go/snippets/tools/function-tools/func_tool.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/go/snippets/tools/function-tools/func_tool.go b/examples/go/snippets/tools/function-tools/func_tool.go index faa5148a..d6f5ed1b 100644 --- a/examples/go/snippets/tools/function-tools/func_tool.go +++ b/examples/go/snippets/tools/function-tools/func_tool.go @@ -135,7 +135,7 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) { if err != nil { fmt.Printf("\nAGENT_ERROR: %v\n", err) } else if event.Partial { - for _, p := range event.LLMResponse.Content.Parts { + for _, p := range event.Content.Parts { fmt.Print(p.Text) } } From dfa0138440063f57d37df50a1f98d55268ef7984 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 22 Oct 2025 20:20:33 -0400 Subject: [PATCH 010/125] feat: add go fmt check --- .github/workflows/go-fmt.yaml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/go-fmt.yaml diff --git a/.github/workflows/go-fmt.yaml b/.github/workflows/go-fmt.yaml new file mode 100644 index 00000000..dda54d0f --- /dev/null +++ b/.github/workflows/go-fmt.yaml @@ -0,0 +1,18 @@ +name: Go Fmt + +on: + pull_request: + paths: + - '**.go' + +jobs: + gofmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: '1.21' + - name: Run gofmt + run: | + test -z "$(gofmt -l .)" From 630a78256a4e8dc991aaeda3191458be1be34c81 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 22 Oct 2025 20:54:24 -0400 Subject: [PATCH 011/125] Run go fmt only on changed files --- .github/workflows/go-fmt.yaml | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go-fmt.yaml b/.github/workflows/go-fmt.yaml index dda54d0f..59f0d72d 100644 --- a/.github/workflows/go-fmt.yaml +++ b/.github/workflows/go-fmt.yaml @@ -10,9 +10,32 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for all tags and branches - uses: actions/setup-go@v5 with: go-version: '1.21' - - name: Run gofmt + - name: Run gofmt on changed files run: | - test -z "$(gofmt -l .)" + # List changed Go files in the pull request. + # The `|| true` is to prevent the command from failing if no go files are found + changed_files=$(git diff --name-only --diff-filter=ACMRT ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} | grep '\.go$' || true) + + if [ -z "$changed_files" ]; then + echo "No Go files changed in this PR." + exit 0 + fi + + echo "Checking formatting for the following files:" + echo "$changed_files" + + # Check formatting of changed files + unformatted_files=$(gofmt -l $changed_files) + + if [ -n "$unformatted_files" ]; then + echo "Unformatted Go files found:" + echo "$unformatted_files" + exit 1 + else + echo "All changed Go files are formatted correctly." + fi \ No newline at end of file From c0c058fecfc811b241dd132d356763f916cf1b9a Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 1 Oct 2025 10:12:32 -0400 Subject: [PATCH 012/125] Added google_search snippet --- .../tools/built-in-tools/google_search.go | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/examples/go/snippets/tools/built-in-tools/google_search.go b/examples/go/snippets/tools/built-in-tools/google_search.go index bad89c34..2e2cc097 100644 --- a/examples/go/snippets/tools/built-in-tools/google_search.go +++ b/examples/go/snippets/tools/built-in-tools/google_search.go @@ -7,9 +7,9 @@ import ( "google.golang.org/adk/agent" "google.golang.org/adk/agent/llmagent" - "google.golang.org/adk/model/gemini" + "google.golang.org/adk/llm/gemini" "google.golang.org/adk/runner" - "google.golang.org/adk/session" + "google.golang.org/adk/sessionservice" "google.golang.org/adk/tool" "google.golang.org/adk/tool/geminitool" "google.golang.org/genai" @@ -35,58 +35,52 @@ const ( appName = "Google Search_agent" ) -func callAgent(ctx context.Context, a agent.Agent, prompt string) error { - sessionService := session.InMemoryService() - session, err := sessionService.Create(ctx, &session.CreateRequest{ +func callAgent(ctx context.Context, a agent.Agent, prompt string) { + sessionService := sessionservice.Mem() + session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ AppName: appName, UserID: userID, }) if err != nil { - return fmt.Errorf("failed to create the session service: %v", err) + log.Fatalf("failed to create the session service: %v", err) } - config := runner.Config{ + config := &runner.Config{ AppName: appName, Agent: a, SessionService: sessionService, } r, err := runner.New(config) if err != nil { - return fmt.Errorf("failed to create the runner: %v", err) + log.Fatalf("failed to create the runner: %v", err) } - sessionID := session.Session.ID() + sessionID := session.Session.ID().SessionID userMsg := &genai.Content{ Parts: []*genai.Part{{Text: prompt}}, Role: string(genai.RoleUser), } - // The r.Run method streams events and errors. - // The loop iterates over the results, handling them as they arrive. - for event, err := range r.Run(ctx, userID, sessionID, userMsg, &agent.RunConfig{ - StreamingMode: agent.StreamingModeSSE, + for event, err := range r.Run(ctx, userID, sessionID, userMsg, &runner.RunConfig{ + StreamingMode: runner.StreamingModeSSE, }) { if err != nil { fmt.Printf("\nAGENT_ERROR: %v\n", err) - } else if event.Partial { + } else { for _, p := range event.LLMResponse.Content.Parts { fmt.Print(p.Text) } } } - return nil } func main() { agent, err := createSearchAgent(context.Background()) if err != nil { - log.Fatalf("Failed to create agent: %v", err) + panic(err) } fmt.Println("Agent created:", agent.Name()) - prompt := "what's the latest ai news?" - fmt.Printf("\nPrompt: %s\nResponse: ", prompt) - if err := callAgent(context.Background(), agent, prompt); err != nil { - log.Fatalf("Error calling agent: %v", err) - } + fmt.Printf("\nPrompt: %s\nResponse: ", "what's the latest ai news?") + callAgent(context.Background(), agent, "what's the latest ai news?") fmt.Println("\n---") } From 1864dcdff71c48225b9c1e0e8daa01a50594b627 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 1 Oct 2025 11:00:26 -0400 Subject: [PATCH 013/125] Fix: Address Go style guide issues in google_search.go --- .../tools/built-in-tools/google_search.go | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/examples/go/snippets/tools/built-in-tools/google_search.go b/examples/go/snippets/tools/built-in-tools/google_search.go index 2e2cc097..6f258530 100644 --- a/examples/go/snippets/tools/built-in-tools/google_search.go +++ b/examples/go/snippets/tools/built-in-tools/google_search.go @@ -35,14 +35,14 @@ const ( appName = "Google Search_agent" ) -func callAgent(ctx context.Context, a agent.Agent, prompt string) { +func callAgent(ctx context.Context, a agent.Agent, prompt string) error { sessionService := sessionservice.Mem() session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ AppName: appName, UserID: userID, }) if err != nil { - log.Fatalf("failed to create the session service: %v", err) + return fmt.Errorf("failed to create the session service: %v", err) } config := &runner.Config{ @@ -52,7 +52,7 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) { } r, err := runner.New(config) if err != nil { - log.Fatalf("failed to create the runner: %v", err) + return fmt.Errorf("failed to create the runner: %v", err) } sessionID := session.Session.ID().SessionID @@ -61,6 +61,8 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) { Role: string(genai.RoleUser), } + // The r.Run method streams events and errors. + // The loop iterates over the results, handling them as they arrive. for event, err := range r.Run(ctx, userID, sessionID, userMsg, &runner.RunConfig{ StreamingMode: runner.StreamingModeSSE, }) { @@ -72,15 +74,19 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) { } } } + return nil } func main() { agent, err := createSearchAgent(context.Background()) if err != nil { - panic(err) + log.Fatalf("Failed to create agent: %v", err) } fmt.Println("Agent created:", agent.Name()) - fmt.Printf("\nPrompt: %s\nResponse: ", "what's the latest ai news?") - callAgent(context.Background(), agent, "what's the latest ai news?") + prompt := "what's the latest ai news?" + fmt.Printf("\nPrompt: %s\nResponse: ", prompt) + if err := callAgent(context.Background(), agent, prompt); err != nil { + log.Fatalf("Error calling agent: %v", err) + } fmt.Println("\n---") } From 3297579407e8e28faab3becf7b8754f6b372b02a Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Thu, 9 Oct 2025 20:33:39 -0400 Subject: [PATCH 014/125] Migrated to new API --- .../tools/built-in-tools/google_search.go | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/examples/go/snippets/tools/built-in-tools/google_search.go b/examples/go/snippets/tools/built-in-tools/google_search.go index 6f258530..99ed259e 100644 --- a/examples/go/snippets/tools/built-in-tools/google_search.go +++ b/examples/go/snippets/tools/built-in-tools/google_search.go @@ -7,9 +7,9 @@ import ( "google.golang.org/adk/agent" "google.golang.org/adk/agent/llmagent" - "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/model/gemini" "google.golang.org/adk/runner" - "google.golang.org/adk/sessionservice" + "google.golang.org/adk/session" "google.golang.org/adk/tool" "google.golang.org/adk/tool/geminitool" "google.golang.org/genai" @@ -36,8 +36,8 @@ const ( ) func callAgent(ctx context.Context, a agent.Agent, prompt string) error { - sessionService := sessionservice.Mem() - session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ + sessionService := session.InMemoryService() + session, err := sessionService.Create(ctx, &session.CreateRequest{ AppName: appName, UserID: userID, }) @@ -45,7 +45,7 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) error { return fmt.Errorf("failed to create the session service: %v", err) } - config := &runner.Config{ + config := runner.Config{ AppName: appName, Agent: a, SessionService: sessionService, @@ -55,7 +55,7 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) error { return fmt.Errorf("failed to create the runner: %v", err) } - sessionID := session.Session.ID().SessionID + sessionID := session.Session.ID() userMsg := &genai.Content{ Parts: []*genai.Part{{Text: prompt}}, Role: string(genai.RoleUser), @@ -63,8 +63,8 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) error { // The r.Run method streams events and errors. // The loop iterates over the results, handling them as they arrive. - for event, err := range r.Run(ctx, userID, sessionID, userMsg, &runner.RunConfig{ - StreamingMode: runner.StreamingModeSSE, + for event, err := range r.Run(ctx, userID, sessionID, userMsg, &agent.RunConfig{ + StreamingMode: agent.StreamingModeSSE, }) { if err != nil { fmt.Printf("\nAGENT_ERROR: %v\n", err) From 12be4c6372c47020274a933cb59a5448c642e0e0 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 10 Oct 2025 09:37:53 -0400 Subject: [PATCH 015/125] Added partial check --- examples/go/snippets/tools/built-in-tools/google_search.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/go/snippets/tools/built-in-tools/google_search.go b/examples/go/snippets/tools/built-in-tools/google_search.go index 99ed259e..bad89c34 100644 --- a/examples/go/snippets/tools/built-in-tools/google_search.go +++ b/examples/go/snippets/tools/built-in-tools/google_search.go @@ -68,7 +68,7 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) error { }) { if err != nil { fmt.Printf("\nAGENT_ERROR: %v\n", err) - } else { + } else if event.Partial { for _, p := range event.LLMResponse.Content.Parts { fmt.Print(p.Text) } From f5cddabf219437048c3a8b859a06e7027f95fa34 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Tue, 30 Sep 2025 13:18:57 -0400 Subject: [PATCH 016/125] Added function-tools sample --- .../tools/function-tools/func_tool.go | 49 ++++++++----------- 1 file changed, 20 insertions(+), 29 deletions(-) diff --git a/examples/go/snippets/tools/function-tools/func_tool.go b/examples/go/snippets/tools/function-tools/func_tool.go index d6f5ed1b..cf642b08 100644 --- a/examples/go/snippets/tools/function-tools/func_tool.go +++ b/examples/go/snippets/tools/function-tools/func_tool.go @@ -8,9 +8,9 @@ import ( "google.golang.org/adk/agent" "google.golang.org/adk/agent/llmagent" - "google.golang.org/adk/model/gemini" + "google.golang.org/adk/llm/gemini" "google.golang.org/adk/runner" - "google.golang.org/adk/session" + "google.golang.org/adk/sessionservice" "google.golang.org/adk/tool" "google.golang.org/genai" @@ -32,24 +32,17 @@ type getStockPriceArgs struct { Symbol string } -// getStockPriceResults defines the output schema for the getStockPrice tool. -type getStockPriceResults struct { - Symbol string `json:"symbol"` - Price float64 `json:"price,omitempty"` - Error string `json:"error,omitempty"` -} - // getStockPrice is a tool that retrieves the stock price for a given ticker symbol // from the mockStockPrices map. It demonstrates how a function can be used as a -// tool by an agent. If the symbol is found, it returns a struct containing the -// symbol and its price. Otherwise, it returns a struct with an error message. -func getStockPrice(ctx tool.Context, input getStockPriceArgs) getStockPriceResults { +// tool by an agent. If the symbol is found, it returns a map containing the +// symbol and its price. Otherwise, it returns an error message. +func getStockPrice(ctx context.Context, input getStockPriceArgs) map[string]any { symbolUpper := strings.ToUpper(input.Symbol) if price, ok := mockStockPrices[symbolUpper]; ok { fmt.Printf("Tool: Found price for %s: %f\n", input.Symbol, price) - return getStockPriceResults{Symbol: input.Symbol, Price: price} + return map[string]any{"symbol": input.Symbol, "price": price} } - return getStockPriceResults{Symbol: input.Symbol, Error: "No data found for symbol"} + return map[string]any{"symbol": input.Symbol, "error": "No data found for symbol"} } // createStockAgent initializes and configures an LlmAgent. @@ -97,19 +90,22 @@ const ( // It sets up the necessary services, creates a session, and uses a runner // to manage the agent's lifecycle. It streams the agent's responses and // prints them to the console, handling any potential errors during the run. -func callAgent(ctx context.Context, a agent.Agent, prompt string) { - sessionService := session.InMemoryService() +func callAgent(ctx context.Context, agent agent.Agent, prompt string) { + + sessionService := sessionservice.Mem() + // Create a new session for the agent interactions. - session, err := sessionService.Create(ctx, &session.CreateRequest{ + session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ AppName: appName, UserID: userID, }) if err != nil { log.Fatalf("Failed to create the session service: %v", err) } - config := runner.Config{ + + config := &runner.Config{ AppName: appName, - Agent: a, + Agent: agent, SessionService: sessionService, } @@ -120,7 +116,7 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) { log.Fatalf("Failed to create the runner: %v", err) } - sessionID := session.Session.ID() + sessionID := session.Session.ID().SessionID userMsg := &genai.Content{ Parts: []*genai.Part{ @@ -129,13 +125,13 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) { Role: string(genai.RoleUser), } - for event, err := range r.Run(ctx, userID, sessionID, userMsg, &agent.RunConfig{ - StreamingMode: agent.StreamingModeSSE, + for event, err := range r.Run(ctx, userID, sessionID, userMsg, &runner.RunConfig{ + StreamingMode: runner.StreamingModeSSE, }) { if err != nil { fmt.Printf("\nAGENT_ERROR: %v\n", err) - } else if event.Partial { - for _, p := range event.Content.Parts { + } else { + for _, p := range event.LLMResponse.Content.Parts { fmt.Print(p.Text) } } @@ -169,8 +165,3 @@ func RunAgentSimulation() { fmt.Println("\n---") } } - -func main() { - fmt.Println("Attempting to run the agent simulation...") - RunAgentSimulation() -} From 7ccad9e9f223a199a878f8fab6c361f541d32e65 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Thu, 9 Oct 2025 21:01:04 -0400 Subject: [PATCH 017/125] Migrated to new API --- .../tools/function-tools/func_tool.go | 45 +++++++++++-------- 1 file changed, 27 insertions(+), 18 deletions(-) diff --git a/examples/go/snippets/tools/function-tools/func_tool.go b/examples/go/snippets/tools/function-tools/func_tool.go index cf642b08..7fedec07 100644 --- a/examples/go/snippets/tools/function-tools/func_tool.go +++ b/examples/go/snippets/tools/function-tools/func_tool.go @@ -8,9 +8,9 @@ import ( "google.golang.org/adk/agent" "google.golang.org/adk/agent/llmagent" - "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/model/gemini" "google.golang.org/adk/runner" - "google.golang.org/adk/sessionservice" + "google.golang.org/adk/session" "google.golang.org/adk/tool" "google.golang.org/genai" @@ -32,17 +32,24 @@ type getStockPriceArgs struct { Symbol string } +// getStockPriceResults defines the output schema for the getStockPrice tool. +type getStockPriceResults struct { + Symbol string `json:"symbol"` + Price float64 `json:"price,omitempty"` + Error string `json:"error,omitempty"` +} + // getStockPrice is a tool that retrieves the stock price for a given ticker symbol // from the mockStockPrices map. It demonstrates how a function can be used as a -// tool by an agent. If the symbol is found, it returns a map containing the -// symbol and its price. Otherwise, it returns an error message. -func getStockPrice(ctx context.Context, input getStockPriceArgs) map[string]any { +// tool by an agent. If the symbol is found, it returns a struct containing the +// symbol and its price. Otherwise, it returns a struct with an error message. +func getStockPrice(ctx tool.Context, input getStockPriceArgs) getStockPriceResults { symbolUpper := strings.ToUpper(input.Symbol) if price, ok := mockStockPrices[symbolUpper]; ok { fmt.Printf("Tool: Found price for %s: %f\n", input.Symbol, price) - return map[string]any{"symbol": input.Symbol, "price": price} + return getStockPriceResults{Symbol: input.Symbol, Price: price} } - return map[string]any{"symbol": input.Symbol, "error": "No data found for symbol"} + return getStockPriceResults{Symbol: input.Symbol, Error: "No data found for symbol"} } // createStockAgent initializes and configures an LlmAgent. @@ -90,22 +97,19 @@ const ( // It sets up the necessary services, creates a session, and uses a runner // to manage the agent's lifecycle. It streams the agent's responses and // prints them to the console, handling any potential errors during the run. -func callAgent(ctx context.Context, agent agent.Agent, prompt string) { - - sessionService := sessionservice.Mem() - +func callAgent(ctx context.Context, a agent.Agent, prompt string) { + sessionService := session.InMemoryService() // Create a new session for the agent interactions. - session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ + session, err := sessionService.Create(ctx, &session.CreateRequest{ AppName: appName, UserID: userID, }) if err != nil { log.Fatalf("Failed to create the session service: %v", err) } - - config := &runner.Config{ + config := runner.Config{ AppName: appName, - Agent: agent, + Agent: a, SessionService: sessionService, } @@ -116,7 +120,7 @@ func callAgent(ctx context.Context, agent agent.Agent, prompt string) { log.Fatalf("Failed to create the runner: %v", err) } - sessionID := session.Session.ID().SessionID + sessionID := session.Session.ID() userMsg := &genai.Content{ Parts: []*genai.Part{ @@ -125,8 +129,8 @@ func callAgent(ctx context.Context, agent agent.Agent, prompt string) { Role: string(genai.RoleUser), } - for event, err := range r.Run(ctx, userID, sessionID, userMsg, &runner.RunConfig{ - StreamingMode: runner.StreamingModeSSE, + for event, err := range r.Run(ctx, userID, sessionID, userMsg, &agent.RunConfig{ + StreamingMode: agent.StreamingModeSSE, }) { if err != nil { fmt.Printf("\nAGENT_ERROR: %v\n", err) @@ -165,3 +169,8 @@ func RunAgentSimulation() { fmt.Println("\n---") } } + +func main() { + fmt.Println("Attempting to run the agent simulation...") + RunAgentSimulation() +} From 2eb726d27863d7062846e5debe79a11426590e81 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 10 Oct 2025 09:35:25 -0400 Subject: [PATCH 018/125] Added partial check --- examples/go/snippets/tools/function-tools/func_tool.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/go/snippets/tools/function-tools/func_tool.go b/examples/go/snippets/tools/function-tools/func_tool.go index 7fedec07..faa5148a 100644 --- a/examples/go/snippets/tools/function-tools/func_tool.go +++ b/examples/go/snippets/tools/function-tools/func_tool.go @@ -134,7 +134,7 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) { }) { if err != nil { fmt.Printf("\nAGENT_ERROR: %v\n", err) - } else { + } else if event.Partial { for _, p := range event.LLMResponse.Content.Parts { fmt.Print(p.Text) } From 6419a76b2399acf4411e05108d617610bf32ff1a Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 10 Oct 2025 09:41:15 -0400 Subject: [PATCH 019/125] Ran linter --- examples/go/snippets/tools/function-tools/func_tool.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/go/snippets/tools/function-tools/func_tool.go b/examples/go/snippets/tools/function-tools/func_tool.go index faa5148a..d6f5ed1b 100644 --- a/examples/go/snippets/tools/function-tools/func_tool.go +++ b/examples/go/snippets/tools/function-tools/func_tool.go @@ -135,7 +135,7 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) { if err != nil { fmt.Printf("\nAGENT_ERROR: %v\n", err) } else if event.Partial { - for _, p := range event.LLMResponse.Content.Parts { + for _, p := range event.Content.Parts { fmt.Print(p.Text) } } From 259a9c1275e27b36d17e2888a2e57b9c9c08b5a2 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 22 Oct 2025 20:20:33 -0400 Subject: [PATCH 020/125] feat: add go fmt check --- .github/workflows/go-fmt.yaml | 27 ++------------------------- 1 file changed, 2 insertions(+), 25 deletions(-) diff --git a/.github/workflows/go-fmt.yaml b/.github/workflows/go-fmt.yaml index 59f0d72d..dda54d0f 100644 --- a/.github/workflows/go-fmt.yaml +++ b/.github/workflows/go-fmt.yaml @@ -10,32 +10,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Fetch all history for all tags and branches - uses: actions/setup-go@v5 with: go-version: '1.21' - - name: Run gofmt on changed files + - name: Run gofmt run: | - # List changed Go files in the pull request. - # The `|| true` is to prevent the command from failing if no go files are found - changed_files=$(git diff --name-only --diff-filter=ACMRT ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} | grep '\.go$' || true) - - if [ -z "$changed_files" ]; then - echo "No Go files changed in this PR." - exit 0 - fi - - echo "Checking formatting for the following files:" - echo "$changed_files" - - # Check formatting of changed files - unformatted_files=$(gofmt -l $changed_files) - - if [ -n "$unformatted_files" ]; then - echo "Unformatted Go files found:" - echo "$unformatted_files" - exit 1 - else - echo "All changed Go files are formatted correctly." - fi \ No newline at end of file + test -z "$(gofmt -l .)" From 18342d6c2936a9766907967e39cabbe8dbc092a4 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 22 Oct 2025 20:54:24 -0400 Subject: [PATCH 021/125] Run go fmt only on changed files --- .github/workflows/go-fmt.yaml | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/.github/workflows/go-fmt.yaml b/.github/workflows/go-fmt.yaml index dda54d0f..59f0d72d 100644 --- a/.github/workflows/go-fmt.yaml +++ b/.github/workflows/go-fmt.yaml @@ -10,9 +10,32 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 # Fetch all history for all tags and branches - uses: actions/setup-go@v5 with: go-version: '1.21' - - name: Run gofmt + - name: Run gofmt on changed files run: | - test -z "$(gofmt -l .)" + # List changed Go files in the pull request. + # The `|| true` is to prevent the command from failing if no go files are found + changed_files=$(git diff --name-only --diff-filter=ACMRT ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} | grep '\.go$' || true) + + if [ -z "$changed_files" ]; then + echo "No Go files changed in this PR." + exit 0 + fi + + echo "Checking formatting for the following files:" + echo "$changed_files" + + # Check formatting of changed files + unformatted_files=$(gofmt -l $changed_files) + + if [ -n "$unformatted_files" ]; then + echo "Unformatted Go files found:" + echo "$unformatted_files" + exit 1 + else + echo "All changed Go files are formatted correctly." + fi \ No newline at end of file From 646eb46b34c817cbea7c30d3453f86de26238eaa Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 1 Oct 2025 21:19:01 -0400 Subject: [PATCH 022/125] Feat: Add Golang example for LLM agents --- docs/agents/llm-agents.md | 6 + examples/go/snippets/agents/llm-agents/go.mod | 34 +++++ examples/go/snippets/agents/llm-agents/go.sum | 134 ++++++++++++++++++ .../go/snippets/agents/llm-agents/main.go | 108 ++++++++++++++ 4 files changed, 282 insertions(+) create mode 100644 examples/go/snippets/agents/llm-agents/go.mod create mode 100644 examples/go/snippets/agents/llm-agents/go.sum create mode 100644 examples/go/snippets/agents/llm-agents/main.go diff --git a/docs/agents/llm-agents.md b/docs/agents/llm-agents.md index a03437a5..76a2d5c1 100644 --- a/docs/agents/llm-agents.md +++ b/docs/agents/llm-agents.md @@ -551,6 +551,12 @@ call_agent("If it's raining in New York right now, what is the current temperatu --8<-- "examples/java/snippets/src/main/java/agents/LlmAgentExample.java:full_code" ``` + === "Golang" + + ```go + --8<-- "examples/go/snippets/agents/llm-agents/main.go" + ``` + _(This example demonstrates the core concepts. More complex agents might incorporate schemas, context control, planning, etc.)_ ## Related Concepts (Deferred Topics) diff --git a/examples/go/snippets/agents/llm-agents/go.mod b/examples/go/snippets/agents/llm-agents/go.mod new file mode 100644 index 00000000..d4b1a062 --- /dev/null +++ b/examples/go/snippets/agents/llm-agents/go.mod @@ -0,0 +1,34 @@ +module main + +go 1.25.1 + +replace google.golang.org/adk => /Users/ivanmkc/Documents/code/adk-go + +require ( + google.golang.org/adk v0.0.0-00010101000000-000000000000 + google.golang.org/genai v1.27.0 +) + +require ( + cloud.google.com/go v0.116.0 // indirect + cloud.google.com/go/auth v0.9.3 // indirect + cloud.google.com/go/compute/metadata v0.5.0 // indirect + github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/jsonschema-go v0.2.0 // indirect + github.com/google/s2a-go v0.1.8 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + go.opencensus.io v0.24.0 // indirect + golang.org/x/crypto v0.39.0 // indirect + golang.org/x/net v0.41.0 // indirect + golang.org/x/sync v0.15.0 // indirect + golang.org/x/sys v0.33.0 // indirect + golang.org/x/text v0.26.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect + google.golang.org/grpc v1.66.2 // indirect + google.golang.org/protobuf v1.34.2 // indirect + rsc.io/omap v1.2.0 // indirect + rsc.io/ordered v1.1.1 // indirect +) diff --git a/examples/go/snippets/agents/llm-agents/go.sum b/examples/go/snippets/agents/llm-agents/go.sum new file mode 100644 index 00000000..c827babb --- /dev/null +++ b/examples/go/snippets/agents/llm-agents/go.sum @@ -0,0 +1,134 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= +cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= +cloud.google.com/go/auth v0.9.3 h1:VOEUIAADkkLtyfr3BLa3R8Ed/j6w1jTBmARx+wb5w5U= +cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk= +cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= +cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/jsonschema-go v0.2.0 h1:Uh19091iHC56//WOsAd1oRg6yy1P9BpSvpjOL6RcjLQ= +github.com/google/jsonschema-go v0.2.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= +github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= +github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= +github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= +go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= +golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= +golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= +golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= +golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= +golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genai v1.27.0 h1:y4Vvs7E7Vfa2EBWznyNTbO1uukDM7tvYLKRtor+Lc/w= +google.golang.org/genai v1.27.0/go.mod h1:7pAilaICJlQBonjKKJNhftDFv3SREhZcTe9F6nRcjbg= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= +google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= +google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +rsc.io/omap v1.2.0 h1:c1M8jchnHbzmJALzGLclfH3xDWXrPxSUHXzH5C+8Kdw= +rsc.io/omap v1.2.0/go.mod h1:C8pkI0AWexHopQtZX+qiUeJGzvc8HkdgnsWK4/mAa00= +rsc.io/ordered v1.1.1 h1:1kZM6RkTmceJgsFH/8DLQvkCVEYomVDJfBRLT595Uak= +rsc.io/ordered v1.1.1/go.mod h1:evAi8739bWVBRG9aaufsjVc202+6okf8u2QeVL84BCM= diff --git a/examples/go/snippets/agents/llm-agents/main.go b/examples/go/snippets/agents/llm-agents/main.go new file mode 100644 index 00000000..947e92ed --- /dev/null +++ b/examples/go/snippets/agents/llm-agents/main.go @@ -0,0 +1,108 @@ +package main + +import ( + "context" + "fmt" + "log" + "strings" + + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/sessionservice" + "google.golang.org/adk/tool" + "google.golang.org/genai" +) + +// getCapitalCityArgs defines the schema for the arguments passed to the getCapitalCity tool. +type getCapitalCityArgs struct { + Country string `json:"country" adk:"description=The country to get the capital for."` +} + +// getCapitalCity is a tool that retrieves the capital city for a given country. +func getCapitalCity(ctx context.Context, args getCapitalCityArgs) map[string]any { + capitals := map[string]string{ + "france": "Paris", + "japan": "Tokyo", + "canada": "Ottawa", + } + capital, ok := capitals[strings.ToLower(args.Country)] + if !ok { + return map[string]any{"result": fmt.Sprintf("Sorry, I don't know the capital of %s.", args.Country)} + } + return map[string]any{"result": capital} +} + +func main() { + ctx := context.Background() + + capitalTool, err := tool.NewFunctionTool( + tool.FunctionToolConfig{ + Name: "getCapitalCity", + Description: "The country to get capital for.", + }, + getCapitalCity, + ) + if err != nil { + log.Fatalf("Failed to create function tool: %v", err) + } + + model, err := gemini.NewModel(ctx, "gemini-2.0-flash", &genai.ClientConfig{}) + if err != nil { + log.Fatalf("Failed to create model: %v", err) + } + + capitalAgent, err := llmagent.New(llmagent.Config{ + Name: "capital_agent", + Model: model, + Description: "Answers user questions about the capital city of a given country.", + Instruction: `You are an agent that provides the capital city of a country. +When a user asks for the capital of a country: +1. Identify the country name from the user's query. +2. Use the 'get_capital_city' tool to find the capital. +3. Respond clearly to the user, stating the capital city. +Example Query: "What's the capital of {country}?" +Example Response: "The capital of France is Paris."`, + Tools: []tool.Tool{capitalTool}, + }) + if err != nil { + log.Fatalf("Failed to create agent: %v", err) + } + + sessionService := sessionservice.Mem() + r, err := runner.New(&runner.Config{ + AppName: "capital-agent-example", + Agent: capitalAgent, + SessionService: sessionService, + }) + if err != nil { + log.Fatalf("Failed to create runner: %v", err) + } + + session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ + AppName: "capital-agent-example", + UserID: "user123", + }) + if err != nil { + log.Fatalf("Failed to create session: %v", err) + } + + userMsg := &genai.Content{ + Parts: []*genai.Part{{Text: "What is the capital of France?"}}, + Role: string(genai.RoleUser), + } + + fmt.Println("Running Capital Agent...") + for event, err := range r.Run(ctx, "user123", session.Session.ID().SessionID, userMsg, &runner.RunConfig{ + StreamingMode: runner.StreamingModeSSE, + }) { + if err != nil { + fmt.Printf("\nAGENT_ERROR: %v\n", err) + } else { + for _, p := range event.LLMResponse.Content.Parts { + fmt.Print(p.Text) + } + } + } + fmt.Println() +} From 9ec4bba55c6dd06d1938cade32bd9603b60cd382 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 1 Oct 2025 22:20:07 -0400 Subject: [PATCH 023/125] Added schema agent --- .../go/snippets/agents/llm-agents/main.go | 150 +++++++++++++----- 1 file changed, 111 insertions(+), 39 deletions(-) diff --git a/examples/go/snippets/agents/llm-agents/main.go b/examples/go/snippets/agents/llm-agents/main.go index 947e92ed..c837cdcd 100644 --- a/examples/go/snippets/agents/llm-agents/main.go +++ b/examples/go/snippets/agents/llm-agents/main.go @@ -2,10 +2,12 @@ package main import ( "context" + "encoding/json" "fmt" "log" "strings" + "google.golang.org/adk/agent" "google.golang.org/adk/agent/llmagent" "google.golang.org/adk/llm/gemini" "google.golang.org/adk/runner" @@ -14,32 +16,76 @@ import ( "google.golang.org/genai" ) -// getCapitalCityArgs defines the schema for the arguments passed to the getCapitalCity tool. +const ( + modelName = "gemini-2.0-flash" + appName = "agent_comparison_app" + userID = "test_user_456" +) + +var ( + countryInputSchema = &genai.Schema{ + Type: genai.TypeObject, + Description: "Input for specifying a country.", + Properties: map[string]*genai.Schema{ + "country": { + Type: genai.TypeString, + Description: "The country to get information about.", + }, + }, + Required: []string{"country"}, + } + + capitalInfoOutputSchema = &genai.Schema{ + Type: genai.TypeObject, + Description: "Schema for capital city information.", + Properties: map[string]*genai.Schema{ + "capital": { + Type: genai.TypeString, + Description: "The capital city of the country.", + }, + "population_estimate": { + Type: genai.TypeString, + Description: "An estimated population of the capital city.", + }, + }, + Required: []string{"capital", "population_estimate"}, + } +) + type getCapitalCityArgs struct { - Country string `json:"country" adk:"description=The country to get the capital for."` + Country string `json:"country"` } -// getCapitalCity is a tool that retrieves the capital city for a given country. func getCapitalCity(ctx context.Context, args getCapitalCityArgs) map[string]any { + fmt.Printf("\n-- Tool Call: getCapitalCity(country='%s') --\n", args.Country) capitals := map[string]string{ - "france": "Paris", - "japan": "Tokyo", - "canada": "Ottawa", + "united states": "Washington, D.C.", + "canada": "Ottawa", + "france": "Paris", + "japan": "Tokyo", } capital, ok := capitals[strings.ToLower(args.Country)] if !ok { - return map[string]any{"result": fmt.Sprintf("Sorry, I don't know the capital of %s.", args.Country)} + result := fmt.Sprintf("Sorry, I couldn't find the capital for %s.", args.Country) + fmt.Printf("-- Tool Result: '%s' --\n", result) + return map[string]any{"result": result} } + fmt.Printf("-- Tool Result: '%s' --\n", capital) return map[string]any{"result": capital} } func main() { ctx := context.Background() + model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + log.Fatalf("Failed to create model: %v", err) + } + capitalTool, err := tool.NewFunctionTool( tool.FunctionToolConfig{ - Name: "getCapitalCity", - Description: "The country to get capital for.", + Name: "get_capital_city", + Description: "Retrieves the capital city for a given country.", }, getCapitalCity, ) @@ -47,53 +93,80 @@ func main() { log.Fatalf("Failed to create function tool: %v", err) } - model, err := gemini.NewModel(ctx, "gemini-2.0-flash", &genai.ClientConfig{}) + capitalAgentWithTool, err := llmagent.New(llmagent.Config{ + Name: "capital_agent_tool", + Model: model, + Description: "Retrieves the capital city using a specific tool.", + Instruction: `You are a helpful agent that provides the capital city of a country using a tool. +The user will provide the country name in a JSON format like {"country": "country_name"}. +1. Extract the country name. +2. Use the 'get_capital_city' tool to find the capital. +3. Respond clearly to the user, stating the capital city found by the tool.`, + Tools: []tool.Tool{capitalTool}, + InputSchema: countryInputSchema, + }) if err != nil { - log.Fatalf("Failed to create model: %v", err) + log.Fatalf("Failed to create capital agent with tool: %v", err) } - capitalAgent, err := llmagent.New(llmagent.Config{ - Name: "capital_agent", + schemaJSON, _ := json.Marshal(capitalInfoOutputSchema) + structuredInfoAgentSchema, err := llmagent.New(llmagent.Config{ + Name: "structured_info_agent_schema", Model: model, - Description: "Answers user questions about the capital city of a given country.", - Instruction: `You are an agent that provides the capital city of a country. -When a user asks for the capital of a country: -1. Identify the country name from the user's query. -2. Use the 'get_capital_city' tool to find the capital. -3. Respond clearly to the user, stating the capital city. -Example Query: "What's the capital of {country}?" -Example Response: "The capital of France is Paris."`, - Tools: []tool.Tool{capitalTool}, + Description: "Provides capital and estimated population in a specific JSON format.", + Instruction: fmt.Sprintf(`You are an agent that provides country information. +The user will provide the country name in a JSON format like {"country": "country_name"}. +Respond ONLY with a JSON object matching this exact schema: +%s +Use your knowledge to determine the capital and estimate the population. Do not use any tools.`, string(schemaJSON)), + InputSchema: countryInputSchema, + OutputSchema: capitalInfoOutputSchema, }) if err != nil { - log.Fatalf("Failed to create agent: %v", err) + log.Fatalf("Failed to create structured info agent: %v", err) } + fmt.Println("--- Testing Agent with Tool ---") + callAgent(ctx, capitalAgentWithTool, `{"country": "France"}`) + callAgent(ctx, capitalAgentWithTool, `{"country": "Canada"}`) + + fmt.Println("\n\n--- Testing Agent with Output Schema (No Tool Use) ---") + callAgent(ctx, structuredInfoAgentSchema, `{"country": "France"}`) + callAgent(ctx, structuredInfoAgentSchema, `{"country": "Japan"}`) +} + +func callAgent(ctx context.Context, agent agent.Agent, prompt string) { + fmt.Printf("\n>>> Calling Agent: '%s' | Query: %s\n", agent.Name(), prompt) sessionService := sessionservice.Mem() - r, err := runner.New(&runner.Config{ - AppName: "capital-agent-example", - Agent: capitalAgent, - SessionService: sessionService, + + session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ + AppName: appName, + UserID: userID, }) if err != nil { - log.Fatalf("Failed to create runner: %v", err) + log.Fatalf("Failed to create the session service: %v", err) } - session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ - AppName: "capital-agent-example", - UserID: "user123", - }) + config := &runner.Config{ + AppName: appName, + Agent: agent, + SessionService: sessionService, + } + + r, err := runner.New(config) if err != nil { - log.Fatalf("Failed to create session: %v", err) + log.Fatalf("Failed to create the runner: %v", err) } + sessionID := session.Session.ID().SessionID userMsg := &genai.Content{ - Parts: []*genai.Part{{Text: "What is the capital of France?"}}, - Role: string(genai.RoleUser), + Parts: []*genai.Part{ + genai.NewPartFromText(prompt), + }, + Role: string(genai.RoleUser), } - fmt.Println("Running Capital Agent...") - for event, err := range r.Run(ctx, "user123", session.Session.ID().SessionID, userMsg, &runner.RunConfig{ + for event, err := range r.Run(ctx, userID, sessionID, userMsg, &runner.RunConfig{ StreamingMode: runner.StreamingModeSSE, }) { if err != nil { @@ -104,5 +177,4 @@ Example Response: "The capital of France is Paris."`, } } } - fmt.Println() -} +} \ No newline at end of file From 5aa9de01a53b735712db128b48569b1274db77e5 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 1 Oct 2025 22:39:42 -0400 Subject: [PATCH 024/125] Added session support --- examples/go/snippets/agents/llm-agents/go.mod | 34 ----- examples/go/snippets/agents/llm-agents/go.sum | 134 ------------------ .../go/snippets/agents/llm-agents/main.go | 44 +++++- 3 files changed, 37 insertions(+), 175 deletions(-) delete mode 100644 examples/go/snippets/agents/llm-agents/go.mod delete mode 100644 examples/go/snippets/agents/llm-agents/go.sum diff --git a/examples/go/snippets/agents/llm-agents/go.mod b/examples/go/snippets/agents/llm-agents/go.mod deleted file mode 100644 index d4b1a062..00000000 --- a/examples/go/snippets/agents/llm-agents/go.mod +++ /dev/null @@ -1,34 +0,0 @@ -module main - -go 1.25.1 - -replace google.golang.org/adk => /Users/ivanmkc/Documents/code/adk-go - -require ( - google.golang.org/adk v0.0.0-00010101000000-000000000000 - google.golang.org/genai v1.27.0 -) - -require ( - cloud.google.com/go v0.116.0 // indirect - cloud.google.com/go/auth v0.9.3 // indirect - cloud.google.com/go/compute/metadata v0.5.0 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/google/jsonschema-go v0.2.0 // indirect - github.com/google/s2a-go v0.1.8 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.4 // indirect - github.com/gorilla/websocket v1.5.3 // indirect - go.opencensus.io v0.24.0 // indirect - golang.org/x/crypto v0.39.0 // indirect - golang.org/x/net v0.41.0 // indirect - golang.org/x/sync v0.15.0 // indirect - golang.org/x/sys v0.33.0 // indirect - golang.org/x/text v0.26.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect - google.golang.org/grpc v1.66.2 // indirect - google.golang.org/protobuf v1.34.2 // indirect - rsc.io/omap v1.2.0 // indirect - rsc.io/ordered v1.1.1 // indirect -) diff --git a/examples/go/snippets/agents/llm-agents/go.sum b/examples/go/snippets/agents/llm-agents/go.sum deleted file mode 100644 index c827babb..00000000 --- a/examples/go/snippets/agents/llm-agents/go.sum +++ /dev/null @@ -1,134 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE= -cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U= -cloud.google.com/go/auth v0.9.3 h1:VOEUIAADkkLtyfr3BLa3R8Ed/j6w1jTBmARx+wb5w5U= -cloud.google.com/go/auth v0.9.3/go.mod h1:7z6VY+7h3KUdRov5F1i8NDP5ZzWKYmEPO842BgCsmTk= -cloud.google.com/go/compute/metadata v0.5.0 h1:Zr0eK8JbFv6+Wi4ilXAR8FJ3wyNdpxHKJNPos6LTZOY= -cloud.google.com/go/compute/metadata v0.5.0/go.mod h1:aHnloV2TPI38yx4s9+wAZhHykWvVCfu7hQbF+9CWoiY= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/jsonschema-go v0.2.0 h1:Uh19091iHC56//WOsAd1oRg6yy1P9BpSvpjOL6RcjLQ= -github.com/google/jsonschema-go v0.2.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= -github.com/google/s2a-go v0.1.8 h1:zZDs9gcbt9ZPLV0ndSyQk6Kacx2g/X+SKYovpnz3SMM= -github.com/google/s2a-go v0.1.8/go.mod h1:6iNWHTpQ+nfNRN5E00MSdfDwVesa8hhS32PhPO8deJA= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.4 h1:XYIDZApgAnrN1c855gTgghdIA6Stxb52D5RnLI1SLyw= -github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.39.0 h1:SHs+kF4LP+f+p14esP5jAoDpHU8Gu/v9lFRK6IT5imM= -golang.org/x/crypto v0.39.0/go.mod h1:L+Xg3Wf6HoL4Bn4238Z6ft6KfEpN0tJGo53AAPC632U= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.41.0 h1:vBTly1HeNPEn3wtREYfy4GZ/NECgw2Cnl+nK6Nz3uvw= -golang.org/x/net v0.41.0/go.mod h1:B/K4NNqkfmg07DQYrbwvSluqCJOOXwUjeb/5lOisjbA= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.15.0 h1:KWH3jNZsfyT6xfAfKiz6MRNmd46ByHDYaZ7KSkCtdW8= -golang.org/x/sync v0.15.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= -golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.26.0 h1:P42AVeLghgTYr4+xUnTRKDMqpar+PtX7KWuNQL21L8M= -golang.org/x/text v0.26.0/go.mod h1:QK15LZJUUQVJxhz7wXgxSy/CJaTFjd0G+YLonydOVQA= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/genai v1.27.0 h1:y4Vvs7E7Vfa2EBWznyNTbO1uukDM7tvYLKRtor+Lc/w= -google.golang.org/genai v1.27.0/go.mod h1:7pAilaICJlQBonjKKJNhftDFv3SREhZcTe9F6nRcjbg= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.66.2 h1:3QdXkuq3Bkh7w+ywLdLvM56cmGvQHUMZpiCzt6Rqaoo= -google.golang.org/grpc v1.66.2/go.mod h1:s3/l6xSSCURdVfAnL+TqCNMyTDAGN6+lZeVxnZR128Y= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= -google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -rsc.io/omap v1.2.0 h1:c1M8jchnHbzmJALzGLclfH3xDWXrPxSUHXzH5C+8Kdw= -rsc.io/omap v1.2.0/go.mod h1:C8pkI0AWexHopQtZX+qiUeJGzvc8HkdgnsWK4/mAa00= -rsc.io/ordered v1.1.1 h1:1kZM6RkTmceJgsFH/8DLQvkCVEYomVDJfBRLT595Uak= -rsc.io/ordered v1.1.1/go.mod h1:evAi8739bWVBRG9aaufsjVc202+6okf8u2QeVL84BCM= diff --git a/examples/go/snippets/agents/llm-agents/main.go b/examples/go/snippets/agents/llm-agents/main.go index c837cdcd..40648d09 100644 --- a/examples/go/snippets/agents/llm-agents/main.go +++ b/examples/go/snippets/agents/llm-agents/main.go @@ -93,6 +93,7 @@ func main() { log.Fatalf("Failed to create function tool: %v", err) } + capitalToolOutputKey := "capital_tool_result" capitalAgentWithTool, err := llmagent.New(llmagent.Config{ Name: "capital_agent_tool", Model: model, @@ -104,12 +105,14 @@ The user will provide the country name in a JSON format like {"country": "countr 3. Respond clearly to the user, stating the capital city found by the tool.`, Tools: []tool.Tool{capitalTool}, InputSchema: countryInputSchema, + // OutputKey: capitalToolOutputKey, }) if err != nil { log.Fatalf("Failed to create capital agent with tool: %v", err) } schemaJSON, _ := json.Marshal(capitalInfoOutputSchema) + structuredInfoAgentOutputKey := "structured_info_result" structuredInfoAgentSchema, err := llmagent.New(llmagent.Config{ Name: "structured_info_agent_schema", Model: model, @@ -121,25 +124,26 @@ Respond ONLY with a JSON object matching this exact schema: Use your knowledge to determine the capital and estimate the population. Do not use any tools.`, string(schemaJSON)), InputSchema: countryInputSchema, OutputSchema: capitalInfoOutputSchema, + // OutputKey: structuredInfoAgentOutputKey, }) if err != nil { log.Fatalf("Failed to create structured info agent: %v", err) } fmt.Println("--- Testing Agent with Tool ---") - callAgent(ctx, capitalAgentWithTool, `{"country": "France"}`) - callAgent(ctx, capitalAgentWithTool, `{"country": "Canada"}`) + callAgent(ctx, capitalAgentWithTool, capitalToolOutputKey, `{"country": "France"}`) + callAgent(ctx, capitalAgentWithTool, capitalToolOutputKey, `{"country": "Canada"}`) fmt.Println("\n\n--- Testing Agent with Output Schema (No Tool Use) ---") - callAgent(ctx, structuredInfoAgentSchema, `{"country": "France"}`) - callAgent(ctx, structuredInfoAgentSchema, `{"country": "Japan"}`) + callAgent(ctx, structuredInfoAgentSchema, structuredInfoAgentOutputKey, `{"country": "France"}`) + callAgent(ctx, structuredInfoAgentSchema, structuredInfoAgentOutputKey, `{"country": "Japan"}`) } -func callAgent(ctx context.Context, agent agent.Agent, prompt string) { +func callAgent(ctx context.Context, agent agent.Agent, outputKey string, prompt string) { fmt.Printf("\n>>> Calling Agent: '%s' | Query: %s\n", agent.Name(), prompt) sessionService := sessionservice.Mem() - session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ + sessionCreateResponse, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ AppName: appName, UserID: userID, }) @@ -147,6 +151,8 @@ func callAgent(ctx context.Context, agent agent.Agent, prompt string) { log.Fatalf("Failed to create the session service: %v", err) } + session := sessionCreateResponse.Session + config := &runner.Config{ AppName: appName, Agent: agent, @@ -158,7 +164,7 @@ func callAgent(ctx context.Context, agent agent.Agent, prompt string) { log.Fatalf("Failed to create the runner: %v", err) } - sessionID := session.Session.ID().SessionID + sessionID := session.ID().SessionID userMsg := &genai.Content{ Parts: []*genai.Part{ genai.NewPartFromText(prompt), @@ -177,4 +183,28 @@ func callAgent(ctx context.Context, agent agent.Agent, prompt string) { } } } + + if outputKey != "" { + storedOutput, error := session.State().Get(outputKey) + if error == nil { + fmt.Printf("--- Session State ['%s']: ", outputKey) + storedString, isString := storedOutput.(string) + if isString { + var prettyJSON map[string]interface{} + if err := json.Unmarshal([]byte(storedString), &prettyJSON); err == nil { + indentedJSON, err := json.MarshalIndent(prettyJSON, "", " ") + if err == nil { + fmt.Println(string(indentedJSON)) + } else { + fmt.Println(storedString) + } + } else { + fmt.Println(storedString) + } + } else { + fmt.Println(storedOutput) + } + fmt.Println(strings.Repeat("-", 30)) + } + } } \ No newline at end of file From 62cd58c0f0154d17569da401e5a69382a80d36eb Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 10 Oct 2025 10:11:29 -0400 Subject: [PATCH 025/125] Migrated to new API --- .../go/snippets/agents/llm-agents/main.go | 33 ++++++++++--------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/examples/go/snippets/agents/llm-agents/main.go b/examples/go/snippets/agents/llm-agents/main.go index 40648d09..9117790f 100644 --- a/examples/go/snippets/agents/llm-agents/main.go +++ b/examples/go/snippets/agents/llm-agents/main.go @@ -9,10 +9,11 @@ import ( "google.golang.org/adk/agent" "google.golang.org/adk/agent/llmagent" - "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/model/gemini" "google.golang.org/adk/runner" - "google.golang.org/adk/sessionservice" + "google.golang.org/adk/session" "google.golang.org/adk/tool" + "google.golang.org/genai" ) @@ -56,7 +57,7 @@ type getCapitalCityArgs struct { Country string `json:"country"` } -func getCapitalCity(ctx context.Context, args getCapitalCityArgs) map[string]any { +func getCapitalCity(ctx tool.Context, args getCapitalCityArgs) map[string]any { fmt.Printf("\n-- Tool Call: getCapitalCity(country='%s') --\n", args.Country) capitals := map[string]string{ "united states": "Washington, D.C.", @@ -103,7 +104,7 @@ The user will provide the country name in a JSON format like {"country": "countr 1. Extract the country name. 2. Use the 'get_capital_city' tool to find the capital. 3. Respond clearly to the user, stating the capital city found by the tool.`, - Tools: []tool.Tool{capitalTool}, + Tools: []tool.Tool{capitalTool}, InputSchema: countryInputSchema, // OutputKey: capitalToolOutputKey, }) @@ -139,11 +140,11 @@ Use your knowledge to determine the capital and estimate the population. Do not callAgent(ctx, structuredInfoAgentSchema, structuredInfoAgentOutputKey, `{"country": "Japan"}`) } -func callAgent(ctx context.Context, agent agent.Agent, outputKey string, prompt string) { - fmt.Printf("\n>>> Calling Agent: '%s' | Query: %s\n", agent.Name(), prompt) - sessionService := sessionservice.Mem() +func callAgent(ctx context.Context, a agent.Agent, outputKey string, prompt string) { + fmt.Printf("\n>>> Calling Agent: '%s' | Query: %s\n", a.Name(), prompt) + sessionService := session.InMemoryService() - sessionCreateResponse, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ + sessionCreateResponse, err := sessionService.Create(ctx, &session.CreateRequest{ AppName: appName, UserID: userID, }) @@ -153,9 +154,9 @@ func callAgent(ctx context.Context, agent agent.Agent, outputKey string, prompt session := sessionCreateResponse.Session - config := &runner.Config{ + config := runner.Config{ AppName: appName, - Agent: agent, + Agent: a, SessionService: sessionService, } @@ -164,7 +165,7 @@ func callAgent(ctx context.Context, agent agent.Agent, outputKey string, prompt log.Fatalf("Failed to create the runner: %v", err) } - sessionID := session.ID().SessionID + sessionID := session.ID() userMsg := &genai.Content{ Parts: []*genai.Part{ genai.NewPartFromText(prompt), @@ -172,13 +173,13 @@ func callAgent(ctx context.Context, agent agent.Agent, outputKey string, prompt Role: string(genai.RoleUser), } - for event, err := range r.Run(ctx, userID, sessionID, userMsg, &runner.RunConfig{ - StreamingMode: runner.StreamingModeSSE, + for event, err := range r.Run(ctx, userID, sessionID, userMsg, &agent.RunConfig{ + StreamingMode: agent.StreamingModeSSE, }) { if err != nil { fmt.Printf("\nAGENT_ERROR: %v\n", err) - } else { - for _, p := range event.LLMResponse.Content.Parts { + } else if event.Partial { + for _, p := range event.Content.Parts { fmt.Print(p.Text) } } @@ -207,4 +208,4 @@ func callAgent(ctx context.Context, agent agent.Agent, outputKey string, prompt fmt.Println(strings.Repeat("-", 30)) } } -} \ No newline at end of file +} From eb4a3aac6fd5999bb56c863a8686a960bbc291b6 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Mon, 13 Oct 2025 20:13:29 -0400 Subject: [PATCH 026/125] Added back OutputKey --- examples/go/snippets/agents/llm-agents/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/go/snippets/agents/llm-agents/main.go b/examples/go/snippets/agents/llm-agents/main.go index 9117790f..b95f9558 100644 --- a/examples/go/snippets/agents/llm-agents/main.go +++ b/examples/go/snippets/agents/llm-agents/main.go @@ -106,7 +106,7 @@ The user will provide the country name in a JSON format like {"country": "countr 3. Respond clearly to the user, stating the capital city found by the tool.`, Tools: []tool.Tool{capitalTool}, InputSchema: countryInputSchema, - // OutputKey: capitalToolOutputKey, + OutputKey: capitalToolOutputKey, }) if err != nil { log.Fatalf("Failed to create capital agent with tool: %v", err) @@ -125,7 +125,7 @@ Respond ONLY with a JSON object matching this exact schema: Use your knowledge to determine the capital and estimate the population. Do not use any tools.`, string(schemaJSON)), InputSchema: countryInputSchema, OutputSchema: capitalInfoOutputSchema, - // OutputKey: structuredInfoAgentOutputKey, + OutputKey: structuredInfoAgentOutputKey, }) if err != nil { log.Fatalf("Failed to create structured info agent: %v", err) From 084cd46c9b782181b8a3014b0e1207ac32aeec65 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Tue, 14 Oct 2025 00:15:03 -0400 Subject: [PATCH 027/125] Added missing snippets --- docs/agents/llm-agents.md | 72 ++++-- .../go/snippets/agents/llm-agents/main.go | 218 +++++++++++++++--- 2 files changed, 246 insertions(+), 44 deletions(-) diff --git a/docs/agents/llm-agents.md b/docs/agents/llm-agents.md index 76a2d5c1..832c5eb1 100644 --- a/docs/agents/llm-agents.md +++ b/docs/agents/llm-agents.md @@ -66,6 +66,13 @@ First, you need to establish what the agent *is* and what it's *for*. .build(); ``` +=== "Golang" + + ```go + // Example: Defining the basic identity + --8<-- "examples/go/snippets/agents/llm-agents/main.go:identity" + ``` + ## Guiding the Agent: Instructions (`instruction`) @@ -129,13 +136,19 @@ tells the agent: 1. Identify the country name from the user's query. 2. Use the `get_capital_city` tool to find the capital. 3. Respond clearly to the user, stating the capital city. - Example Query: "What's the capital of {country}?" - Example Response: "The capital of France is Paris." + Example Query: \"What's the capital of {country}?\"\n Example Response: \"The capital of France is Paris.\" """) // tools will be added next .build(); ``` +=== "Golang" + + ```go + // Example: Adding instructions + --8<-- "examples/go/snippets/agents/llm-agents/main.go:instruction" + ``` + *(Note: For instructions that apply to *all* agents in a system, consider using `global_instruction` on the root agent, detailed further in the [Multi-Agents](multi-agents.md) section.)* @@ -170,7 +183,7 @@ on the conversation and its instructions. model="gemini-2.0-flash", name="capital_agent", description="Answers user questions about the capital city of a given country.", - instruction="""You are an agent that provides the capital city of a country... (previous instruction text)""", + instruction="""You are an agent that provides the capital city of a country... (previous instruction text) """, tools=[get_capital_city] # Provide the function directly ) ``` @@ -208,6 +221,12 @@ on the conversation and its instructions. .build(); ``` +=== "Golang" + + ```go + --8<-- "examples/go/snippets/agents/llm-agents/main.go:tool_example" + ``` + Learn more about Tools in the [Tools](../tools/index.md) section. ## Advanced Configuration & Control @@ -249,12 +268,20 @@ You can adjust how the underlying LLM generates responses using `generate_conten LlmAgent.builder() // ... other params .generateContentConfig(GenerateContentConfig.builder() - .temperature(0.2F) // More deterministic output + .temperature(0.2F) # More deterministic output .maxOutputTokens(250) .build()) .build(); ``` +=== "Golang" + + ```go + import "google.golang.org/genai" + + --8<-- "examples/go/snippets/agents/llm-agents/main.go:gen_config" + ``` + ### Structuring Data (`input_schema`, `output_schema`, `output_key`) For scenarios requiring structured data exchange with an `LLM Agent`, the ADK provides mechanisms to define expected input and desired output formats using schema definitions. @@ -306,15 +333,23 @@ For scenarios requiring structured data exchange with an `LLM Agent`, the ADK pr LlmAgent structuredCapitalAgent = LlmAgent.builder() - // ... name, model, description + # ... name, model, description .instruction( "You are a Capital Information Agent. Given a country, respond ONLY with a JSON object containing the capital. Format: {\"capital\": \"capital_name\"}") - .outputSchema(capitalOutput) // Enforce JSON output - .outputKey("found_capital") // Store result in state.get("found_capital") - // Cannot use tools(getCapitalCity) effectively here + .outputSchema(capitalOutput) # Enforce JSON output + .outputKey("found_capital") # Store result in state.get("found_capital") + # Cannot use tools(getCapitalCity) effectively here .build(); ``` +=== "Golang" + + The input and output schema is a `google.genai.types.Schema` object. + + ```go + --8<-- "examples/go/snippets/agents/llm-agents/main.go:schema_example" + ``` + ### Managing Context (`include_contents`) Control whether the agent receives the prior conversation history. @@ -339,11 +374,19 @@ Control whether the agent receives the prior conversation history. LlmAgent statelessAgent = LlmAgent.builder() - // ... other params + # ... other params .includeContents(IncludeContents.NONE) .build(); ``` +=== "Golang" + + ```go + import "google.golang.org/adk/agent/llmagent" + + --8<-- "examples/go/snippets/agents/llm-agents/main.go:include_contents" + ``` + ### Planner
@@ -487,9 +530,8 @@ def get_current_time(city: str) -> dict: tz = ZoneInfo(tz_identifier) now = datetime.datetime.now(tz) - report = ( + report = f'The current time in {city} is {now.strftime("%Y-%m-%d %H:%M:%S %Z%z")}' - ) return {"status": "success", "report": report} # Step 1: Create a ThinkingConfig @@ -539,13 +581,13 @@ call_agent("If it's raining in New York right now, what is the current temperatu ??? "Code" Here's the complete basic `capital_agent`: - === "Python" + === "Python" ```python --8<-- "examples/python/snippets/agents/llm-agent/capital_agent.py" ``` - === "Java" + === "Java" ```java --8<-- "examples/java/snippets/src/main/java/agents/LlmAgentExample.java:full_code" @@ -554,12 +596,12 @@ call_agent("If it's raining in New York right now, what is the current temperatu === "Golang" ```go - --8<-- "examples/go/snippets/agents/llm-agents/main.go" + --8<-- "examples/go/snippets/agents/llm-agents/main.go:full_code" ``` _(This example demonstrates the core concepts. More complex agents might incorporate schemas, context control, planning, etc.)_ -## Related Concepts (Deferred Topics) +## Related Concepts (Deferred Topics) While this page covers the core configuration of `LlmAgent`, several related concepts provide more advanced control and are detailed elsewhere: diff --git a/examples/go/snippets/agents/llm-agents/main.go b/examples/go/snippets/agents/llm-agents/main.go index b95f9558..62cb4681 100644 --- a/examples/go/snippets/agents/llm-agents/main.go +++ b/examples/go/snippets/agents/llm-agents/main.go @@ -1,3 +1,4 @@ +// --8<-- [start:full_code] package main import ( @@ -9,6 +10,7 @@ import ( "google.golang.org/adk/agent" "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/model" "google.golang.org/adk/model/gemini" "google.golang.org/adk/runner" "google.golang.org/adk/session" @@ -17,26 +19,102 @@ import ( "google.golang.org/genai" ) -const ( - modelName = "gemini-2.0-flash" - appName = "agent_comparison_app" - userID = "test_user_456" -) +// --- Documentation Snippets --- +// The following functions are self-contained examples for documentation. +// They are not called by the main application. -var ( - countryInputSchema = &genai.Schema{ - Type: genai.TypeObject, - Description: "Input for specifying a country.", - Properties: map[string]*genai.Schema{ - "country": { - Type: genai.TypeString, - Description: "The country to get information about.", - }, +func _snippet_identity(model model.LLM) { + // --8<-- [start:identity] + // Example: Defining the basic identity + agent, err := llmagent.New(llmagent.Config{ + Name: "capital_agent", + Model: model, + Description: "Answers user questions about the capital city of a given country.", + // instruction and tools will be added next + }) + // --8<-- [end:identity] + + if err != nil { + log.Fatal(err) + } + + fmt.Println("Agent created:", agent.Name()) +} + + +func _snippet_instruction(model model.LLM) { + // --8<-- [start:instruction] + // Example: Adding instructions + agent, err := llmagent.New(llmagent.Config{ + Name: "capital_agent", + Model: model, + Description: "Answers user questions about the capital city of a given country.", + Instruction: `You are an agent that provides the capital city of a country. +When a user asks for the capital of a country: +1. Identify the country name from the user's query. +2. Use the 'get_capital_city' tool to find the capital. +3. Respond clearly to the user, stating the capital city. +Example Query: "What's the capital of {country}?" +Example Response: "The capital of France is Paris."`, + // tools will be added next + }) + // --8<-- [end:instruction] + + if err != nil { + log.Fatal(err) + } + + fmt.Println("Agent with instruction created:", agent.Name()) +} + + +func _snippet_tool_example(model model.LLM) { + // --8<-- [start:tool_example] + // Define a tool function + type getCapitalCityArgs struct { + Country string `json:"country"` + } + getCapitalCity := func(ctx tool.Context, args getCapitalCityArgs) map[string]any { + // Replace with actual logic (e.g., API call, database lookup) + capitals := map[string]string{"france": "Paris", "japan": "Tokyo", "canada": "Ottawa"} + capital, ok := capitals[strings.ToLower(args.Country)] + if !ok { + return map[string]any{"result": fmt.Sprintf("Sorry, I don't know the capital of %s.", args.Country)} + } + return map[string]any{"result": capital} + } + + // Add the tool to the agent + capitalTool, err := tool.NewFunctionTool( + tool.FunctionToolConfig{ + Name: "get_capital_city", + Description: "Retrieves the capital city for a given country.", }, - Required: []string{"country"}, + getCapitalCity, + ) + if err != nil { + log.Fatal(err) } + agent, err := llmagent.New(llmagent.Config{ + Name: "capital_agent", + Model: model, + Description: "Answers user questions about the capital city of a given country.", + Instruction: "You are an agent that provides the capital city of a country... (previous instruction text)", + Tools: []tool.Tool{capitalTool}, + }) + // --8<-- [end:tool_example] - capitalInfoOutputSchema = &genai.Schema{ + if err != nil { + log.Fatal(err) + } + + fmt.Println("Agent with tool created:", agent.Name()) +} + + +// --8<-- [start:schema_example] +func _snippet_schema_example(model model.LLM) { + capitalOutput := &genai.Schema{ Type: genai.TypeObject, Description: "Schema for capital city information.", Properties: map[string]*genai.Schema{ @@ -44,19 +122,75 @@ var ( Type: genai.TypeString, Description: "The capital city of the country.", }, - "population_estimate": { - Type: genai.TypeString, - Description: "An estimated population of the capital city.", - }, }, - Required: []string{"capital", "population_estimate"}, } + + agent, err := llmagent.New(llmagent.Config{ + Name: "structured_capital_agent", + Model: model, + Description: "Provides capital information in a structured format.", + Instruction: `You are a Capital Information Agent. Given a country, respond ONLY with a JSON object containing the capital. Format: {"capital": "capital_name"}`, + OutputSchema: capitalOutput, + OutputKey: "found_capital", + // Cannot use tools effectively here + }) + if err != nil { + log.Fatal(err) + } + fmt.Println("Agent with output schema created:", agent.Name()) +} +// --8<-- [end:schema_example] + + +func _snippet_gen_config(model model.LLM) { + // --8<-- [start:gen_config] + temperature := float32(0.2) + agent, err := llmagent.New(llmagent.Config{ + Name: "gen_config_agent", + Model: model, + GenerateContentConfig: &genai.GenerateContentConfig{ + Temperature: &temperature, + MaxOutputTokens: 250, + }, + }) + // --8<-- [end:gen_config] + + if err != nil { + log.Fatalf("Failed to create agent with generation config: %v", err) + } + fmt.Println("Agent with generation config created:", agent.Name()) +} + + +func _snippet_include_contents(model model.LLM) { + // --8<-- [start:include_contents] + agent, err := llmagent.New(llmagent.Config{ + Name: "stateless_agent", + Model: model, + IncludeContents: "None", + }) + // --8<-- [end:include_contents] + if err != nil { + log.Fatalf("Failed to create agent with include contents none: %v", err) + } + fmt.Println("Stateless agent created:", agent.Name()) +} + + + +// --- Main Runnable Example --- + +const ( + modelName = "gemini-2.0-flash" + appName = "agent_comparison_app" + userID = "test_user_456" ) type getCapitalCityArgs struct { Country string `json:"country"` } +// getCapitalCity retrieves the capital city of a given country. func getCapitalCity(ctx tool.Context, args getCapitalCityArgs) map[string]any { fmt.Printf("\n-- Tool Call: getCapitalCity(country='%s') --\n", args.Country) capitals := map[string]string{ @@ -94,7 +228,18 @@ func main() { log.Fatalf("Failed to create function tool: %v", err) } - capitalToolOutputKey := "capital_tool_result" + countryInputSchema := &genai.Schema{ + Type: genai.TypeObject, + Description: "Input for specifying a country.", + Properties: map[string]*genai.Schema{ + "country": { + Type: genai.TypeString, + Description: "The country to get information about.", + }, + }, + Required: []string{"country"}, + } + capitalAgentWithTool, err := llmagent.New(llmagent.Config{ Name: "capital_agent_tool", Model: model, @@ -106,14 +251,28 @@ The user will provide the country name in a JSON format like {"country": "countr 3. Respond clearly to the user, stating the capital city found by the tool.`, Tools: []tool.Tool{capitalTool}, InputSchema: countryInputSchema, - OutputKey: capitalToolOutputKey, + OutputKey: "capital_tool_result", }) if err != nil { log.Fatalf("Failed to create capital agent with tool: %v", err) } + capitalInfoOutputSchema := &genai.Schema{ + Type: genai.TypeObject, + Description: "Schema for capital city information.", + Properties: map[string]*genai.Schema{ + "capital": { + Type: genai.TypeString, + Description: "The capital city of the country.", + }, + "population_estimate": { + Type: genai.TypeString, + Description: "An estimated population of the capital city.", + }, + }, + Required: []string{"capital", "population_estimate"}, + } schemaJSON, _ := json.Marshal(capitalInfoOutputSchema) - structuredInfoAgentOutputKey := "structured_info_result" structuredInfoAgentSchema, err := llmagent.New(llmagent.Config{ Name: "structured_info_agent_schema", Model: model, @@ -125,19 +284,19 @@ Respond ONLY with a JSON object matching this exact schema: Use your knowledge to determine the capital and estimate the population. Do not use any tools.`, string(schemaJSON)), InputSchema: countryInputSchema, OutputSchema: capitalInfoOutputSchema, - OutputKey: structuredInfoAgentOutputKey, + OutputKey: "structured_info_result", }) if err != nil { log.Fatalf("Failed to create structured info agent: %v", err) } fmt.Println("--- Testing Agent with Tool ---") - callAgent(ctx, capitalAgentWithTool, capitalToolOutputKey, `{"country": "France"}`) - callAgent(ctx, capitalAgentWithTool, capitalToolOutputKey, `{"country": "Canada"}`) + callAgent(ctx, capitalAgentWithTool, "capital_tool_result", `{"country": "France"}`) + callAgent(ctx, capitalAgentWithTool, "capital_tool_result", `{"country": "Canada"}`) fmt.Println("\n\n--- Testing Agent with Output Schema (No Tool Use) ---") - callAgent(ctx, structuredInfoAgentSchema, structuredInfoAgentOutputKey, `{"country": "France"}`) - callAgent(ctx, structuredInfoAgentSchema, structuredInfoAgentOutputKey, `{"country": "Japan"}`) + callAgent(ctx, structuredInfoAgentSchema, "structured_info_result", `{"country": "France"}`) + callAgent(ctx, structuredInfoAgentSchema, "structured_info_result", `{"country": "Japan"}`) } func callAgent(ctx context.Context, a agent.Agent, outputKey string, prompt string) { @@ -209,3 +368,4 @@ func callAgent(ctx context.Context, a agent.Agent, outputKey string, prompt stri } } } +// --8<-- [end:full_code] \ No newline at end of file From 72a229ac7bc0c2f56ce4ef1640893abbc2ef8f4b Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Tue, 14 Oct 2025 00:28:39 -0400 Subject: [PATCH 028/125] Moved snipped to its own package --- docs/agents/llm-agents.md | 14 +- .../go/snippets/agents/llm-agents/main.go | 160 --------------- .../agents/llm-agents/snippets/main.go | 193 ++++++++++++++++++ 3 files changed, 200 insertions(+), 167 deletions(-) create mode 100644 examples/go/snippets/agents/llm-agents/snippets/main.go diff --git a/docs/agents/llm-agents.md b/docs/agents/llm-agents.md index 832c5eb1..4ce25688 100644 --- a/docs/agents/llm-agents.md +++ b/docs/agents/llm-agents.md @@ -70,7 +70,7 @@ First, you need to establish what the agent *is* and what it's *for*. ```go // Example: Defining the basic identity - --8<-- "examples/go/snippets/agents/llm-agents/main.go:identity" + --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:identity" ``` @@ -146,7 +146,7 @@ tells the agent: ```go // Example: Adding instructions - --8<-- "examples/go/snippets/agents/llm-agents/main.go:instruction" + --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:instruction" ``` *(Note: For instructions that apply to *all* agents in a system, consider using @@ -224,7 +224,7 @@ on the conversation and its instructions. === "Golang" ```go - --8<-- "examples/go/snippets/agents/llm-agents/main.go:tool_example" + --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:tool_example" ``` Learn more about Tools in the [Tools](../tools/index.md) section. @@ -279,7 +279,7 @@ You can adjust how the underlying LLM generates responses using `generate_conten ```go import "google.golang.org/genai" - --8<-- "examples/go/snippets/agents/llm-agents/main.go:gen_config" + --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:gen_config" ``` ### Structuring Data (`input_schema`, `output_schema`, `output_key`) @@ -347,7 +347,7 @@ For scenarios requiring structured data exchange with an `LLM Agent`, the ADK pr The input and output schema is a `google.genai.types.Schema` object. ```go - --8<-- "examples/go/snippets/agents/llm-agents/main.go:schema_example" + --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:schema_example" ``` ### Managing Context (`include_contents`) @@ -384,7 +384,7 @@ Control whether the agent receives the prior conversation history. ```go import "google.golang.org/adk/agent/llmagent" - --8<-- "examples/go/snippets/agents/llm-agents/main.go:include_contents" + --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:include_contents" ``` ### Planner @@ -596,7 +596,7 @@ call_agent("If it's raining in New York right now, what is the current temperatu === "Golang" ```go - --8<-- "examples/go/snippets/agents/llm-agents/main.go:full_code" + --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:full_code" ``` _(This example demonstrates the core concepts. More complex agents might incorporate schemas, context control, planning, etc.)_ diff --git a/examples/go/snippets/agents/llm-agents/main.go b/examples/go/snippets/agents/llm-agents/main.go index 62cb4681..3b7f2761 100644 --- a/examples/go/snippets/agents/llm-agents/main.go +++ b/examples/go/snippets/agents/llm-agents/main.go @@ -10,7 +10,6 @@ import ( "google.golang.org/adk/agent" "google.golang.org/adk/agent/llmagent" - "google.golang.org/adk/model" "google.golang.org/adk/model/gemini" "google.golang.org/adk/runner" "google.golang.org/adk/session" @@ -19,165 +18,6 @@ import ( "google.golang.org/genai" ) -// --- Documentation Snippets --- -// The following functions are self-contained examples for documentation. -// They are not called by the main application. - -func _snippet_identity(model model.LLM) { - // --8<-- [start:identity] - // Example: Defining the basic identity - agent, err := llmagent.New(llmagent.Config{ - Name: "capital_agent", - Model: model, - Description: "Answers user questions about the capital city of a given country.", - // instruction and tools will be added next - }) - // --8<-- [end:identity] - - if err != nil { - log.Fatal(err) - } - - fmt.Println("Agent created:", agent.Name()) -} - - -func _snippet_instruction(model model.LLM) { - // --8<-- [start:instruction] - // Example: Adding instructions - agent, err := llmagent.New(llmagent.Config{ - Name: "capital_agent", - Model: model, - Description: "Answers user questions about the capital city of a given country.", - Instruction: `You are an agent that provides the capital city of a country. -When a user asks for the capital of a country: -1. Identify the country name from the user's query. -2. Use the 'get_capital_city' tool to find the capital. -3. Respond clearly to the user, stating the capital city. -Example Query: "What's the capital of {country}?" -Example Response: "The capital of France is Paris."`, - // tools will be added next - }) - // --8<-- [end:instruction] - - if err != nil { - log.Fatal(err) - } - - fmt.Println("Agent with instruction created:", agent.Name()) -} - - -func _snippet_tool_example(model model.LLM) { - // --8<-- [start:tool_example] - // Define a tool function - type getCapitalCityArgs struct { - Country string `json:"country"` - } - getCapitalCity := func(ctx tool.Context, args getCapitalCityArgs) map[string]any { - // Replace with actual logic (e.g., API call, database lookup) - capitals := map[string]string{"france": "Paris", "japan": "Tokyo", "canada": "Ottawa"} - capital, ok := capitals[strings.ToLower(args.Country)] - if !ok { - return map[string]any{"result": fmt.Sprintf("Sorry, I don't know the capital of %s.", args.Country)} - } - return map[string]any{"result": capital} - } - - // Add the tool to the agent - capitalTool, err := tool.NewFunctionTool( - tool.FunctionToolConfig{ - Name: "get_capital_city", - Description: "Retrieves the capital city for a given country.", - }, - getCapitalCity, - ) - if err != nil { - log.Fatal(err) - } - agent, err := llmagent.New(llmagent.Config{ - Name: "capital_agent", - Model: model, - Description: "Answers user questions about the capital city of a given country.", - Instruction: "You are an agent that provides the capital city of a country... (previous instruction text)", - Tools: []tool.Tool{capitalTool}, - }) - // --8<-- [end:tool_example] - - if err != nil { - log.Fatal(err) - } - - fmt.Println("Agent with tool created:", agent.Name()) -} - - -// --8<-- [start:schema_example] -func _snippet_schema_example(model model.LLM) { - capitalOutput := &genai.Schema{ - Type: genai.TypeObject, - Description: "Schema for capital city information.", - Properties: map[string]*genai.Schema{ - "capital": { - Type: genai.TypeString, - Description: "The capital city of the country.", - }, - }, - } - - agent, err := llmagent.New(llmagent.Config{ - Name: "structured_capital_agent", - Model: model, - Description: "Provides capital information in a structured format.", - Instruction: `You are a Capital Information Agent. Given a country, respond ONLY with a JSON object containing the capital. Format: {"capital": "capital_name"}`, - OutputSchema: capitalOutput, - OutputKey: "found_capital", - // Cannot use tools effectively here - }) - if err != nil { - log.Fatal(err) - } - fmt.Println("Agent with output schema created:", agent.Name()) -} -// --8<-- [end:schema_example] - - -func _snippet_gen_config(model model.LLM) { - // --8<-- [start:gen_config] - temperature := float32(0.2) - agent, err := llmagent.New(llmagent.Config{ - Name: "gen_config_agent", - Model: model, - GenerateContentConfig: &genai.GenerateContentConfig{ - Temperature: &temperature, - MaxOutputTokens: 250, - }, - }) - // --8<-- [end:gen_config] - - if err != nil { - log.Fatalf("Failed to create agent with generation config: %v", err) - } - fmt.Println("Agent with generation config created:", agent.Name()) -} - - -func _snippet_include_contents(model model.LLM) { - // --8<-- [start:include_contents] - agent, err := llmagent.New(llmagent.Config{ - Name: "stateless_agent", - Model: model, - IncludeContents: "None", - }) - // --8<-- [end:include_contents] - if err != nil { - log.Fatalf("Failed to create agent with include contents none: %v", err) - } - fmt.Println("Stateless agent created:", agent.Name()) -} - - - // --- Main Runnable Example --- const ( diff --git a/examples/go/snippets/agents/llm-agents/snippets/main.go b/examples/go/snippets/agents/llm-agents/snippets/main.go new file mode 100644 index 00000000..f3358243 --- /dev/null +++ b/examples/go/snippets/agents/llm-agents/snippets/main.go @@ -0,0 +1,193 @@ +package main + +import ( + "context" + "fmt" + "log" + "strings" + + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/model" + "google.golang.org/adk/model/gemini" + "google.golang.org/adk/tool" + + "google.golang.org/genai" +) + +// --- Documentation Snippets --- +// The following functions are self-contained examples for documentation. +// They are not called by the main application. + + +func _snippet_identity(model model.LLM) { + // --8<-- [start:identity] + // Example: Defining the basic identity + agent, err := llmagent.New(llmagent.Config{ + Name: "capital_agent", + Model: model, + Description: "Answers user questions about the capital city of a given country.", + // instruction and tools will be added next + }) + // --8<-- [end:identity] + + if err != nil { + log.Fatal(err) + } + + fmt.Println("Agent created:", agent.Name()) +} + + +func _snippet_instruction(model model.LLM) { + // --8<-- [start:instruction] + // Example: Adding instructions + agent, err := llmagent.New(llmagent.Config{ + Name: "capital_agent", + Model: model, + Description: "Answers user questions about the capital city of a given country.", + Instruction: `You are an agent that provides the capital city of a country. +When a user asks for the capital of a country: +1. Identify the country name from the user's query. +2. Use the 'get_capital_city' tool to find the capital. +3. Respond clearly to the user, stating the capital city. +Example Query: "What's the capital of {country}?" +Example Response: "The capital of France is Paris."`, + // tools will be added next + }) + // --8<-- [end:instruction] + + if err != nil { + log.Fatal(err) + } + + fmt.Println("Agent with instruction created:", agent.Name()) +} + + +func _snippet_tool_example(model model.LLM) { + // --8<-- [start:tool_example] + // Define a tool function + type getCapitalCityArgs struct { + Country string `json:"country"` + } + getCapitalCity := func(ctx tool.Context, args getCapitalCityArgs) map[string]any { + // Replace with actual logic (e.g., API call, database lookup) + capitals := map[string]string{"france": "Paris", "japan": "Tokyo", "canada": "Ottawa"} + capital, ok := capitals[strings.ToLower(args.Country)] + if !ok { + return map[string]any{"result": fmt.Sprintf("Sorry, I don't know the capital of %s.", args.Country)} + } + return map[string]any{"result": capital} + } + + // Add the tool to the agent + capitalTool, err := tool.NewFunctionTool( + tool.FunctionToolConfig{ + Name: "get_capital_city", + Description: "Retrieves the capital city for a given country.", + }, + getCapitalCity, + ) + if err != nil { + log.Fatal(err) + } + agent, err := llmagent.New(llmagent.Config{ + Name: "capital_agent", + Model: model, + Description: "Answers user questions about the capital city of a given country.", + Instruction: "You are an agent that provides the capital city of a country... (previous instruction text)", + Tools: []tool.Tool{capitalTool}, + }) + // --8<-- [end:tool_example] + + if err != nil { + log.Fatal(err) + } + + fmt.Println("Agent with tool created:", agent.Name()) +} + + +// --8<-- [start:schema_example] +func _snippet_schema_example(model model.LLM) { + capitalOutput := &genai.Schema{ + Type: genai.TypeObject, + Description: "Schema for capital city information.", + Properties: map[string]*genai.Schema{ + "capital": { + Type: genai.TypeString, + Description: "The capital city of the country.", + }, + }, + } + + agent, err := llmagent.New(llmagent.Config{ + Name: "structured_capital_agent", + Model: model, + Description: "Provides capital information in a structured format.", + Instruction: `You are a Capital Information Agent. Given a country, respond ONLY with a JSON object containing the capital. Format: {"capital": "capital_name"}`, + OutputSchema: capitalOutput, + OutputKey: "found_capital", + // Cannot use tools effectively here + }) + if err != nil { + log.Fatal(err) + } + fmt.Println("Agent with output schema created:", agent.Name()) +} +// --8<-- [end:schema_example] + + +func _snippet_gen_config(model model.LLM) { + // --8<-- [start:gen_config] + temperature := float32(0.2) + agent, err := llmagent.New(llmagent.Config{ + Name: "gen_config_agent", + Model: model, + GenerateContentConfig: &genai.GenerateContentConfig{ + Temperature: &temperature, + MaxOutputTokens: 250, + }, + }) + // --8<-- [end:gen_config] + + if err != nil { + log.Fatalf("Failed to create agent with generation config: %v", err) + } + fmt.Println("Agent with generation config created:", agent.Name()) +} + + +func _snippet_include_contents(model model.LLM) { + // --8<-- [start:include_contents] + agent, err := llmagent.New(llmagent.Config{ + Name: "stateless_agent", + Model: model, + IncludeContents: "None", + }) + // --8<-- [end:include_contents] + if err != nil { + log.Fatalf("Failed to create agent with include contents none: %v", err) + } + fmt.Println("Stateless agent created:", agent.Name()) +} + + +func main() { + // Call all snippet functions to ensure they compile. + ctx := context.Background() + + modelName := "gemini-2.5-flash" + model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + log.Fatalf("Failed to create model: %v", err) + } + + _snippet_include_contents(model) + _snippet_identity(model) + _snippet_instruction(model) + _snippet_tool_example(model) + _snippet_gen_config(model) + _snippet_schema_example(model) + // Note: The full runnable example is in the ../main.go file. +} \ No newline at end of file From 46672f2c6d75b2cca21536a093a983b7f2ca6f57 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Tue, 14 Oct 2025 00:42:43 -0400 Subject: [PATCH 029/125] Minor cleanup --- docs/agents/llm-agents.md | 3 +- .../go/snippets/agents/llm-agents/main.go | 150 ++++++++++-------- .../agents/llm-agents/snippets/main.go | 6 +- 3 files changed, 86 insertions(+), 73 deletions(-) diff --git a/docs/agents/llm-agents.md b/docs/agents/llm-agents.md index 4ce25688..da358e3f 100644 --- a/docs/agents/llm-agents.md +++ b/docs/agents/llm-agents.md @@ -293,6 +293,7 @@ For scenarios requiring structured data exchange with an `LLM Agent`, the ADK pr * **`output_key` (Optional):** Provide a string key. If set, the text content of the agent's *final* response will be automatically saved to the session's state dictionary under this key. This is useful for passing results between agents or steps in a workflow. * In Python, this might look like: `session.state[output_key] = agent_response_text` * In Java: `session.state().put(outputKey, agentResponseText)` + * In Golang, within a callback handler: `ctx.State().Set(output_key, agentResponseText)` === "Python" @@ -596,7 +597,7 @@ call_agent("If it's raining in New York right now, what is the current temperatu === "Golang" ```go - --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:full_code" + --8<-- "examples/go/snippets/agents/llm-agents/main.go:full_code" ``` _(This example demonstrates the core concepts. More complex agents might incorporate schemas, context control, planning, etc.)_ diff --git a/examples/go/snippets/agents/llm-agents/main.go b/examples/go/snippets/agents/llm-agents/main.go index 3b7f2761..f9804829 100644 --- a/examples/go/snippets/agents/llm-agents/main.go +++ b/examples/go/snippets/agents/llm-agents/main.go @@ -49,6 +49,87 @@ func getCapitalCity(ctx tool.Context, args getCapitalCityArgs) map[string]any { return map[string]any{"result": capital} } + +// callAgent is a helper function to execute an agent with a given prompt and handle its output. +func callAgent(ctx context.Context, a agent.Agent, outputKey string, prompt string) { + fmt.Printf("\n>>> Calling Agent: '%s' | Query: %s\n", a.Name(), prompt) + // Create an in-memory session service to manage agent state. + sessionService := session.InMemoryService() + + // Create a new session for the agent interaction. + sessionCreateResponse, err := sessionService.Create(ctx, &session.CreateRequest{ + AppName: appName, + UserID: userID, + }) + if err != nil { + log.Fatalf("Failed to create the session service: %v", err) + } + + session := sessionCreateResponse.Session + + // Configure the runner with the application name, agent, and session service. + config := runner.Config{ + AppName: appName, + Agent: a, + SessionService: sessionService, + } + + // Create a new runner instance. + r, err := runner.New(config) + if err != nil { + log.Fatalf("Failed to create the runner: %v", err) + } + + // Prepare the user's message to send to the agent. + sessionID := session.ID() + userMsg := &genai.Content{ + Parts: []*genai.Part{ + genai.NewPartFromText(prompt), + }, + Role: string(genai.RoleUser), + } + + // Run the agent and process the streaming events. + for event, err := range r.Run(ctx, userID, sessionID, userMsg, &agent.RunConfig{ + StreamingMode: agent.StreamingModeSSE, + }) { + if err != nil { + fmt.Printf("\nAGENT_ERROR: %v\n", err) + } else if event.Partial { + // Print partial responses as they are received. + for _, p := range event.Content.Parts { + fmt.Print(p.Text) + } + } + } + + // After the run, check if there's an expected output key in the session state. + if outputKey != "" { + storedOutput, error := session.State().Get(outputKey) + if error == nil { + // Pretty-print the stored output if it's a JSON string. + fmt.Printf("\n--- Session State ['%s']: ", outputKey) + storedString, isString := storedOutput.(string) + if isString { + var prettyJSON map[string]interface{} + if err := json.Unmarshal([]byte(storedString), &prettyJSON); err == nil { + indentedJSON, err := json.MarshalIndent(prettyJSON, "", " ") + if err == nil { + fmt.Println(string(indentedJSON)) + } else { + fmt.Println(storedString) + } + } else { + fmt.Println(storedString) + } + } else { + fmt.Println(storedOutput) + } + fmt.Println(strings.Repeat("-", 30)) + } + } +} + func main() { ctx := context.Background() @@ -139,73 +220,4 @@ Use your knowledge to determine the capital and estimate the population. Do not callAgent(ctx, structuredInfoAgentSchema, "structured_info_result", `{"country": "Japan"}`) } -func callAgent(ctx context.Context, a agent.Agent, outputKey string, prompt string) { - fmt.Printf("\n>>> Calling Agent: '%s' | Query: %s\n", a.Name(), prompt) - sessionService := session.InMemoryService() - - sessionCreateResponse, err := sessionService.Create(ctx, &session.CreateRequest{ - AppName: appName, - UserID: userID, - }) - if err != nil { - log.Fatalf("Failed to create the session service: %v", err) - } - - session := sessionCreateResponse.Session - - config := runner.Config{ - AppName: appName, - Agent: a, - SessionService: sessionService, - } - - r, err := runner.New(config) - if err != nil { - log.Fatalf("Failed to create the runner: %v", err) - } - - sessionID := session.ID() - userMsg := &genai.Content{ - Parts: []*genai.Part{ - genai.NewPartFromText(prompt), - }, - Role: string(genai.RoleUser), - } - - for event, err := range r.Run(ctx, userID, sessionID, userMsg, &agent.RunConfig{ - StreamingMode: agent.StreamingModeSSE, - }) { - if err != nil { - fmt.Printf("\nAGENT_ERROR: %v\n", err) - } else if event.Partial { - for _, p := range event.Content.Parts { - fmt.Print(p.Text) - } - } - } - - if outputKey != "" { - storedOutput, error := session.State().Get(outputKey) - if error == nil { - fmt.Printf("--- Session State ['%s']: ", outputKey) - storedString, isString := storedOutput.(string) - if isString { - var prettyJSON map[string]interface{} - if err := json.Unmarshal([]byte(storedString), &prettyJSON); err == nil { - indentedJSON, err := json.MarshalIndent(prettyJSON, "", " ") - if err == nil { - fmt.Println(string(indentedJSON)) - } else { - fmt.Println(storedString) - } - } else { - fmt.Println(storedString) - } - } else { - fmt.Println(storedOutput) - } - fmt.Println(strings.Repeat("-", 30)) - } - } -} // --8<-- [end:full_code] \ No newline at end of file diff --git a/examples/go/snippets/agents/llm-agents/snippets/main.go b/examples/go/snippets/agents/llm-agents/snippets/main.go index f3358243..ef0279ab 100644 --- a/examples/go/snippets/agents/llm-agents/snippets/main.go +++ b/examples/go/snippets/agents/llm-agents/snippets/main.go @@ -108,8 +108,8 @@ func _snippet_tool_example(model model.LLM) { } -// --8<-- [start:schema_example] func _snippet_schema_example(model model.LLM) { + // --8<-- [start:schema_example] capitalOutput := &genai.Schema{ Type: genai.TypeObject, Description: "Schema for capital city information.", @@ -128,14 +128,14 @@ func _snippet_schema_example(model model.LLM) { Instruction: `You are a Capital Information Agent. Given a country, respond ONLY with a JSON object containing the capital. Format: {"capital": "capital_name"}`, OutputSchema: capitalOutput, OutputKey: "found_capital", - // Cannot use tools effectively here + // Cannot use the capitalTool tool effectively here }) + // --8<-- [end:schema_example] if err != nil { log.Fatal(err) } fmt.Println("Agent with output schema created:", agent.Name()) } -// --8<-- [end:schema_example] func _snippet_gen_config(model model.LLM) { From e01aafe426bf37086b9a782ade50a3cebdb26f14 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 24 Oct 2025 10:39:04 -0400 Subject: [PATCH 030/125] Reverted llm-agents.md --- docs/agents/llm-agents.md | 77 +++++++-------------------------------- 1 file changed, 14 insertions(+), 63 deletions(-) diff --git a/docs/agents/llm-agents.md b/docs/agents/llm-agents.md index da358e3f..a03437a5 100644 --- a/docs/agents/llm-agents.md +++ b/docs/agents/llm-agents.md @@ -66,13 +66,6 @@ First, you need to establish what the agent *is* and what it's *for*. .build(); ``` -=== "Golang" - - ```go - // Example: Defining the basic identity - --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:identity" - ``` - ## Guiding the Agent: Instructions (`instruction`) @@ -136,19 +129,13 @@ tells the agent: 1. Identify the country name from the user's query. 2. Use the `get_capital_city` tool to find the capital. 3. Respond clearly to the user, stating the capital city. - Example Query: \"What's the capital of {country}?\"\n Example Response: \"The capital of France is Paris.\" + Example Query: "What's the capital of {country}?" + Example Response: "The capital of France is Paris." """) // tools will be added next .build(); ``` -=== "Golang" - - ```go - // Example: Adding instructions - --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:instruction" - ``` - *(Note: For instructions that apply to *all* agents in a system, consider using `global_instruction` on the root agent, detailed further in the [Multi-Agents](multi-agents.md) section.)* @@ -183,7 +170,7 @@ on the conversation and its instructions. model="gemini-2.0-flash", name="capital_agent", description="Answers user questions about the capital city of a given country.", - instruction="""You are an agent that provides the capital city of a country... (previous instruction text) """, + instruction="""You are an agent that provides the capital city of a country... (previous instruction text)""", tools=[get_capital_city] # Provide the function directly ) ``` @@ -221,12 +208,6 @@ on the conversation and its instructions. .build(); ``` -=== "Golang" - - ```go - --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:tool_example" - ``` - Learn more about Tools in the [Tools](../tools/index.md) section. ## Advanced Configuration & Control @@ -268,20 +249,12 @@ You can adjust how the underlying LLM generates responses using `generate_conten LlmAgent.builder() // ... other params .generateContentConfig(GenerateContentConfig.builder() - .temperature(0.2F) # More deterministic output + .temperature(0.2F) // More deterministic output .maxOutputTokens(250) .build()) .build(); ``` -=== "Golang" - - ```go - import "google.golang.org/genai" - - --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:gen_config" - ``` - ### Structuring Data (`input_schema`, `output_schema`, `output_key`) For scenarios requiring structured data exchange with an `LLM Agent`, the ADK provides mechanisms to define expected input and desired output formats using schema definitions. @@ -293,7 +266,6 @@ For scenarios requiring structured data exchange with an `LLM Agent`, the ADK pr * **`output_key` (Optional):** Provide a string key. If set, the text content of the agent's *final* response will be automatically saved to the session's state dictionary under this key. This is useful for passing results between agents or steps in a workflow. * In Python, this might look like: `session.state[output_key] = agent_response_text` * In Java: `session.state().put(outputKey, agentResponseText)` - * In Golang, within a callback handler: `ctx.State().Set(output_key, agentResponseText)` === "Python" @@ -334,23 +306,15 @@ For scenarios requiring structured data exchange with an `LLM Agent`, the ADK pr LlmAgent structuredCapitalAgent = LlmAgent.builder() - # ... name, model, description + // ... name, model, description .instruction( "You are a Capital Information Agent. Given a country, respond ONLY with a JSON object containing the capital. Format: {\"capital\": \"capital_name\"}") - .outputSchema(capitalOutput) # Enforce JSON output - .outputKey("found_capital") # Store result in state.get("found_capital") - # Cannot use tools(getCapitalCity) effectively here + .outputSchema(capitalOutput) // Enforce JSON output + .outputKey("found_capital") // Store result in state.get("found_capital") + // Cannot use tools(getCapitalCity) effectively here .build(); ``` -=== "Golang" - - The input and output schema is a `google.genai.types.Schema` object. - - ```go - --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:schema_example" - ``` - ### Managing Context (`include_contents`) Control whether the agent receives the prior conversation history. @@ -375,19 +339,11 @@ Control whether the agent receives the prior conversation history. LlmAgent statelessAgent = LlmAgent.builder() - # ... other params + // ... other params .includeContents(IncludeContents.NONE) .build(); ``` -=== "Golang" - - ```go - import "google.golang.org/adk/agent/llmagent" - - --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:include_contents" - ``` - ### Planner
@@ -531,8 +487,9 @@ def get_current_time(city: str) -> dict: tz = ZoneInfo(tz_identifier) now = datetime.datetime.now(tz) - report = + report = ( f'The current time in {city} is {now.strftime("%Y-%m-%d %H:%M:%S %Z%z")}' + ) return {"status": "success", "report": report} # Step 1: Create a ThinkingConfig @@ -582,27 +539,21 @@ call_agent("If it's raining in New York right now, what is the current temperatu ??? "Code" Here's the complete basic `capital_agent`: - === "Python" + === "Python" ```python --8<-- "examples/python/snippets/agents/llm-agent/capital_agent.py" ``` - === "Java" + === "Java" ```java --8<-- "examples/java/snippets/src/main/java/agents/LlmAgentExample.java:full_code" ``` - === "Golang" - - ```go - --8<-- "examples/go/snippets/agents/llm-agents/main.go:full_code" - ``` - _(This example demonstrates the core concepts. More complex agents might incorporate schemas, context control, planning, etc.)_ -## Related Concepts (Deferred Topics) +## Related Concepts (Deferred Topics) While this page covers the core configuration of `LlmAgent`, several related concepts provide more advanced control and are detailed elsewhere: From 2d2e439f0fc1c2f6b29ac431eadb1841d43306c3 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 24 Oct 2025 10:45:33 -0400 Subject: [PATCH 031/125] Added back correct changes to llm-agents.md --- docs/agents/llm-agents.md | 51 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/docs/agents/llm-agents.md b/docs/agents/llm-agents.md index a03437a5..239fa795 100644 --- a/docs/agents/llm-agents.md +++ b/docs/agents/llm-agents.md @@ -66,6 +66,13 @@ First, you need to establish what the agent *is* and what it's *for*. .build(); ``` +=== "Golang" + + ```go + // Example: Defining the basic identity + --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:identity" + ``` + ## Guiding the Agent: Instructions (`instruction`) @@ -136,6 +143,13 @@ tells the agent: .build(); ``` +=== "Golang" + + ```go + // Example: Adding instructions + --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:instruction" + ``` + *(Note: For instructions that apply to *all* agents in a system, consider using `global_instruction` on the root agent, detailed further in the [Multi-Agents](multi-agents.md) section.)* @@ -208,6 +222,12 @@ on the conversation and its instructions. .build(); ``` +=== "Golang" + + ```go + --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:tool_example" + ``` + Learn more about Tools in the [Tools](../tools/index.md) section. ## Advanced Configuration & Control @@ -255,6 +275,14 @@ You can adjust how the underlying LLM generates responses using `generate_conten .build(); ``` +=== "Golang" + + ```go + import "google.golang.org/genai" + + --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:gen_config" + ``` + ### Structuring Data (`input_schema`, `output_schema`, `output_key`) For scenarios requiring structured data exchange with an `LLM Agent`, the ADK provides mechanisms to define expected input and desired output formats using schema definitions. @@ -266,6 +294,7 @@ For scenarios requiring structured data exchange with an `LLM Agent`, the ADK pr * **`output_key` (Optional):** Provide a string key. If set, the text content of the agent's *final* response will be automatically saved to the session's state dictionary under this key. This is useful for passing results between agents or steps in a workflow. * In Python, this might look like: `session.state[output_key] = agent_response_text` * In Java: `session.state().put(outputKey, agentResponseText)` + * In Golang, within a callback handler: `ctx.State().Set(output_key, agentResponseText)` === "Python" @@ -315,6 +344,14 @@ For scenarios requiring structured data exchange with an `LLM Agent`, the ADK pr .build(); ``` +=== "Golang" + + The input and output schema is a `google.genai.types.Schema` object. + + ```go + --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:schema_example" + ``` + ### Managing Context (`include_contents`) Control whether the agent receives the prior conversation history. @@ -344,6 +381,14 @@ Control whether the agent receives the prior conversation history. .build(); ``` +=== "Golang" + + ```go + import "google.golang.org/adk/agent/llmagent" + + --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:include_contents" + ``` + ### Planner
@@ -551,6 +596,12 @@ call_agent("If it's raining in New York right now, what is the current temperatu --8<-- "examples/java/snippets/src/main/java/agents/LlmAgentExample.java:full_code" ``` + === "Golang" + + ```go + --8<-- "examples/go/snippets/agents/llm-agents/main.go:full_code" + ``` + _(This example demonstrates the core concepts. More complex agents might incorporate schemas, context control, planning, etc.)_ ## Related Concepts (Deferred Topics) From 7621fdffee2eabef08f4b91237b07bf47d565295 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 24 Oct 2025 10:51:27 -0400 Subject: [PATCH 032/125] Updated to newest API --- examples/go/snippets/agents/llm-agents/main.go | 7 ++++--- examples/go/snippets/agents/llm-agents/snippets/main.go | 7 ++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/examples/go/snippets/agents/llm-agents/main.go b/examples/go/snippets/agents/llm-agents/main.go index f9804829..ef060fcb 100644 --- a/examples/go/snippets/agents/llm-agents/main.go +++ b/examples/go/snippets/agents/llm-agents/main.go @@ -14,6 +14,7 @@ import ( "google.golang.org/adk/runner" "google.golang.org/adk/session" "google.golang.org/adk/tool" + "google.golang.org/adk/tool/functiontool" "google.golang.org/genai" ) @@ -90,7 +91,7 @@ func callAgent(ctx context.Context, a agent.Agent, outputKey string, prompt stri } // Run the agent and process the streaming events. - for event, err := range r.Run(ctx, userID, sessionID, userMsg, &agent.RunConfig{ + for event, err := range r.Run(ctx, userID, sessionID, userMsg, agent.RunConfig{ StreamingMode: agent.StreamingModeSSE, }) { if err != nil { @@ -138,8 +139,8 @@ func main() { log.Fatalf("Failed to create model: %v", err) } - capitalTool, err := tool.NewFunctionTool( - tool.FunctionToolConfig{ + capitalTool, err := functiontool.New( + functiontool.Config{ Name: "get_capital_city", Description: "Retrieves the capital city for a given country.", }, diff --git a/examples/go/snippets/agents/llm-agents/snippets/main.go b/examples/go/snippets/agents/llm-agents/snippets/main.go index ef0279ab..49346076 100644 --- a/examples/go/snippets/agents/llm-agents/snippets/main.go +++ b/examples/go/snippets/agents/llm-agents/snippets/main.go @@ -10,6 +10,7 @@ import ( "google.golang.org/adk/model" "google.golang.org/adk/model/gemini" "google.golang.org/adk/tool" + "google.golang.org/adk/tool/functiontool" "google.golang.org/genai" ) @@ -81,8 +82,8 @@ func _snippet_tool_example(model model.LLM) { } // Add the tool to the agent - capitalTool, err := tool.NewFunctionTool( - tool.FunctionToolConfig{ + capitalTool, err := functiontool.New( + functiontool.Config{ Name: "get_capital_city", Description: "Retrieves the capital city for a given country.", }, @@ -163,7 +164,7 @@ func _snippet_include_contents(model model.LLM) { agent, err := llmagent.New(llmagent.Config{ Name: "stateless_agent", Model: model, - IncludeContents: "None", + IncludeContents: llmagent.IncludeContentsNone, }) // --8<-- [end:include_contents] if err != nil { From 9361c0fa83ad42ee19d381c6275918fe6cd9af7b Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 24 Oct 2025 10:53:19 -0400 Subject: [PATCH 033/125] Ran linter --- examples/go/snippets/agents/llm-agents/main.go | 3 +-- .../snippets/agents/llm-agents/snippets/main.go | 17 +++++------------ 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/examples/go/snippets/agents/llm-agents/main.go b/examples/go/snippets/agents/llm-agents/main.go index ef060fcb..5c393a35 100644 --- a/examples/go/snippets/agents/llm-agents/main.go +++ b/examples/go/snippets/agents/llm-agents/main.go @@ -50,7 +50,6 @@ func getCapitalCity(ctx tool.Context, args getCapitalCityArgs) map[string]any { return map[string]any{"result": capital} } - // callAgent is a helper function to execute an agent with a given prompt and handle its output. func callAgent(ctx context.Context, a agent.Agent, outputKey string, prompt string) { fmt.Printf("\n>>> Calling Agent: '%s' | Query: %s\n", a.Name(), prompt) @@ -221,4 +220,4 @@ Use your knowledge to determine the capital and estimate the population. Do not callAgent(ctx, structuredInfoAgentSchema, "structured_info_result", `{"country": "Japan"}`) } -// --8<-- [end:full_code] \ No newline at end of file +// --8<-- [end:full_code] diff --git a/examples/go/snippets/agents/llm-agents/snippets/main.go b/examples/go/snippets/agents/llm-agents/snippets/main.go index 49346076..3f5ae958 100644 --- a/examples/go/snippets/agents/llm-agents/snippets/main.go +++ b/examples/go/snippets/agents/llm-agents/snippets/main.go @@ -19,7 +19,6 @@ import ( // The following functions are self-contained examples for documentation. // They are not called by the main application. - func _snippet_identity(model model.LLM) { // --8<-- [start:identity] // Example: Defining the basic identity @@ -34,11 +33,10 @@ func _snippet_identity(model model.LLM) { if err != nil { log.Fatal(err) } - + fmt.Println("Agent created:", agent.Name()) } - func _snippet_instruction(model model.LLM) { // --8<-- [start:instruction] // Example: Adding instructions @@ -60,11 +58,10 @@ Example Response: "The capital of France is Paris."`, if err != nil { log.Fatal(err) } - + fmt.Println("Agent with instruction created:", agent.Name()) } - func _snippet_tool_example(model model.LLM) { // --8<-- [start:tool_example] // Define a tool function @@ -104,11 +101,10 @@ func _snippet_tool_example(model model.LLM) { if err != nil { log.Fatal(err) } - + fmt.Println("Agent with tool created:", agent.Name()) } - func _snippet_schema_example(model model.LLM) { // --8<-- [start:schema_example] capitalOutput := &genai.Schema{ @@ -138,7 +134,6 @@ func _snippet_schema_example(model model.LLM) { fmt.Println("Agent with output schema created:", agent.Name()) } - func _snippet_gen_config(model model.LLM) { // --8<-- [start:gen_config] temperature := float32(0.2) @@ -158,7 +153,6 @@ func _snippet_gen_config(model model.LLM) { fmt.Println("Agent with generation config created:", agent.Name()) } - func _snippet_include_contents(model model.LLM) { // --8<-- [start:include_contents] agent, err := llmagent.New(llmagent.Config{ @@ -173,7 +167,6 @@ func _snippet_include_contents(model model.LLM) { fmt.Println("Stateless agent created:", agent.Name()) } - func main() { // Call all snippet functions to ensure they compile. ctx := context.Background() @@ -183,7 +176,7 @@ func main() { if err != nil { log.Fatalf("Failed to create model: %v", err) } - + _snippet_include_contents(model) _snippet_identity(model) _snippet_instruction(model) @@ -191,4 +184,4 @@ func main() { _snippet_gen_config(model) _snippet_schema_example(model) // Note: The full runnable example is in the ../main.go file. -} \ No newline at end of file +} From 1f59a6deaced3f02ab869d244fb2383911fb4100 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 1 Oct 2025 16:25:45 -0400 Subject: [PATCH 034/125] WIP --- docs/agents/workflow-agents/loop-agents.md | 5 + .../workflow-agents/sequential-agents.md | 5 + .../loop_agent_doc_improv_agent.go | 131 ++++++++++++++ ...sequential_agent_code_development_agent.go | 162 ++++++++++++++++++ 4 files changed, 303 insertions(+) create mode 100644 examples/go/snippets/agents/workflow-agents/loop_agent_doc_improv_agent.go create mode 100644 examples/go/snippets/agents/workflow-agents/sequential_agent_code_development_agent.go diff --git a/docs/agents/workflow-agents/loop-agents.md b/docs/agents/workflow-agents/loop-agents.md index 5c943e14..80670937 100644 --- a/docs/agents/workflow-agents/loop-agents.md +++ b/docs/agents/workflow-agents/loop-agents.md @@ -52,3 +52,8 @@ In this setup, the `LoopAgent` would manage the iterative process. The `CriticA --8<-- "examples/java/snippets/src/main/java/agents/workflow/LoopAgentExample.java:init" ``` + === "Golang" + ```go + --8<-- "examples/go/snippets/agents/workflow-agents/loop_agent_doc_improv_agent.go:init" + ``` + diff --git a/docs/agents/workflow-agents/sequential-agents.md b/docs/agents/workflow-agents/sequential-agents.md index 459b5a57..aaae7c66 100644 --- a/docs/agents/workflow-agents/sequential-agents.md +++ b/docs/agents/workflow-agents/sequential-agents.md @@ -53,4 +53,9 @@ This ensures the code is written, *then* reviewed, and *finally* refactored, in --8<-- "examples/java/snippets/src/main/java/agents/workflow/SequentialAgentExample.java:init" ``` + === "Golang" + ```go + --8<-- "examples/go/snippets/agents/workflow-agents/sequential_agent_code_development_agent.go:init" + ``` + diff --git a/examples/go/snippets/agents/workflow-agents/loop_agent_doc_improv_agent.go b/examples/go/snippets/agents/workflow-agents/loop_agent_doc_improv_agent.go new file mode 100644 index 00000000..86ccf8db --- /dev/null +++ b/examples/go/snippets/agents/workflow-agents/loop_agent_doc_improv_agent.go @@ -0,0 +1,131 @@ +package main + +import ( + "context" + "fmt" + "log" + + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/agent/workflowagents/loopagent" + "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/sessionservice" + "google.golang.org/adk/tool" + "google.golang.org/adk/functiontool" + "google.golang.org/genai" +) + +const ( + appName = "DocImprovAgent" + userID = "test_user_123" + modelName = "gemini-1.5-flash" +) + +// init_start +// ExitLoop is a tool that signals the loop to terminate. +func ExitLoop(ctx context.Context, toolCtx *tool.Context) { + toolCtx.EventActions.Escalate = true +} + +func main() { + if err := runAgent("Write a short document about the benefits of exercise."); err != nil { + log.Fatalf("Agent execution failed: %v", err) + } +} + +func runAgent(prompt string) error { + ctx := context.Background() + + model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + return fmt.Errorf("failed to create model: %v", err) + } + + writerAgent, err := llmagent.New(llmagent.Config{ + Name: "WriterAgent", + Model: model, + Description: "Generates and refines a document.", + Instruction: `You are a document writer. +Based on the user's request and any feedback from the critic, write or revise the document. +The current document is: +{document} + +Critic's feedback: +{feedback} + +Rewrite the document to address the feedback. If there is no feedback, write the initial draft.`, + OutputKey: "document", + }) + if err != nil { + return fmt.Errorf("failed to create writer agent: %v", err) + } + + criticAgent, err := llmagent.New(llmagent.Config{ + Name: "CriticAgent", + Model: model, + Description: "Critiques the document and decides if it's good enough.", + Instruction: `You are a document critic. +Review the following document: +{document} + +If the document is well-written and complete, call the "ExitLoop" tool. +Otherwise, provide constructive feedback for improvement.`, + OutputKey: "feedback", + Tools: []tool.Tool{functiontool.Must(ExitLoop)}, + }) + if err != nil { + return fmt.Errorf("failed to create critic agent: %v", err) + } + + refinementLoop, err := loopagent.New(loopagent.Config{ + AgentConfig: agent.Config{ + Name: "RefinementLoop", + Description: "Iteratively refines a document.", + SubAgents: []agent.Agent{ + writerAgent, + criticAgent, + }, + }, + MaxIterations: 5, + }) + if err != nil { + return fmt.Errorf("failed to create loop agent: %v", err) + } + // init_end + + sessionService := sessionservice.Mem() + r, err := runner.New(&runner.Config{ + AppName: appName, + Agent: refinementLoop, + SessionService: sessionService, + }) + if err != nil { + return fmt.Errorf("failed to create runner: %v", err) + } + + session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ + AppName: appName, + UserID: userID, + }) + if err != nil { + return fmt.Errorf("failed to create session: %v", err) + } + + userMsg := &genai.Content{ + Parts: []*genai.Part{{Text: prompt}}, + Role: string(genai.RoleUser), + } + + fmt.Printf("Running agent loop for prompt: %q\n---\n", prompt) + for event, err := range r.Run(ctx, userID, session.Session.ID().SessionID, userMsg, nil) { + if err != nil { + return fmt.Errorf("error during agent execution: %v", err) + } + for _, p := range event.LLMResponse.Content.Parts { + fmt.Print(p.Text) + } + } + fmt.Println("\n---\nLoop finished.") + return nil +} diff --git a/examples/go/snippets/agents/workflow-agents/sequential_agent_code_development_agent.go b/examples/go/snippets/agents/workflow-agents/sequential_agent_code_development_agent.go new file mode 100644 index 00000000..a1a93489 --- /dev/null +++ b/examples/go/snippets/agents/workflow-agents/sequential_agent_code_development_agent.go @@ -0,0 +1,162 @@ +package main + +import ( + "context" + "fmt" + "log" + + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/agent/workflowagents/sequentialagent" + "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/sessionservice" + "google.golang.org/genai" +) + +const ( + appName = "CodePipelineAgent" + userID = "test_user_456" + modelName = "gemini-2.5-flash" +) + +func main() { + if err := runAgent("Write a Go function to calculate the factorial of a number."); err != nil { + log.Fatalf("Agent execution failed: %v", err) + } +} + +func runAgent(prompt string) error { + ctx := context.Background() + + // init_start + model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + return fmt.Errorf("failed to create model: %v", err) + } + + codeWriterAgent, err := llmagent.New(llmagent.Config{ + Name: "CodeWriterAgent", + Model: model, + Description: "Writes initial Go code based on a specification.", + Instruction: `You are a Go Code Generator. +Based *only* on the user's request, write Go code that fulfills the requirement. +Output *only* the complete Go code block, enclosed in triple backticks ('''go ... '''). +Do not add any other text before or after the code block.`, + }) + if err != nil { + return fmt.Errorf("failed to create code writer agent: %v", err) + } + + codeReviewerAgent, err := llmagent.New(llmagent.Config{ + Name: "CodeReviewerAgent", + Model: model, + Description: "Reviews code and provides feedback.", + Instruction: `You are an expert Go Code Reviewer. +Your task is to provide constructive feedback on the provided code. + +**Code to Review:** +'''go +{generated_code} +''' + +**Review Criteria:** +1. **Correctness:** Does the code work as intended? Are there logic errors? +2. **Readability:** Is the code clear and easy to understand? Follows Go style guidelines? +3. **Idiomatic Go:** Does the code use Go's features in a natural and standard way? +4. **Edge Cases:** Does the code handle potential edge cases or invalid inputs gracefully? +5. **Best Practices:** Does the code follow common Go best practices? + +**Output:** +Provide your feedback as a concise, bulleted list. Focus on the most important points for improvement. +If the code is excellent and requires no changes, simply state: "No major issues found." +Output *only* the review comments or the "No major issues" statement.`, + }) + if err != nil { + return fmt.Errorf("failed to create code reviewer agent: %v", err) + } + + codeRefactorerAgent, err := llmagent.New(llmagent.Config{ + Name: "CodeRefactorerAgent", + Model: model, + Description: "Refactors code based on review comments.", + Instruction: `You are a Go Code Refactoring AI. +Your goal is to improve the given Go code based on the provided review comments. + +**Original Code:** +'''go +{generated_code} +''' + +**Review Comments:** +{review_comments} + +**Task:** +Carefully apply the suggestions from the review comments to refactor the original code. +If the review comments state "No major issues found," return the original code unchanged. +Ensure the final code is complete, functional, and includes necessary imports. + +**Output:** +Output *only* the final, refactored Go code block, enclosed in triple backticks ('''go ... '''). +Do not add any other text before or after the code block.`, + }) + if err != nil { + return fmt.Errorf("failed to create code refactorer agent: %v", err) + } + + codePipelineAgent, err := sequentialagent.New(sequentialagent.Config{ + AgentConfig: agent.Config{ + Name: appName, + Description: "Executes a sequence of code writing, reviewing, and refactoring.", + SubAgents: []agent.Agent{ + codeWriterAgent, + codeReviewerAgent, + codeRefactorerAgent, + }, + }, + }) + if err != nil { + return fmt.Errorf("failed to create sequential agent: %v", err) + } + // init_end + + sessionService := sessionservice.Mem() + r, err := runner.New(&runner.Config{ + AppName: appName, + Agent: codePipelineAgent, + SessionService: sessionService, + }) + if err != nil { + return fmt.Errorf("failed to create runner: %v", err) + } + + session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ + AppName: appName, + UserID: userID, + }) + if err != nil { + return fmt.Errorf("failed to create session: %v", err) + } + + userMsg := &genai.Content{ + Parts: []*genai.Part{{Text: prompt}}, + Role: string(genai.RoleUser), + } + + fmt.Printf("Running agent pipeline for prompt: %q\n---\n", prompt) + + for event, err := range r.Run(ctx, userID, session.Session.ID().SessionID, userMsg, &runner.RunConfig{ + StreamingMode: runner.StreamingModeSSE, + }) { + if err != nil { + return fmt.Errorf("error during agent execution: %v", err) + } + // The Go runner streams all events. For this example, we print the text + // from each part of the LLM response as it arrives. + for _, p := range event.LLMResponse.Content.Parts { + fmt.Print(p.Text) + } + } + fmt.Println("\n---\nPipeline finished.") + return nil +} \ No newline at end of file From ca3075aec13597a5db8be9ab1b8d2927b7667e0a Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 1 Oct 2025 20:41:06 -0400 Subject: [PATCH 035/125] WIP: Added the rest of the agents --- docs/agents/workflow-agents/loop-agents.md | 2 +- .../agents/workflow-agents/parallel-agents.md | 5 + .../workflow-agents/sequential-agents.md | 2 +- .../main.go} | 30 +++- .../agents/workflow-agents/parallel/main.go | 132 ++++++++++++++++++ .../main.go} | 6 +- 6 files changed, 166 insertions(+), 11 deletions(-) rename examples/go/snippets/agents/workflow-agents/{loop_agent_doc_improv_agent.go => loop/main.go} (80%) create mode 100644 examples/go/snippets/agents/workflow-agents/parallel/main.go rename examples/go/snippets/agents/workflow-agents/{sequential_agent_code_development_agent.go => sequential/main.go} (97%) diff --git a/docs/agents/workflow-agents/loop-agents.md b/docs/agents/workflow-agents/loop-agents.md index 80670937..db97d9cb 100644 --- a/docs/agents/workflow-agents/loop-agents.md +++ b/docs/agents/workflow-agents/loop-agents.md @@ -54,6 +54,6 @@ In this setup, the `LoopAgent` would manage the iterative process. The `CriticA === "Golang" ```go - --8<-- "examples/go/snippets/agents/workflow-agents/loop_agent_doc_improv_agent.go:init" + --8<-- "examples/go/snippets/agents/workflow-agents/loop/main.go:init" ``` diff --git a/docs/agents/workflow-agents/parallel-agents.md b/docs/agents/workflow-agents/parallel-agents.md index 5da9eff1..5ad8a8c2 100644 --- a/docs/agents/workflow-agents/parallel-agents.md +++ b/docs/agents/workflow-agents/parallel-agents.md @@ -56,3 +56,8 @@ These research tasks are independent. Using a `ParallelAgent` allows them to ru ```java --8<-- "examples/java/snippets/src/main/java/agents/workflow/ParallelResearchPipeline.java:full_code" ``` + + === "Golang" + ```go + --8<-- "examples/go/snippets/agents/workflow-agents/parallel/main.go:init" + ``` diff --git a/docs/agents/workflow-agents/sequential-agents.md b/docs/agents/workflow-agents/sequential-agents.md index aaae7c66..ce41243e 100644 --- a/docs/agents/workflow-agents/sequential-agents.md +++ b/docs/agents/workflow-agents/sequential-agents.md @@ -55,7 +55,7 @@ This ensures the code is written, *then* reviewed, and *finally* refactored, in === "Golang" ```go - --8<-- "examples/go/snippets/agents/workflow-agents/sequential_agent_code_development_agent.go:init" + --8<-- "examples/go/snippets/agents/workflow-agents/sequential/main.go:init" ``` diff --git a/examples/go/snippets/agents/workflow-agents/loop_agent_doc_improv_agent.go b/examples/go/snippets/agents/workflow-agents/loop/main.go similarity index 80% rename from examples/go/snippets/agents/workflow-agents/loop_agent_doc_improv_agent.go rename to examples/go/snippets/agents/workflow-agents/loop/main.go index 86ccf8db..23c52c63 100644 --- a/examples/go/snippets/agents/workflow-agents/loop_agent_doc_improv_agent.go +++ b/examples/go/snippets/agents/workflow-agents/loop/main.go @@ -12,7 +12,6 @@ import ( "google.golang.org/adk/runner" "google.golang.org/adk/sessionservice" "google.golang.org/adk/tool" - "google.golang.org/adk/functiontool" "google.golang.org/genai" ) @@ -23,9 +22,17 @@ const ( ) // init_start -// ExitLoop is a tool that signals the loop to terminate. -func ExitLoop(ctx context.Context, toolCtx *tool.Context) { +// ExitLoopArgs defines the (empty) arguments for the ExitLoop tool. +type ExitLoopArgs struct{} + +// ExitLoop is a tool that signals the loop to terminate by setting Escalate to true. +func ExitLoop(ctx context.Context, args ExitLoopArgs) (map[string]any, error) { + toolCtx, ok := tool.FromContext(ctx) + if !ok { + return nil, fmt.Errorf("tool.Context not found in context") + } toolCtx.EventActions.Escalate = true + return map[string]any{"status": "exiting loop"}, nil } func main() { @@ -55,12 +62,22 @@ Critic's feedback: {feedback} Rewrite the document to address the feedback. If there is no feedback, write the initial draft.`, - OutputKey: "document", }) if err != nil { return fmt.Errorf("failed to create writer agent: %v", err) } + exitLoopTool, err := tool.NewFunctionTool( + tool.FunctionToolConfig{ + Name: "ExitLoop", + Description: "Signals the loop to terminate when the document is well-written and complete.", + }, + ExitLoop, + ) + if err != nil { + return fmt.Errorf("failed to create exit loop tool: %v", err) + } + criticAgent, err := llmagent.New(llmagent.Config{ Name: "CriticAgent", Model: model, @@ -71,8 +88,7 @@ Review the following document: If the document is well-written and complete, call the "ExitLoop" tool. Otherwise, provide constructive feedback for improvement.`, - OutputKey: "feedback", - Tools: []tool.Tool{functiontool.Must(ExitLoop)}, + Tools: []tool.Tool{exitLoopTool}, }) if err != nil { return fmt.Errorf("failed to create critic agent: %v", err) @@ -128,4 +144,4 @@ Otherwise, provide constructive feedback for improvement.`, } fmt.Println("\n---\nLoop finished.") return nil -} +} \ No newline at end of file diff --git a/examples/go/snippets/agents/workflow-agents/parallel/main.go b/examples/go/snippets/agents/workflow-agents/parallel/main.go new file mode 100644 index 00000000..772052ac --- /dev/null +++ b/examples/go/snippets/agents/workflow-agents/parallel/main.go @@ -0,0 +1,132 @@ +package main + +import ( + "context" + "fmt" + "log" + + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/agent/workflowagents/parallelagent" + "google.golang.org/adk/agent/workflowagents/sequentialagent" + "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/sessionservice" + "google.golang.org/genai" +) + +const ( + appName = "ParallelResearchAgent" + userID = "test_user_789" + modelName = "gemini-1.5-flash" +) + +func main() { + if err := runAgent("Research the latest trends in renewable energy, electric vehicles, and carbon capture."); err != nil { + log.Fatalf("Agent execution failed: %v", err) + } +} + +func runAgent(prompt string) error { + ctx := context.Background() + + // init_start + model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + return fmt.Errorf("failed to create model: %v", err) + } + + createResearcherAgent := func(topic, outputKey string) (agent.Agent, error) { + return llmagent.New(llmagent.Config{ + Name: fmt.Sprintf("ResearcherAgent-%s", topic), + Model: model, + Description: fmt.Sprintf("Researches the topic of %s.", topic), + Instruction: fmt.Sprintf("You are a research assistant. Your task is to research the following topic: %s. Summarize your findings.", topic), + }) + } + + researcher1, err := createResearcherAgent("renewable energy", "renewable_energy_summary") + if err != nil { + return err + } + researcher2, err := createResearcherAgent("electric vehicle technology", "ev_summary") + if err != nil { + return err + } + researcher3, err := createResearcherAgent("carbon capture methods", "carbon_capture_summary") + if err != nil { + return err + } + + parallelResearchAgent, err := parallelagent.New(parallelagent.Config{ + AgentConfig: agent.Config{ + Name: "ParallelResearcher", + Description: "Runs multiple researchers concurrently.", + SubAgents: []agent.Agent{researcher1, researcher2, researcher3}, + }, + }) + if err != nil { + return fmt.Errorf("failed to create parallel agent: %v", err) + } + + summaryAgent, err := llmagent.New(llmagent.Config{ + Name: "SummaryAgent", + Model: model, + Description: "Summarizes the research findings from all researchers.", + Instruction: `You are a summary assistant. +Combine the following research summaries into a single, coherent report: +- Renewable Energy: {renewable_energy_summary} +- Electric Vehicles: {ev_summary} +- Carbon Capture: {carbon_capture_summary}`, + }) + if err != nil { + return fmt.Errorf("failed to create summary agent: %v", err) + } + + pipeline, err := sequentialagent.New(sequentialagent.Config{ + AgentConfig: agent.Config{ + Name: "ResearchPipeline", + Description: "Runs a parallel research task and then summarizes the results.", + SubAgents: []agent.Agent{parallelResearchAgent, summaryAgent}, + }, + }) + if err != nil { + return fmt.Errorf("failed to create sequential agent pipeline: %v", err) + } + // init_end + + sessionService := sessionservice.Mem() + r, err := runner.New(&runner.Config{ + AppName: appName, + Agent: pipeline, + SessionService: sessionService, + }) + if err != nil { + return fmt.Errorf("failed to create runner: %v", err) + } + + session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ + AppName: appName, + UserID: userID, + }) + if err != nil { + return fmt.Errorf("failed to create session: %v", err) + } + + userMsg := &genai.Content{ + Parts: []*genai.Part{{Text: prompt}}, + Role: string(genai.RoleUser), + } + + fmt.Printf("Running parallel research pipeline for prompt: %q\n---\n", prompt) + for event, err := range r.Run(ctx, userID, session.Session.ID().SessionID, userMsg, nil) { + if err != nil { + return fmt.Errorf("error during agent execution: %v", err) + } + for _, p := range event.LLMResponse.Content.Parts { + fmt.Print(p.Text) + } + } + fmt.Println("\n---\nPipeline finished.") + return nil +} diff --git a/examples/go/snippets/agents/workflow-agents/sequential_agent_code_development_agent.go b/examples/go/snippets/agents/workflow-agents/sequential/main.go similarity index 97% rename from examples/go/snippets/agents/workflow-agents/sequential_agent_code_development_agent.go rename to examples/go/snippets/agents/workflow-agents/sequential/main.go index a1a93489..2c60087e 100644 --- a/examples/go/snippets/agents/workflow-agents/sequential_agent_code_development_agent.go +++ b/examples/go/snippets/agents/workflow-agents/sequential/main.go @@ -43,6 +43,7 @@ func runAgent(prompt string) error { Based *only* on the user's request, write Go code that fulfills the requirement. Output *only* the complete Go code block, enclosed in triple backticks ('''go ... '''). Do not add any other text before or after the code block.`, + // OutputKey: "generated_code", }) if err != nil { return fmt.Errorf("failed to create code writer agent: %v", err) @@ -71,6 +72,7 @@ Your task is to provide constructive feedback on the provided code. Provide your feedback as a concise, bulleted list. Focus on the most important points for improvement. If the code is excellent and requires no changes, simply state: "No major issues found." Output *only* the review comments or the "No major issues" statement.`, + // OutputKey: "review_comments", }) if err != nil { return fmt.Errorf("failed to create code reviewer agent: %v", err) @@ -99,6 +101,7 @@ Ensure the final code is complete, functional, and includes necessary imports. **Output:** Output *only* the final, refactored Go code block, enclosed in triple backticks ('''go ... '''). Do not add any other text before or after the code block.`, + // OutputKey: "refactored_code", }) if err != nil { return fmt.Errorf("failed to create code refactorer agent: %v", err) @@ -144,7 +147,6 @@ Do not add any other text before or after the code block.`, } fmt.Printf("Running agent pipeline for prompt: %q\n---\n", prompt) - for event, err := range r.Run(ctx, userID, session.Session.ID().SessionID, userMsg, &runner.RunConfig{ StreamingMode: runner.StreamingModeSSE, }) { @@ -159,4 +161,4 @@ Do not add any other text before or after the code block.`, } fmt.Println("\n---\nPipeline finished.") return nil -} \ No newline at end of file +} From 92b26259491cefd65d63f2f5adc33ffec74961bc Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Thu, 9 Oct 2025 18:06:13 -0400 Subject: [PATCH 036/125] Added sequential agent --- .../agents/workflow-agents/sequential/main.go | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/go/snippets/agents/workflow-agents/sequential/main.go b/examples/go/snippets/agents/workflow-agents/sequential/main.go index 2c60087e..613b1a14 100644 --- a/examples/go/snippets/agents/workflow-agents/sequential/main.go +++ b/examples/go/snippets/agents/workflow-agents/sequential/main.go @@ -8,9 +8,9 @@ import ( "google.golang.org/adk/agent" "google.golang.org/adk/agent/llmagent" "google.golang.org/adk/agent/workflowagents/sequentialagent" - "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/model/gemini" "google.golang.org/adk/runner" - "google.golang.org/adk/sessionservice" + "google.golang.org/adk/session" "google.golang.org/genai" ) @@ -123,8 +123,8 @@ Do not add any other text before or after the code block.`, } // init_end - sessionService := sessionservice.Mem() - r, err := runner.New(&runner.Config{ + sessionService := session.InMemoryService() + r, err := runner.New(runner.Config{ AppName: appName, Agent: codePipelineAgent, SessionService: sessionService, @@ -133,7 +133,7 @@ Do not add any other text before or after the code block.`, return fmt.Errorf("failed to create runner: %v", err) } - session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ + session, err := sessionService.Create(ctx, &session.CreateRequest{ AppName: appName, UserID: userID, }) @@ -147,8 +147,8 @@ Do not add any other text before or after the code block.`, } fmt.Printf("Running agent pipeline for prompt: %q\n---\n", prompt) - for event, err := range r.Run(ctx, userID, session.Session.ID().SessionID, userMsg, &runner.RunConfig{ - StreamingMode: runner.StreamingModeSSE, + for event, err := range r.Run(ctx, userID, session.Session.ID(), userMsg, &agent.RunConfig{ + StreamingMode: agent.StreamingModeSSE, }) { if err != nil { return fmt.Errorf("error during agent execution: %v", err) From a54b5ff1e5ccf9501281a2272d9dfb2ba0985655 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Thu, 9 Oct 2025 18:12:04 -0400 Subject: [PATCH 037/125] Migrated agents to new API --- .../agents/workflow-agents/loop/main.go | 31 ++++++++++--------- .../agents/workflow-agents/parallel/main.go | 16 +++++----- 2 files changed, 26 insertions(+), 21 deletions(-) diff --git a/examples/go/snippets/agents/workflow-agents/loop/main.go b/examples/go/snippets/agents/workflow-agents/loop/main.go index 23c52c63..972bbe8b 100644 --- a/examples/go/snippets/agents/workflow-agents/loop/main.go +++ b/examples/go/snippets/agents/workflow-agents/loop/main.go @@ -8,9 +8,9 @@ import ( "google.golang.org/adk/agent" "google.golang.org/adk/agent/llmagent" "google.golang.org/adk/agent/workflowagents/loopagent" - "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/model/gemini" "google.golang.org/adk/runner" - "google.golang.org/adk/sessionservice" + "google.golang.org/adk/session" "google.golang.org/adk/tool" "google.golang.org/genai" ) @@ -18,21 +18,22 @@ import ( const ( appName = "DocImprovAgent" userID = "test_user_123" - modelName = "gemini-1.5-flash" + modelName = "gemini-2.5-flash" ) // init_start // ExitLoopArgs defines the (empty) arguments for the ExitLoop tool. type ExitLoopArgs struct{} +// ExitLoopResults defines the output of the ExitLoop tool. +type ExitLoopResults struct { + Status string `json:"status"` +} + // ExitLoop is a tool that signals the loop to terminate by setting Escalate to true. -func ExitLoop(ctx context.Context, args ExitLoopArgs) (map[string]any, error) { - toolCtx, ok := tool.FromContext(ctx) - if !ok { - return nil, fmt.Errorf("tool.Context not found in context") - } - toolCtx.EventActions.Escalate = true - return map[string]any{"status": "exiting loop"}, nil +func ExitLoop(ctx tool.Context, input ExitLoopArgs) ExitLoopResults { + ctx.Actions().Escalate = true + return ExitLoopResults{Status: "exiting loop"} } func main() { @@ -110,8 +111,8 @@ Otherwise, provide constructive feedback for improvement.`, } // init_end - sessionService := sessionservice.Mem() - r, err := runner.New(&runner.Config{ + sessionService := session.InMemoryService() + r, err := runner.New(runner.Config{ AppName: appName, Agent: refinementLoop, SessionService: sessionService, @@ -120,7 +121,7 @@ Otherwise, provide constructive feedback for improvement.`, return fmt.Errorf("failed to create runner: %v", err) } - session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ + session, err := sessionService.Create(ctx, &session.CreateRequest{ AppName: appName, UserID: userID, }) @@ -134,7 +135,9 @@ Otherwise, provide constructive feedback for improvement.`, } fmt.Printf("Running agent loop for prompt: %q\n---\n", prompt) - for event, err := range r.Run(ctx, userID, session.Session.ID().SessionID, userMsg, nil) { + for event, err := range r.Run(ctx, userID, session.Session.ID(), userMsg, &agent.RunConfig{ + StreamingMode: agent.StreamingModeSSE, + }) { if err != nil { return fmt.Errorf("error during agent execution: %v", err) } diff --git a/examples/go/snippets/agents/workflow-agents/parallel/main.go b/examples/go/snippets/agents/workflow-agents/parallel/main.go index 772052ac..8a0052da 100644 --- a/examples/go/snippets/agents/workflow-agents/parallel/main.go +++ b/examples/go/snippets/agents/workflow-agents/parallel/main.go @@ -9,16 +9,16 @@ import ( "google.golang.org/adk/agent/llmagent" "google.golang.org/adk/agent/workflowagents/parallelagent" "google.golang.org/adk/agent/workflowagents/sequentialagent" - "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/model/gemini" "google.golang.org/adk/runner" - "google.golang.org/adk/sessionservice" + "google.golang.org/adk/session" "google.golang.org/genai" ) const ( appName = "ParallelResearchAgent" userID = "test_user_789" - modelName = "gemini-1.5-flash" + modelName = "gemini-2.5-flash" ) func main() { @@ -95,8 +95,8 @@ Combine the following research summaries into a single, coherent report: } // init_end - sessionService := sessionservice.Mem() - r, err := runner.New(&runner.Config{ + sessionService := session.InMemoryService() + r, err := runner.New(runner.Config{ AppName: appName, Agent: pipeline, SessionService: sessionService, @@ -105,7 +105,7 @@ Combine the following research summaries into a single, coherent report: return fmt.Errorf("failed to create runner: %v", err) } - session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ + session, err := sessionService.Create(ctx, &session.CreateRequest{ AppName: appName, UserID: userID, }) @@ -119,7 +119,9 @@ Combine the following research summaries into a single, coherent report: } fmt.Printf("Running parallel research pipeline for prompt: %q\n---\n", prompt) - for event, err := range r.Run(ctx, userID, session.Session.ID().SessionID, userMsg, nil) { + for event, err := range r.Run(ctx, userID, session.Session.ID(), userMsg, &agent.RunConfig{ + StreamingMode: agent.StreamingModeSSE, + }) { if err != nil { return fmt.Errorf("error during agent execution: %v", err) } From f32ef3f3c9c16a4513c4e8441242041b93e8259d Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Thu, 9 Oct 2025 19:35:43 -0400 Subject: [PATCH 038/125] Fixed snippet tags --- examples/go/snippets/agents/workflow-agents/loop/main.go | 4 ++-- examples/go/snippets/agents/workflow-agents/parallel/main.go | 4 ++-- .../go/snippets/agents/workflow-agents/sequential/main.go | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/go/snippets/agents/workflow-agents/loop/main.go b/examples/go/snippets/agents/workflow-agents/loop/main.go index 972bbe8b..013be554 100644 --- a/examples/go/snippets/agents/workflow-agents/loop/main.go +++ b/examples/go/snippets/agents/workflow-agents/loop/main.go @@ -21,7 +21,7 @@ const ( modelName = "gemini-2.5-flash" ) -// init_start +// --8<-- [start:init] // ExitLoopArgs defines the (empty) arguments for the ExitLoop tool. type ExitLoopArgs struct{} @@ -109,7 +109,7 @@ Otherwise, provide constructive feedback for improvement.`, if err != nil { return fmt.Errorf("failed to create loop agent: %v", err) } - // init_end + // --8<-- [end:init] sessionService := session.InMemoryService() r, err := runner.New(runner.Config{ diff --git a/examples/go/snippets/agents/workflow-agents/parallel/main.go b/examples/go/snippets/agents/workflow-agents/parallel/main.go index 8a0052da..0144bb9b 100644 --- a/examples/go/snippets/agents/workflow-agents/parallel/main.go +++ b/examples/go/snippets/agents/workflow-agents/parallel/main.go @@ -30,7 +30,7 @@ func main() { func runAgent(prompt string) error { ctx := context.Background() - // init_start + // --8<-- [start:init] model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) if err != nil { return fmt.Errorf("failed to create model: %v", err) @@ -93,7 +93,7 @@ Combine the following research summaries into a single, coherent report: if err != nil { return fmt.Errorf("failed to create sequential agent pipeline: %v", err) } - // init_end + // --8<-- [end:init] sessionService := session.InMemoryService() r, err := runner.New(runner.Config{ diff --git a/examples/go/snippets/agents/workflow-agents/sequential/main.go b/examples/go/snippets/agents/workflow-agents/sequential/main.go index 613b1a14..82abc688 100644 --- a/examples/go/snippets/agents/workflow-agents/sequential/main.go +++ b/examples/go/snippets/agents/workflow-agents/sequential/main.go @@ -29,7 +29,7 @@ func main() { func runAgent(prompt string) error { ctx := context.Background() - // init_start + // --8<-- [start:init] model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) if err != nil { return fmt.Errorf("failed to create model: %v", err) @@ -121,7 +121,7 @@ Do not add any other text before or after the code block.`, if err != nil { return fmt.Errorf("failed to create sequential agent: %v", err) } - // init_end + // --8<-- [end:init] sessionService := session.InMemoryService() r, err := runner.New(runner.Config{ From 430bfff242a805ac0687a8b5b25a89b1490f175f Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 10 Oct 2025 21:26:55 -0400 Subject: [PATCH 039/125] Updated parallel and sequential with OutputKey --- .../agents/workflow-agents/parallel/main.go | 142 +++++++++++++----- .../agents/workflow-agents/sequential/main.go | 6 +- 2 files changed, 110 insertions(+), 38 deletions(-) diff --git a/examples/go/snippets/agents/workflow-agents/parallel/main.go b/examples/go/snippets/agents/workflow-agents/parallel/main.go index 0144bb9b..d8a7c0c8 100644 --- a/examples/go/snippets/agents/workflow-agents/parallel/main.go +++ b/examples/go/snippets/agents/workflow-agents/parallel/main.go @@ -16,13 +16,13 @@ import ( ) const ( - appName = "ParallelResearchAgent" - userID = "test_user_789" - modelName = "gemini-2.5-flash" + appName = "parallel_research_app" + userID = "research_user_01" + modelName = "gemini-2.0-flash" ) func main() { - if err := runAgent("Research the latest trends in renewable energy, electric vehicles, and carbon capture."); err != nil { + if err := runAgent("Summarize recent sustainable tech advancements."); err != nil { log.Fatalf("Agent execution failed: %v", err) } } @@ -36,32 +36,55 @@ func runAgent(prompt string) error { return fmt.Errorf("failed to create model: %v", err) } - createResearcherAgent := func(topic, outputKey string) (agent.Agent, error) { - return llmagent.New(llmagent.Config{ - Name: fmt.Sprintf("ResearcherAgent-%s", topic), - Model: model, - Description: fmt.Sprintf("Researches the topic of %s.", topic), - Instruction: fmt.Sprintf("You are a research assistant. Your task is to research the following topic: %s. Summarize your findings.", topic), - }) - } - - researcher1, err := createResearcherAgent("renewable energy", "renewable_energy_summary") + // --- 1. Define Researcher Sub-Agents (to run in parallel) --- + researcher1, err := llmagent.New(llmagent.Config{ + Name: "RenewableEnergyResearcher", + Model: model, + Instruction: `You are an AI Research Assistant specializing in energy. +Research the latest advancements in 'renewable energy sources'. +Use the Google Search tool provided. +Summarize your key findings concisely (1-2 sentences). +Output *only* the summary.`, + Description: "Researches renewable energy sources.", + OutputKey: "renewable_energy_result", + }) if err != nil { return err } - researcher2, err := createResearcherAgent("electric vehicle technology", "ev_summary") + researcher2, err := llmagent.New(llmagent.Config{ + Name: "EVResearcher", + Model: model, + Instruction: `You are an AI Research Assistant specializing in transportation. +Research the latest developments in 'electric vehicle technology'. +Use the Google Search tool provided. +Summarize your key findings concisely (1-2 sentences). +Output *only* the summary.`, + Description: "Researches electric vehicle technology.", + OutputKey: "ev_technology_result", + }) if err != nil { return err } - researcher3, err := createResearcherAgent("carbon capture methods", "carbon_capture_summary") + researcher3, err := llmagent.New(llmagent.Config{ + Name: "CarbonCaptureResearcher", + Model: model, + Instruction: `You are an AI Research Assistant specializing in climate solutions. +Research the current state of 'carbon capture methods'. +Use the Google Search tool provided. +Summarize your key findings concisely (1-2 sentences). +Output *only* the summary.`, + Description: "Researches carbon capture methods.", + OutputKey: "carbon_capture_result", + }) if err != nil { return err } + // --- 2. Create the ParallelAgent (Runs researchers concurrently) --- parallelResearchAgent, err := parallelagent.New(parallelagent.Config{ AgentConfig: agent.Config{ - Name: "ParallelResearcher", - Description: "Runs multiple researchers concurrently.", + Name: "ParallelWebResearchAgent", + Description: "Runs multiple research agents in parallel to gather information.", SubAgents: []agent.Agent{researcher1, researcher2, researcher3}, }, }) @@ -69,25 +92,56 @@ func runAgent(prompt string) error { return fmt.Errorf("failed to create parallel agent: %v", err) } - summaryAgent, err := llmagent.New(llmagent.Config{ - Name: "SummaryAgent", - Model: model, - Description: "Summarizes the research findings from all researchers.", - Instruction: `You are a summary assistant. -Combine the following research summaries into a single, coherent report: -- Renewable Energy: {renewable_energy_summary} -- Electric Vehicles: {ev_summary} -- Carbon Capture: {carbon_capture_summary}`, + // --- 3. Define the Merger Agent (Runs *after* the parallel agents) --- + synthesisAgent, err := llmagent.New(llmagent.Config{ + Name: "SynthesisAgent", + Model: model, + Instruction: `You are an AI Assistant responsible for combining research findings into a structured report. +Your primary task is to synthesize the following research summaries, clearly attributing findings to their source areas. Structure your response using headings for each topic. Ensure the report is coherent and integrates the key points smoothly. +**Crucially: Your entire response MUST be grounded *exclusively* on the information provided in the 'Input Summaries' below. Do NOT add any external knowledge, facts, or details not present in these specific summaries.** +**Input Summaries:** + +* **Renewable Energy:** + {renewable_energy_result} + +* **Electric Vehicles:** + {ev_technology_result} + +* **Carbon Capture:** + {carbon_capture_result} + +**Output Format:** + +## Summary of Recent Sustainable Technology Advancements + +### Renewable Energy Findings +(Based on RenewableEnergyResearcher's findings) +[Synthesize and elaborate *only* on the renewable energy input summary provided above.] + +### Electric Vehicle Findings +(Based on EVResearcher's findings) +[Synthesize and elaborate *only* on the EV input summary provided above.] + +### Carbon Capture Findings +(Based on CarbonCaptureResearcher's findings) +[Synthesize and elaborate *only* on the carbon capture input summary provided above.] + +### Overall Conclusion +[Provide a brief (1-2 sentence) concluding statement that connects *only* the findings presented above.] + +Output *only* the structured report following this format. Do not include introductory or concluding phrases outside this structure, and strictly adhere to using only the provided input summary content.`, + Description: "Combines research findings from parallel agents into a structured, cited report, strictly grounded on provided inputs.", }) if err != nil { - return fmt.Errorf("failed to create summary agent: %v", err) + return fmt.Errorf("failed to create synthesis agent: %v", err) } + // --- 4. Create the SequentialAgent (Orchestrates the overall flow) --- pipeline, err := sequentialagent.New(sequentialagent.Config{ AgentConfig: agent.Config{ - Name: "ResearchPipeline", - Description: "Runs a parallel research task and then summarizes the results.", - SubAgents: []agent.Agent{parallelResearchAgent, summaryAgent}, + Name: "ResearchAndSynthesisPipeline", + Description: "Coordinates parallel research and synthesizes the results.", + SubAgents: []agent.Agent{parallelResearchAgent, synthesisAgent}, }, }) if err != nil { @@ -118,15 +172,33 @@ Combine the following research summaries into a single, coherent report: Role: string(genai.RoleUser), } - fmt.Printf("Running parallel research pipeline for prompt: %q\n---\n", prompt) + fmt.Printf("Running Research & Synthesis Pipeline for query: %q\n---\n", prompt) + researcherNames := map[string]bool{ + "RenewableEnergyResearcher": true, + "EVResearcher": true, + "CarbonCaptureResearcher": true, + } + synthesisAgentName := "SynthesisAgent" + for event, err := range r.Run(ctx, userID, session.Session.ID(), userMsg, &agent.RunConfig{ StreamingMode: agent.StreamingModeSSE, }) { if err != nil { return fmt.Errorf("error during agent execution: %v", err) - } - for _, p := range event.LLMResponse.Content.Parts { - fmt.Print(p.Text) + } else if event.Partial { + if _, ok := researcherNames[event.Author]; ok { + fmt.Printf(" -> Intermediate Result from %s:\n", event.Author) + for _, p := range event.LLMResponse.Content.Parts { + fmt.Print(p.Text) + } + fmt.Println() + } else if event.Author == synthesisAgentName { + fmt.Printf("\n<<< Final Synthesized Response (from %s):\n", event.Author) + for _, p := range event.LLMResponse.Content.Parts { + fmt.Print(p.Text) + } + fmt.Println() + } } } fmt.Println("\n---\nPipeline finished.") diff --git a/examples/go/snippets/agents/workflow-agents/sequential/main.go b/examples/go/snippets/agents/workflow-agents/sequential/main.go index 82abc688..a26dab75 100644 --- a/examples/go/snippets/agents/workflow-agents/sequential/main.go +++ b/examples/go/snippets/agents/workflow-agents/sequential/main.go @@ -43,7 +43,7 @@ func runAgent(prompt string) error { Based *only* on the user's request, write Go code that fulfills the requirement. Output *only* the complete Go code block, enclosed in triple backticks ('''go ... '''). Do not add any other text before or after the code block.`, - // OutputKey: "generated_code", + OutputKey: "generated_code", }) if err != nil { return fmt.Errorf("failed to create code writer agent: %v", err) @@ -72,7 +72,7 @@ Your task is to provide constructive feedback on the provided code. Provide your feedback as a concise, bulleted list. Focus on the most important points for improvement. If the code is excellent and requires no changes, simply state: "No major issues found." Output *only* the review comments or the "No major issues" statement.`, - // OutputKey: "review_comments", + OutputKey: "review_comments", }) if err != nil { return fmt.Errorf("failed to create code reviewer agent: %v", err) @@ -101,7 +101,7 @@ Ensure the final code is complete, functional, and includes necessary imports. **Output:** Output *only* the final, refactored Go code block, enclosed in triple backticks ('''go ... '''). Do not add any other text before or after the code block.`, - // OutputKey: "refactored_code", + OutputKey: "refactored_code", }) if err != nil { return fmt.Errorf("failed to create code refactorer agent: %v", err) From c401233f6d6e33256636b64132780f231f3dea8c Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 10 Oct 2025 21:35:37 -0400 Subject: [PATCH 040/125] Updated loop agent --- .../agents/workflow-agents/loop/main.go | 156 ++++++++++++------ 1 file changed, 109 insertions(+), 47 deletions(-) diff --git a/examples/go/snippets/agents/workflow-agents/loop/main.go b/examples/go/snippets/agents/workflow-agents/loop/main.go index 013be554..b6d98f41 100644 --- a/examples/go/snippets/agents/workflow-agents/loop/main.go +++ b/examples/go/snippets/agents/workflow-agents/loop/main.go @@ -4,10 +4,12 @@ import ( "context" "fmt" "log" + "strings" "google.golang.org/adk/agent" "google.golang.org/adk/agent/llmagent" "google.golang.org/adk/agent/workflowagents/loopagent" + "google.golang.org/adk/agent/workflowagents/sequentialagent" "google.golang.org/adk/model/gemini" "google.golang.org/adk/runner" "google.golang.org/adk/session" @@ -16,9 +18,12 @@ import ( ) const ( - appName = "DocImprovAgent" - userID = "test_user_123" - modelName = "gemini-2.5-flash" + appName = "IterativeWritingPipeline" + userID = "test_user_456" + modelName = "gemini-2.5-flash" + stateDoc = "current_document" + stateCrit = "criticism" + donePhrase = "No major issues found." ) // --8<-- [start:init] @@ -26,18 +31,17 @@ const ( type ExitLoopArgs struct{} // ExitLoopResults defines the output of the ExitLoop tool. -type ExitLoopResults struct { - Status string `json:"status"` -} +type ExitLoopResults struct{} // ExitLoop is a tool that signals the loop to terminate by setting Escalate to true. func ExitLoop(ctx tool.Context, input ExitLoopArgs) ExitLoopResults { + fmt.Printf("[Tool Call] exitLoop triggered by %s \n", ctx.AgentName()) ctx.Actions().Escalate = true - return ExitLoopResults{Status: "exiting loop"} + return ExitLoopResults{} } func main() { - if err := runAgent("Write a short document about the benefits of exercise."); err != nil { + if err := runAgent("Write a document about a cat"); err != nil { log.Fatalf("Agent execution failed: %v", err) } } @@ -50,28 +54,47 @@ func runAgent(prompt string) error { return fmt.Errorf("failed to create model: %v", err) } - writerAgent, err := llmagent.New(llmagent.Config{ - Name: "WriterAgent", + // STEP 1: Initial Writer Agent (Runs ONCE at the beginning) + initialWriterAgent, err := llmagent.New(llmagent.Config{ + Name: "InitialWriterAgent", Model: model, - Description: "Generates and refines a document.", - Instruction: `You are a document writer. -Based on the user's request and any feedback from the critic, write or revise the document. -The current document is: -{document} - -Critic's feedback: -{feedback} + Description: "Writes the initial document draft based on the topic.", + Instruction: `You are a Creative Writing Assistant tasked with starting a story. +Write the *first draft* of a short story (aim for 2-4 sentences). +Base the content *only* on the topic provided in the user's prompt. +Output *only* the story/document text. Do not add introductions or explanations.`, + OutputKey: stateDoc, + }) + if err != nil { + return fmt.Errorf("failed to create initial writer agent: %v", err) + } -Rewrite the document to address the feedback. If there is no feedback, write the initial draft.`, + // STEP 2a: Critic Agent (Inside the Refinement Loop) + criticAgentInLoop, err := llmagent.New(llmagent.Config{ + Name: "CriticAgent", + Model: model, + Description: "Reviews the current draft, providing critique or signaling completion.", + Instruction: fmt.Sprintf(`You are a Constructive Critic AI reviewing a short document draft. +**Document to Review:** +{`+"`"+`}{`+"`"+`}{`+"`"+`} +{%s} +{`+"`"+`}{`+"`"+`}{`+"`"+`} +**Task:** +Review the document. +IF you identify 1-2 *clear and actionable* ways it could be improved: +Provide these specific suggestions concisely. Output *only* the critique text. +ELSE IF the document is coherent and addresses the topic adequately: +Respond *exactly* with the phrase "%s" and nothing else.`, stateDoc, donePhrase), + OutputKey: stateCrit, }) if err != nil { - return fmt.Errorf("failed to create writer agent: %v", err) + return fmt.Errorf("failed to create critic agent: %v", err) } exitLoopTool, err := tool.NewFunctionTool( tool.FunctionToolConfig{ - Name: "ExitLoop", - Description: "Signals the loop to terminate when the document is well-written and complete.", + Name: "exitLoop", + Description: "Call this function ONLY when the critique indicates no further changes are needed.", }, ExitLoop, ) @@ -79,42 +102,61 @@ Rewrite the document to address the feedback. If there is no feedback, write the return fmt.Errorf("failed to create exit loop tool: %v", err) } - criticAgent, err := llmagent.New(llmagent.Config{ - Name: "CriticAgent", - Model: model, - Description: "Critiques the document and decides if it's good enough.", - Instruction: `You are a document critic. -Review the following document: -{document} - -If the document is well-written and complete, call the "ExitLoop" tool. -Otherwise, provide constructive feedback for improvement.`, - Tools: []tool.Tool{exitLoopTool}, + // STEP 2b: Refiner/Exiter Agent (Inside the Refinement Loop) + refinerAgentInLoop, err := llmagent.New(llmagent.Config{ + Name: "RefinerAgent", + Model: model, + Instruction: fmt.Sprintf(`You are a Creative Writing Assistant refining a document based on feedback OR exiting the process. +**Current Document:** + +""" +{%s} +""" + +**Critique/Suggestions:** +{%s} +**Task:** +Analyze the 'Critique/Suggestions'. +IF the critique is *exactly* "%s": +You MUST call the 'exitLoop' function. Do not output any text. +ELSE (the critique contains actionable feedback): +Carefully apply the suggestions to improve the 'Current Document'. Output *only* the refined document text.`, stateDoc, stateCrit, donePhrase), + Description: "Refines the document based on critique, or calls exitLoop if critique indicates completion.", + Tools: []tool.Tool{exitLoopTool}, + OutputKey: stateDoc, }) if err != nil { - return fmt.Errorf("failed to create critic agent: %v", err) + return fmt.Errorf("failed to create refiner agent: %v", err) } + // STEP 2: Refinement Loop Agent refinementLoop, err := loopagent.New(loopagent.Config{ AgentConfig: agent.Config{ - Name: "RefinementLoop", - Description: "Iteratively refines a document.", - SubAgents: []agent.Agent{ - writerAgent, - criticAgent, - }, + Name: "RefinementLoop", + SubAgents: []agent.Agent{criticAgentInLoop, refinerAgentInLoop}, }, MaxIterations: 5, }) if err != nil { return fmt.Errorf("failed to create loop agent: %v", err) } + + // STEP 3: Overall Sequential Pipeline + iterativeWriterAgent, err := sequentialagent.New(sequentialagent.Config{ + AgentConfig: agent.Config{ + Name: appName, + SubAgents: []agent.Agent{initialWriterAgent, refinementLoop}, + }, + }) + if err != nil { + return fmt.Errorf("failed to create sequential agent pipeline: %v", err) + } // --8<-- [end:init] sessionService := session.InMemoryService() r, err := runner.New(runner.Config{ AppName: appName, - Agent: refinementLoop, + Agent: iterativeWriterAgent, SessionService: sessionService, }) if err != nil { @@ -134,17 +176,37 @@ Otherwise, provide constructive feedback for improvement.`, Role: string(genai.RoleUser), } - fmt.Printf("Running agent loop for prompt: %q\n---\n", prompt) + fmt.Printf("---" + " Starting Iterative Writing Pipeline for topic: %q ---" + "\n", prompt) + loopIteration := 0 + for event, err := range r.Run(ctx, userID, session.Session.ID(), userMsg, &agent.RunConfig{ - StreamingMode: agent.StreamingModeSSE, + StreamingMode: agent.StreamingModeNone, }) { if err != nil { return fmt.Errorf("error during agent execution: %v", err) + } + outputText := "" + for _, p := range event.Content.Parts { + outputText += p.Text } - for _, p := range event.LLMResponse.Content.Parts { - fmt.Print(p.Text) + outputText = strings.TrimSpace(outputText) + + switch event.Author { + case "InitialWriterAgent": + fmt.Printf("\n[Initial Draft] By %s (%s):\n%s\n", event.Author, stateDoc, outputText) + case "CriticAgent": + loopIteration++ + fmt.Printf("\n[Loop Iteration %d] Critique by %s (%s):\n%s\n", loopIteration, event.Author, stateCrit, outputText) + case "RefinerAgent": + if !event.Actions.Escalate { + fmt.Printf("[Loop Iteration %d] Refinement by %s (%s):\n%s\n", loopIteration, event.Author, stateDoc, outputText) + } + } + + if event.Actions.Escalate { + fmt.Println("\n--- Refinement Loop terminated (Escalation detected) ---") } } - fmt.Println("\n---\nLoop finished.") + fmt.Printf("\n--- Pipeline Finished ---\n") return nil -} \ No newline at end of file +} From a8a94a6ceaa08e362b9e1a081faa2e3a2ef7ac9c Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 10 Oct 2025 21:50:27 -0400 Subject: [PATCH 041/125] Cleanup --- .../agents/workflow-agents/loop/main.go | 5 ++-- .../agents/workflow-agents/parallel/main.go | 28 +++++++++---------- .../agents/workflow-agents/sequential/main.go | 4 +-- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/examples/go/snippets/agents/workflow-agents/loop/main.go b/examples/go/snippets/agents/workflow-agents/loop/main.go index b6d98f41..d0ab69d2 100644 --- a/examples/go/snippets/agents/workflow-agents/loop/main.go +++ b/examples/go/snippets/agents/workflow-agents/loop/main.go @@ -76,9 +76,9 @@ Output *only* the story/document text. Do not add introductions or explanations. Description: "Reviews the current draft, providing critique or signaling completion.", Instruction: fmt.Sprintf(`You are a Constructive Critic AI reviewing a short document draft. **Document to Review:** -{`+"`"+`}{`+"`"+`}{`+"`"+`} +""" {%s} -{`+"`"+`}{`+"`"+`}{`+"`"+`} +""" **Task:** Review the document. IF you identify 1-2 *clear and actionable* ways it could be improved: @@ -185,6 +185,7 @@ Carefully apply the suggestions to improve the 'Current Document'. Output *only* if err != nil { return fmt.Errorf("error during agent execution: %v", err) } + outputText := "" for _, p := range event.Content.Parts { outputText += p.Text diff --git a/examples/go/snippets/agents/workflow-agents/parallel/main.go b/examples/go/snippets/agents/workflow-agents/parallel/main.go index d8a7c0c8..ea2085c0 100644 --- a/examples/go/snippets/agents/workflow-agents/parallel/main.go +++ b/examples/go/snippets/agents/workflow-agents/parallel/main.go @@ -181,24 +181,24 @@ Output *only* the structured report following this format. Do not include introd synthesisAgentName := "SynthesisAgent" for event, err := range r.Run(ctx, userID, session.Session.ID(), userMsg, &agent.RunConfig{ - StreamingMode: agent.StreamingModeSSE, + StreamingMode: agent.StreamingModeNone, }) { if err != nil { return fmt.Errorf("error during agent execution: %v", err) - } else if event.Partial { - if _, ok := researcherNames[event.Author]; ok { - fmt.Printf(" -> Intermediate Result from %s:\n", event.Author) - for _, p := range event.LLMResponse.Content.Parts { - fmt.Print(p.Text) - } - fmt.Println() - } else if event.Author == synthesisAgentName { - fmt.Printf("\n<<< Final Synthesized Response (from %s):\n", event.Author) - for _, p := range event.LLMResponse.Content.Parts { - fmt.Print(p.Text) - } - fmt.Println() + } + + if _, ok := researcherNames[event.Author]; ok { + fmt.Printf(" -> Intermediate Result from %s:\n", event.Author) + for _, p := range event.Content.Parts { + fmt.Print(p.Text) + } + fmt.Println() + } else if event.Author == synthesisAgentName { + fmt.Printf("\n<<< Final Synthesized Response (from %s):\n", event.Author) + for _, p := range event.Content.Parts { + fmt.Print(p.Text) } + fmt.Println() } } fmt.Println("\n---\nPipeline finished.") diff --git a/examples/go/snippets/agents/workflow-agents/sequential/main.go b/examples/go/snippets/agents/workflow-agents/sequential/main.go index a26dab75..70e44640 100644 --- a/examples/go/snippets/agents/workflow-agents/sequential/main.go +++ b/examples/go/snippets/agents/workflow-agents/sequential/main.go @@ -148,14 +148,14 @@ Do not add any other text before or after the code block.`, fmt.Printf("Running agent pipeline for prompt: %q\n---\n", prompt) for event, err := range r.Run(ctx, userID, session.Session.ID(), userMsg, &agent.RunConfig{ - StreamingMode: agent.StreamingModeSSE, + StreamingMode: agent.StreamingModeNone, }) { if err != nil { return fmt.Errorf("error during agent execution: %v", err) } // The Go runner streams all events. For this example, we print the text // from each part of the LLM response as it arrives. - for _, p := range event.LLMResponse.Content.Parts { + for _, p := range event.Content.Parts { fmt.Print(p.Text) } } From cae9698ff1887b5bd72cc9d4397104536a44d605 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 24 Oct 2025 11:07:44 -0400 Subject: [PATCH 042/125] Fixed and ran linter --- .../agents/workflow-agents/loop/main.go | 33 ++++++++++--------- .../agents/workflow-agents/parallel/main.go | 14 ++++---- .../agents/workflow-agents/sequential/main.go | 10 +++--- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/examples/go/snippets/agents/workflow-agents/loop/main.go b/examples/go/snippets/agents/workflow-agents/loop/main.go index d0ab69d2..f9d99099 100644 --- a/examples/go/snippets/agents/workflow-agents/loop/main.go +++ b/examples/go/snippets/agents/workflow-agents/loop/main.go @@ -14,16 +14,17 @@ import ( "google.golang.org/adk/runner" "google.golang.org/adk/session" "google.golang.org/adk/tool" + "google.golang.org/adk/tool/functiontool" "google.golang.org/genai" ) const ( - appName = "IterativeWritingPipeline" - userID = "test_user_456" - modelName = "gemini-2.5-flash" - stateDoc = "current_document" - stateCrit = "criticism" - donePhrase = "No major issues found." + appName = "IterativeWritingPipeline" + userID = "test_user_456" + modelName = "gemini-2.5-flash" + stateDoc = "current_document" + stateCrit = "criticism" + donePhrase = "No major issues found." ) // --8<-- [start:init] @@ -41,14 +42,14 @@ func ExitLoop(ctx tool.Context, input ExitLoopArgs) ExitLoopResults { } func main() { - if err := runAgent("Write a document about a cat"); err != nil { + ctx := context.Background() + + if err := runAgent(ctx, "Write a document about a cat"); err != nil { log.Fatalf("Agent execution failed: %v", err) } } -func runAgent(prompt string) error { - ctx := context.Background() - +func runAgent(ctx context.Context, prompt string) error { model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) if err != nil { return fmt.Errorf("failed to create model: %v", err) @@ -91,8 +92,8 @@ Respond *exactly* with the phrase "%s" and nothing else.`, stateDoc, donePhrase) return fmt.Errorf("failed to create critic agent: %v", err) } - exitLoopTool, err := tool.NewFunctionTool( - tool.FunctionToolConfig{ + exitLoopTool, err := functiontool.New( + functiontool.Config{ Name: "exitLoop", Description: "Call this function ONLY when the critique indicates no further changes are needed.", }, @@ -176,16 +177,16 @@ Carefully apply the suggestions to improve the 'Current Document'. Output *only* Role: string(genai.RoleUser), } - fmt.Printf("---" + " Starting Iterative Writing Pipeline for topic: %q ---" + "\n", prompt) + fmt.Printf("---"+" Starting Iterative Writing Pipeline for topic: %q ---"+"\n", prompt) loopIteration := 0 - for event, err := range r.Run(ctx, userID, session.Session.ID(), userMsg, &agent.RunConfig{ + for event, err := range r.Run(ctx, userID, session.Session.ID(), userMsg, agent.RunConfig{ StreamingMode: agent.StreamingModeNone, }) { if err != nil { return fmt.Errorf("error during agent execution: %v", err) - } - + } + outputText := "" for _, p := range event.Content.Parts { outputText += p.Text diff --git a/examples/go/snippets/agents/workflow-agents/parallel/main.go b/examples/go/snippets/agents/workflow-agents/parallel/main.go index ea2085c0..60fe71e1 100644 --- a/examples/go/snippets/agents/workflow-agents/parallel/main.go +++ b/examples/go/snippets/agents/workflow-agents/parallel/main.go @@ -22,14 +22,14 @@ const ( ) func main() { - if err := runAgent("Summarize recent sustainable tech advancements."); err != nil { + ctx := context.Background() + + if err := runAgent(ctx, "Summarize recent sustainable tech advancements."); err != nil { log.Fatalf("Agent execution failed: %v", err) } } -func runAgent(prompt string) error { - ctx := context.Background() - +func runAgent(ctx context.Context, prompt string) error { // --8<-- [start:init] model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) if err != nil { @@ -94,7 +94,7 @@ Output *only* the summary.`, // --- 3. Define the Merger Agent (Runs *after* the parallel agents) --- synthesisAgent, err := llmagent.New(llmagent.Config{ - Name: "SynthesisAgent", + Name: "SynthesisAgent", Model: model, Instruction: `You are an AI Assistant responsible for combining research findings into a structured report. Your primary task is to synthesize the following research summaries, clearly attributing findings to their source areas. Structure your response using headings for each topic. Ensure the report is coherent and integrates the key points smoothly. @@ -180,12 +180,12 @@ Output *only* the structured report following this format. Do not include introd } synthesisAgentName := "SynthesisAgent" - for event, err := range r.Run(ctx, userID, session.Session.ID(), userMsg, &agent.RunConfig{ + for event, err := range r.Run(ctx, userID, session.Session.ID(), userMsg, agent.RunConfig{ StreamingMode: agent.StreamingModeNone, }) { if err != nil { return fmt.Errorf("error during agent execution: %v", err) - } + } if _, ok := researcherNames[event.Author]; ok { fmt.Printf(" -> Intermediate Result from %s:\n", event.Author) diff --git a/examples/go/snippets/agents/workflow-agents/sequential/main.go b/examples/go/snippets/agents/workflow-agents/sequential/main.go index 70e44640..1b3cc5a0 100644 --- a/examples/go/snippets/agents/workflow-agents/sequential/main.go +++ b/examples/go/snippets/agents/workflow-agents/sequential/main.go @@ -21,14 +21,14 @@ const ( ) func main() { - if err := runAgent("Write a Go function to calculate the factorial of a number."); err != nil { + ctx := context.Background() + + if err := runAgent(ctx, "Write a Go function to calculate the factorial of a number."); err != nil { log.Fatalf("Agent execution failed: %v", err) } } -func runAgent(prompt string) error { - ctx := context.Background() - +func runAgent(ctx context.Context, prompt string) error { // --8<-- [start:init] model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) if err != nil { @@ -147,7 +147,7 @@ Do not add any other text before or after the code block.`, } fmt.Printf("Running agent pipeline for prompt: %q\n---\n", prompt) - for event, err := range r.Run(ctx, userID, session.Session.ID(), userMsg, &agent.RunConfig{ + for event, err := range r.Run(ctx, userID, session.Session.ID(), userMsg, agent.RunConfig{ StreamingMode: agent.StreamingModeNone, }) { if err != nil { From d12c1ab19b8d2d17192b9bacbd7e559a420c3b0a Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 24 Oct 2025 11:09:37 -0400 Subject: [PATCH 043/125] Removed comment --- examples/go/snippets/agents/workflow-agents/sequential/main.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/examples/go/snippets/agents/workflow-agents/sequential/main.go b/examples/go/snippets/agents/workflow-agents/sequential/main.go index 1b3cc5a0..4e41487e 100644 --- a/examples/go/snippets/agents/workflow-agents/sequential/main.go +++ b/examples/go/snippets/agents/workflow-agents/sequential/main.go @@ -153,8 +153,7 @@ Do not add any other text before or after the code block.`, if err != nil { return fmt.Errorf("error during agent execution: %v", err) } - // The Go runner streams all events. For this example, we print the text - // from each part of the LLM response as it arrives. + for _, p := range event.Content.Parts { fmt.Print(p.Text) } From f95ae7043e5d9bbd92db122762ef78c74de30c89 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 29 Oct 2025 16:00:06 -0400 Subject: [PATCH 044/125] Ran linter --- examples/go/snippets/agents/workflow-agents/sequential/main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/go/snippets/agents/workflow-agents/sequential/main.go b/examples/go/snippets/agents/workflow-agents/sequential/main.go index 4e41487e..78709c05 100644 --- a/examples/go/snippets/agents/workflow-agents/sequential/main.go +++ b/examples/go/snippets/agents/workflow-agents/sequential/main.go @@ -153,7 +153,7 @@ Do not add any other text before or after the code block.`, if err != nil { return fmt.Errorf("error during agent execution: %v", err) } - + for _, p := range event.Content.Parts { fmt.Print(p.Text) } From 2d67e4195fd10b4e72d1e7a6830629b9c6d9a940 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 3 Oct 2025 22:41:47 -0400 Subject: [PATCH 045/125] Added custom agent snippets --- docs/agents/custom-agents.md | 83 ++++++ .../agents/custom-agent/storyflow_agent.go | 278 ++++++++++++++++++ 2 files changed, 361 insertions(+) create mode 100644 examples/go/snippets/agents/custom-agent/storyflow_agent.go diff --git a/docs/agents/custom-agents.md b/docs/agents/custom-agents.md index 33e1cf87..7c18c2b3 100644 --- a/docs/agents/custom-agents.md +++ b/docs/agents/custom-agents.md @@ -53,6 +53,15 @@ The core of any custom agent is the method where you define its unique asynchron * **Reactive Stream (`Flowable`):** It must return an `io.reactivex.rxjava3.core.Flowable`. This `Flowable` represents a stream of events that will be produced by the custom agent's logic, often by combining or transforming multiple `Flowable` from sub-agents. * **`ctx` (InvocationContext):** Provides access to crucial runtime information, most importantly `ctx.session().state()`, which is a `java.util.concurrent.ConcurrentMap`. This is the primary way to share data between steps orchestrated by your custom agent. +=== "Go" + + In Go, you implement the `Run` method of the `agent.Agent` interface. + + * **Signature:** `Run(ctx context.Context, userID, sessionID string, input *genai.Content, runConfig *runner.RunConfig) (<-chan runner.Event, <-chan error)` + * **Channels:** The `Run` method returns two channels: one for events (`runner.Event`) and one for errors. This is the standard way to handle asynchronous operations in Go. + * **`ctx` (Context):** The `context.Context` provides a way to manage cancellation and deadlines across API boundaries. + * **Session State:** You can access the session state through the `sessionservice`. + **Key Capabilities within the Core Asynchronous Method:** === "Python" @@ -127,6 +136,39 @@ The core of any custom agent is the method where you define its unique asynchron * **Conditional:** `Flowable.defer()` to choose which `Flowable` to subscribe to based on a condition, or `filter()` if you're filtering events within a stream. * **Iterative:** Operators like `repeat()`, `retry()`, or by structuring your `Flowable` chain to recursively call parts of itself based on conditions (often managed with `flatMapPublisher` or `concatMap`). +=== "Go" + + 1. **Calling Sub-Agents:** You invoke sub-agents by calling their `Run` method. + + ```go + // Example: Running one sub-agent + eventChan, errChan := someSubAgent.Run(ctx, userID, sessionID, input, runConfig) + // Handle events and errors + ``` + + 2. **Managing State:** Read from and write to the session state to pass data between sub-agent calls or make decisions. The `ctx` (`agent.Context`) is passed directly to your agent's `Run` function. + ```go + // Read data set by a previous agent + previousResult, err := ctx.Session().State().Get("some_key") + if err != nil { + // Handle cases where the key might not exist yet + } + + // Make a decision based on state + if val, ok := previousResult.(string); ok && val == "some_value" { + // ... call a specific sub-agent ... + } else { + // ... call another sub-agent ... + } + + // Store a result for a later step + if err := ctx.Session().State().Set("my_custom_result", "calculated_value"); err != nil { + // Handle error + } + ``` + + 3. **Implementing Control Flow:** Use standard Go constructs (`if`/`else`, `for`/`switch` loops, goroutines, channels) to create sophisticated, conditional, or iterative workflows involving your sub-agents. + ## Managing Sub-Agents and State Typically, a custom agent orchestrates other agents (like `LlmAgent`, `LoopAgent`, etc.). @@ -162,6 +204,14 @@ Let's illustrate the power of custom agents with an example pattern: a multi-sta ```java --8<-- "examples/java/snippets/src/main/java/agents/StoryFlowAgentExample.java:init" ``` + +=== "Go" + + We define the `StoryFlowAgent` struct and a constructor. In the constructor, we store the necessary sub-agents and tell the `BaseAgent` framework about the top-level agents this custom agent will directly orchestrate. + + ```go + --8<-- "examples/go/snippets/agents/custom-agent/storyflow_agent.go:init" + ``` --- ### Part 2: Defining the Custom Execution Logic { #part-2-defining-the-custom-execution-logic } @@ -195,6 +245,20 @@ Let's illustrate the power of custom agents with an example pattern: a multi-sta 3. Then, the `sequentialAgent's` Flowable executes. It calls the `grammar_check` then `tone_check`, reading `current_story` and writing `grammar_suggestions` and `tone_check_result` to the state. 4. **Custom Part:** After the sequentialAgent completes, logic within a `Flowable.defer` checks the "tone_check_result" from `invocationContext.session().state()`. If it's "negative", the `storyGenerator` Flowable is *conditionally concatenated* and executed again, overwriting "current_story". Otherwise, an empty Flowable is used, and the overall workflow proceeds to completion. +=== "Go" + + The `Run` method orchestrates the sub-agents using goroutines and channels for asynchronous control flow. + + ```go + --8<-- "examples/go/snippets/agents/custom-agent/storyflow_agent.go:executionlogic" + ``` + **Explanation of Logic:** + + 1. The initial `story_generator` runs. Its output is expected to be in the session state under the key `"current_story"`. + 2. The `loop_agent` runs, which internally calls the `critic` and `reviser` sequentially for `max_iterations` times. They read/write `current_story` and `criticism` from/to the state. + 3. The `sequential_agent` runs, calling `grammar_check` then `tone_check`, reading `current_story` and writing `grammar_suggestions` and `tone_check_result` to the state. + 4. **Custom Part:** The code checks the `tone_check_result` from the state. If it's "negative", the `story_generator` is called *again*, overwriting the `current_story` in the state. Otherwise, the flow ends. + --- ### Part 3: Defining the LLM Sub-Agents { #part-3-defining-the-llm-sub-agents } @@ -216,6 +280,12 @@ These are standard `LlmAgent` definitions, responsible for specific tasks. Their --8<-- "examples/java/snippets/src/main/java/agents/StoryFlowAgentExample.java:llmagents" ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/agents/custom-agent/storyflow_agent.go:llmagents" + ``` + --- ### Part 4: Instantiating and Running the custom agent { #part-4-instantiating-and-running-the-custom-agent } @@ -234,6 +304,12 @@ Finally, you instantiate your `StoryFlowAgent` and use the `Runner` as usual. --8<-- "examples/java/snippets/src/main/java/agents/StoryFlowAgentExample.java:story_flow_agent" ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/agents/custom-agent/storyflow_agent.go:story_flow_agent" + ``` + *(Note: The full runnable code, including imports and execution logic, can be found linked below.)* --- @@ -255,3 +331,10 @@ Finally, you instantiate your `StoryFlowAgent` and use the `Runner` as usual. # Full runnable code for the StoryFlowAgent example --8<-- "examples/java/snippets/src/main/java/agents/StoryFlowAgentExample.java:full_code" ``` + + === "Go" + + ```go + # Full runnable code for the StoryFlowAgent example + --8<-- "examples/go/snippets/agents/custom-agent/storyflow_agent.go:full_code" + ``` diff --git a/examples/go/snippets/agents/custom-agent/storyflow_agent.go b/examples/go/snippets/agents/custom-agent/storyflow_agent.go new file mode 100644 index 00000000..dbf6b3a1 --- /dev/null +++ b/examples/go/snippets/agents/custom-agent/storyflow_agent.go @@ -0,0 +1,278 @@ +// --8<-- [start:full_code] +package main + +import ( + "context" + "fmt" + "iter" + "log" + + "google.golang.org/adk/agent/workflowagents/loopagent" + "google.golang.org/adk/agent/workflowagents/sequentialagent" + + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/session" + "google.golang.org/adk/sessionservice" + "google.golang.org/genai" +) + +// --8<-- [start:init] +// StoryFlowAgent is a custom agent that orchestrates a story generation workflow. +// It encapsulates the logic of running sub-agents in a specific sequence. +type StoryFlowAgent struct { + storyGenerator agent.Agent + loopAgent agent.Agent + sequentialAgent agent.Agent +} + +// NewStoryFlowAgent creates and configures the entire custom agent workflow. +// It takes individual LLM agents as input and internally creates the necessary +// workflow agents (loop, sequential), returning the final orchestrator agent. +func NewStoryFlowAgent( + storyGenerator, + critic, + reviser, + grammarCheck, + toneCheck agent.Agent, +) (agent.Agent, error) { + loopAgent, err := loopagent.New(loopagent.Config{ + MaxIterations: 2, + AgentConfig: agent.Config{ + Name: "CriticReviserLoop", + SubAgents: []agent.Agent{critic, reviser}, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to create loop agent: %w", err) + } + + sequentialAgent, err := sequentialagent.New(sequentialagent.Config{ + AgentConfig: agent.Config{ + Name: "PostProcessing", + SubAgents: []agent.Agent{grammarCheck, toneCheck}, + }, + }) + if err != nil { + return nil, fmt.Errorf("failed to create sequential agent: %w", err) + } + + // The StoryFlowAgent struct holds the agents needed for the Run method. + orchestrator := &StoryFlowAgent{ + storyGenerator: storyGenerator, + loopAgent: loopAgent, + sequentialAgent: sequentialAgent, + } + + // agent.New creates the final agent, wiring up the Run method. + return agent.New(agent.Config{ + Name: "StoryFlowAgent", + Description: "Orchestrates story generation, critique, revision, and checks.", + SubAgents: []agent.Agent{storyGenerator, loopAgent, sequentialAgent}, + Run: orchestrator.Run, + }) +} +// --8<-- [end:init] + +// --8<-- [start:executionlogic] +// Run defines the custom execution logic for the StoryFlowAgent. +func (s *StoryFlowAgent) Run(ctx agent.Context) iter.Seq2[*session.Event, error] { + return func(yield func(*session.Event, error) bool) { + // Stage 1: Initial Story Generation + for event, err := range s.storyGenerator.Run(ctx) { + if err != nil { + yield(nil, fmt.Errorf("story generator failed: %w", err)) + return + } + if !yield(event, nil) { + return + } + } + + // Check if story was generated before proceeding + currentStory, err := ctx.Session().State().Get("current_story") + if err != nil || currentStory == "" { + log.Println("Failed to generate initial story. Aborting workflow.") + return + } + + // Stage 2: Critic-Reviser Loop + for event, err := range s.loopAgent.Run(ctx) { + if err != nil { + yield(nil, fmt.Errorf("loop agent failed: %w", err)) + return + } + if !yield(event, nil) { + return + } + } + + // Stage 3: Post-Processing + for event, err := range s.sequentialAgent.Run(ctx) { + if err != nil { + yield(nil, fmt.Errorf("sequential agent failed: %w", err)) + return + } + if !yield(event, nil) { + return + } + } + + // Stage 4: Conditional Regeneration + toneResult, err := ctx.Session().State().Get("tone_check_result") + if err != nil { + log.Printf("Could not read tone_check_result from state: %v. Assuming tone is not negative.", err) + return + } + + if tone, ok := toneResult.(string); ok && tone == "negative" { + log.Println("Tone is negative. Regenerating story...") + for event, err := range s.storyGenerator.Run(ctx) { + if err != nil { + yield(nil, fmt.Errorf("story regeneration failed: %w", err)) + return + } + if !yield(event, nil) { + return + } + } + } else { + log.Println("Tone is not negative. Keeping current story.") + } + } +} +// --8<-- [end:executionlogic] + +const ( + modelName = "gemini-2.0-flash" + appName = "story_app" + userID = "user_12345" +) + +func main() { + ctx := context.Background() + model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + log.Fatalf("Failed to create model: %v", err) + } + + // --8<-- [start:llmagents] + // --- Define the individual LLM agents --- + storyGenerator, err := llmagent.New(llmagent.Config{ + Name: "StoryGenerator", + Model: model, + Description: "Generates the initial story.", + Instruction: "You are a story writer. Write a short story (around 100 words) about a cat, based on the topic: {topic}", + // OutputKey: "current_story", + }) + if err != nil { + log.Fatalf("Failed to create StoryGenerator agent: %v", err) + } + + critic, err := llmagent.New(llmagent.Config{ + Name: "Critic", + Model: model, + Description: "Critiques the story.", + Instruction: "You are a story critic. Review the story: {current_story}. Provide 1-2 sentences of constructive criticism on how to improve it. Focus on plot or character.", + // OutputKey: "criticism", + }) + if err != nil { + log.Fatalf("Failed to create Critic agent: %v", err) + } + + reviser, err := llmagent.New(llmagent.Config{ + Name: "Reviser", + Model: model, + Description: "Revises the story based on criticism.", + Instruction: "You are a story reviser. Revise the story: {current_story}, based on the criticism: {criticism}. Output only the revised story.", + // OutputKey: "current_story", + }) + if err != nil { + log.Fatalf("Failed to create Reviser agent: %v", err) + } + + grammarCheck, err := llmagent.New(llmagent.Config{ + Name: "GrammarCheck", + Model: model, + Description: "Checks grammar and suggests corrections.", + Instruction: "You are a grammar checker. Check the grammar of the story: {current_story}. Output only the suggested corrections as a list, or output 'Grammar is good!' if there are no errors.", + // OutputKey: "grammar_suggestions", + }) + if err != nil { + log.Fatalf("Failed to create GrammarCheck agent: %v", err) + } + + toneCheck, err := llmagent.New(llmagent.Config{ + Name: "ToneCheck", + Model: model, + Description: "Analyzes the tone of the story.", + Instruction: "You are a tone analyzer. Analyze the tone of the story: {current_story}. Output only one word: 'positive' if the tone is generally positive, 'negative' if the tone is generally negative, or 'neutral' otherwise.", + // OutputKey: "tone_check_result", + }) + if err != nil { + log.Fatalf("Failed to create ToneCheck agent: %v", err) + } + // --8<-- [end:llmagents] + + // --8<-- [start:story_flow_agent] + // Instantiate the custom agent, which encapsulates the workflow agents. + storyFlowAgent, err := NewStoryFlowAgent( + storyGenerator, + critic, + reviser, + grammarCheck, + toneCheck, + ) + if err != nil { + log.Fatalf("Failed to create story flow agent: %v", err) + } + + // --- Run the Agent --- + sessionService := sessionservice.Mem() + initialState := map[string]any{ + "topic": "a brave kitten exploring a haunted house", + } + session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ + AppName: appName, + UserID: userID, + State: initialState, + }) + if err != nil { + log.Fatalf("Failed to create session: %v", err) + } + + userTopic := "a lonely robot finding a friend in a junkyard" + + r, err := runner.New(&runner.Config{ + AppName: appName, + Agent: storyFlowAgent, + SessionService: sessionService, + }) + if err != nil { + log.Fatalf("Failed to create runner: %v", err) + } + + input := genai.NewContentFromText("Generate a story about: " + userTopic, genai.RoleUser) + eventChan := r.Run(ctx, userID, session.Session.ID().SessionID, input, &runner.RunConfig{}) + + var finalResponse string + for event := range eventChan { + if event.LLMResponse != nil && event.LLMResponse.Content != nil { + for _, part := range event.LLMResponse.Content.Parts { + // Accumulate text from all parts of the final response. + finalResponse += part.Text + } + } + } + + fmt.Println("\n--- Agent Interaction Result ---") + fmt.Println("Agent Final Response: " + finalResponse) + + finalSession, _ := sessionService.Get(ctx, &sessionservice.GetRequest{ID: session.Session.ID()}) + fmt.Println("Final Session State:", finalSession.Session.State()) + fmt.Println("-------------------------------\n") +} +// --8<-- [end:story_flow_agent] +// --8<-- [end:full_code] \ No newline at end of file From 08c40582f9c57b9c198e8567116d5cd0fa4337ec Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Thu, 9 Oct 2025 17:01:36 -0400 Subject: [PATCH 046/125] Migrated to new API --- .../agents/custom-agent/storyflow_agent.go | 29 ++++++++++++------- 1 file changed, 19 insertions(+), 10 deletions(-) diff --git a/examples/go/snippets/agents/custom-agent/storyflow_agent.go b/examples/go/snippets/agents/custom-agent/storyflow_agent.go index dbf6b3a1..7a8b9e6c 100644 --- a/examples/go/snippets/agents/custom-agent/storyflow_agent.go +++ b/examples/go/snippets/agents/custom-agent/storyflow_agent.go @@ -12,10 +12,9 @@ import ( "google.golang.org/adk/agent" "google.golang.org/adk/agent/llmagent" - "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/model/gemini" "google.golang.org/adk/runner" "google.golang.org/adk/session" - "google.golang.org/adk/sessionservice" "google.golang.org/genai" ) @@ -78,7 +77,7 @@ func NewStoryFlowAgent( // --8<-- [start:executionlogic] // Run defines the custom execution logic for the StoryFlowAgent. -func (s *StoryFlowAgent) Run(ctx agent.Context) iter.Seq2[*session.Event, error] { +func (s *StoryFlowAgent) Run(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] { return func(yield func(*session.Event, error) bool) { // Stage 1: Initial Story Generation for event, err := range s.storyGenerator.Run(ctx) { @@ -230,11 +229,11 @@ func main() { } // --- Run the Agent --- - sessionService := sessionservice.Mem() + sessionService := session.InMemoryService() initialState := map[string]any{ "topic": "a brave kitten exploring a haunted house", } - session, err := sessionService.Create(ctx, &sessionservice.CreateRequest{ + sessionInstance, err := sessionService.Create(ctx, &session.CreateRequest{ AppName: appName, UserID: userID, State: initialState, @@ -245,7 +244,7 @@ func main() { userTopic := "a lonely robot finding a friend in a junkyard" - r, err := runner.New(&runner.Config{ + r, err := runner.New(runner.Config{ AppName: appName, Agent: storyFlowAgent, SessionService: sessionService, @@ -255,10 +254,12 @@ func main() { } input := genai.NewContentFromText("Generate a story about: " + userTopic, genai.RoleUser) - eventChan := r.Run(ctx, userID, session.Session.ID().SessionID, input, &runner.RunConfig{}) + events := r.Run(ctx, userID, sessionInstance.Session.ID(), input, &agent.RunConfig{ + StreamingMode: agent.StreamingModeSSE, + }) var finalResponse string - for event := range eventChan { + for event := range events { if event.LLMResponse != nil && event.LLMResponse.Content != nil { for _, part := range event.LLMResponse.Content.Parts { // Accumulate text from all parts of the final response. @@ -270,9 +271,17 @@ func main() { fmt.Println("\n--- Agent Interaction Result ---") fmt.Println("Agent Final Response: " + finalResponse) - finalSession, _ := sessionService.Get(ctx, &sessionservice.GetRequest{ID: session.Session.ID()}) + finalSession, err := sessionService.Get(ctx, &session.GetRequest{ + UserID: userID, + AppName: appName, + SessionID: sessionInstance.Session.ID(), + }) + + if err != nil { + log.Fatalf("Failed to retrieve final session: %v", err) + } + fmt.Println("Final Session State:", finalSession.Session.State()) - fmt.Println("-------------------------------\n") } // --8<-- [end:story_flow_agent] // --8<-- [end:full_code] \ No newline at end of file From 1d8bad013ac1ec1214924ac14889427dc68d8ee5 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Thu, 9 Oct 2025 17:18:58 -0400 Subject: [PATCH 047/125] Fixed incorrect info about channels since run returns an Iterator, not Channel --- docs/agents/custom-agents.md | 30 +++++++++++-------- .../agents/custom-agent/storyflow_agent.go | 5 +++- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/docs/agents/custom-agents.md b/docs/agents/custom-agents.md index 7c18c2b3..c48d3525 100644 --- a/docs/agents/custom-agents.md +++ b/docs/agents/custom-agents.md @@ -55,11 +55,11 @@ The core of any custom agent is the method where you define its unique asynchron === "Go" - In Go, you implement the `Run` method of the `agent.Agent` interface. + In Go, you implement the `Run` method as part of a struct that satisfies the `agent.Agent` interface. The actual logic is typically a method on your custom agent struct. - * **Signature:** `Run(ctx context.Context, userID, sessionID string, input *genai.Content, runConfig *runner.RunConfig) (<-chan runner.Event, <-chan error)` - * **Channels:** The `Run` method returns two channels: one for events (`runner.Event`) and one for errors. This is the standard way to handle asynchronous operations in Go. - * **`ctx` (Context):** The `context.Context` provides a way to manage cancellation and deadlines across API boundaries. + * **Signature:** `Run(ctx agent.InvocationContext) iter.Seq2[*session.Event, error]` + * **Iterator:** The `Run` method returns an iterator (`iter.Seq2`) that yields events and errors. This is the standard way to handle streaming results from an agent's execution. + * **`ctx` (InvocationContext):** The `agent.InvocationContext` provides access to the session, including state, and other crucial runtime information. * **Session State:** You can access the session state through the `sessionservice`. **Key Capabilities within the Core Asynchronous Method:** @@ -141,12 +141,18 @@ The core of any custom agent is the method where you define its unique asynchron 1. **Calling Sub-Agents:** You invoke sub-agents by calling their `Run` method. ```go - // Example: Running one sub-agent - eventChan, errChan := someSubAgent.Run(ctx, userID, sessionID, input, runConfig) - // Handle events and errors + // Example: Running one sub-agent and yielding its events + for event, err := range someSubAgent.Run(ctx) { + if err != nil { + // Handle or propagate the error + return + } + // Yield the event up to the caller + yield(event, nil) + } ``` - 2. **Managing State:** Read from and write to the session state to pass data between sub-agent calls or make decisions. The `ctx` (`agent.Context`) is passed directly to your agent's `Run` function. + // The `ctx` (`agent.InvocationContext`) is passed directly to your agent's `Run` function. ```go // Read data set by a previous agent previousResult, err := ctx.Session().State().Get("some_key") @@ -247,16 +253,16 @@ Let's illustrate the power of custom agents with an example pattern: a multi-sta === "Go" - The `Run` method orchestrates the sub-agents using goroutines and channels for asynchronous control flow. + The `Run` method orchestrates the sub-agents by calling their respective `Run` methods in a loop and yielding their events. ```go --8<-- "examples/go/snippets/agents/custom-agent/storyflow_agent.go:executionlogic" ``` **Explanation of Logic:** - 1. The initial `story_generator` runs. Its output is expected to be in the session state under the key `"current_story"`. - 2. The `loop_agent` runs, which internally calls the `critic` and `reviser` sequentially for `max_iterations` times. They read/write `current_story` and `criticism` from/to the state. - 3. The `sequential_agent` runs, calling `grammar_check` then `tone_check`, reading `current_story` and writing `grammar_suggestions` and `tone_check_result` to the state. + 1. The initial `storyGenerator` runs. Its output is expected to be in the session state under the key `"current_story"`. + 2. The `loopAgent` runs, which internally calls the `critic` and `reviser` sequentially for `max_iterations` times. They read/write `current_story` and `criticism` from/to the state. + 3. The `sequentialAgent` runs, calling `grammar_check` then `tone_check`, reading `current_story` and writing `grammar_suggestions` and `tone_check_result` to the state. 4. **Custom Part:** The code checks the `tone_check_result` from the state. If it's "negative", the `story_generator` is called *again*, overwriting the `current_story` in the state. Otherwise, the flow ends. --- diff --git a/examples/go/snippets/agents/custom-agent/storyflow_agent.go b/examples/go/snippets/agents/custom-agent/storyflow_agent.go index 7a8b9e6c..9a059919 100644 --- a/examples/go/snippets/agents/custom-agent/storyflow_agent.go +++ b/examples/go/snippets/agents/custom-agent/storyflow_agent.go @@ -259,7 +259,10 @@ func main() { }) var finalResponse string - for event := range events { + for event, err := range events { + if err != nil { + log.Fatalf("An error occurred during agent execution: %v", err) + } if event.LLMResponse != nil && event.LLMResponse.Content != nil { for _, part := range event.LLMResponse.Content.Parts { // Accumulate text from all parts of the final response. From 2ec04ecc16c35f9c541778a01c3dfdb305ad0e24 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 10 Oct 2025 20:53:16 -0400 Subject: [PATCH 048/125] Cleanup and renaming --- .../agents/custom-agent/storyflow_agent.go | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/examples/go/snippets/agents/custom-agent/storyflow_agent.go b/examples/go/snippets/agents/custom-agent/storyflow_agent.go index 9a059919..b5d0bffc 100644 --- a/examples/go/snippets/agents/custom-agent/storyflow_agent.go +++ b/examples/go/snippets/agents/custom-agent/storyflow_agent.go @@ -23,8 +23,8 @@ import ( // It encapsulates the logic of running sub-agents in a specific sequence. type StoryFlowAgent struct { storyGenerator agent.Agent - loopAgent agent.Agent - sequentialAgent agent.Agent + revisionLoopAgent agent.Agent + postProcessorAgent agent.Agent } // NewStoryFlowAgent creates and configures the entire custom agent workflow. @@ -61,8 +61,8 @@ func NewStoryFlowAgent( // The StoryFlowAgent struct holds the agents needed for the Run method. orchestrator := &StoryFlowAgent{ storyGenerator: storyGenerator, - loopAgent: loopAgent, - sequentialAgent: sequentialAgent, + revisionLoopAgent: loopAgent, + postProcessorAgent: sequentialAgent, } // agent.New creates the final agent, wiring up the Run method. @@ -98,7 +98,7 @@ func (s *StoryFlowAgent) Run(ctx agent.InvocationContext) iter.Seq2[*session.Eve } // Stage 2: Critic-Reviser Loop - for event, err := range s.loopAgent.Run(ctx) { + for event, err := range s.revisionLoopAgent.Run(ctx) { if err != nil { yield(nil, fmt.Errorf("loop agent failed: %w", err)) return @@ -109,7 +109,7 @@ func (s *StoryFlowAgent) Run(ctx agent.InvocationContext) iter.Seq2[*session.Eve } // Stage 3: Post-Processing - for event, err := range s.sequentialAgent.Run(ctx) { + for event, err := range s.postProcessorAgent.Run(ctx) { if err != nil { yield(nil, fmt.Errorf("sequential agent failed: %w", err)) return @@ -164,7 +164,7 @@ func main() { Model: model, Description: "Generates the initial story.", Instruction: "You are a story writer. Write a short story (around 100 words) about a cat, based on the topic: {topic}", - // OutputKey: "current_story", + OutputKey: "current_story", }) if err != nil { log.Fatalf("Failed to create StoryGenerator agent: %v", err) @@ -175,7 +175,7 @@ func main() { Model: model, Description: "Critiques the story.", Instruction: "You are a story critic. Review the story: {current_story}. Provide 1-2 sentences of constructive criticism on how to improve it. Focus on plot or character.", - // OutputKey: "criticism", + OutputKey: "criticism", }) if err != nil { log.Fatalf("Failed to create Critic agent: %v", err) @@ -186,7 +186,7 @@ func main() { Model: model, Description: "Revises the story based on criticism.", Instruction: "You are a story reviser. Revise the story: {current_story}, based on the criticism: {criticism}. Output only the revised story.", - // OutputKey: "current_story", + OutputKey: "current_story", }) if err != nil { log.Fatalf("Failed to create Reviser agent: %v", err) @@ -197,7 +197,7 @@ func main() { Model: model, Description: "Checks grammar and suggests corrections.", Instruction: "You are a grammar checker. Check the grammar of the story: {current_story}. Output only the suggested corrections as a list, or output 'Grammar is good!' if there are no errors.", - // OutputKey: "grammar_suggestions", + OutputKey: "grammar_suggestions", }) if err != nil { log.Fatalf("Failed to create GrammarCheck agent: %v", err) @@ -208,7 +208,7 @@ func main() { Model: model, Description: "Analyzes the tone of the story.", Instruction: "You are a tone analyzer. Analyze the tone of the story: {current_story}. Output only one word: 'positive' if the tone is generally positive, 'negative' if the tone is generally negative, or 'neutral' otherwise.", - // OutputKey: "tone_check_result", + OutputKey: "tone_check_result", }) if err != nil { log.Fatalf("Failed to create ToneCheck agent: %v", err) @@ -263,8 +263,8 @@ func main() { if err != nil { log.Fatalf("An error occurred during agent execution: %v", err) } - if event.LLMResponse != nil && event.LLMResponse.Content != nil { - for _, part := range event.LLMResponse.Content.Parts { + if event.LLMResponse != nil && event.Content != nil { + for _, part := range event.Content.Parts { // Accumulate text from all parts of the final response. finalResponse += part.Text } From deacff27fbeaf82ec6b1e67a6eb8945493829549 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 24 Oct 2025 11:18:19 -0400 Subject: [PATCH 049/125] Ran linter --- .../agents/custom-agent/storyflow_agent.go | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/examples/go/snippets/agents/custom-agent/storyflow_agent.go b/examples/go/snippets/agents/custom-agent/storyflow_agent.go index b5d0bffc..4e6af708 100644 --- a/examples/go/snippets/agents/custom-agent/storyflow_agent.go +++ b/examples/go/snippets/agents/custom-agent/storyflow_agent.go @@ -22,8 +22,8 @@ import ( // StoryFlowAgent is a custom agent that orchestrates a story generation workflow. // It encapsulates the logic of running sub-agents in a specific sequence. type StoryFlowAgent struct { - storyGenerator agent.Agent - revisionLoopAgent agent.Agent + storyGenerator agent.Agent + revisionLoopAgent agent.Agent postProcessorAgent agent.Agent } @@ -60,8 +60,8 @@ func NewStoryFlowAgent( // The StoryFlowAgent struct holds the agents needed for the Run method. orchestrator := &StoryFlowAgent{ - storyGenerator: storyGenerator, - revisionLoopAgent: loopAgent, + storyGenerator: storyGenerator, + revisionLoopAgent: loopAgent, postProcessorAgent: sequentialAgent, } @@ -73,6 +73,7 @@ func NewStoryFlowAgent( Run: orchestrator.Run, }) } + // --8<-- [end:init] // --8<-- [start:executionlogic] @@ -142,6 +143,7 @@ func (s *StoryFlowAgent) Run(ctx agent.InvocationContext) iter.Seq2[*session.Eve } } } + // --8<-- [end:executionlogic] const ( @@ -253,8 +255,8 @@ func main() { log.Fatalf("Failed to create runner: %v", err) } - input := genai.NewContentFromText("Generate a story about: " + userTopic, genai.RoleUser) - events := r.Run(ctx, userID, sessionInstance.Session.ID(), input, &agent.RunConfig{ + input := genai.NewContentFromText("Generate a story about: "+userTopic, genai.RoleUser) + events := r.Run(ctx, userID, sessionInstance.Session.ID(), input, agent.RunConfig{ StreamingMode: agent.StreamingModeSSE, }) @@ -263,11 +265,10 @@ func main() { if err != nil { log.Fatalf("An error occurred during agent execution: %v", err) } - if event.LLMResponse != nil && event.Content != nil { - for _, part := range event.Content.Parts { - // Accumulate text from all parts of the final response. - finalResponse += part.Text - } + + for _, part := range event.Content.Parts { + // Accumulate text from all parts of the final response. + finalResponse += part.Text } } @@ -286,5 +287,6 @@ func main() { fmt.Println("Final Session State:", finalSession.Session.State()) } + // --8<-- [end:story_flow_agent] -// --8<-- [end:full_code] \ No newline at end of file +// --8<-- [end:full_code] From b551d9fd93708bf998f3909f5d381070b7f15c2b Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 24 Oct 2025 11:26:19 -0400 Subject: [PATCH 050/125] Fixed md --- docs/agents/custom-agents.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/agents/custom-agents.md b/docs/agents/custom-agents.md index c48d3525..46e74e91 100644 --- a/docs/agents/custom-agents.md +++ b/docs/agents/custom-agents.md @@ -60,7 +60,7 @@ The core of any custom agent is the method where you define its unique asynchron * **Signature:** `Run(ctx agent.InvocationContext) iter.Seq2[*session.Event, error]` * **Iterator:** The `Run` method returns an iterator (`iter.Seq2`) that yields events and errors. This is the standard way to handle streaming results from an agent's execution. * **`ctx` (InvocationContext):** The `agent.InvocationContext` provides access to the session, including state, and other crucial runtime information. - * **Session State:** You can access the session state through the `sessionservice`. + * **Session State:** You can access the session state through `ctx.Session().State()`. **Key Capabilities within the Core Asynchronous Method:** @@ -152,8 +152,9 @@ The core of any custom agent is the method where you define its unique asynchron } ``` - // The `ctx` (`agent.InvocationContext`) is passed directly to your agent's `Run` function. + 2. **Managing State:** Read from and write to the session state to pass data between sub-agent calls or make decisions. ```go + // The `ctx` (`agent.InvocationContext`) is passed directly to your agent's `Run` function. // Read data set by a previous agent previousResult, err := ctx.Session().State().Get("some_key") if err != nil { @@ -261,8 +262,8 @@ Let's illustrate the power of custom agents with an example pattern: a multi-sta **Explanation of Logic:** 1. The initial `storyGenerator` runs. Its output is expected to be in the session state under the key `"current_story"`. - 2. The `loopAgent` runs, which internally calls the `critic` and `reviser` sequentially for `max_iterations` times. They read/write `current_story` and `criticism` from/to the state. - 3. The `sequentialAgent` runs, calling `grammar_check` then `tone_check`, reading `current_story` and writing `grammar_suggestions` and `tone_check_result` to the state. + 2. The `revisionLoopAgent` runs, which internally calls the `critic` and `reviser` sequentially for `max_iterations` times. They read/write `current_story` and `criticism` from/to the state. + 3. The `postProcessorAgent` runs, calling `grammar_check` then `tone_check`, reading `current_story` and writing `grammar_suggestions` and `tone_check_result` to the state. 4. **Custom Part:** The code checks the `tone_check_result` from the state. If it's "negative", the `story_generator` is called *again*, overwriting the `current_story` in the state. Otherwise, the flow ends. --- From 61ca974a881d361fa7c2fbdb2be349298f30d26f Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Sat, 4 Oct 2025 00:04:28 -0400 Subject: [PATCH 051/125] Added multi-agent docs --- docs/agents/multi-agents.md | 84 +++++ .../agents/multi-agent/multi_agent.go | 325 ++++++++++++++++++ 2 files changed, 409 insertions(+) create mode 100644 examples/go/snippets/agents/multi-agent/multi_agent.go diff --git a/docs/agents/multi-agents.md b/docs/agents/multi-agents.md index 48d3b032..a0bc4ea9 100644 --- a/docs/agents/multi-agents.md +++ b/docs/agents/multi-agents.md @@ -81,6 +81,12 @@ The foundation for structuring multi-agent systems is the parent-child relations // assert taskDoer.parentAgent().equals(coordinator); ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:hierarchy" + ``` + ### 1.2. Workflow Agents as Orchestrators { #workflow-agents-as-orchestrators } ADK includes specialized agents derived from `BaseAgent` that don't perform tasks themselves but orchestrate the execution flow of their `sub_agents`. @@ -115,6 +121,12 @@ ADK includes specialized agents derived from `BaseAgent` that don't perform task // When pipeline runs, Step2 can access the state.get("data") set by Step1. ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:sequential-pipeline" + ``` + * **[`ParallelAgent`](workflow-agents/parallel-agents.md):** Executes its `sub_agents` in parallel. Events from sub-agents may be interleaved. * **Context:** Modifies the `InvocationContext.branch` for each child agent (e.g., `ParentBranch.ChildName`), providing a distinct contextual path which can be useful for isolating history in some memory implementations. * **State:** Despite different branches, all parallel children access the *same shared* `session.state`, enabling them to read initial state and write results (use distinct keys to avoid race conditions). @@ -159,6 +171,12 @@ ADK includes specialized agents derived from `BaseAgent` that don't perform task // A subsequent agent could read state['weather'] and state['news']. ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:parallel-execution" + ``` + * **[`LoopAgent`](workflow-agents/loop-agents.md):** Executes its `sub_agents` sequentially in a loop. * **Termination:** The loop stops if the optional `max_iterations` is reached, or if any sub-agent returns an [`Event`](../events/index.md) with `escalate=True` in it's Event Actions. * **Context & State:** Passes the *same* `InvocationContext` in each iteration, allowing state changes (e.g., counters, flags) to persist across loops. @@ -227,6 +245,12 @@ ADK includes specialized agents derived from `BaseAgent` that don't perform task // until Checker escalates (state.get("status") == "completed") or 10 iterations pass. ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:loop-with-condition" + ``` + ### 1.3. Interaction & Communication Mechanisms { #interaction-communication-mechanisms } Agents within a system often need to exchange data or trigger actions in one another. ADK facilitates this through: @@ -281,6 +305,12 @@ The most fundamental way for agents operating within the same invocation (and th // AgentB runs, its instruction processor reads state.get("capital_city") to get "Paris". ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:output-key-state" + ``` + #### b) LLM-Driven Delegation (Agent Transfer) Leverages an [`LlmAgent`](llm-agents.md)'s understanding to dynamically route tasks to other suitable agents within the hierarchy. @@ -344,6 +374,12 @@ Leverages an [`LlmAgent`](llm-agents.md)'s understanding to dynamically route ta // ADK framework then routes execution to bookingAgent. ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:llm-transfer" + ``` + #### c) Explicit Invocation (`AgentTool`) Allows an [`LlmAgent`](llm-agents.md) to treat another `BaseAgent` instance as a callable function or [Tool](../tools/index.md). @@ -449,6 +485,12 @@ Allows an [`LlmAgent`](llm-agents.md) to treat another `BaseAgent` instance as a // The resulting image Part is returned to the Artist agent as the tool result. ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:agent-as-tool" + ``` + These primitives provide the flexibility to design multi-agent interactions ranging from tightly coupled sequential workflows to dynamic, LLM-driven delegation networks. ## 2. Common Multi-Agent Patterns using ADK Primitives { #common-multi-agent-patterns-using-adk-primitives } @@ -516,6 +558,12 @@ By combining ADK's composition primitives, you can implement various established // transferToAgent(agentName='Support') ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:coordinator-pattern" + ``` + ### Sequential Pipeline Pattern * **Structure:** A [`SequentialAgent`](workflow-agents/sequential-agents.md) contains `sub_agents` executed in a fixed order. @@ -576,6 +624,12 @@ By combining ADK's composition primitives, you can implement various established // reporter runs -> reads state['result'] ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:sequential-pipeline-pattern" + ``` + ### Parallel Fan-Out/Gather Pattern * **Structure:** A [`ParallelAgent`](workflow-agents/parallel-agents.md) runs multiple `sub_agents` concurrently, often followed by a later agent (in a `SequentialAgent`) that aggregates results. @@ -649,6 +703,12 @@ By combining ADK's composition primitives, you can implement various established // synthesizer runs afterwards, reading state['api1_data'] and state['api2_data']. ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:parallel-gather-pattern" + ``` + ### Hierarchical Task Decomposition @@ -732,6 +792,12 @@ By combining ADK's composition primitives, you can implement various established // Results flow back up. ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:hierarchical-pattern" + ``` + ### Review/Critique Pattern (Generator-Critic) * **Structure:** Typically involves two agents within a [`SequentialAgent`](workflow-agents/sequential-agents.md): a Generator and a Critic/Reviewer. @@ -798,6 +864,12 @@ By combining ADK's composition primitives, you can implement various established // reviewer runs -> reads state['draft_text'], saves status to state['review_status'] ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:generator-critic-pattern" + ``` + ### Iterative Refinement Pattern * **Structure:** Uses a [`LoopAgent`](workflow-agents/loop-agents.md) containing one or more agents that work on a task over multiple iterations. @@ -903,6 +975,12 @@ By combining ADK's composition primitives, you can implement various established // iterations. ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:iterative-refinement-pattern" + ``` + ### Human-in-the-Loop Pattern * **Structure:** Integrates human intervention points within an agent workflow. @@ -1000,4 +1078,10 @@ By combining ADK's composition primitives, you can implement various established .build(); ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:human-in-loop-pattern" + ``` + These patterns provide starting points for structuring your multi-agent systems. You can mix and match them as needed to create the most effective architecture for your specific application. diff --git a/examples/go/snippets/agents/multi-agent/multi_agent.go b/examples/go/snippets/agents/multi-agent/multi_agent.go new file mode 100644 index 00000000..e1092fef --- /dev/null +++ b/examples/go/snippets/agents/multi-agent/multi_agent.go @@ -0,0 +1,325 @@ +package main + +import ( + "context" + "iter" + + "google.golang.org/adk/agent/workflowagents/loopagent" + "google.golang.org/adk/agent/workflowagents/parallelagent" + "google.golang.org/adk/agent/workflowagents/sequentialagent" + + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/session" + "google.golang.org/genai" +) + +func conceptualSnippets() { + ctx := context.Background() + model, _ := gemini.NewModel(ctx, "gemini-1.5-flash", &genai.ClientConfig{}) + + // --8<-- [start:hierarchy] + // Conceptual Example: Defining Hierarchy + // Define individual agents + greeter, _ := llmagent.New(llmagent.Config{Name: "Greeter", Model: model}) + taskDoer, _ := agent.New(agent.Config{Name: "TaskExecutor"}) // Custom non-LLM agent + + // Create parent agent and assign children via sub_agents + coordinator, _ := llmagent.New(llmagent.Config{ + Name: "Coordinator", + Model: model, + Description: "I coordinate greetings and tasks.", + SubAgents: []agent.Agent{greeter, taskDoer}, // Assign sub_agents here + }) + // --8<-- [end:hierarchy] + _ = coordinator // Avoid unused variable error + + // --8<-- [start:sequential-pipeline] + // Conceptual Example: Sequential Pipeline + step1, _ := llmagent.New(llmagent.Config{Name: "Step1_Fetch", OutputKey: "data", Model: model}) // Saves output to state["data"] + step2, _ := llmagent.New(llmagent.Config{Name: "Step2_Process", Instruction: "Process data from {data}.", Model: model}) + + pipeline, _ := sequentialagent.New(sequentialagent.Config{ + AgentConfig: agent.Config{Name: "MyPipeline", SubAgents: []agent.Agent{step1, step2}}, + }) + // When pipeline runs, Step2 can access the state["data"] set by Step1. + // --8<-- [end:sequential-pipeline] + _ = pipeline // Avoid unused variable error + + // --8<-- [start:parallel-execution] + // Conceptual Example: Parallel Execution + fetchWeather, _ := llmagent.New(llmagent.Config{Name: "WeatherFetcher", OutputKey: "weather", Model: model}) + fetchNews, _ := llmagent.New(llmagent.Config{Name: "NewsFetcher", OutputKey: "news", Model: model}) + + gatherer, _ := parallelagent.New(parallelagent.Config{ + AgentConfig: agent.Config{Name: "InfoGatherer", SubAgents: []agent.Agent{fetchWeather, fetchNews}}, + }) + // When gatherer runs, WeatherFetcher and NewsFetcher run concurrently. + // A subsequent agent could read state["weather"] and state["news"]. + // --8<-- [end:parallel-execution] + _ = gatherer // Avoid unused variable error + + // --8<-- [start:loop-with-condition] + // Conceptual Example: Loop with Condition + // Custom agent to check state + checkCondition, _ := agent.New(agent.Config{ + Name: "Checker", + Run: func(ctx agent.Context) iter.Seq2[*session.Event, error] { + return func(yield func(*session.Event, error) bool) { + status, _ := ctx.Session().State().Get("status") + isDone := status == "completed" + yield(&session.Event{Author: "Checker", Actions: &session.EventActions{Escalate: isDone}}, nil) + } + }, + }) + + processStep, _ := llmagent.New(llmagent.Config{Name: "ProcessingStep", Model: model}) // Agent that might update state["status"] + + poller, _ := loopagent.New(loopagent.Config{ + MaxIterations: 10, + AgentConfig: agent.Config{Name: "StatusPoller", SubAgents: []agent.Agent{processStep, checkCondition}}, + }) + // When poller runs, it executes processStep then Checker repeatedly + // until Checker escalates (state["status"] == "completed") or 10 iterations pass. + // --8<-- [end:loop-with-condition] + _ = poller // Avoid unused variable error + + // --8<-- [start:output-key-state] + // Conceptual Example: Using output_key and reading state + agentA, _ := llmagent.New(llmagent.Config{Name: "AgentA", Instruction: "Find the capital of France.", OutputKey: "capital_city", Model: model}) + agentB, _ := llmagent.New(llmagent.Config{Name: "AgentB", Instruction: "Tell me about the city stored in {capital_city}.", Model: model}) + + pipeline2, _ := sequentialagent.New(sequentialagent.Config{ + AgentConfig: agent.Config{Name: "CityInfo", SubAgents: []agent.Agent{agentA, agentB}}, + }) + // AgentA runs, saves "Paris" to state["capital_city"]. + // AgentB runs, its instruction processor reads state["capital_city"] to get "Paris". + // --8<-- [end:output-key-state] + _ = pipeline2 // Avoid unused variable error + + // --8<-- [start:llm-transfer] + // Conceptual Setup: LLM Transfer + bookingAgent, _ := llmagent.New(llmagent.Config{Name: "Booker", Description: "Handles flight and hotel bookings.", Model: model}) + infoAgent, _ := llmagent.New(llmagent.Config{Name: "Info", Description: "Provides general information and answers questions.", Model: model}) + + coordinator2, _ := llmagent.New(llmagent.Config{ + Name: "Coordinator", + Model: model, + Instruction: "You are an assistant. Delegate booking tasks to Booker and info requests to Info.", + Description: "Main coordinator.", + SubAgents: []agent.Agent{bookingAgent, infoAgent}, + }) + // If coordinator receives "Book a flight", its LLM should generate: + // FunctionCall{Name: "transfer_to_agent", Args: map[string]any{"agent_name": "Booker"}} + // ADK framework then routes execution to bookingAgent. + // --8<-- [end:llm-transfer] + _ = coordinator2 // Avoid unused variable error + + // --8<-- [start:agent-as-tool] + // Conceptual Setup: Agent as a Tool + // Define a target agent (could be LlmAgent or custom BaseAgent) + imageAgent, _ := agent.New(agent.Config{ + Name: "ImageGen", + Description: "Generates an image based on a prompt.", + // ... internal logic ... + }) + + imageTool, _ := tool.NewAgentTool(imageAgent) // Wrap the agent + + // Parent agent uses the AgentTool + artistAgent, _ := llmagent.New(llmagent.Config{ + Name: "Artist", + Model: model, + Instruction: "Create a prompt and use the ImageGen tool to generate the image.", + Tools: []tool.Tool{imageTool}, // Include the AgentTool + }) + // Artist LLM generates a prompt, then calls: + // FunctionCall{Name: "ImageGen", Args: map[string]any{"image_prompt": "a cat wearing a hat"}} + // Framework calls imageTool.Run(...), which runs ImageGeneratorAgent. + // The resulting image Part is returned to the Artist agent as the tool result. + // --8<-- [end:agent-as-tool] + _ = artistAgent // Avoid unused variable error + + // --8<-- [start:coordinator-pattern] + // Conceptual Code: Coordinator using LLM Transfer + billingAgent2, _ := llmagent.New(llmagent.Config{Name: "Billing", Description: "Handles billing inquiries.", Model: model}) + supportAgent2, _ := llmagent.New(llmagent.Config{Name: "Support", Description: "Handles technical support requests.", Model: model}) + + coordinator3, _ := llmagent.New(llmagent.Config{ + Name: "HelpDeskCoordinator", + Model: model, + Instruction: "Route user requests: Use Billing agent for payment issues, Support agent for technical problems.", + Description: "Main help desk router.", + SubAgents: []agent.Agent{billingAgent2, supportAgent2}, + }) + // User asks "My payment failed" -> Coordinator's LLM should call transfer_to_agent(agent_name='Billing') + // User asks "I can't log in" -> Coordinator's LLM should call transfer_to_agent(agent_name='Support') + // --8<-- [end:coordinator-pattern] + _ = coordinator3 // Avoid unused variable error + + // --8<-- [start:sequential-pipeline-pattern] + // Conceptual Code: Sequential Data Pipeline + validator, _ := llmagent.New(llmagent.Config{Name: "ValidateInput", Instruction: "Validate the input.", OutputKey: "validation_status", Model: model}) + processor, _ := llmagent.New(llmagent.Config{Name: "ProcessData", Instruction: "Process data if {validation_status} is 'valid'.", OutputKey: "result", Model: model}) + reporter, _ := llmagent.New(llmagent.Config{Name: "ReportResult", Instruction: "Report the result from {result}.", Model: model}) + + dataPipeline, _ := sequentialagent.New(sequentialagent.Config{ + AgentConfig: agent.Config{Name: "DataPipeline", SubAgents: []agent.Agent{validator, processor, reporter}}, + }) + // validator runs -> saves to state["validation_status"] + // processor runs -> reads state["validation_status"], saves to state["result"] + // reporter runs -> reads state["result"] + // --8<-- [end:sequential-pipeline-pattern] + _ = dataPipeline // Avoid unused variable error + + // --8<-- [start:parallel-gather-pattern] + // Conceptual Code: Parallel Information Gathering + fetchAPI1, _ := llmagent.New(llmagent.Config{Name: "API1Fetcher", Instruction: "Fetch data from API 1.", OutputKey: "api1_data", Model: model}) + fetchAPI2, _ := llmagent.New(llmagent.Config{Name: "API2Fetcher", Instruction: "Fetch data from API 2.", OutputKey: "api2_data", Model: model}) + + gatherConcurrently, _ := parallelagent.New(parallelagent.Config{ + AgentConfig: agent.Config{Name: "ConcurrentFetch", SubAgents: []agent.Agent{fetchAPI1, fetchAPI2}}, + }) + + synthesizer, _ := llmagent.New(llmagent.Config{Name: "Synthesizer", Instruction: "Combine results from {api1_data} and {api2_data}.", Model: model}) + + overallWorkflow, _ := sequentialagent.New(sequentialagent.Config{ + AgentConfig: agent.Config{Name: "FetchAndSynthesize", SubAgents: []agent.Agent{gatherConcurrently, synthesizer}}, + }) + // fetch_api1 and fetch_api2 run concurrently, saving to state. + // synthesizer runs afterwards, reading state["api1_data"] and state["api2_data"]. + // --8<-- [end:parallel-gather-pattern] + _ = overallWorkflow // Avoid unused variable error + + // --8<-- [start:hierarchical-pattern] + // Conceptual Code: Hierarchical Research Task + // Low-level tool-like agents + webSearcher, _ := llmagent.New(llmagent.Config{Name: "WebSearch", Description: "Performs web searches for facts.", Model: model}) + summarizer, _ := llmagent.New(llmagent.Config{Name: "Summarizer", Description: "Summarizes text.", Model: model}) + + // Mid-level agent combining tools + webSearcherTool, _ := tool.NewAgentTool(webSearcher) + summarizerTool, _ := tool.NewAgentTool(summarizer) + researchAssistant, _ := llmagent.New(llmagent.Config{ + Name: "ResearchAssistant", + Model: model, + Description: "Finds and summarizes information on a topic.", + Tools: []tool.Tool{webSearcherTool, summarizerTool}, + }) + + // High-level agent delegating research + researchAssistantTool, _ := tool.NewAgentTool(researchAssistant) + reportWriter, _ := llmagent.New(llmagent.Config{ + Name: "ReportWriter", + Model: model, + Instruction: "Write a report on topic X. Use the ResearchAssistant to gather information.", + Tools: []tool.Tool{researchAssistantTool}, + }) + // User interacts with ReportWriter. + // ReportWriter calls ResearchAssistant tool. + // ResearchAssistant calls WebSearch and Summarizer tools. + // Results flow back up. + // --8<-- [end:hierarchical-pattern] + _ = reportWriter // Avoid unused variable error + + // --8<-- [start:generator-critic-pattern] + // Conceptual Code: Generator-Critic + generator, _ := llmagent.New(llmagent.Config{ + Name: "DraftWriter", + Instruction: "Write a short paragraph about subject X.", + OutputKey: "draft_text", + Model: model, + }) + + reviewer, _ := llmagent.New(llmagent.Config{ + Name: "FactChecker", + Instruction: "Review the text in {draft_text} for factual accuracy. Output 'valid' or 'invalid' with reasons.", + OutputKey: "review_status", + Model: model, + }) + + reviewPipeline, _ := sequentialagent.New(sequentialagent.Config{ + AgentConfig: agent.Config{Name: "WriteAndReview", SubAgents: []agent.Agent{generator, reviewer}}, + }) + // generator runs -> saves draft to state["draft_text"] + // reviewer runs -> reads state["draft_text"], saves status to state["review_status"] + // --8<-- [end:generator-critic-pattern] + _ = reviewPipeline // Avoid unused variable error + + // --8<-- [start:iterative-refinement-pattern] + // Conceptual Code: Iterative Code Refinement + codeRefiner, _ := llmagent.New(llmagent.Config{ + Name: "CodeRefiner", + Instruction: "Read state['current_code'] (if exists) and state['requirements']. Generate/refine Python code to meet requirements. Save to state['current_code'].", + OutputKey: "current_code", + Model: model, + }) + + qualityChecker, _ := llmagent.New(llmagent.Config{ + Name: "QualityChecker", + Instruction: "Evaluate the code in state['current_code'] against state['requirements']. Output 'pass' or 'fail'.", + OutputKey: "quality_status", + Model: model, + }) + + checkStatusAndEscalate, _ := agent.New(agent.Config{ + Name: "StopChecker", + Run: func(ctx agent.Context) iter.Seq2[*session.Event, error] { + return func(yield func(*session.Event, error) bool) { + status, _ := ctx.Session().State().Get("quality_status") + shouldStop := status == "pass" + yield(&session.Event{Author: "StopChecker", Actions: &session.EventActions{Escalate: shouldStop}}, nil) + } + }, + }) + + refinementLoop, _ := loopagent.New(loopagent.Config{ + MaxIterations: 5, + AgentConfig: agent.Config{Name: "CodeRefinementLoop", SubAgents: []agent.Agent{codeRefiner, qualityChecker, checkStatusAndEscalate}}, + }) + // Loop runs: Refiner -> Checker -> StopChecker + // State["current_code"] is updated each iteration. + // Loop stops if QualityChecker outputs 'pass' (leading to StopChecker escalating) or after 5 iterations. + // --8<-- [end:iterative-refinement-pattern] + _ = refinementLoop // Avoid unused variable error + + // --8<-- [start:human-in-loop-pattern] + // Conceptual Code: Using a Tool for Human Approval + // --- Assume externalApprovalTool exists --- + // func externalApprovalTool(amount float64, reason string) string { ... } + var externalApprovalTool func(amount float64, reason string) string + approvalTool, _ := tool.NewFunctionTool( + tool.FunctionToolConfig{ + Name: "external_approval_tool", + Description: "Sends a request for human approval.", + }, + externalApprovalTool, + ) + + prepareRequest, _ := llmagent.New(llmagent.Config{ + Name: "PrepareApproval", + Instruction: "Prepare the approval request details based on user input. Store amount and reason in state.", + Model: model, + }) + + requestApproval, _ := llmagent.New(llmagent.Config{ + Name: "RequestHumanApproval", + Instruction: "Use the external_approval_tool with amount from state['approval_amount'] and reason from state['approval_reason'].", + Tools: []tool.Tool{approvalTool}, + OutputKey: "human_decision", + Model: model, + }) + + processDecision, _ := llmagent.New(llmagent.Config{ + Name: "ProcessDecision", + Instruction: "Check {human_decision}. If 'approved', proceed. If 'rejected', inform user.", + Model: model, + }) + + approvalWorkflow, _ := sequentialagent.New(sequentialagent.Config{ + AgentConfig: agent.Config{Name: "HumanApprovalWorkflow", SubAgents: []agent.Agent{prepareRequest, requestApproval, processDecision}}, + }) + // --8<-- [end:human-in-loop-pattern] + _ = approvalWorkflow // Avoid unused variable error +} \ No newline at end of file From e7c4716383068f6a37f2b53e008bd8fbbfb1a40b Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Tue, 14 Oct 2025 13:05:51 -0400 Subject: [PATCH 052/125] Updated to use AgentTool --- .../agents/multi-agent/multi_agent.go | 32 ++++++++++++------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/examples/go/snippets/agents/multi-agent/multi_agent.go b/examples/go/snippets/agents/multi-agent/multi_agent.go index e1092fef..9ad6eceb 100644 --- a/examples/go/snippets/agents/multi-agent/multi_agent.go +++ b/examples/go/snippets/agents/multi-agent/multi_agent.go @@ -10,8 +10,10 @@ import ( "google.golang.org/adk/agent" "google.golang.org/adk/agent/llmagent" - "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/model/gemini" "google.golang.org/adk/session" + "google.golang.org/adk/tool" + "google.golang.org/adk/tool/agenttool" "google.golang.org/genai" ) @@ -65,11 +67,11 @@ func conceptualSnippets() { // Custom agent to check state checkCondition, _ := agent.New(agent.Config{ Name: "Checker", - Run: func(ctx agent.Context) iter.Seq2[*session.Event, error] { + Run: func(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] { return func(yield func(*session.Event, error) bool) { status, _ := ctx.Session().State().Get("status") isDone := status == "completed" - yield(&session.Event{Author: "Checker", Actions: &session.EventActions{Escalate: isDone}}, nil) + yield(&session.Event{Author: "Checker", Actions: session.EventActions{Escalate: isDone}}, nil) } }, }) @@ -124,8 +126,12 @@ func conceptualSnippets() { Description: "Generates an image based on a prompt.", // ... internal logic ... }) + // Wrap the agent + imageTool := agenttool.New(imageAgent, &agenttool.Config{ + SkipSummarization: true, + }) - imageTool, _ := tool.NewAgentTool(imageAgent) // Wrap the agent + // Now imageTool can be used as a tool by other agents. // Parent agent uses the AgentTool artistAgent, _ := llmagent.New(llmagent.Config{ @@ -199,8 +205,8 @@ func conceptualSnippets() { summarizer, _ := llmagent.New(llmagent.Config{Name: "Summarizer", Description: "Summarizes text.", Model: model}) // Mid-level agent combining tools - webSearcherTool, _ := tool.NewAgentTool(webSearcher) - summarizerTool, _ := tool.NewAgentTool(summarizer) + webSearcherTool := agenttool.New(webSearcher, nil) + summarizerTool := agenttool.New(summarizer, nil) researchAssistant, _ := llmagent.New(llmagent.Config{ Name: "ResearchAssistant", Model: model, @@ -209,7 +215,7 @@ func conceptualSnippets() { }) // High-level agent delegating research - researchAssistantTool, _ := tool.NewAgentTool(researchAssistant) + researchAssistantTool := agenttool.New(researchAssistant, nil) reportWriter, _ := llmagent.New(llmagent.Config{ Name: "ReportWriter", Model: model, @@ -265,11 +271,11 @@ func conceptualSnippets() { checkStatusAndEscalate, _ := agent.New(agent.Config{ Name: "StopChecker", - Run: func(ctx agent.Context) iter.Seq2[*session.Event, error] { + Run: func(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] { return func(yield func(*session.Event, error) bool) { status, _ := ctx.Session().State().Get("quality_status") shouldStop := status == "pass" - yield(&session.Event{Author: "StopChecker", Actions: &session.EventActions{Escalate: shouldStop}}, nil) + yield(&session.Event{Author: "StopChecker", Actions: session.EventActions{Escalate: shouldStop}}, nil) } }, }) @@ -288,7 +294,11 @@ func conceptualSnippets() { // Conceptual Code: Using a Tool for Human Approval // --- Assume externalApprovalTool exists --- // func externalApprovalTool(amount float64, reason string) string { ... } - var externalApprovalTool func(amount float64, reason string) string + type externalApprovalToolArgs struct { + Amount float64 `json:"amount"` + Reason string `json:"reason"` + } + var externalApprovalTool func(tool.Context, externalApprovalToolArgs) string approvalTool, _ := tool.NewFunctionTool( tool.FunctionToolConfig{ Name: "external_approval_tool", @@ -322,4 +332,4 @@ func conceptualSnippets() { }) // --8<-- [end:human-in-loop-pattern] _ = approvalWorkflow // Avoid unused variable error -} \ No newline at end of file +} From 4ee59b8ae86e6dd46756bfb61eaedd82a13ab535 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Tue, 14 Oct 2025 23:07:49 -0400 Subject: [PATCH 053/125] Split snippets --- docs/agents/multi-agents.md | 120 ++++++- .../agents/multi-agent/multi_agent.go | 335 ------------------ 2 files changed, 106 insertions(+), 349 deletions(-) delete mode 100644 examples/go/snippets/agents/multi-agent/multi_agent.go diff --git a/docs/agents/multi-agents.md b/docs/agents/multi-agents.md index a0bc4ea9..fa67eaa1 100644 --- a/docs/agents/multi-agents.md +++ b/docs/agents/multi-agents.md @@ -84,7 +84,12 @@ The foundation for structuring multi-agent systems is the parent-child relations === "Go" ```go - --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:hierarchy" + import ( + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + ) + + --8<-- "examples/go/snippets/agents/multi-agent/main.go:hierarchy" ``` ### 1.2. Workflow Agents as Orchestrators { #workflow-agents-as-orchestrators } @@ -124,7 +129,13 @@ ADK includes specialized agents derived from `BaseAgent` that don't perform task === "Go" ```go - --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:sequential-pipeline" + import ( + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/agent/workflowagents/sequentialagent" + ) + + --8<-- "examples/go/snippets/agents/multi-agent/main.go:sequential-pipeline" ``` * **[`ParallelAgent`](workflow-agents/parallel-agents.md):** Executes its `sub_agents` in parallel. Events from sub-agents may be interleaved. @@ -174,7 +185,13 @@ ADK includes specialized agents derived from `BaseAgent` that don't perform task === "Go" ```go - --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:parallel-execution" + import ( + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/agent/workflowagents/parallelagent" + ) + + --8<-- "examples/go/snippets/agents/multi-agent/main.go:parallel-execution" ``` * **[`LoopAgent`](workflow-agents/loop-agents.md):** Executes its `sub_agents` sequentially in a loop. @@ -248,7 +265,15 @@ ADK includes specialized agents derived from `BaseAgent` that don't perform task === "Go" ```go - --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:loop-with-condition" + import ( + "iter" + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/agent/workflowagents/loopagent" + "google.golang.org/adk/session" + ) + + --8<-- "examples/go/snippets/agents/multi-agent/main.go:loop-with-condition" ``` ### 1.3. Interaction & Communication Mechanisms { #interaction-communication-mechanisms } @@ -308,7 +333,13 @@ The most fundamental way for agents operating within the same invocation (and th === "Go" ```go - --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:output-key-state" + import ( + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/agent/workflowagents/sequentialagent" + ) + + --8<-- "examples/go/snippets/agents/multi-agent/main.go:output-key-state" ``` #### b) LLM-Driven Delegation (Agent Transfer) @@ -377,7 +408,11 @@ Leverages an [`LlmAgent`](llm-agents.md)'s understanding to dynamically route ta === "Go" ```go - --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:llm-transfer" + import ( + "google.golang.org/adk/agent/llmagent" + ) + + --8<-- "examples/go/snippets/agents/multi-agent/main.go:llm-transfer" ``` #### c) Explicit Invocation (`AgentTool`) @@ -488,7 +523,19 @@ Allows an [`LlmAgent`](llm-agents.md) to treat another `BaseAgent` instance as a === "Go" ```go - --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:agent-as-tool" + import ( + "fmt" + "iter" + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/model" + "google.golang.org/adk/session" + "google.golang.org/adk/tool" + "google.golang.org/adk/tool/agenttool" + "google.golang.org/genai" + ) + + --8<-- "examples/go/snippets/agents/multi-agent/main.go:agent-as-tool" ``` These primitives provide the flexibility to design multi-agent interactions ranging from tightly coupled sequential workflows to dynamic, LLM-driven delegation networks. @@ -561,7 +608,12 @@ By combining ADK's composition primitives, you can implement various established === "Go" ```go - --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:coordinator-pattern" + import ( + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + ) + + --8<-- "examples/go/snippets/agents/multi-agent/main.go:coordinator-pattern" ``` ### Sequential Pipeline Pattern @@ -627,7 +679,13 @@ By combining ADK's composition primitives, you can implement various established === "Go" ```go - --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:sequential-pipeline-pattern" + import ( + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/agent/workflowagents/sequentialagent" + ) + + --8<-- "examples/go/snippets/agents/multi-agent/main.go:sequential-pipeline-pattern" ``` ### Parallel Fan-Out/Gather Pattern @@ -706,7 +764,14 @@ By combining ADK's composition primitives, you can implement various established === "Go" ```go - --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:parallel-gather-pattern" + import ( + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/agent/workflowagents/parallelagent" + "google.golang.org/adk/agent/workflowagents/sequentialagent" + ) + + --8<-- "examples/go/snippets/agents/multi-agent/main.go:parallel-gather-pattern" ``` @@ -795,7 +860,13 @@ By combining ADK's composition primitives, you can implement various established === "Go" ```go - --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:hierarchical-pattern" + import ( + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/tool" + "google.golang.org/adk/tool/agenttool" + ) + + --8<-- "examples/go/snippets/agents/multi-agent/main.go:hierarchical-pattern" ``` ### Review/Critique Pattern (Generator-Critic) @@ -867,7 +938,13 @@ By combining ADK's composition primitives, you can implement various established === "Go" ```go - --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:generator-critic-pattern" + import ( + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/agent/workflowagents/sequentialagent" + ) + + --8<-- "examples/go/snippets/agents/multi-agent/main.go:generator-critic-pattern" ``` ### Iterative Refinement Pattern @@ -978,7 +1055,15 @@ By combining ADK's composition primitives, you can implement various established === "Go" ```go - --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:iterative-refinement-pattern" + import ( + "iter" + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/agent/workflowagents/loopagent" + "google.golang.org/adk/session" + ) + + --8<-- "examples/go/snippets/agents/multi-agent/main.go:iterative-refinement-pattern" ``` ### Human-in-the-Loop Pattern @@ -1081,7 +1166,14 @@ By combining ADK's composition primitives, you can implement various established === "Go" ```go - --8<-- "examples/go/snippets/agents/multi-agent/multi_agent.go:human-in-loop-pattern" + import ( + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/agent/workflowagents/sequentialagent" + "google.golang.org/adk/tool" + ) + + --8<-- "examples/go/snippets/agents/multi-agent/main.go:human-in-loop-pattern" ``` These patterns provide starting points for structuring your multi-agent systems. You can mix and match them as needed to create the most effective architecture for your specific application. diff --git a/examples/go/snippets/agents/multi-agent/multi_agent.go b/examples/go/snippets/agents/multi-agent/multi_agent.go deleted file mode 100644 index 9ad6eceb..00000000 --- a/examples/go/snippets/agents/multi-agent/multi_agent.go +++ /dev/null @@ -1,335 +0,0 @@ -package main - -import ( - "context" - "iter" - - "google.golang.org/adk/agent/workflowagents/loopagent" - "google.golang.org/adk/agent/workflowagents/parallelagent" - "google.golang.org/adk/agent/workflowagents/sequentialagent" - - "google.golang.org/adk/agent" - "google.golang.org/adk/agent/llmagent" - "google.golang.org/adk/model/gemini" - "google.golang.org/adk/session" - "google.golang.org/adk/tool" - "google.golang.org/adk/tool/agenttool" - "google.golang.org/genai" -) - -func conceptualSnippets() { - ctx := context.Background() - model, _ := gemini.NewModel(ctx, "gemini-1.5-flash", &genai.ClientConfig{}) - - // --8<-- [start:hierarchy] - // Conceptual Example: Defining Hierarchy - // Define individual agents - greeter, _ := llmagent.New(llmagent.Config{Name: "Greeter", Model: model}) - taskDoer, _ := agent.New(agent.Config{Name: "TaskExecutor"}) // Custom non-LLM agent - - // Create parent agent and assign children via sub_agents - coordinator, _ := llmagent.New(llmagent.Config{ - Name: "Coordinator", - Model: model, - Description: "I coordinate greetings and tasks.", - SubAgents: []agent.Agent{greeter, taskDoer}, // Assign sub_agents here - }) - // --8<-- [end:hierarchy] - _ = coordinator // Avoid unused variable error - - // --8<-- [start:sequential-pipeline] - // Conceptual Example: Sequential Pipeline - step1, _ := llmagent.New(llmagent.Config{Name: "Step1_Fetch", OutputKey: "data", Model: model}) // Saves output to state["data"] - step2, _ := llmagent.New(llmagent.Config{Name: "Step2_Process", Instruction: "Process data from {data}.", Model: model}) - - pipeline, _ := sequentialagent.New(sequentialagent.Config{ - AgentConfig: agent.Config{Name: "MyPipeline", SubAgents: []agent.Agent{step1, step2}}, - }) - // When pipeline runs, Step2 can access the state["data"] set by Step1. - // --8<-- [end:sequential-pipeline] - _ = pipeline // Avoid unused variable error - - // --8<-- [start:parallel-execution] - // Conceptual Example: Parallel Execution - fetchWeather, _ := llmagent.New(llmagent.Config{Name: "WeatherFetcher", OutputKey: "weather", Model: model}) - fetchNews, _ := llmagent.New(llmagent.Config{Name: "NewsFetcher", OutputKey: "news", Model: model}) - - gatherer, _ := parallelagent.New(parallelagent.Config{ - AgentConfig: agent.Config{Name: "InfoGatherer", SubAgents: []agent.Agent{fetchWeather, fetchNews}}, - }) - // When gatherer runs, WeatherFetcher and NewsFetcher run concurrently. - // A subsequent agent could read state["weather"] and state["news"]. - // --8<-- [end:parallel-execution] - _ = gatherer // Avoid unused variable error - - // --8<-- [start:loop-with-condition] - // Conceptual Example: Loop with Condition - // Custom agent to check state - checkCondition, _ := agent.New(agent.Config{ - Name: "Checker", - Run: func(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] { - return func(yield func(*session.Event, error) bool) { - status, _ := ctx.Session().State().Get("status") - isDone := status == "completed" - yield(&session.Event{Author: "Checker", Actions: session.EventActions{Escalate: isDone}}, nil) - } - }, - }) - - processStep, _ := llmagent.New(llmagent.Config{Name: "ProcessingStep", Model: model}) // Agent that might update state["status"] - - poller, _ := loopagent.New(loopagent.Config{ - MaxIterations: 10, - AgentConfig: agent.Config{Name: "StatusPoller", SubAgents: []agent.Agent{processStep, checkCondition}}, - }) - // When poller runs, it executes processStep then Checker repeatedly - // until Checker escalates (state["status"] == "completed") or 10 iterations pass. - // --8<-- [end:loop-with-condition] - _ = poller // Avoid unused variable error - - // --8<-- [start:output-key-state] - // Conceptual Example: Using output_key and reading state - agentA, _ := llmagent.New(llmagent.Config{Name: "AgentA", Instruction: "Find the capital of France.", OutputKey: "capital_city", Model: model}) - agentB, _ := llmagent.New(llmagent.Config{Name: "AgentB", Instruction: "Tell me about the city stored in {capital_city}.", Model: model}) - - pipeline2, _ := sequentialagent.New(sequentialagent.Config{ - AgentConfig: agent.Config{Name: "CityInfo", SubAgents: []agent.Agent{agentA, agentB}}, - }) - // AgentA runs, saves "Paris" to state["capital_city"]. - // AgentB runs, its instruction processor reads state["capital_city"] to get "Paris". - // --8<-- [end:output-key-state] - _ = pipeline2 // Avoid unused variable error - - // --8<-- [start:llm-transfer] - // Conceptual Setup: LLM Transfer - bookingAgent, _ := llmagent.New(llmagent.Config{Name: "Booker", Description: "Handles flight and hotel bookings.", Model: model}) - infoAgent, _ := llmagent.New(llmagent.Config{Name: "Info", Description: "Provides general information and answers questions.", Model: model}) - - coordinator2, _ := llmagent.New(llmagent.Config{ - Name: "Coordinator", - Model: model, - Instruction: "You are an assistant. Delegate booking tasks to Booker and info requests to Info.", - Description: "Main coordinator.", - SubAgents: []agent.Agent{bookingAgent, infoAgent}, - }) - // If coordinator receives "Book a flight", its LLM should generate: - // FunctionCall{Name: "transfer_to_agent", Args: map[string]any{"agent_name": "Booker"}} - // ADK framework then routes execution to bookingAgent. - // --8<-- [end:llm-transfer] - _ = coordinator2 // Avoid unused variable error - - // --8<-- [start:agent-as-tool] - // Conceptual Setup: Agent as a Tool - // Define a target agent (could be LlmAgent or custom BaseAgent) - imageAgent, _ := agent.New(agent.Config{ - Name: "ImageGen", - Description: "Generates an image based on a prompt.", - // ... internal logic ... - }) - // Wrap the agent - imageTool := agenttool.New(imageAgent, &agenttool.Config{ - SkipSummarization: true, - }) - - // Now imageTool can be used as a tool by other agents. - - // Parent agent uses the AgentTool - artistAgent, _ := llmagent.New(llmagent.Config{ - Name: "Artist", - Model: model, - Instruction: "Create a prompt and use the ImageGen tool to generate the image.", - Tools: []tool.Tool{imageTool}, // Include the AgentTool - }) - // Artist LLM generates a prompt, then calls: - // FunctionCall{Name: "ImageGen", Args: map[string]any{"image_prompt": "a cat wearing a hat"}} - // Framework calls imageTool.Run(...), which runs ImageGeneratorAgent. - // The resulting image Part is returned to the Artist agent as the tool result. - // --8<-- [end:agent-as-tool] - _ = artistAgent // Avoid unused variable error - - // --8<-- [start:coordinator-pattern] - // Conceptual Code: Coordinator using LLM Transfer - billingAgent2, _ := llmagent.New(llmagent.Config{Name: "Billing", Description: "Handles billing inquiries.", Model: model}) - supportAgent2, _ := llmagent.New(llmagent.Config{Name: "Support", Description: "Handles technical support requests.", Model: model}) - - coordinator3, _ := llmagent.New(llmagent.Config{ - Name: "HelpDeskCoordinator", - Model: model, - Instruction: "Route user requests: Use Billing agent for payment issues, Support agent for technical problems.", - Description: "Main help desk router.", - SubAgents: []agent.Agent{billingAgent2, supportAgent2}, - }) - // User asks "My payment failed" -> Coordinator's LLM should call transfer_to_agent(agent_name='Billing') - // User asks "I can't log in" -> Coordinator's LLM should call transfer_to_agent(agent_name='Support') - // --8<-- [end:coordinator-pattern] - _ = coordinator3 // Avoid unused variable error - - // --8<-- [start:sequential-pipeline-pattern] - // Conceptual Code: Sequential Data Pipeline - validator, _ := llmagent.New(llmagent.Config{Name: "ValidateInput", Instruction: "Validate the input.", OutputKey: "validation_status", Model: model}) - processor, _ := llmagent.New(llmagent.Config{Name: "ProcessData", Instruction: "Process data if {validation_status} is 'valid'.", OutputKey: "result", Model: model}) - reporter, _ := llmagent.New(llmagent.Config{Name: "ReportResult", Instruction: "Report the result from {result}.", Model: model}) - - dataPipeline, _ := sequentialagent.New(sequentialagent.Config{ - AgentConfig: agent.Config{Name: "DataPipeline", SubAgents: []agent.Agent{validator, processor, reporter}}, - }) - // validator runs -> saves to state["validation_status"] - // processor runs -> reads state["validation_status"], saves to state["result"] - // reporter runs -> reads state["result"] - // --8<-- [end:sequential-pipeline-pattern] - _ = dataPipeline // Avoid unused variable error - - // --8<-- [start:parallel-gather-pattern] - // Conceptual Code: Parallel Information Gathering - fetchAPI1, _ := llmagent.New(llmagent.Config{Name: "API1Fetcher", Instruction: "Fetch data from API 1.", OutputKey: "api1_data", Model: model}) - fetchAPI2, _ := llmagent.New(llmagent.Config{Name: "API2Fetcher", Instruction: "Fetch data from API 2.", OutputKey: "api2_data", Model: model}) - - gatherConcurrently, _ := parallelagent.New(parallelagent.Config{ - AgentConfig: agent.Config{Name: "ConcurrentFetch", SubAgents: []agent.Agent{fetchAPI1, fetchAPI2}}, - }) - - synthesizer, _ := llmagent.New(llmagent.Config{Name: "Synthesizer", Instruction: "Combine results from {api1_data} and {api2_data}.", Model: model}) - - overallWorkflow, _ := sequentialagent.New(sequentialagent.Config{ - AgentConfig: agent.Config{Name: "FetchAndSynthesize", SubAgents: []agent.Agent{gatherConcurrently, synthesizer}}, - }) - // fetch_api1 and fetch_api2 run concurrently, saving to state. - // synthesizer runs afterwards, reading state["api1_data"] and state["api2_data"]. - // --8<-- [end:parallel-gather-pattern] - _ = overallWorkflow // Avoid unused variable error - - // --8<-- [start:hierarchical-pattern] - // Conceptual Code: Hierarchical Research Task - // Low-level tool-like agents - webSearcher, _ := llmagent.New(llmagent.Config{Name: "WebSearch", Description: "Performs web searches for facts.", Model: model}) - summarizer, _ := llmagent.New(llmagent.Config{Name: "Summarizer", Description: "Summarizes text.", Model: model}) - - // Mid-level agent combining tools - webSearcherTool := agenttool.New(webSearcher, nil) - summarizerTool := agenttool.New(summarizer, nil) - researchAssistant, _ := llmagent.New(llmagent.Config{ - Name: "ResearchAssistant", - Model: model, - Description: "Finds and summarizes information on a topic.", - Tools: []tool.Tool{webSearcherTool, summarizerTool}, - }) - - // High-level agent delegating research - researchAssistantTool := agenttool.New(researchAssistant, nil) - reportWriter, _ := llmagent.New(llmagent.Config{ - Name: "ReportWriter", - Model: model, - Instruction: "Write a report on topic X. Use the ResearchAssistant to gather information.", - Tools: []tool.Tool{researchAssistantTool}, - }) - // User interacts with ReportWriter. - // ReportWriter calls ResearchAssistant tool. - // ResearchAssistant calls WebSearch and Summarizer tools. - // Results flow back up. - // --8<-- [end:hierarchical-pattern] - _ = reportWriter // Avoid unused variable error - - // --8<-- [start:generator-critic-pattern] - // Conceptual Code: Generator-Critic - generator, _ := llmagent.New(llmagent.Config{ - Name: "DraftWriter", - Instruction: "Write a short paragraph about subject X.", - OutputKey: "draft_text", - Model: model, - }) - - reviewer, _ := llmagent.New(llmagent.Config{ - Name: "FactChecker", - Instruction: "Review the text in {draft_text} for factual accuracy. Output 'valid' or 'invalid' with reasons.", - OutputKey: "review_status", - Model: model, - }) - - reviewPipeline, _ := sequentialagent.New(sequentialagent.Config{ - AgentConfig: agent.Config{Name: "WriteAndReview", SubAgents: []agent.Agent{generator, reviewer}}, - }) - // generator runs -> saves draft to state["draft_text"] - // reviewer runs -> reads state["draft_text"], saves status to state["review_status"] - // --8<-- [end:generator-critic-pattern] - _ = reviewPipeline // Avoid unused variable error - - // --8<-- [start:iterative-refinement-pattern] - // Conceptual Code: Iterative Code Refinement - codeRefiner, _ := llmagent.New(llmagent.Config{ - Name: "CodeRefiner", - Instruction: "Read state['current_code'] (if exists) and state['requirements']. Generate/refine Python code to meet requirements. Save to state['current_code'].", - OutputKey: "current_code", - Model: model, - }) - - qualityChecker, _ := llmagent.New(llmagent.Config{ - Name: "QualityChecker", - Instruction: "Evaluate the code in state['current_code'] against state['requirements']. Output 'pass' or 'fail'.", - OutputKey: "quality_status", - Model: model, - }) - - checkStatusAndEscalate, _ := agent.New(agent.Config{ - Name: "StopChecker", - Run: func(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] { - return func(yield func(*session.Event, error) bool) { - status, _ := ctx.Session().State().Get("quality_status") - shouldStop := status == "pass" - yield(&session.Event{Author: "StopChecker", Actions: session.EventActions{Escalate: shouldStop}}, nil) - } - }, - }) - - refinementLoop, _ := loopagent.New(loopagent.Config{ - MaxIterations: 5, - AgentConfig: agent.Config{Name: "CodeRefinementLoop", SubAgents: []agent.Agent{codeRefiner, qualityChecker, checkStatusAndEscalate}}, - }) - // Loop runs: Refiner -> Checker -> StopChecker - // State["current_code"] is updated each iteration. - // Loop stops if QualityChecker outputs 'pass' (leading to StopChecker escalating) or after 5 iterations. - // --8<-- [end:iterative-refinement-pattern] - _ = refinementLoop // Avoid unused variable error - - // --8<-- [start:human-in-loop-pattern] - // Conceptual Code: Using a Tool for Human Approval - // --- Assume externalApprovalTool exists --- - // func externalApprovalTool(amount float64, reason string) string { ... } - type externalApprovalToolArgs struct { - Amount float64 `json:"amount"` - Reason string `json:"reason"` - } - var externalApprovalTool func(tool.Context, externalApprovalToolArgs) string - approvalTool, _ := tool.NewFunctionTool( - tool.FunctionToolConfig{ - Name: "external_approval_tool", - Description: "Sends a request for human approval.", - }, - externalApprovalTool, - ) - - prepareRequest, _ := llmagent.New(llmagent.Config{ - Name: "PrepareApproval", - Instruction: "Prepare the approval request details based on user input. Store amount and reason in state.", - Model: model, - }) - - requestApproval, _ := llmagent.New(llmagent.Config{ - Name: "RequestHumanApproval", - Instruction: "Use the external_approval_tool with amount from state['approval_amount'] and reason from state['approval_reason'].", - Tools: []tool.Tool{approvalTool}, - OutputKey: "human_decision", - Model: model, - }) - - processDecision, _ := llmagent.New(llmagent.Config{ - Name: "ProcessDecision", - Instruction: "Check {human_decision}. If 'approved', proceed. If 'rejected', inform user.", - Model: model, - }) - - approvalWorkflow, _ := sequentialagent.New(sequentialagent.Config{ - AgentConfig: agent.Config{Name: "HumanApprovalWorkflow", SubAgents: []agent.Agent{prepareRequest, requestApproval, processDecision}}, - }) - // --8<-- [end:human-in-loop-pattern] - _ = approvalWorkflow // Avoid unused variable error -} From 75aef1cc4dad481b69fede961ae1e753d531f8d2 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 15 Oct 2025 00:04:58 -0400 Subject: [PATCH 054/125] Added missing file --- .../go/snippets/agents/multi-agent/main.go | 370 ++++++++++++++++++ 1 file changed, 370 insertions(+) create mode 100644 examples/go/snippets/agents/multi-agent/main.go diff --git a/examples/go/snippets/agents/multi-agent/main.go b/examples/go/snippets/agents/multi-agent/main.go new file mode 100644 index 00000000..ea2f7560 --- /dev/null +++ b/examples/go/snippets/agents/multi-agent/main.go @@ -0,0 +1,370 @@ +package main + +import ( + "context" + "fmt" + "iter" + + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/agent/workflowagents/loopagent" + "google.golang.org/adk/agent/workflowagents/parallelagent" + "google.golang.org/adk/agent/workflowagents/sequentialagent" + "google.golang.org/adk/model" + "google.golang.org/adk/model/gemini" + "google.golang.org/adk/session" + "google.golang.org/adk/tool" + "google.golang.org/adk/tool/agenttool" + "google.golang.org/genai" +) + +func basicWorkflowSnippets(m model.LLM) { + // --8<-- [start:sequential-pipeline] + // Conceptual Example: Sequential Pipeline + step1, _ := llmagent.New(llmagent.Config{Name: "Step1_Fetch", OutputKey: "data", Model: m}) // Saves output to state["data"] + step2, _ := llmagent.New(llmagent.Config{Name: "Step2_Process", Instruction: "Process data from {data}.", Model: m}) + + pipeline, _ := sequentialagent.New(sequentialagent.Config{ + AgentConfig: agent.Config{Name: "MyPipeline", SubAgents: []agent.Agent{step1, step2}}, + }) + // When pipeline runs, Step2 can access the state["data"] set by Step1. + // --8<-- [end:sequential-pipeline] + _ = pipeline // Avoid unused variable error + + // --8<-- [start:parallel-execution] + // Conceptual Example: Parallel Execution + fetchWeather, _ := llmagent.New(llmagent.Config{Name: "WeatherFetcher", OutputKey: "weather", Model: m}) + fetchNews, _ := llmagent.New(llmagent.Config{Name: "NewsFetcher", OutputKey: "news", Model: m}) + + gatherer, _ := parallelagent.New(parallelagent.Config{ + AgentConfig: agent.Config{Name: "InfoGatherer", SubAgents: []agent.Agent{fetchWeather, fetchNews}}, + }) + // When gatherer runs, WeatherFetcher and NewsFetcher run concurrently. + // A subsequent agent could read state["weather"] and state["news"]. + // --8<-- [end:parallel-execution] + _ = gatherer // Avoid unused variable error + + // --8<-- [start:loop-with-condition] + // Conceptual Example: Loop with Condition + // Custom agent to check state + checkCondition, _ := agent.New(agent.Config{ + Name: "Checker", + Run: func(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] { + return func(yield func(*session.Event, error) bool) { + status, err := ctx.Session().State().Get("status") + // If "status" is not in the state, default to "pending". + // This is idiomatic Go for handling a potential error on lookup. + if err != nil { + status = "pending" + } + isDone := status == "completed" + yield(&session.Event{Author: "Checker", Actions: session.EventActions{Escalate: isDone}}, nil) + } + }, + }) + + processStep, _ := llmagent.New(llmagent.Config{Name: "ProcessingStep", Model: m}) // Agent that might update state["status"] + + poller, _ := loopagent.New(loopagent.Config{ + MaxIterations: 10, + AgentConfig: agent.Config{Name: "StatusPoller", SubAgents: []agent.Agent{processStep, checkCondition}}, + }) + // When poller runs, it executes processStep then Checker repeatedly + // until Checker escalates (state["status"] == "completed") or 10 iterations pass. + // --8<-- [end:loop-with-condition] + _ = poller // Avoid unused variable error +} + +func agentInteractionSnippets(m model.LLM) { + // --8<-- [start:hierarchy] + // Conceptual Example: Defining Hierarchy + // Define individual agents + greeter, _ := llmagent.New(llmagent.Config{Name: "Greeter", Model: m}) + taskDoer, _ := agent.New(agent.Config{Name: "TaskExecutor"}) // Custom non-LLM agent + + // Create parent agent and assign children via sub_agents + coordinator, _ := llmagent.New(llmagent.Config{ + Name: "Coordinator", + Model: m, + Description: "I coordinate greetings and tasks.", + SubAgents: []agent.Agent{greeter, taskDoer}, // Assign sub_agents here + }) + // --8<-- [end:hierarchy] + _ = coordinator // Avoid unused variable error + + // --8<-- [start:output-key-state] + // Conceptual Example: Using output_key and reading state + agentA, _ := llmagent.New(llmagent.Config{Name: "AgentA", Instruction: "Find the capital of France.", OutputKey: "capital_city", Model: m}) + agentB, _ := llmagent.New(llmagent.Config{Name: "AgentB", Instruction: "Tell me about the city stored in {capital_city}.", Model: m}) + + pipeline2, _ := sequentialagent.New(sequentialagent.Config{ + AgentConfig: agent.Config{Name: "CityInfo", SubAgents: []agent.Agent{agentA, agentB}}, + }) + // AgentA runs, saves "Paris" to state["capital_city"]. + // AgentB runs, its instruction processor reads state["capital_city"] to get "Paris". + // --8<-- [end:output-key-state] + _ = pipeline2 // Avoid unused variable error + + // --8<-- [start:llm-transfer] + // Conceptual Setup: LLM Transfer + bookingAgent, _ := llmagent.New(llmagent.Config{Name: "Booker", Description: "Handles flight and hotel bookings.", Model: m}) + infoAgent, _ := llmagent.New(llmagent.Config{Name: "Info", Description: "Provides general information and answers questions.", Model: m}) + + coordinator, _ = llmagent.New(llmagent.Config{ + Name: "Coordinator", + Model: m, + Instruction: "You are an assistant. Delegate booking tasks to Booker and info requests to Info.", + Description: "Main coordinator.", + SubAgents: []agent.Agent{bookingAgent, infoAgent}, + }) + + // If coordinator receives "Book a flight", its LLM should generate: + // FunctionCall{Name: "transfer_to_agent", Args: map[string]any{"agent_name": "Booker"}} + // ADK framework then routes execution to bookingAgent. + // --8<-- [end:llm-transfer] + + fmt.Println("Coordinator agent created:", coordinator.Name()) + + // --8<-- [start:agent-as-tool] + // Conceptual Setup: Agent as a Tool + // Define a target agent (could be LlmAgent or custom BaseAgent) + imageAgent, _ := agent.New(agent.Config{ + Name: "ImageGen", + Description: "Generates an image based on a prompt.", + Run: func(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] { + return func(yield func(*session.Event, error) bool) { + prompt, _ := ctx.Session().State().Get("image_prompt") + fmt.Printf("Generating image for prompt: %v\n", prompt) + imageBytes := []byte("...") // Simulate image bytes + yield(&session.Event{ + Author: "ImageGen", + LLMResponse: &model.LLMResponse{ + Content: &genai.Content{ + Parts: []*genai.Part{genai.NewPartFromBytes(imageBytes, "image/png")}, + }, + }, + }, nil) + } + }, + }) + + // Wrap the agent + imageTool := agenttool.New(imageAgent, nil) + + // Now imageTool can be used as a tool by other agents. + + // Parent agent uses the AgentTool + artistAgent, _ := llmagent.New(llmagent.Config{ + Name: "Artist", + Model: m, + Instruction: "Create a prompt and use the ImageGen tool to generate the image.", + Tools: []tool.Tool{imageTool}, // Include the AgentTool + }) + // Artist LLM generates a prompt, then calls: + // FunctionCall{Name: "ImageGen", Args: map[string]any{"image_prompt": "a cat wearing a hat"}} + // Framework calls imageTool.Run(...), which runs ImageGeneratorAgent. + // The resulting image Part is returned to the Artist agent as the tool result. + // --8<-- [end:agent-as-tool] + _ = artistAgent // Avoid unused variable error +} + +func advancedPatternSnippets(m model.LLM) { + // --8<-- [start:coordinator-pattern] + // Conceptual Code: Coordinator using LLM Transfer + billingAgent, _ := llmagent.New(llmagent.Config{Name: "Billing", Description: "Handles billing inquiries.", Model: m}) + supportAgent, _ := llmagent.New(llmagent.Config{Name: "Support", Description: "Handles technical support requests.", Model: m}) + + coordinator, _ := llmagent.New(llmagent.Config{ + Name: "HelpDeskCoordinator", + Model: m, + Instruction: "Route user requests: Use Billing agent for payment issues, Support agent for technical problems.", + Description: "Main help desk router.", + SubAgents: []agent.Agent{billingAgent, supportAgent}, + }) + // User asks "My payment failed" -> Coordinator's LLM should call transfer_to_agent(agent_name='Billing') + // User asks "I can't log in" -> Coordinator's LLM should call transfer_to_agent(agent_name='Support') + // --8<-- [end:coordinator-pattern] + _ = coordinator // Avoid unused variable error + + // --8<-- [start:sequential-pipeline-pattern] + // Conceptual Code: Sequential Data Pipeline + validator, _ := llmagent.New(llmagent.Config{Name: "ValidateInput", Instruction: "Validate the input.", OutputKey: "validation_status", Model: m}) + processor, _ := llmagent.New(llmagent.Config{Name: "ProcessData", Instruction: "Process data if {validation_status} is 'valid'.", OutputKey: "result", Model: m}) + reporter, _ := llmagent.New(llmagent.Config{Name: "ReportResult", Instruction: "Report the result from {result}.", Model: m}) + + dataPipeline, _ := sequentialagent.New(sequentialagent.Config{ + AgentConfig: agent.Config{Name: "DataPipeline", SubAgents: []agent.Agent{validator, processor, reporter}}, + }) + // validator runs -> saves to state["validation_status"] + // processor runs -> reads state["validation_status"], saves to state["result"] + // reporter runs -> reads state["result"] + // --8<-- [end:sequential-pipeline-pattern] + _ = dataPipeline // Avoid unused variable error + + // --8<-- [start:parallel-gather-pattern] + // Conceptual Code: Parallel Information Gathering + fetchAPI1, _ := llmagent.New(llmagent.Config{Name: "API1Fetcher", Instruction: "Fetch data from API 1.", OutputKey: "api1_data", Model: m}) + fetchAPI2, _ := llmagent.New(llmagent.Config{Name: "API2Fetcher", Instruction: "Fetch data from API 2.", OutputKey: "api2_data", Model: m}) + + gatherConcurrently, _ := parallelagent.New(parallelagent.Config{ + AgentConfig: agent.Config{Name: "ConcurrentFetch", SubAgents: []agent.Agent{fetchAPI1, fetchAPI2}}, + }) + + synthesizer, _ := llmagent.New(llmagent.Config{Name: "Synthesizer", Instruction: "Combine results from {api1_data} and {api2_data}.", Model: m}) + + overallWorkflow, _ := sequentialagent.New(sequentialagent.Config{ + AgentConfig: agent.Config{Name: "FetchAndSynthesize", SubAgents: []agent.Agent{gatherConcurrently, synthesizer}}, + }) + // fetch_api1 and fetch_api2 run concurrently, saving to state. + // synthesizer runs afterwards, reading state["api1_data"] and state["api2_data"]. + // --8<-- [end:parallel-gather-pattern] + _ = overallWorkflow // Avoid unused variable error + + // --8<-- [start:hierarchical-pattern] + // Conceptual Code: Hierarchical Research Task + // Low-level tool-like agents + webSearcher, _ := llmagent.New(llmagent.Config{Name: "WebSearch", Description: "Performs web searches for facts.", Model: m}) + summarizer, _ := llmagent.New(llmagent.Config{Name: "Summarizer", Description: "Summarizes text.", Model: m}) + + // Mid-level agent combining tools + webSearcherTool := agenttool.New(webSearcher, nil) + summarizerTool := agenttool.New(summarizer, nil) + researchAssistant, _ := llmagent.New(llmagent.Config{ + Name: "ResearchAssistant", + Model: m, + Description: "Finds and summarizes information on a topic.", + Tools: []tool.Tool{webSearcherTool, summarizerTool}, + }) + + // High-level agent delegating research + researchAssistantTool := agenttool.New(researchAssistant, nil) + reportWriter, _ := llmagent.New(llmagent.Config{ + Name: "ReportWriter", + Model: m, + Instruction: "Write a report on topic X. Use the ResearchAssistant to gather information.", + Tools: []tool.Tool{researchAssistantTool}, + }) + // User interacts with ReportWriter. + // ReportWriter calls ResearchAssistant tool. + // ResearchAssistant calls WebSearch and Summarizer tools. + // Results flow back up. + // --8<-- [end:hierarchical-pattern] + _ = reportWriter // Avoid unused variable error + + // --8<-- [start:generator-critic-pattern] + // Conceptual Code: Generator-Critic + generator, _ := llmagent.New(llmagent.Config{ + Name: "DraftWriter", + Instruction: "Write a short paragraph about subject X.", + OutputKey: "draft_text", + Model: m, + }) + + reviewer, _ := llmagent.New(llmagent.Config{ + Name: "FactChecker", + Instruction: "Review the text in {draft_text} for factual accuracy. Output 'valid' or 'invalid' with reasons.", + OutputKey: "review_status", + Model: m, + }) + + reviewPipeline, _ := sequentialagent.New(sequentialagent.Config{ + AgentConfig: agent.Config{Name: "WriteAndReview", SubAgents: []agent.Agent{generator, reviewer}}, + }) + // generator runs -> saves draft to state["draft_text"] + // reviewer runs -> reads state["draft_text"], saves status to state["review_status"] + // --8<-- [end:generator-critic-pattern] + _ = reviewPipeline // Avoid unused variable error + + // --8<-- [start:iterative-refinement-pattern] + // Conceptual Code: Iterative Code Refinement + codeRefiner, _ := llmagent.New(llmagent.Config{ + Name: "CodeRefiner", + Instruction: "Read state['current_code'] (if exists) and state['requirements']. Generate/refine Python code to meet requirements. Save to state['current_code'].", + OutputKey: "current_code", + Model: m, + }) + + qualityChecker, _ := llmagent.New(llmagent.Config{ + Name: "QualityChecker", + Instruction: "Evaluate the code in state['current_code'] against state['requirements']. Output 'pass' or 'fail'.", + OutputKey: "quality_status", + Model: m, + }) + + checkStatusAndEscalate, _ := agent.New(agent.Config{ + Name: "StopChecker", + Run: func(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] { + return func(yield func(*session.Event, error) bool) { + status, _ := ctx.Session().State().Get("quality_status") + shouldStop := status == "pass" + yield(&session.Event{Author: "StopChecker", Actions: session.EventActions{Escalate: shouldStop}}, nil) + } + }, + }) + + refinementLoop, _ := loopagent.New(loopagent.Config{ + MaxIterations: 5, + AgentConfig: agent.Config{Name: "CodeRefinementLoop", SubAgents: []agent.Agent{codeRefiner, qualityChecker, checkStatusAndEscalate}}, + }) + // Loop runs: Refiner -> Checker -> StopChecker + // State["current_code"] is updated each iteration. + // Loop stops if QualityChecker outputs 'pass' (leading to StopChecker escalating) or after 5 iterations. + // --8<-- [end:iterative-refinement-pattern] + _ = refinementLoop // Avoid unused variable error + + // --8<-- [start:human-in-loop-pattern] + // Conceptual Code: Using a Tool for Human Approval + // --- Assume externalApprovalTool exists --- + // func externalApprovalTool(amount float64, reason string) string { ... } + type externalApprovalToolArgs struct { + Amount float64 `json:"amount"` + Reason string `json:"reason"` + } + var externalApprovalTool func(tool.Context, externalApprovalToolArgs) string + approvalTool, _ := tool.NewFunctionTool( + tool.FunctionToolConfig{ + Name: "external_approval_tool", + Description: "Sends a request for human approval.", + }, + externalApprovalTool, + ) + + prepareRequest, _ := llmagent.New(llmagent.Config{ + Name: "PrepareApproval", + Instruction: "Prepare the approval request details based on user input. Store amount and reason in state.", + Model: m, + }) + + requestApproval, _ := llmagent.New(llmagent.Config{ + Name: "RequestHumanApproval", + Instruction: "Use the external_approval_tool with amount from state['approval_amount'] and reason from state['approval_reason'].", + Tools: []tool.Tool{approvalTool}, + OutputKey: "human_decision", + Model: m, + }) + + processDecision, _ := llmagent.New(llmagent.Config{ + Name: "ProcessDecision", + Instruction: "Check {human_decision}. If 'approved', proceed. If 'rejected', inform user.", + Model: m, + }) + + approvalWorkflow, _ := sequentialagent.New(sequentialagent.Config{ + AgentConfig: agent.Config{Name: "HumanApprovalWorkflow", SubAgents: []agent.Agent{prepareRequest, requestApproval, processDecision}}, + }) + // --8<-- [end:human-in-loop-pattern] + _ = approvalWorkflow // Avoid unused variable error +} + +func conceptualSnippets() { + ctx := context.Background() + model, _ := gemini.NewModel(ctx, "gemini-1.5-flash", &genai.ClientConfig{}) + + basicWorkflowSnippets(model) + agentInteractionSnippets(model) + advancedPatternSnippets(model) +} + +func main() { + conceptualSnippets() +} From b408098076c8e0363cbe0a7969845aa544ce3a73 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 24 Oct 2025 11:32:25 -0400 Subject: [PATCH 055/125] Ran linter and fixed API --- examples/go/snippets/agents/multi-agent/main.go | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/examples/go/snippets/agents/multi-agent/main.go b/examples/go/snippets/agents/multi-agent/main.go index ea2f7560..8db3ca12 100644 --- a/examples/go/snippets/agents/multi-agent/main.go +++ b/examples/go/snippets/agents/multi-agent/main.go @@ -15,6 +15,7 @@ import ( "google.golang.org/adk/session" "google.golang.org/adk/tool" "google.golang.org/adk/tool/agenttool" + "google.golang.org/adk/tool/functiontool" "google.golang.org/genai" ) @@ -138,7 +139,7 @@ func agentInteractionSnippets(m model.LLM) { imageBytes := []byte("...") // Simulate image bytes yield(&session.Event{ Author: "ImageGen", - LLMResponse: &model.LLMResponse{ + LLMResponse: model.LLMResponse{ Content: &genai.Content{ Parts: []*genai.Part{genai.NewPartFromBytes(imageBytes, "image/png")}, }, @@ -147,7 +148,7 @@ func agentInteractionSnippets(m model.LLM) { } }, }) - + // Wrap the agent imageTool := agenttool.New(imageAgent, nil) @@ -321,8 +322,8 @@ func advancedPatternSnippets(m model.LLM) { Reason string `json:"reason"` } var externalApprovalTool func(tool.Context, externalApprovalToolArgs) string - approvalTool, _ := tool.NewFunctionTool( - tool.FunctionToolConfig{ + approvalTool, _ := functiontool.New( + functiontool.Config{ Name: "external_approval_tool", Description: "Sends a request for human approval.", }, From 86d531f62f4600052f6758f0359dfa65f4d97fd0 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Sat, 4 Oct 2025 00:47:39 -0400 Subject: [PATCH 056/125] Added Models snippets --- docs/agents/models.md | 6 +++ examples/go/snippets/agents/models/models.go | 54 ++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 examples/go/snippets/agents/models/models.go diff --git a/docs/agents/models.md b/docs/agents/models.md index 577c3cb8..911f60d6 100644 --- a/docs/agents/models.md +++ b/docs/agents/models.md @@ -184,6 +184,12 @@ For deployed applications, a service account is the standard method. // different availability or quota limitations. ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/agents/models/models.go:gemini-example" + ``` + !!!warning "Secure Your Credentials" Service account credentials or API keys are powerful credentials. Never expose them publicly. Use a secret manager like [Google Secret Manager](https://cloud.google.com/secret-manager) to store and access them securely in production. diff --git a/examples/go/snippets/agents/models/models.go b/examples/go/snippets/agents/models/models.go new file mode 100644 index 00000000..c90200be --- /dev/null +++ b/examples/go/snippets/agents/models/models.go @@ -0,0 +1,54 @@ +package main + +import ( + "context" + "log" + + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/llm/gemini" + "google.golang.org/genai" +) + + +func main() { + ctx := context.Background() + // --8<-- [start:gemini-example] + // --- Example using a stable Gemini Flash model --- + modelFlash, err := gemini.NewModel(ctx, "gemini-2.0-flash", &genai.ClientConfig{}) + if err != nil { + log.Fatalf("failed to create model: %v", err) + } + agentGeminiFlash, err := llmagent.New(llmagent.Config{ + // Use the latest stable Flash model identifier + Model: modelFlash, + Name: "gemini_flash_agent", + Instruction: "You are a fast and helpful Gemini assistant.", + // ... other agent parameters + }) + if err != nil { + log.Fatalf("failed to create agent: %v", err) + } + + // --- Example using a powerful Gemini Pro model --- + // Note: Always check the official Gemini documentation for the latest model names, + // including specific preview versions if needed. Preview models might have + // different availability or quota limitations. + modelPro, err := gemini.NewModel(ctx, "gemini-2.5-pro-preview-03-25", &genai.ClientConfig{}) + if err != nil { + log.Fatalf("failed to create model: %v", err) + } + agentGeminiPro, err := llmagent.New(llmagent.Config{ + // Use the latest generally available Pro model identifier + Model: modelPro, + Name: "gemini_pro_agent", + Instruction: "You are a powerful and knowledgeable Gemini assistant.", + // ... other agent parameters + }) + if err != nil { + log.Fatalf("failed to create agent: %v", err) + } + // --8<-- [end:gemini-example] + log.Println("agentGeminiFlash created successfully.") + log.Println("agentGeminiPro created successfully.") + _, _ = agentGeminiFlash, agentGeminiPro // Avoid unused variable error +} From 96de477dbd0a8c5353c6a01a73667797c1a481e1 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Thu, 9 Oct 2025 16:29:25 -0400 Subject: [PATCH 057/125] Migrated to new API --- examples/go/snippets/agents/models/models.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/go/snippets/agents/models/models.go b/examples/go/snippets/agents/models/models.go index c90200be..66d96c24 100644 --- a/examples/go/snippets/agents/models/models.go +++ b/examples/go/snippets/agents/models/models.go @@ -5,7 +5,7 @@ import ( "log" "google.golang.org/adk/agent/llmagent" - "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/model/gemini" "google.golang.org/genai" ) From 17c1577669e5f799c5dc54188ff0169973147053 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Mon, 20 Oct 2025 14:53:55 -0400 Subject: [PATCH 058/125] Embedded imports --- docs/agents/models.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/agents/models.md b/docs/agents/models.md index 911f60d6..05decd44 100644 --- a/docs/agents/models.md +++ b/docs/agents/models.md @@ -187,6 +187,12 @@ For deployed applications, a service account is the standard method. === "Go" ```go + import ( + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/model/gemini" + "google.golang.org/genai" + ) + --8<-- "examples/go/snippets/agents/models/models.go:gemini-example" ``` From 0a2744426b9478b63ddb5693791d418f740c91c3 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 24 Oct 2025 11:35:43 -0400 Subject: [PATCH 059/125] Ran linter --- examples/go/snippets/agents/models/models.go | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/go/snippets/agents/models/models.go b/examples/go/snippets/agents/models/models.go index 66d96c24..be65fe42 100644 --- a/examples/go/snippets/agents/models/models.go +++ b/examples/go/snippets/agents/models/models.go @@ -9,7 +9,6 @@ import ( "google.golang.org/genai" ) - func main() { ctx := context.Background() // --8<-- [start:gemini-example] From e7e2a8b5da76eb68955aeee9622b1860a46215fc Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 10 Oct 2025 01:39:13 -0400 Subject: [PATCH 060/125] Added long running tool --- docs/tools/function-tools.md | 14 ++ .../long-running-tool/long_running_tool.go | 185 ++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go diff --git a/docs/tools/function-tools.md b/docs/tools/function-tools.md index 0404632c..cc942eed 100644 --- a/docs/tools/function-tools.md +++ b/docs/tools/function-tools.md @@ -255,6 +255,12 @@ Define your tool function and wrap it using the `LongRunningFunctionTool` class: } ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go:create_long_running_tool" + ``` + ### Intermediate / Final result Updates Agent client received an event with long running function calls and check the status of the ticket. Then Agent client can send the intermediate or final response back to update the progress. The framework packages this value (even if it's None) into the content of the `FunctionResponse` sent back to the LLM. @@ -316,6 +322,14 @@ Agent client received an event with long running function calls and check the st --8<-- "examples/java/snippets/src/main/java/tools/LongRunningFunctionExample.java:full_code" ``` +=== "Go" + + The following example demonstrates a multi-turn workflow. First, the user asks the agent to create a ticket. The agent calls the long-running tool and the client captures the `FunctionCall` ID. The client then simulates the asynchronous work completing by sending subsequent `FunctionResponse` messages back to the agent to provide the ticket ID and final status. + + ```go + --8<-- "examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go:run_long_running_tool" + ``` + ??? "Python complete example: File Processing Simulation" diff --git a/examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go b/examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go new file mode 100644 index 00000000..c63a10b1 --- /dev/null +++ b/examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go @@ -0,0 +1,185 @@ +package main + +import ( + "context" + "fmt" + "log" + "sync/atomic" + + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/model/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/session" + "google.golang.org/adk/tool" + + "google.golang.org/genai" +) + +// --8<-- [start:create_long_running_tool] +// CreateTicketArgs defines the arguments for our long-running tool. +type CreateTicketArgs struct { + Urgency string `json:"urgency"` +} + +// CreateTicketResults defines the *initial* output of our long-running tool. +// In this simulation, the tool immediately returns, but in a real scenario, +// it would start a background task. +type CreateTicketResults struct { + Status string `json:"status"` +} + +// createTicketAsync simulates the *initiation* of a long-running ticket creation task. +func createTicketAsync(ctx tool.Context, args CreateTicketArgs) CreateTicketResults { + log.Printf("TOOL_EXEC: 'create_ticket_long_running' called with urgency: %s (Call ID: %s)\n", args.Urgency, ctx.FunctionCallID()) + // This is the initial response. The actual ticket ID will be provided later. + return CreateTicketResults{Status: "started"} +} + +func createTicketAgent(ctx context.Context) (agent.Agent, error) { + ticketTool, err := tool.NewLongRunningFunctionTool( + tool.FunctionToolConfig{ + Name: "create_ticket_long_running", + Description: "Creates a new support ticket with a specified urgency level.", + }, + createTicketAsync, + ) + if err != nil { + return nil, fmt.Errorf("failed to create long running tool: %w", err) + } + + model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{}) + if err != nil { + return nil, fmt.Errorf("failed to create model: %v", err) + } + + return llmagent.New(llmagent.Config{ + Name: "ticket_agent", + Model: model, + Instruction: "You are a helpful assistant for creating support tickets. Provide the status of the ticket at each interaction.", + Tools: []tool.Tool{ticketTool}, + }) +} + +// --8<-- [end:create_long_running_tool] + +const ( + userID = "example_user_id" + appName = "example_app" +) + +// --8<-- [start:run_long_running_tool] +// runTurn executes a single turn with the agent and returns the captured function call ID. +func runTurn(ctx context.Context, r *runner.Runner, sessionID, turnLabel string, content *genai.Content) string { + var funcCallID atomic.Value // Safely store the found ID. + + fmt.Printf("\n--- %s ---\n", turnLabel) + for event, err := range r.Run(ctx, userID, sessionID, content, &agent.RunConfig{ + StreamingMode: agent.StreamingModeNone, + }) { + if err != nil { + fmt.Printf("\nAGENT_ERROR: %v\n", err) + continue + } + // Print a summary of the event for clarity. + printEventSummary(event, turnLabel) + + // Capture the function call ID from the event. + if event.LLMResponse != nil && event.LLMResponse.Content != nil { + for _, part := range event.LLMResponse.Content.Parts { + if fc := part.FunctionCall; fc != nil { + if fc.Name == "create_ticket_long_running" { + funcCallID.Store(fc.ID) + } + } + } + } + } + + if id, ok := funcCallID.Load().(string); ok { + return id + } + return "" +} + +func main() { + ctx := context.Background() + ticketAgent, err := createTicketAgent(ctx) + if err != nil { + log.Fatalf("Failed to create agent: %v", err) + } + + // Setup the runner and session. + sessionService := session.InMemoryService() + session, err := sessionService.Create(ctx, &session.CreateRequest{AppName: appName, UserID: userID}) + if err != nil { + log.Fatalf("Failed to create session: %v", err) + } + r, err := runner.New(runner.Config{AppName: appName, Agent: ticketAgent, SessionService: sessionService}) + if err != nil { + log.Fatalf("Failed to create runner: %v", err) + } + + // --- Turn 1: User requests to create a ticket. --- + initialUserMessage := genai.NewContentFromText("Create a high urgency ticket for me.", genai.RoleUser) + funcCallID := runTurn(ctx, r, session.Session.ID(), "Turn 1: User Request", initialUserMessage) + if funcCallID == "" { + log.Fatal("ERROR: Tool 'create_ticket_long_running' not called in Turn 1.") + } + fmt.Printf("ACTION: Captured FunctionCall ID: %s\n", funcCallID) + + // --- Turn 2: App provides the ticket_id after async processing. --- + ticketID := "TICKET-ABC-123" + willContinue := true // Signal that more updates will follow. + ticketCreatedResponse := &genai.FunctionResponse{ + Name: "create_ticket_long_running", + ID: funcCallID, + Response: map[string]any{ + "status": "pending", + "ticket_id": ticketID, + }, + WillContinue: &willContinue, + } + appResponseWithTicketID := &genai.Content{ + Role: string(genai.RoleUser), + Parts: []*genai.Part{{FunctionResponse: ticketCreatedResponse}}, + } + runTurn(ctx, r, session.Session.ID(), "Turn 2: App provides ticket_id", appResponseWithTicketID) + fmt.Printf("ACTION: Sent ticket_id %s to agent.\n", ticketID) + + // --- Turn 3: App provides the final status of the ticket. --- + willContinue = false // Signal that this is the final response. + ticketStatusResponse := &genai.FunctionResponse{ + Name: "create_ticket_long_running", + ID: funcCallID, + Response: map[string]any{ + "status": "approved", + "ticket_id": ticketID, + }, + WillContinue: &willContinue, + } + appResponseWithStatus := &genai.Content{ + Role: string(genai.RoleUser), + Parts: []*genai.Part{{FunctionResponse: ticketStatusResponse}}, + } + runTurn(ctx, r, session.Session.ID(), "Turn 3: App provides ticket status", appResponseWithStatus) + fmt.Println("Long running function completed successfully.") +} + +// printEventSummary provides a readable log of agent and LLM interactions. +func printEventSummary(event *session.Event, turnLabel string) { + if event.LLMResponse != nil && event.LLMResponse.Content != nil { + for _, part := range event.LLMResponse.Content.Parts { + // Check for a text part. + if part.Text != "" { + fmt.Printf("[%s][%s_TEXT]: %s\n", turnLabel, event.Author, part.Text) + } + // Check for a function call part. + if fc := part.FunctionCall; fc != nil { + fmt.Printf("[%s][%s_CALL]: %s(%v) ID: %s\n", turnLabel, event.Author, fc.Name, fc.Args, fc.ID) + } + } + } +} + +// --8<-- [end:run_long_running_tool] From 059c66e73f589d9e85f213257f88c69b536eccbd Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 22 Oct 2025 20:07:36 -0400 Subject: [PATCH 061/125] Ran linter and fixed API --- .../long-running-tool/long_running_tool.go | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go b/examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go index c63a10b1..f2625a1d 100644 --- a/examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go +++ b/examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go @@ -12,6 +12,7 @@ import ( "google.golang.org/adk/runner" "google.golang.org/adk/session" "google.golang.org/adk/tool" + "google.golang.org/adk/tool/functiontool" "google.golang.org/genai" ) @@ -37,8 +38,8 @@ func createTicketAsync(ctx tool.Context, args CreateTicketArgs) CreateTicketResu } func createTicketAgent(ctx context.Context) (agent.Agent, error) { - ticketTool, err := tool.NewLongRunningFunctionTool( - tool.FunctionToolConfig{ + ticketTool, err := functiontool.New( + functiontool.Config{ Name: "create_ticket_long_running", Description: "Creates a new support ticket with a specified urgency level.", }, @@ -74,7 +75,7 @@ func runTurn(ctx context.Context, r *runner.Runner, sessionID, turnLabel string, var funcCallID atomic.Value // Safely store the found ID. fmt.Printf("\n--- %s ---\n", turnLabel) - for event, err := range r.Run(ctx, userID, sessionID, content, &agent.RunConfig{ + for event, err := range r.Run(ctx, userID, sessionID, content, agent.RunConfig{ StreamingMode: agent.StreamingModeNone, }) { if err != nil { @@ -85,12 +86,10 @@ func runTurn(ctx context.Context, r *runner.Runner, sessionID, turnLabel string, printEventSummary(event, turnLabel) // Capture the function call ID from the event. - if event.LLMResponse != nil && event.LLMResponse.Content != nil { - for _, part := range event.LLMResponse.Content.Parts { - if fc := part.FunctionCall; fc != nil { - if fc.Name == "create_ticket_long_running" { - funcCallID.Store(fc.ID) - } + for _, part := range event.LLMResponse.Content.Parts { + if fc := part.FunctionCall; fc != nil { + if fc.Name == "create_ticket_long_running" { + funcCallID.Store(fc.ID) } } } From 0d0b756db6ef32d66246e65785dc03da5bfbe39a Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 22 Oct 2025 20:46:31 -0400 Subject: [PATCH 062/125] Switched to two-turn style --- .../long-running-tool/long_running_tool.go | 62 ++++++++----------- 1 file changed, 26 insertions(+), 36 deletions(-) diff --git a/examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go b/examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go index f2625a1d..e80657fb 100644 --- a/examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go +++ b/examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go @@ -24,17 +24,25 @@ type CreateTicketArgs struct { } // CreateTicketResults defines the *initial* output of our long-running tool. -// In this simulation, the tool immediately returns, but in a real scenario, -// it would start a background task. type CreateTicketResults struct { - Status string `json:"status"` + Status string `json:"status"` + TicketId string `json:"ticket_id"` } // createTicketAsync simulates the *initiation* of a long-running ticket creation task. func createTicketAsync(ctx tool.Context, args CreateTicketArgs) CreateTicketResults { log.Printf("TOOL_EXEC: 'create_ticket_long_running' called with urgency: %s (Call ID: %s)\n", args.Urgency, ctx.FunctionCallID()) - // This is the initial response. The actual ticket ID will be provided later. - return CreateTicketResults{Status: "started"} + + // "Generate" a ticket ID and return it in the initial response. + ticketID := "TICKET-ABC-123" + log.Printf("ACTION: Generated Ticket ID: %s for Call ID: %s\n", ticketID, ctx.FunctionCallID()) + + // In a real application, you would save the association between the + // FunctionCallID and the ticketID to handle the async response later. + return CreateTicketResults{ + Status: "started", + TicketId: ticketID, + } } func createTicketAgent(ctx context.Context) (agent.Agent, error) { @@ -127,27 +135,11 @@ func main() { } fmt.Printf("ACTION: Captured FunctionCall ID: %s\n", funcCallID) - // --- Turn 2: App provides the ticket_id after async processing. --- + // --- Turn 2: App provides the final status of the ticket. --- + // In a real application, the ticketID would be retrieved from a database + // using the funcCallID. For this example, we'll use the same ID. ticketID := "TICKET-ABC-123" - willContinue := true // Signal that more updates will follow. - ticketCreatedResponse := &genai.FunctionResponse{ - Name: "create_ticket_long_running", - ID: funcCallID, - Response: map[string]any{ - "status": "pending", - "ticket_id": ticketID, - }, - WillContinue: &willContinue, - } - appResponseWithTicketID := &genai.Content{ - Role: string(genai.RoleUser), - Parts: []*genai.Part{{FunctionResponse: ticketCreatedResponse}}, - } - runTurn(ctx, r, session.Session.ID(), "Turn 2: App provides ticket_id", appResponseWithTicketID) - fmt.Printf("ACTION: Sent ticket_id %s to agent.\n", ticketID) - - // --- Turn 3: App provides the final status of the ticket. --- - willContinue = false // Signal that this is the final response. + willContinue := false // Signal that this is the final response. ticketStatusResponse := &genai.FunctionResponse{ Name: "create_ticket_long_running", ID: funcCallID, @@ -161,22 +153,20 @@ func main() { Role: string(genai.RoleUser), Parts: []*genai.Part{{FunctionResponse: ticketStatusResponse}}, } - runTurn(ctx, r, session.Session.ID(), "Turn 3: App provides ticket status", appResponseWithStatus) + runTurn(ctx, r, session.Session.ID(), "Turn 2: App provides ticket status", appResponseWithStatus) fmt.Println("Long running function completed successfully.") } // printEventSummary provides a readable log of agent and LLM interactions. func printEventSummary(event *session.Event, turnLabel string) { - if event.LLMResponse != nil && event.LLMResponse.Content != nil { - for _, part := range event.LLMResponse.Content.Parts { - // Check for a text part. - if part.Text != "" { - fmt.Printf("[%s][%s_TEXT]: %s\n", turnLabel, event.Author, part.Text) - } - // Check for a function call part. - if fc := part.FunctionCall; fc != nil { - fmt.Printf("[%s][%s_CALL]: %s(%v) ID: %s\n", turnLabel, event.Author, fc.Name, fc.Args, fc.ID) - } + for _, part := range event.LLMResponse.Content.Parts { + // Check for a text part. + if part.Text != "" { + fmt.Printf("[%s][%s_TEXT]: %s\n", turnLabel, event.Author, part.Text) + } + // Check for a function call part. + if fc := part.FunctionCall; fc != nil { + fmt.Printf("[%s][%s_CALL]: %s(%v) ID: %s\n", turnLabel, event.Author, fc.Name, fc.Args, fc.ID) } } } From cfd9dcb6afb2c4928b424993430884f1b08a6705 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 24 Oct 2025 13:59:09 -0400 Subject: [PATCH 063/125] Ran linter --- .../function-tools/long-running-tool/long_running_tool.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go b/examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go index e80657fb..b484f9bf 100644 --- a/examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go +++ b/examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go @@ -94,7 +94,7 @@ func runTurn(ctx context.Context, r *runner.Runner, sessionID, turnLabel string, printEventSummary(event, turnLabel) // Capture the function call ID from the event. - for _, part := range event.LLMResponse.Content.Parts { + for _, part := range event.Content.Parts { if fc := part.FunctionCall; fc != nil { if fc.Name == "create_ticket_long_running" { funcCallID.Store(fc.ID) @@ -159,7 +159,7 @@ func main() { // printEventSummary provides a readable log of agent and LLM interactions. func printEventSummary(event *session.Event, turnLabel string) { - for _, part := range event.LLMResponse.Content.Parts { + for _, part := range event.Content.Parts { // Check for a text part. if part.Text != "" { fmt.Printf("[%s][%s_TEXT]: %s\n", turnLabel, event.Author, part.Text) From 8203e1cc0f3b018ba7e3969171293e8cbb713292 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 10 Oct 2025 22:48:27 -0400 Subject: [PATCH 064/125] Added callback snippets --- docs/callbacks/index.md | 11 ++ examples/go/snippets/callbacks/main.go | 188 +++++++++++++++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 examples/go/snippets/callbacks/main.go diff --git a/docs/callbacks/index.md b/docs/callbacks/index.md index 152e4ca8..c146909a 100644 --- a/docs/callbacks/index.md +++ b/docs/callbacks/index.md @@ -46,6 +46,12 @@ Callbacks are a cornerstone feature of ADK, providing a powerful mechanism to ho --8<-- "examples/java/snippets/src/main/java/callbacks/AgentWithBeforeModelCallback.java:init" ``` + === "Golang" + + ```go + --8<-- "examples/go/snippets/callbacks/main.go:callback_basic" + ``` + ## The Callback Mechanism: Interception and Control When the ADK framework encounters a point where a callback can run (e.g., just before calling the LLM), it checks if you provided a corresponding callback function for that agent. If you did, the framework executes your function. @@ -89,5 +95,10 @@ This example demonstrates the common pattern for a guardrail using `before_model ```java --8<-- "examples/java/snippets/src/main/java/callbacks/BeforeModelGuardrailExample.java:init" ``` + + === "Golang" + ```go + --8<-- "examples/go/snippets/callbacks/main.go:guardrail_init" + ``` By understanding this mechanism of returning `None` versus returning specific objects, you can precisely control the agent's execution path, making callbacks an essential tool for building sophisticated and reliable agents with ADK. diff --git a/examples/go/snippets/callbacks/main.go b/examples/go/snippets/callbacks/main.go new file mode 100644 index 00000000..94498e0d --- /dev/null +++ b/examples/go/snippets/callbacks/main.go @@ -0,0 +1,188 @@ +package main + +import ( + "context" + "fmt" + "log" + "strings" + + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/model" + "google.golang.org/adk/model/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/session" + "google.golang.org/genai" +) + +const ( + modelName = "gemini-2.5-flash" +) + +// --8<-- [start:callback_basic] +// onBeforeModel is a callback function that gets triggered before an LLM call. +func onBeforeModel(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) { + log.Println("--- onBeforeModel Callback Triggered ---") + log.Printf("Model Request to be sent: %v\n", req) + // Returning nil allows the default LLM call to proceed. + return nil, nil +} + +func runBasicExample() { + const ( + appName = "CallbackBasicApp" + userID = "test_user_123" + ) + ctx := context.Background() + geminiModel, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + log.Fatalf("Failed to create model: %v", err) + } + + // Register the callback function in the agent configuration. + agentCfg := llmagent.Config{ + Name: "SimpleAgent", + Model: geminiModel, + BeforeModel: []llmagent.BeforeModelCallback{onBeforeModel}, + } + simpleAgent, err := llmagent.New(agentCfg) + if err != nil { + log.Fatalf("Failed to create agent: %v", err) + } + + sessionService := session.InMemoryService() + r, err := runner.New(runner.Config{ + AppName: appName, + Agent: simpleAgent, + SessionService: sessionService, + }) + if err != nil { + log.Fatalf("Failed to create runner: %v", err) + } + // --8<-- [end:callback_basic] + + session, err := sessionService.Create(ctx, &session.CreateRequest{ + AppName: appName, + UserID: userID, + }) + if err != nil { + log.Fatalf("Failed to create session: %v", err) + } + + input := genai.NewContentFromText("Why is the sky blue?", genai.RoleUser) + log.Println("--- Running Agent ---") + events := r.Run(ctx, userID, session.Session.ID(), input, &agent.RunConfig{ + StreamingMode: agent.StreamingModeNone, + }) + + for event, err := range events { + if err != nil { + log.Fatalf("Error during agent execution: %v", err) + } + for _, p := range event.LLMResponse.Content.Parts { + fmt.Printf("Final Response: %s\n", p.Text) + } + } + log.Println("--- Agent Run Finished ---") +} + +// --8<-- [start:guardrail_init] +// onBeforeModelGuardrail is a callback that inspects the LLM request. +// If it contains a forbidden topic, it blocks the request and returns a +// predefined response. Otherwise, it allows the request to proceed. +func onBeforeModelGuardrail(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) { + log.Println("--- onBeforeModelGuardrail Callback Triggered ---") + + // Inspect the request content for forbidden topics. + for _, content := range req.Contents { + for _, part := range content.Parts { + if strings.Contains(part.Text, "finance") { + log.Println("Forbidden topic 'finance' detected. Blocking LLM call.") + // By returning a non-nil response, we override the default behavior + // and prevent the actual LLM call. + return &model.LLMResponse{ + Content: &genai.Content{ + Parts: []*genai.Part{{Text: "I'm sorry, but I cannot discuss financial topics."}}, + Role: "model", + }, + }, nil + } + } + } + + log.Println("No forbidden topics found. Allowing LLM call to proceed.") + // Returning nil allows the default LLM call to proceed. + return nil, nil +} + +func runGuardrailExample() { + const ( + appName = "GuardrailApp" + userID = "test_user_456" + ) + ctx := context.Background() + geminiModel, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + log.Fatalf("Failed to create model: %v", err) + } + + agentCfg := llmagent.Config{ + Name: "ChatAgent", + Model: geminiModel, + BeforeModel: []llmagent.BeforeModelCallback{onBeforeModelGuardrail}, + } + chatAgent, err := llmagent.New(agentCfg) + if err != nil { + log.Fatalf("Failed to create agent: %v", err) + } + + sessionService := session.InMemoryService() + r, err := runner.New(runner.Config{ + AppName: appName, + Agent: chatAgent, + SessionService: sessionService, + }) + if err != nil { + log.Fatalf("Failed to create runner: %v", err) + } + // --8<-- [end:guardrail_init] + + // --- Run with a safe prompt --- + runAndPrint(ctx, r, sessionService, appName, "Tell me a fun fact about the Roman Empire.") + + // --- Run with a forbidden prompt --- + runAndPrint(ctx, r, sessionService, appName, "What is the best way to manage my finance portfolio?") +} + +func runAndPrint(ctx context.Context, r *runner.Runner, sessionService session.Service, appName, prompt string) { + session, err := sessionService.Create(ctx, &session.CreateRequest{ + AppName: appName, + UserID: "test_user", // UserID can be generic here for the helper + }) + if err != nil { + log.Fatalf("Failed to create session: %v", err) + } + + input := genai.NewContentFromText(prompt, genai.RoleUser) + log.Printf("\n--- Running Agent with prompt: %q ---\n", prompt) + events := r.Run(ctx, session.Session.UserID(), session.Session.ID(), input, &agent.RunConfig{ + StreamingMode: agent.StreamingModeNone, + }) + + for event, err := range events { + if err != nil { + log.Fatalf("Error during agent execution: %v", err) + } + for _, p := range event.Content.Parts { + fmt.Printf("Final Response: %s\n", p.Text) + } + } + log.Println("--- Agent Run Finished ---") +} + +func main() { + fmt.Println("--- Running Basic Callback Example ---") + runBasicExample() + fmt.Println("\n\n--- Running Guardrail Callback Example ---") + runGuardrailExample() +} From 2a1ebcd6f73d447271fa45515c0eaf9600b8410d Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 10 Oct 2025 22:59:29 -0400 Subject: [PATCH 065/125] Added back imports --- docs/callbacks/index.md | 6 ++++++ examples/go/snippets/callbacks/main.go | 2 ++ 2 files changed, 8 insertions(+) diff --git a/docs/callbacks/index.md b/docs/callbacks/index.md index c146909a..cbb3502b 100644 --- a/docs/callbacks/index.md +++ b/docs/callbacks/index.md @@ -49,6 +49,9 @@ Callbacks are a cornerstone feature of ADK, providing a powerful mechanism to ho === "Golang" ```go + --8<-- "examples/go/snippets/callbacks/main.go:imports" + + --8<-- "examples/go/snippets/callbacks/main.go:callback_basic" ``` @@ -98,6 +101,9 @@ This example demonstrates the common pattern for a guardrail using `before_model === "Golang" ```go + --8<-- "examples/go/snippets/callbacks/main.go:imports" + + --8<-- "examples/go/snippets/callbacks/main.go:guardrail_init" ``` diff --git a/examples/go/snippets/callbacks/main.go b/examples/go/snippets/callbacks/main.go index 94498e0d..50a16c90 100644 --- a/examples/go/snippets/callbacks/main.go +++ b/examples/go/snippets/callbacks/main.go @@ -1,3 +1,4 @@ +// --8<-- [start:imports] package main import ( @@ -14,6 +15,7 @@ import ( "google.golang.org/adk/session" "google.golang.org/genai" ) +// --8<-- [end:imports] const ( modelName = "gemini-2.5-flash" From b74619b5c9c58f236f056408e9046d3110577c92 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Sat, 11 Oct 2025 00:42:54 -0400 Subject: [PATCH 066/125] Added types of callbacks snippets --- docs/callbacks/types-of-callbacks.md | 71 ++--- .../callbacks/types_of_callbacks/main.go | 281 ++++++++++++++++++ 2 files changed, 308 insertions(+), 44 deletions(-) create mode 100644 examples/go/snippets/callbacks/types_of_callbacks/main.go diff --git a/docs/callbacks/types-of-callbacks.md b/docs/callbacks/types-of-callbacks.md index f6ae1688..1f7c95cc 100644 --- a/docs/callbacks/types-of-callbacks.md +++ b/docs/callbacks/types-of-callbacks.md @@ -29,6 +29,13 @@ These callbacks are available on *any* agent that inherits from `BaseAgent` (inc --8<-- "examples/java/snippets/src/main/java/callbacks/BeforeAgentCallbackExample.java:init" ``` + === "Golang" + + ```go + --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:imports" + --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:before_agent_example" + ``` + **Note on the `before_agent_callback` Example:** @@ -61,6 +68,13 @@ These callbacks are available on *any* agent that inherits from `BaseAgent` (inc --8<-- "examples/java/snippets/src/main/java/callbacks/AfterAgentCallbackExample.java:init" ``` + === "Golang" + + ```go + --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:imports" + --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:after_agent_example" + ``` + **Note on the `after_agent_callback` Example:** @@ -99,6 +113,13 @@ If the callback returns `None` (or a `Maybe.empty()` object in Java), the LLM co --8<-- "examples/java/snippets/src/main/java/callbacks/BeforeModelCallbackExample.java:init" ``` + === "Golang" + + ```go + --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:imports" + --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:before_model_example" + ``` + ### After Model Callback **When:** Called just after a response (`LlmResponse`) is received from the LLM, before it's processed further by the invoking agent. @@ -124,57 +145,19 @@ If the callback returns `None` (or a `Maybe.empty()` object in Java), the LLM co --8<-- "examples/java/snippets/src/main/java/callbacks/AfterModelCallbackExample.java:init" ``` -## Tool Execution Callbacks - -These callbacks are also specific to `LlmAgent` and trigger around the execution of tools (including `FunctionTool`, `AgentTool`, etc.) that the LLM might request. - -### Before Tool Callback - -**When:** Called just before a specific tool's `run_async` method is invoked, after the LLM has generated a function call for it. - -**Purpose:** Allows inspection and modification of tool arguments, performing authorization checks before execution, logging tool usage attempts, or implementing tool-level caching. + === "Golang" -**Return Value Effect:** - -1. If the callback returns `None` (or a `Maybe.empty()` object in Java), the tool's `run_async` method is executed with the (potentially modified) `args`. -2. If a dictionary (or `Map` in Java) is returned, the tool's `run_async` method is **skipped**. The returned dictionary is used directly as the result of the tool call. This is useful for caching or overriding tool behavior. - - -??? "Code" - === "Python" - - ```python - --8<-- "examples/python/snippets/callbacks/before_tool_callback.py" - ``` - - === "Java" - - ```java - --8<-- "examples/java/snippets/src/main/java/callbacks/BeforeToolCallbackExample.java:init" + ```go + --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:imports" + --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:after_model_example" ``` +## Tool Execution Callbacks +These callbacks are also specific to `LlmAgent` and trigger around the execution of tools (including `FunctionTool`, `AgentTool`, etc.) that the LLM might request. -### After Tool Callback -**When:** Called just after the tool's `run_async` method completes successfully. -**Purpose:** Allows inspection and modification of the tool's result before it's sent back to the LLM (potentially after summarization). Useful for logging tool results, post-processing or formatting results, or saving specific parts of the result to the session state. -**Return Value Effect:** -1. If the callback returns `None` (or a `Maybe.empty()` object in Java), the original `tool_response` is used. -2. If a new dictionary is returned, it **replaces** the original `tool_response`. This allows modifying or filtering the result seen by the LLM. -??? "Code" - === "Python" - - ```python - --8<-- "examples/python/snippets/callbacks/after_tool_callback.py" - ``` - - === "Java" - - ```java - --8<-- "examples/java/snippets/src/main/java/callbacks/AfterToolCallbackExample.java:init" - ``` diff --git a/examples/go/snippets/callbacks/types_of_callbacks/main.go b/examples/go/snippets/callbacks/types_of_callbacks/main.go new file mode 100644 index 00000000..8c069692 --- /dev/null +++ b/examples/go/snippets/callbacks/types_of_callbacks/main.go @@ -0,0 +1,281 @@ +package main + +import ( + "context" + "fmt" + "log" + "strings" + + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/model" + "google.golang.org/adk/model/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/session" + "google.golang.org/genai" +) + +const ( + modelName = "gemini-2.5-flash" +) + +// --8<-- [start:imports] +// The package and import block is included here. +// In the documentation, this snippet is reused for each example. +// --8<-- [end:imports] + +// --8<-- [start:before_agent_example] +// 1. Define the Callback Function +func onBeforeAgent(ctx agent.CallbackContext) (*genai.Content, error) { + agentName := ctx.AgentName() + log.Printf("[Callback] Entering agent: %s", agentName) + if skip, _ := ctx.State().Get("skip_llm_agent"); skip == true { + log.Printf("[Callback] State condition met: Skipping agent %s", agentName) + return genai.NewContentFromText( + fmt.Sprintf("Agent %s skipped by before_agent_callback.", agentName), + genai.RoleModel, + ), + nil + } + log.Printf("[Callback] State condition not met: Running agent %s", agentName) + return nil, nil +} + +// 2. Define a function to set up and run the agent with the callback. +func runBeforeAgentExample() { + ctx := context.Background() + geminiModel, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + log.Fatalf("FATAL: Failed to create model: %v", err) + } + + // 3. Register the callback in the agent configuration. + llmCfg := llmagent.Config{ + Name: "AgentWithBeforeAgentCallback", + BeforeAgent: []agent.BeforeAgentCallback{onBeforeAgent}, + Model: geminiModel, + Instruction: "You are a concise assistant.", + } + testAgent, err := llmagent.New(llmCfg) + if err != nil { + log.Fatalf("FATAL: Failed to create agent: %v", err) + } + + const appName = "BeforeAgentApp" + sessionService := session.InMemoryService() + r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) + if err != nil { + log.Fatalf("FATAL: Failed to create runner: %v", err) + } + + // 4. Run scenarios to demonstrate the callback's behavior. + log.Println("--- SCENARIO 1: Agent should run normally ---") + runScenario(ctx, r, sessionService, appName, "session_normal", nil, "Hello, world!") + + log.Println("\n--- SCENARIO 2: Agent should be skipped ---") + runScenario(ctx, r, sessionService, appName, "session_skip", map[string]any{"skip_llm_agent": true}, "This should be skipped.") +} +// --8<-- [end:before_agent_example] + +// --8<-- [start:after_agent_example] +func onAfterAgent(ctx agent.CallbackContext, finalEvent *session.Event, runErr error) (*genai.Content, error) { + agentName := ctx.AgentName() + invocationID := ctx.InvocationID() + state := ctx.State() + + log.Printf("\n[Callback] Exiting agent: %s (Inv: %s)", agentName, invocationID) + log.Printf("[Callback] Current State: %v", state) + + if runErr != nil { + log.Printf("[Callback] Agent run produced an error: %v. Passing through.", runErr) + return nil, runErr + } + + if addNote, _ := state.Get("add_concluding_note"); addNote == true { + log.Printf("[Callback] State condition 'add_concluding_note=True' met: Replacing agent %s's output.", agentName) + return genai.NewContentFromText( + "Concluding note added by after_agent_callback, replacing original output.", + genai.RoleModel, + ), nil + } + + log.Printf("[Callback] State condition not met: Using agent %s's original output.", agentName) + return nil, nil +} + +func runAfterAgentExample() { + ctx := context.Background() + geminiModel, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + log.Fatalf("FATAL: Failed to create model: %v", err) + } + + llmCfg := llmagent.Config{ + Name: "AgentWithAfterAgentCallback", + AfterAgent: []agent.AfterAgentCallback{onAfterAgent}, + Model: geminiModel, + Instruction: "You are a simple agent. Just say 'Processing complete!'", + } + testAgent, err := llmagent.New(llmCfg) + if err != nil { + log.Fatalf("FATAL: Failed to create agent: %v", err) + } + + const appName = "AfterAgentApp" + sessionService := session.InMemoryService() + r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) + if err != nil { + log.Fatalf("FATAL: Failed to create runner: %v", err) + } + + log.Println("--- SCENARIO 1: Should use original output ---") + runScenario(ctx, r, sessionService, appName, "session_normal", nil, "Process this.") + + log.Println("\n--- SCENARIO 2: Should replace output ---") + runScenario(ctx, r, sessionService, appName, "session_modify", map[string]any{"add_concluding_note": true}, "Process and add note.") +} +// --8<-- [end:after_agent_example] + +// --8<-- [start:before_model_example] +func onBeforeModel(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) { + log.Printf("[Callback] BeforeModel triggered for agent %q.", ctx.AgentName()) + for _, content := range req.Contents { + for _, part := range content.Parts { + if strings.Contains(part.Text, "BLOCK") { + log.Println("[Callback] 'BLOCK' keyword found. Skipping LLM call.") + return &model.LLMResponse{ + Content: &genai.Content{ + Parts: []*genai.Part{{Text: "LLM call was blocked by before_model_callback."}}, + Role: "model", + }, + }, + nil + } + } + } + return nil, nil +} + +func runBeforeModelExample() { + ctx := context.Background() + geminiModel, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + log.Fatalf("FATAL: Failed to create model: %v", err) + } + + llmCfg := llmagent.Config{ + Name: "AgentWithBeforeModelCallback", + Model: geminiModel, + BeforeModel: []llmagent.BeforeModelCallback{onBeforeModel}, + } + testAgent, err := llmagent.New(llmCfg) + if err != nil { + log.Fatalf("FATAL: Failed to create agent: %v", err) + } + + const appName = "BeforeModelApp" + sessionService := session.InMemoryService() + r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) + if err != nil { + log.Fatalf("FATAL: Failed to create runner: %v", err) + } + + log.Println("--- SCENARIO 1: Should proceed to LLM ---") + runScenario(ctx, r, sessionService, appName, "session_normal", nil, "This is a safe prompt.") + + log.Println("\n--- SCENARIO 2: Should be blocked by callback ---") + runScenario(ctx, r, sessionService, appName, "session_blocked", nil, "This prompt should be BLOCKED.") +} +// --8<-- [end:before_model_example] + +// --8<-- [start:after_model_example] +func onAfterModel(ctx agent.CallbackContext, resp *model.LLMResponse, respErr error) (*model.LLMResponse, error) { + log.Printf("[Callback] AfterModel triggered for agent %q.", ctx.AgentName()) + if respErr != nil { + log.Printf("[Callback] Model returned an error: %v. Passing it through.", respErr) + return nil, respErr + } + if resp == nil || resp.Content == nil || len(resp.Content.Parts) == 0 { + log.Println("[Callback] Response is nil or has no parts, nothing to process.") + return nil, nil + } + if censor, _ := ctx.State().Get("censor_response"); censor == true { + log.Println("[Callback] 'censor_response' is true. Censoring response.") + originalText := resp.Content.Parts[0].Text + censoredText := strings.ReplaceAll(originalText, "blue", "[CENSORED]") + resp.Content.Parts[0].Text = censoredText + return resp, nil + } + return resp, nil +} + +func runAfterModelExample() { + ctx := context.Background() + geminiModel, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + log.Fatalf("FATAL: Failed to create model: %v", err) + } + + llmCfg := llmagent.Config{ + Name: "AgentWithAfterModelCallback", + Model: geminiModel, + AfterModel: []llmagent.AfterModelCallback{onAfterModel}, + } + testAgent, err := llmagent.New(llmCfg) + if err != nil { + log.Fatalf("FATAL: Failed to create agent: %v", err) + } + + const appName = "AfterModelApp" + sessionService := session.InMemoryService() + r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) + if err != nil { + log.Fatalf("FATAL: Failed to create runner: %v", err) + } + + log.Println("--- SCENARIO 1: Response should be censored ---") + runScenario(ctx, r, sessionService, appName, "session_censor", map[string]any{"censor_response": true}, "Why is the sky blue?") + + log.Println("\n--- SCENARIO 2: Response should be normal ---") + runScenario(ctx, r, sessionService, appName, "session_normal", map[string]any{"censor_response": false}, "Why is the sky blue?") +} +// --8<-- [end:after_model_example] + +func main() { + log.Println("--- Running BeforeAgent Example ---") + runBeforeAgentExample() + + log.Println("\n\n--- Running AfterAgent Example ---") + runAfterAgentExample() + + log.Println("\n\n--- Running BeforeModel Example ---") + runBeforeModelExample() + + log.Println("\n\n--- Running AfterModel Example ---") + runAfterModelExample() +} + +// Generic helper to run a single scenario. +func runScenario(ctx context.Context, r *runner.Runner, sessionService session.Service, appName, sessionID string, initialState map[string]any, prompt string) { + log.Printf("Running scenario for session: %s, initial state: %v", sessionID, initialState) + sessionResp, err := sessionService.Create(ctx, &session.CreateRequest{AppName: appName, UserID: "test_user", SessionID: sessionID, State: initialState}) + if err != nil { + log.Fatalf("FATAL: Failed to create session: %v", err) + } + + input := genai.NewContentFromText(prompt, genai.RoleUser) + events := r.Run(ctx, sessionResp.Session.UserID(), sessionResp.Session.ID(), input, &agent.RunConfig{}) + for event, err := range events { + if err != nil { + log.Printf("ERROR during agent execution: %v", err) + return + } + + // Print only the final output from the agent. + if event.LLMResponse != nil && event.LLMResponse.Content != nil && len(event.LLMResponse.Content.Parts) > 0 { + fmt.Printf("Final Output for %s: [%s] %s\n", sessionID, event.Author, event.LLMResponse.Content.Parts[0].Text) + } else { + log.Printf("Final response for %s received, but it has no content to display.", sessionID) + } + } +} From a1d58f3fd426d9968668f7b9710c580adcf2aa6a Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Sat, 11 Oct 2025 22:59:03 -0400 Subject: [PATCH 067/125] feat(go): align callback snippets with other languages --- docs/callbacks/types-of-callbacks.md | 53 ++++++++++++ .../callbacks/types_of_callbacks/main.go | 82 +++++++++++++------ 2 files changed, 109 insertions(+), 26 deletions(-) diff --git a/docs/callbacks/types-of-callbacks.md b/docs/callbacks/types-of-callbacks.md index 1f7c95cc..ea952b45 100644 --- a/docs/callbacks/types-of-callbacks.md +++ b/docs/callbacks/types-of-callbacks.md @@ -33,6 +33,8 @@ These callbacks are available on *any* agent that inherits from `BaseAgent` (inc ```go --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:imports" + + --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:before_agent_example" ``` @@ -72,6 +74,8 @@ These callbacks are available on *any* agent that inherits from `BaseAgent` (inc ```go --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:imports" + + --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:after_agent_example" ``` @@ -117,6 +121,8 @@ If the callback returns `None` (or a `Maybe.empty()` object in Java), the LLM co ```go --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:imports" + + --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:before_model_example" ``` @@ -149,6 +155,8 @@ If the callback returns `None` (or a `Maybe.empty()` object in Java), the LLM co ```go --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:imports" + + --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:after_model_example" ``` @@ -156,8 +164,53 @@ If the callback returns `None` (or a `Maybe.empty()` object in Java), the LLM co These callbacks are also specific to `LlmAgent` and trigger around the execution of tools (including `FunctionTool`, `AgentTool`, etc.) that the LLM might request. +### Before Tool Callback + +**When:** Called just before a specific tool's `run_async` method is invoked, after the LLM has generated a function call for it. + +**Purpose:** Allows inspection and modification of tool arguments, performing authorization checks before execution, logging tool usage attempts, or implementing tool-level caching. + +**Return Value Effect:** + +1. If the callback returns `None` (or a `Maybe.empty()` object in Java), the tool's `run_async` method is executed with the (potentially modified) `args`. +2. If a dictionary (or `Map` in Java) is returned, the tool's `run_async` method is **skipped**. The returned dictionary is used directly as the result of the tool call. This is useful for caching or overriding tool behavior. + + +??? "Code" + === "Python" + + ```python + --8<-- "examples/python/snippets/callbacks/before_tool_callback.py" + ``` + + === "Java" + + ```java + --8<-- "examples/java/snippets/src/main/java/callbacks/BeforeToolCallbackExample.java:init" + ``` + + +### After Tool Callback +**When:** Called just after the tool's `run_async` method completes successfully. +**Purpose:** Allows inspection and modification of the tool's result before it's sent back to the LLM (potentially after summarization). Useful for logging tool results, post-processing or formatting results, or saving specific parts of the result to the session state. +**Return Value Effect:** +1. If the callback returns `None` (or a `Maybe.empty()` object in Java), the original `tool_response` is used. +2. If a new dictionary is returned, it **replaces** the original `tool_response`. This allows modifying or filtering the result seen by the LLM. + +??? "Code" + === "Python" + + ```python + --8<-- "examples/python/snippets/callbacks/after_tool_callback.py" + ``` + + === "Java" + + ```java + --8<-- "examples/java/snippets/src/main/java/callbacks/AfterToolCallbackExample.java:init" + ``` diff --git a/examples/go/snippets/callbacks/types_of_callbacks/main.go b/examples/go/snippets/callbacks/types_of_callbacks/main.go index 8c069692..de53db81 100644 --- a/examples/go/snippets/callbacks/types_of_callbacks/main.go +++ b/examples/go/snippets/callbacks/types_of_callbacks/main.go @@ -1,9 +1,11 @@ +// --8<-- [start:imports] package main import ( "context" "fmt" "log" + "regexp" "strings" "google.golang.org/adk/agent" @@ -15,15 +17,14 @@ import ( "google.golang.org/genai" ) +// --8<-- [end:imports] + const ( modelName = "gemini-2.5-flash" + userID = "user_1" + appName = "CallbackExamplesApp" ) -// --8<-- [start:imports] -// The package and import block is included here. -// In the documentation, this snippet is reused for each example. -// --8<-- [end:imports] - // --8<-- [start:before_agent_example] // 1. Define the Callback Function func onBeforeAgent(ctx agent.CallbackContext) (*genai.Content, error) { @@ -61,7 +62,6 @@ func runBeforeAgentExample() { log.Fatalf("FATAL: Failed to create agent: %v", err) } - const appName = "BeforeAgentApp" sessionService := session.InMemoryService() r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) if err != nil { @@ -121,7 +121,6 @@ func runAfterAgentExample() { log.Fatalf("FATAL: Failed to create agent: %v", err) } - const appName = "AfterAgentApp" sessionService := session.InMemoryService() r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) if err != nil { @@ -139,20 +138,35 @@ func runAfterAgentExample() { // --8<-- [start:before_model_example] func onBeforeModel(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) { log.Printf("[Callback] BeforeModel triggered for agent %q.", ctx.AgentName()) + + // Modification Example: Add a prefix to the system instruction. + if req.Config.SystemInstruction != nil { + prefix := "[Modified by Callback] " + // This is a simplified example; production code might need deeper checks. + if len(req.Config.SystemInstruction.Parts) > 0 { + req.Config.SystemInstruction.Parts[0].Text = prefix + req.Config.SystemInstruction.Parts[0].Text + } else { + req.Config.SystemInstruction.Parts = append(req.Config.SystemInstruction.Parts, &genai.Part{Text: prefix}) + } + log.Printf("[Callback] Modified system instruction.") + } + + // Skip Example: Check for "BLOCK" in the user's prompt. for _, content := range req.Contents { for _, part := range content.Parts { - if strings.Contains(part.Text, "BLOCK") { + if strings.Contains(strings.ToUpper(part.Text), "BLOCK") { log.Println("[Callback] 'BLOCK' keyword found. Skipping LLM call.") return &model.LLMResponse{ Content: &genai.Content{ Parts: []*genai.Part{{Text: "LLM call was blocked by before_model_callback."}}, Role: "model", }, - }, - nil + }, nil } } } + + log.Println("[Callback] Proceeding with LLM call.") return nil, nil } @@ -173,7 +187,6 @@ func runBeforeModelExample() { log.Fatalf("FATAL: Failed to create agent: %v", err) } - const appName = "BeforeModelApp" sessionService := session.InMemoryService() r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) if err != nil { @@ -181,10 +194,10 @@ func runBeforeModelExample() { } log.Println("--- SCENARIO 1: Should proceed to LLM ---") - runScenario(ctx, r, sessionService, appName, "session_normal", nil, "This is a safe prompt.") + runScenario(ctx, r, sessionService, appName, "session_normal", nil, "Tell me a fun fact.") log.Println("\n--- SCENARIO 2: Should be blocked by callback ---") - runScenario(ctx, r, sessionService, appName, "session_blocked", nil, "This prompt should be BLOCKED.") + runScenario(ctx, r, sessionService, appName, "session_blocked", nil, "write a joke on BLOCK") } // --8<-- [end:before_model_example] @@ -199,13 +212,34 @@ func onAfterModel(ctx agent.CallbackContext, resp *model.LLMResponse, respErr er log.Println("[Callback] Response is nil or has no parts, nothing to process.") return nil, nil } - if censor, _ := ctx.State().Get("censor_response"); censor == true { - log.Println("[Callback] 'censor_response' is true. Censoring response.") - originalText := resp.Content.Parts[0].Text - censoredText := strings.ReplaceAll(originalText, "blue", "[CENSORED]") - resp.Content.Parts[0].Text = censoredText - return resp, nil + // Check for function calls and pass them through without modification. + if resp.Content.Parts[0].FunctionCall != nil { + log.Println("[Callback] Response is a function call. No modification.") + return nil, nil + } + + originalText := resp.Content.Parts[0].Text + + // Use a case-insensitive regex with word boundaries to find "joke". + re := regexp.MustCompile(`(?i)\bjoke\b`) + if !re.MatchString(originalText) { + log.Println("[Callback] 'joke' not found. Passing original response through.") + return nil, nil } + + log.Println("[Callback] 'joke' found. Modifying response.") + // Use a replacer function to handle capitalization. + modifiedText := re.ReplaceAllStringFunc(originalText, func(s string) string { + if strings.ToUpper(s) == "JOKE" { + if s == "Joke" { + return "Funny story" + } + return "funny story" + } + return s // Should not be reached with this regex, but it's safe. + }) + + resp.Content.Parts[0].Text = modifiedText return resp, nil } @@ -226,18 +260,14 @@ func runAfterModelExample() { log.Fatalf("FATAL: Failed to create agent: %v", err) } - const appName = "AfterModelApp" sessionService := session.InMemoryService() r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) if err != nil { log.Fatalf("FATAL: Failed to create runner: %v", err) } - log.Println("--- SCENARIO 1: Response should be censored ---") - runScenario(ctx, r, sessionService, appName, "session_censor", map[string]any{"censor_response": true}, "Why is the sky blue?") - - log.Println("\n--- SCENARIO 2: Response should be normal ---") - runScenario(ctx, r, sessionService, appName, "session_normal", map[string]any{"censor_response": false}, "Why is the sky blue?") + log.Println("--- SCENARIO 1: Response should be modified ---") + runScenario(ctx, r, sessionService, appName, "session_modify", nil, `Give me a paragraph about different styles of jokes.`) } // --8<-- [end:after_model_example] @@ -258,7 +288,7 @@ func main() { // Generic helper to run a single scenario. func runScenario(ctx context.Context, r *runner.Runner, sessionService session.Service, appName, sessionID string, initialState map[string]any, prompt string) { log.Printf("Running scenario for session: %s, initial state: %v", sessionID, initialState) - sessionResp, err := sessionService.Create(ctx, &session.CreateRequest{AppName: appName, UserID: "test_user", SessionID: sessionID, State: initialState}) + sessionResp, err := sessionService.Create(ctx, &session.CreateRequest{AppName: appName, UserID: userID, SessionID: sessionID, State: initialState}) if err != nil { log.Fatalf("FATAL: Failed to create session: %v", err) } From dd9cc7d14d3c9db873b3f4391cefe1eff90b15fd Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 24 Oct 2025 14:03:46 -0400 Subject: [PATCH 068/125] Ran linter and updated API --- examples/go/snippets/callbacks/main.go | 25 +++++----- .../callbacks/types_of_callbacks/main.go | 48 ++++++++++--------- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/examples/go/snippets/callbacks/main.go b/examples/go/snippets/callbacks/main.go index 50a16c90..52a05e97 100644 --- a/examples/go/snippets/callbacks/main.go +++ b/examples/go/snippets/callbacks/main.go @@ -15,6 +15,7 @@ import ( "google.golang.org/adk/session" "google.golang.org/genai" ) + // --8<-- [end:imports] const ( @@ -43,9 +44,9 @@ func runBasicExample() { // Register the callback function in the agent configuration. agentCfg := llmagent.Config{ - Name: "SimpleAgent", - Model: geminiModel, - BeforeModel: []llmagent.BeforeModelCallback{onBeforeModel}, + Name: "SimpleAgent", + Model: geminiModel, + BeforeModelCallbacks: []llmagent.BeforeModelCallback{onBeforeModel}, } simpleAgent, err := llmagent.New(agentCfg) if err != nil { @@ -73,7 +74,7 @@ func runBasicExample() { input := genai.NewContentFromText("Why is the sky blue?", genai.RoleUser) log.Println("--- Running Agent ---") - events := r.Run(ctx, userID, session.Session.ID(), input, &agent.RunConfig{ + events := r.Run(ctx, userID, session.Session.ID(), input, agent.RunConfig{ StreamingMode: agent.StreamingModeNone, }) @@ -81,7 +82,7 @@ func runBasicExample() { if err != nil { log.Fatalf("Error during agent execution: %v", err) } - for _, p := range event.LLMResponse.Content.Parts { + for _, p := range event.Content.Parts { fmt.Printf("Final Response: %s\n", p.Text) } } @@ -129,9 +130,9 @@ func runGuardrailExample() { } agentCfg := llmagent.Config{ - Name: "ChatAgent", - Model: geminiModel, - BeforeModel: []llmagent.BeforeModelCallback{onBeforeModelGuardrail}, + Name: "ChatAgent", + Model: geminiModel, + BeforeModelCallbacks: []llmagent.BeforeModelCallback{onBeforeModelGuardrail}, } chatAgent, err := llmagent.New(agentCfg) if err != nil { @@ -167,7 +168,7 @@ func runAndPrint(ctx context.Context, r *runner.Runner, sessionService session.S input := genai.NewContentFromText(prompt, genai.RoleUser) log.Printf("\n--- Running Agent with prompt: %q ---\n", prompt) - events := r.Run(ctx, session.Session.UserID(), session.Session.ID(), input, &agent.RunConfig{ + events := r.Run(ctx, session.Session.UserID(), session.Session.ID(), input, agent.RunConfig{ StreamingMode: agent.StreamingModeNone, }) @@ -175,9 +176,9 @@ func runAndPrint(ctx context.Context, r *runner.Runner, sessionService session.S if err != nil { log.Fatalf("Error during agent execution: %v", err) } - for _, p := range event.Content.Parts { - fmt.Printf("Final Response: %s\n", p.Text) - } + for _, p := range event.Content.Parts { + fmt.Printf("Final Response: %s\n", p.Text) + } } log.Println("--- Agent Run Finished ---") } diff --git a/examples/go/snippets/callbacks/types_of_callbacks/main.go b/examples/go/snippets/callbacks/types_of_callbacks/main.go index de53db81..fbe487a0 100644 --- a/examples/go/snippets/callbacks/types_of_callbacks/main.go +++ b/examples/go/snippets/callbacks/types_of_callbacks/main.go @@ -33,10 +33,10 @@ func onBeforeAgent(ctx agent.CallbackContext) (*genai.Content, error) { if skip, _ := ctx.State().Get("skip_llm_agent"); skip == true { log.Printf("[Callback] State condition met: Skipping agent %s", agentName) return genai.NewContentFromText( - fmt.Sprintf("Agent %s skipped by before_agent_callback.", agentName), - genai.RoleModel, - ), - nil + fmt.Sprintf("Agent %s skipped by before_agent_callback.", agentName), + genai.RoleModel, + ), + nil } log.Printf("[Callback] State condition not met: Running agent %s", agentName) return nil, nil @@ -52,10 +52,10 @@ func runBeforeAgentExample() { // 3. Register the callback in the agent configuration. llmCfg := llmagent.Config{ - Name: "AgentWithBeforeAgentCallback", - BeforeAgent: []agent.BeforeAgentCallback{onBeforeAgent}, - Model: geminiModel, - Instruction: "You are a concise assistant.", + Name: "AgentWithBeforeAgentCallback", + BeforeAgentCallbacks: []agent.BeforeAgentCallback{onBeforeAgent}, + Model: geminiModel, + Instruction: "You are a concise assistant.", } testAgent, err := llmagent.New(llmCfg) if err != nil { @@ -75,6 +75,7 @@ func runBeforeAgentExample() { log.Println("\n--- SCENARIO 2: Agent should be skipped ---") runScenario(ctx, r, sessionService, appName, "session_skip", map[string]any{"skip_llm_agent": true}, "This should be skipped.") } + // --8<-- [end:before_agent_example] // --8<-- [start:after_agent_example] @@ -111,10 +112,10 @@ func runAfterAgentExample() { } llmCfg := llmagent.Config{ - Name: "AgentWithAfterAgentCallback", - AfterAgent: []agent.AfterAgentCallback{onAfterAgent}, - Model: geminiModel, - Instruction: "You are a simple agent. Just say 'Processing complete!'", + Name: "AgentWithAfterAgentCallback", + AfterAgentCallbacks: []agent.AfterAgentCallback{onAfterAgent}, + Model: geminiModel, + Instruction: "You are a simple agent. Just say 'Processing complete!'", } testAgent, err := llmagent.New(llmCfg) if err != nil { @@ -133,6 +134,7 @@ func runAfterAgentExample() { log.Println("\n--- SCENARIO 2: Should replace output ---") runScenario(ctx, r, sessionService, appName, "session_modify", map[string]any{"add_concluding_note": true}, "Process and add note.") } + // --8<-- [end:after_agent_example] // --8<-- [start:before_model_example] @@ -178,9 +180,9 @@ func runBeforeModelExample() { } llmCfg := llmagent.Config{ - Name: "AgentWithBeforeModelCallback", - Model: geminiModel, - BeforeModel: []llmagent.BeforeModelCallback{onBeforeModel}, + Name: "AgentWithBeforeModelCallback", + Model: geminiModel, + BeforeModelCallbacks: []llmagent.BeforeModelCallback{onBeforeModel}, } testAgent, err := llmagent.New(llmCfg) if err != nil { @@ -199,6 +201,7 @@ func runBeforeModelExample() { log.Println("\n--- SCENARIO 2: Should be blocked by callback ---") runScenario(ctx, r, sessionService, appName, "session_blocked", nil, "write a joke on BLOCK") } + // --8<-- [end:before_model_example] // --8<-- [start:after_model_example] @@ -219,7 +222,7 @@ func onAfterModel(ctx agent.CallbackContext, resp *model.LLMResponse, respErr er } originalText := resp.Content.Parts[0].Text - + // Use a case-insensitive regex with word boundaries to find "joke". re := regexp.MustCompile(`(?i)\bjoke\b`) if !re.MatchString(originalText) { @@ -251,9 +254,9 @@ func runAfterModelExample() { } llmCfg := llmagent.Config{ - Name: "AgentWithAfterModelCallback", - Model: geminiModel, - AfterModel: []llmagent.AfterModelCallback{onAfterModel}, + Name: "AgentWithAfterModelCallback", + Model: geminiModel, + AfterModelCallbacks: []llmagent.AfterModelCallback{onAfterModel}, } testAgent, err := llmagent.New(llmCfg) if err != nil { @@ -269,6 +272,7 @@ func runAfterModelExample() { log.Println("--- SCENARIO 1: Response should be modified ---") runScenario(ctx, r, sessionService, appName, "session_modify", nil, `Give me a paragraph about different styles of jokes.`) } + // --8<-- [end:after_model_example] func main() { @@ -294,15 +298,15 @@ func runScenario(ctx context.Context, r *runner.Runner, sessionService session.S } input := genai.NewContentFromText(prompt, genai.RoleUser) - events := r.Run(ctx, sessionResp.Session.UserID(), sessionResp.Session.ID(), input, &agent.RunConfig{}) + events := r.Run(ctx, sessionResp.Session.UserID(), sessionResp.Session.ID(), input, agent.RunConfig{}) for event, err := range events { if err != nil { log.Printf("ERROR during agent execution: %v", err) return } - + // Print only the final output from the agent. - if event.LLMResponse != nil && event.LLMResponse.Content != nil && len(event.LLMResponse.Content.Parts) > 0 { + if event != nil && event.Content != nil && len(event.Content.Parts) > 0 { fmt.Printf("Final Output for %s: [%s] %s\n", sessionID, event.Author, event.LLMResponse.Content.Parts[0].Text) } else { log.Printf("Final response for %s received, but it has no content to display.", sessionID) From ee7486f05a266a4c925e097b2998a91d4d22f173 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Tue, 28 Oct 2025 14:15:35 -0400 Subject: [PATCH 069/125] Added tool callbacks --- docs/callbacks/types-of-callbacks.md | 16 ++ .../callbacks/types_of_callbacks/main.go | 157 ++++++++++++++++++ 2 files changed, 173 insertions(+) diff --git a/docs/callbacks/types-of-callbacks.md b/docs/callbacks/types-of-callbacks.md index ea952b45..c9b976f1 100644 --- a/docs/callbacks/types-of-callbacks.md +++ b/docs/callbacks/types-of-callbacks.md @@ -189,6 +189,14 @@ These callbacks are also specific to `LlmAgent` and trigger around the execution --8<-- "examples/java/snippets/src/main/java/callbacks/BeforeToolCallbackExample.java:init" ``` + === "Golang" + + ```go + --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:imports" + --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:tool_defs" + --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:before_tool_example" + ``` + ### After Tool Callback @@ -214,3 +222,11 @@ These callbacks are also specific to `LlmAgent` and trigger around the execution ```java --8<-- "examples/java/snippets/src/main/java/callbacks/AfterToolCallbackExample.java:init" ``` + + === "Golang" + + ```go + --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:imports" + --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:tool_defs" + --8<-- "examples/go/snippets/callbacks/types_of_callbacks/main.go:after_tool_example" + ``` \ No newline at end of file diff --git a/examples/go/snippets/callbacks/types_of_callbacks/main.go b/examples/go/snippets/callbacks/types_of_callbacks/main.go index fbe487a0..040b0570 100644 --- a/examples/go/snippets/callbacks/types_of_callbacks/main.go +++ b/examples/go/snippets/callbacks/types_of_callbacks/main.go @@ -14,6 +14,8 @@ import ( "google.golang.org/adk/model/gemini" "google.golang.org/adk/runner" "google.golang.org/adk/session" + "google.golang.org/adk/tool" + "google.golang.org/adk/tool/functiontool" "google.golang.org/genai" ) @@ -275,6 +277,155 @@ func runAfterModelExample() { // --8<-- [end:after_model_example] +// --8<-- [start:tool_defs] +// GetCapitalCityArgs defines the arguments for the getCapitalCity tool. +type GetCapitalCityArgs struct { + Country string `json:"country" adk:"description=The country to get the capital of."` +} + +// getCapitalCity is a tool that returns the capital of a given country. +func getCapitalCity(ctx tool.Context, args *GetCapitalCityArgs) string { + capitals := map[string]string{ + "canada": "Ottawa", + "france": "Paris", + "germany": "Berlin", + "united states": "Washington, D.C.", + } + capital, ok := capitals[strings.ToLower(args.Country)] + if !ok { + return "" + } + return capital +} + +// --8<-- [end:tool_defs] + +// --8<-- [start:before_tool_example] +func onBeforeTool(ctx tool.Context, t tool.Tool, args map[string]any) (map[string]any, error) { + log.Printf("[Callback] BeforeTool triggered for tool %q in agent %q.", t.Name(), ctx.AgentName()) + log.Printf("[Callback] Original args: %v", args) + + if t.Name() == "getCapitalCity" { + if country, ok := args["country"].(string); ok { + if strings.ToLower(country) == "canada" { + log.Println("[Callback] Detected 'Canada'. Modifying args to 'France'.") + args["country"] = "France" + return args, nil // Proceed with modified args + } else if strings.ToUpper(country) == "BLOCK" { + log.Println("[Callback] Detected 'BLOCK'. Skipping tool execution.") + // Skip tool and return a custom result. + return map[string]any{"result": "Tool execution was blocked by before_tool_callback."}, nil + } + } + } + log.Println("[Callback] Proceeding with original or previously modified args.") + return nil, nil // Proceed with original args +} + +func runBeforeToolExample() { + ctx := context.Background() + geminiModel, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + log.Fatalf("FATAL: Failed to create model: %v", err) + } + capitalTool, err := functiontool.New[*GetCapitalCityArgs, string](functiontool.Config{ + Name: "getCapitalCity", + Description: "Retrieves the capital city of a given country.", + }, getCapitalCity) + if err != nil { + log.Fatalf("FATAL: Failed to create function tool: %v", err) + } + + llmCfg := llmagent.Config{ + Name: "AgentWithBeforeToolCallback", + Model: geminiModel, + Tools: []tool.Tool{capitalTool}, + BeforeToolCallbacks: []llmagent.BeforeToolCallback{onBeforeTool}, + Instruction: "You are an agent that can find capital cities. Use the getCapitalCity tool.", + } + testAgent, err := llmagent.New(llmCfg) + if err != nil { + log.Fatalf("FATAL: Failed to create agent: %v", err) + } + sessionService := session.InMemoryService() + r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) + if err != nil { + log.Fatalf("FATAL: Failed to create runner: %v", err) + } + + log.Println("--- SCENARIO 1: Args should be modified ---") + runScenario(ctx, r, sessionService, appName, "session_tool_modify", nil, "What is the capital of Canada?") + + log.Println("--- SCENARIO 2: Tool call should be blocked ---") + runScenario(ctx, r, sessionService, appName, "session_tool_block", nil, "capital of BLOCK") +} + +// --8<-- [end:before_tool_example] + +// --8<-- [start:after_tool_example] +func onAfterTool(ctx tool.Context, t tool.Tool, args map[string]any, result map[string]any, err error) (map[string]any, error) { + log.Printf("[Callback] AfterTool triggered for tool %q in agent %q.", t.Name(), ctx.AgentName()) + log.Printf("[Callback] Original result: %v", result) + + if err != nil { + log.Printf("[Callback] Tool run produced an error: %v. Passing through.", err) + return nil, err + } + + if t.Name() == "getCapitalCity" { + if originalResult, ok := result["result"].(string); ok && originalResult == "Washington, D.C." { + log.Println("[Callback] Detected 'Washington, D.C.'. Modifying tool response.") + modifiedResult := make(map[string]any) + for k, v := range result { + modifiedResult[k] = v + } + modifiedResult["result"] = fmt.Sprintf("%s (Note: This is the capital of the USA).", originalResult) + modifiedResult["note_added_by_callback"] = true + return modifiedResult, nil + } + } + + log.Println("[Callback] Passing original tool response through.") + return nil, nil +} + +func runAfterToolExample() { + ctx := context.Background() + geminiModel, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + log.Fatalf("FATAL: Failed to create model: %v", err) + } + capitalTool, err := functiontool.New[*GetCapitalCityArgs, string](functiontool.Config{ + Name: "getCapitalCity", + Description: "Retrieves the capital city of a given country.", + }, getCapitalCity) + if err != nil { + log.Fatalf("FATAL: Failed to create function tool: %v", err) + } + + llmCfg := llmagent.Config{ + Name: "AgentWithAfterToolCallback", + Model: geminiModel, + Tools: []tool.Tool{capitalTool}, + AfterToolCallbacks: []llmagent.AfterToolCallback{onAfterTool}, + Instruction: "You are an agent that finds capital cities. Use the getCapitalCity tool.", + } + testAgent, err := llmagent.New(llmCfg) + if err != nil { + log.Fatalf("FATAL: Failed to create agent: %v", err) + } + sessionService := session.InMemoryService() + r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) + if err != nil { + log.Fatalf("FATAL: Failed to create runner: %v", err) + } + + log.Println("--- SCENARIO 1: Result should be modified ---") + runScenario(ctx, r, sessionService, appName, "session_tool_after_modify", nil, "capital of united states") +} + +// --8<-- [end:after_tool_example] + func main() { log.Println("--- Running BeforeAgent Example ---") runBeforeAgentExample() @@ -287,6 +438,12 @@ func main() { log.Println("\n\n--- Running AfterModel Example ---") runAfterModelExample() + + log.Println("\n\n--- Running BeforeTool Example ---") + runBeforeToolExample() + + log.Println("\n\n--- Running AfterTool Example ---") + runAfterToolExample() } // Generic helper to run a single scenario. From 3d2620d9f188bd6dc4b24a05b28171f0efb9294d Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 8 Oct 2025 09:48:43 -0400 Subject: [PATCH 070/125] Added artifact snippets --- docs/artifacts/index.md | 176 ++++++--- examples/go/snippets/artifacts/image.png | Bin 0 -> 268999 bytes examples/go/snippets/artifacts/main.go | 442 +++++++++++++++++++++++ 3 files changed, 570 insertions(+), 48 deletions(-) create mode 100644 examples/go/snippets/artifacts/image.png create mode 100644 examples/go/snippets/artifacts/main.go diff --git a/docs/artifacts/index.md b/docs/artifacts/index.md index 57538abe..c402154f 100644 --- a/docs/artifacts/index.md +++ b/docs/artifacts/index.md @@ -64,6 +64,18 @@ In ADK, **Artifacts** represent a crucial mechanism for managing named, versione } ``` +=== "Go" + + ```go + import ( + "log" + + "google.golang.org/genai" + ) + + --8<-- "examples/go/snippets/artifacts/main.go:representation" + ``` + * **Persistence & Management:** Artifacts are not stored directly within the agent or session state. Their storage and retrieval are managed by a dedicated **Artifact Service** (an implementation of `BaseArtifactService`, defined in `google.adk.artifacts`. ADK provides various implementations, such as: * An in-memory service for testing or temporary storage (e.g., `InMemoryArtifactService` in Python, defined in `google.adk.artifacts.in_memory_artifact_service.py`). * A service for persistent storage using Google Cloud Storage (GCS) (e.g., `GcsArtifactService` in Python, defined in `google.adk.artifacts.gcs_artifact_service.py`). @@ -77,7 +89,7 @@ While session `state` is suitable for storing small pieces of configuration or c 2. **Persisting Large Data:** Session state is generally not optimized for storing large amounts of data. Artifacts provide a dedicated mechanism for persisting larger blobs without cluttering the session state. 3. **User File Management:** Provide capabilities for users to upload files (which can be saved as artifacts) and retrieve or download files generated by the agent (loaded from artifacts). 4. **Sharing Outputs:** Enable tools or agents to generate binary outputs (like a PDF report or a generated image) that can be saved via `save_artifact` and later accessed by other parts of the application or even in subsequent sessions (if using user namespacing). -5. **Caching Binary Data:** Store the results of computationally expensive operations that produce binary data (e.g., rendering a complex chart image) as artifacts to avoid regenerating them on subsequent requests. +5. **Caching Binary Data:** Store the results of computationally expensive operations that produce binary data (e.g., rendering a complex chart image) as artifacts to avoid regenerating them on subsequent requests. In essence, whenever your agent needs to work with file-like binary data that needs to be persisted, versioned, or shared, Artifacts managed by an `ArtifactService` are the appropriate mechanism within ADK. @@ -91,7 +103,7 @@ Here are some typical scenarios where they prove valuable: * **Generated Reports/Files:** * A tool or agent generates a report (e.g., a PDF analysis, a CSV data export, an image chart). -* **Handling User Uploads:** +* **Handling User Uploads:** * A user uploads a file (e.g., an image for analysis, a document for summarization) through a front-end interface. @@ -169,6 +181,24 @@ Understanding artifacts involves grasping a few key components: the service that // Now, contexts within runs managed by this runner can use artifact methods ``` +=== "Go" + + ```go + import ( + "context" + "log" + + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/artifactservice" + "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/sessionservice" + "google.golang.org/genai" + ) + + --8<-- "examples/go/snippets/artifacts/main.go:configure-runner" + ``` + ### Artifact Data * **Standard Representation:** Artifact content is universally represented using the `google.genai.types.Part` object, the same structure used for parts of LLM messages. @@ -204,6 +234,19 @@ Understanding artifacts involves grasping a few key components: the service that --8<-- "examples/java/snippets/src/main/java/artifacts/ArtifactDataExample.java:full_code" ``` +=== "Go" + + ```go + import ( + "log" + "os" + + "google.golang.org/genai" + ) + + --8<-- "examples/go/snippets/artifacts/main.go:artifact-data" + ``` + ### Filename * **Identifier:** A simple string used to name and retrieve an artifact within its specific namespace. @@ -269,6 +312,16 @@ Understanding artifacts involves grasping a few key components: the service that // artifactService.saveArtifact(appName, userId, sessionId1, userConfigFilename, someData); ``` +=== "Go" + + ```go + import ( + "log" + ) + + --8<-- "examples/go/snippets/artifacts/main.go:namespacing" + ``` + These core concepts work together to provide a flexible system for managing binary data within the ADK framework. ## Interacting with Artifacts (via Context Objects) @@ -339,11 +392,31 @@ Before you can use any artifact methods via the context objects, you **must** pr ``` In Java, if an `ArtifactService` instance is not available (e.g., `null`) when artifact operations are attempted, it would typically result in a `NullPointerException` or a custom error, depending on how your application is structured. Robust applications often use dependency injection frameworks to manage service lifecycles and ensure availability. +=== "Go" + + ```go + import ( + "context" + "log" + + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/artifactservice" + "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/sessionservice" + "google.golang.org/genai" + ) + + --8<-- "examples/go/snippets/artifacts/main.go:prerequisite" + ``` + ### Accessing Methods The artifact interaction methods are available directly on instances of `CallbackContext` (passed to agent and model callbacks) and `ToolContext` (passed to tool callbacks). Remember that `ToolContext` inherits from `CallbackContext`. +#### Saving Artifacts + * **Code Example:** === "Python" @@ -412,6 +485,19 @@ The artifact interaction methods are available directly on instances of `Callbac } ``` + === "Go" + + ```go + import ( + "log" + + "google.golang.org/adk/agent" + "google.golang.org/adk/llm" + "google.golang.org/genai" + ) + + --8<-- "examples/go/snippets/artifacts/main.go:saving-artifacts" + ``` #### Loading Artifacts * **Code Example:** @@ -506,9 +592,9 @@ The artifact interaction methods are available directly on instances of `Callbac public void onError(Throwable e) { // Handle potential storage errors or other exceptions System.err.println( - "An error occurred during Java artifact load for '" + "An error occurred during Java artifact load for '" + filename - + "': " + + "': " + e.getMessage()); } @@ -522,7 +608,7 @@ The artifact interaction methods are available directly on instances of `Callbac // Example: Load a specific version (e.g., version 0) /* artifactService.loadArtifact(appName, userId, sessionId, filename, Optional.of(0)) - .subscribe(part -> { + .subscribe(part -> { System.out.println("Loaded version 0 of Java artifact '" + filename + "'."); }, throwable -> { System.err.println("Error loading version 0 of '" + filename + "': " + throwable.getMessage()); @@ -542,6 +628,19 @@ The artifact interaction methods are available directly on instances of `Callbac } ``` + === "Go" + + ```go + import ( + "log" + + "google.golang.org/adk/agent" + "google.golang.org/adk/llm" + ) + + --8<-- "examples/go/snippets/artifacts/main.go:loading-artifacts" + ``` + #### Listing Artifact Filenames * **Code Example:** @@ -614,7 +713,7 @@ The artifact interaction methods are available directly on instances of `Callbac + sessionId + " has no saved Java artifacts."); } else { - StringBuilder fileListStr = + StringBuilder fileListStr = new StringBuilder( "Here are the available Java artifacts for user " + userId @@ -652,6 +751,22 @@ The artifact interaction methods are available directly on instances of `Callbac } ``` + === "Go" + + ```go + import ( + "fmt" + "log" + "strings" + + "google.golang.org/adk/agent" + "google.golang.org/adk/llm" + "google.golang.org/genai" + ) + + --8<-- "examples/go/snippets/artifacts/main.go:listing-artifacts" + ``` + These methods for saving, loading, and listing provide a convenient and consistent way to manage binary data persistence within ADK, whether using Python's context objects or directly interacting with the `BaseArtifactService` in Java, regardless of the chosen backend storage implementation. ## Available Implementations @@ -706,49 +821,14 @@ ADK provides concrete implementations of the `BaseArtifactService` interface, of } ``` -### GcsArtifactService + === "Go" + ```go + import ( + "google.golang.org/adk/artifactservice" + ) -* **Storage Mechanism:** Leverages Google Cloud Storage (GCS) for persistent artifact storage. Each version of an artifact is stored as a separate object (blob) within a specified GCS bucket. -* **Object Naming Convention:** It constructs GCS object names (blob names) using a hierarchical path structure. -* **Key Features:** - * **Persistence:** Artifacts stored in GCS persist across application restarts and deployments. - * **Scalability:** Leverages the scalability and durability of Google Cloud Storage. - * **Versioning:** Explicitly stores each version as a distinct GCS object. The `saveArtifact` method in `GcsArtifactService`. - * **Permissions Required:** The application environment needs appropriate credentials (e.g., Application Default Credentials) and IAM permissions to read from and write to the specified GCS bucket. -* **Use Cases:** - * Production environments requiring persistent artifact storage. - * Scenarios where artifacts need to be shared across different application instances or services (by accessing the same GCS bucket). - * Applications needing long-term storage and retrieval of user or session data. -* **Instantiation:** - - === "Python" - - ```python - from google.adk.artifacts import GcsArtifactService - - # Specify the GCS bucket name - gcs_bucket_name_py = "your-gcs-bucket-for-adk-artifacts" # Replace with your bucket name - - try: - gcs_service_py = GcsArtifactService(bucket_name=gcs_bucket_name_py) - print(f"Python GcsArtifactService initialized for bucket: {gcs_bucket_name_py}") - # Ensure your environment has credentials to access this bucket. - # e.g., via Application Default Credentials (ADC) - - # Then pass it to the Runner - # runner = Runner(..., artifact_service=gcs_service_py) - - except Exception as e: - # Catch potential errors during GCS client initialization (e.g., auth issues) - print(f"Error initializing Python GcsArtifactService: {e}") - # Handle the error appropriately - maybe fall back to InMemory or raise - ``` - - === "Java" - - ```java - --8<-- "examples/java/snippets/src/main/java/artifacts/GcsServiceSetup.java:full_code" + --8<-- "examples/go/snippets/artifacts/main.go:in-memory-service" ``` Choosing the appropriate `ArtifactService` implementation depends on your application's requirements for data persistence, scalability, and operational environment. @@ -770,4 +850,4 @@ To use artifacts effectively and maintainably: * **Cleanup Strategy:** For persistent storage like `GcsArtifactService`, artifacts remain until explicitly deleted. If artifacts represent temporary data or have a limited lifespan, implement a strategy for cleanup. This might involve: * Using GCS lifecycle policies on the bucket. * Building specific tools or administrative functions that utilize the `artifact_service.delete_artifact` method (note: delete is *not* exposed via context objects for safety). - * Carefully managing filenames to allow pattern-based deletion if needed. + * Carefully managing filenames to allow pattern-based deletion if needed. \ No newline at end of file diff --git a/examples/go/snippets/artifacts/image.png b/examples/go/snippets/artifacts/image.png new file mode 100644 index 0000000000000000000000000000000000000000..b8457eae005a4c6c01b232fcf690f1453ce2501d GIT binary patch literal 268999 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%0AV1=>4VMF2lb${<%J0 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

yeP_yzO;`&L*38 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 literal 0 HcmV?d00001 diff --git a/examples/go/snippets/artifacts/main.go b/examples/go/snippets/artifacts/main.go new file mode 100644 index 00000000..e320992b --- /dev/null +++ b/examples/go/snippets/artifacts/main.go @@ -0,0 +1,442 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "strings" + + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/artifactservice" + "google.golang.org/adk/llm" + "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/session" + "google.golang.org/adk/sessionservice" + "google.golang.org/genai" +) + +// This file contains snippets for the artifacts documentation. + +// BeforeModelCallback saves any images from the user input before calling the LLM. +func BeforeModelCallback(ctx agent.Context, req *llm.Request) (*llm.Response, error) { + log.Println("[Callback] BeforeModelCallback triggered.") + // Get the artifact manager from the context. + artifacts := ctx.Artifacts() + // Check if there are any contents in the request. + if req.Contents != nil && len(req.Contents) > 0 { + // Get the last content from the user. + lastContent := req.Contents[len(req.Contents)-1] + // Check if the last content is from the user. + if lastContent.Role == genai.RoleUser { + // Iterate over the parts of the content. + for i, part := range lastContent.Parts { + // Check if the part is an image. + if part.InlineData != nil && strings.HasPrefix(part.InlineData.MIMEType, "image/") { + // Create a unique filename for the image. + fileName := fmt.Sprintf("user_image_%d.%s", i, strings.Split(part.InlineData.MIMEType, "/")[1]) + // Save the image as an artifact. + if err := artifacts.Save(fileName, *part); err != nil { + log.Printf("[WARN] Failed to save user image: %v\n", err) + } else { + log.Printf("[INFO] Saved user image artifact: %s\n", fileName) + } + } + } + } + } + // Return nil to continue to the next callback or the LLM. + return nil, nil // Continue to next callback or LLM call +} + +// configureRunner configures the runner with an in-memory artifact service. +func configureRunner() { +// --8<-- [start:configure-runner] +// --8<-- [start:prerequisite] + // Create a new context. + ctx := context.Background() + // Set the app name. + const appName = "my_artifact_app" + // Create a new Gemini model. + model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{}) + if err != nil { + log.Fatalf("Failed to create model: %v", err) + } + + // Create a new LLM agent. + myAgent, err := llmagent.New(llmagent.Config{ + Model: model, + Name: "artifact_user_agent", + Instruction: "You are an agent that describes images.", + BeforeModel: []llmagent.BeforeModelCallback{ + BeforeModelCallback, + }, + }) + if err != nil { + log.Fatalf("failed to create agent: %v", err) + } + + // Create a new in-memory artifact service. + artifactService := artifactservice.Mem() // In-memory ArtifactService + // Create a new in-memory session service. + sessionService := sessionservice.Mem() + + // Create a new runner. + r, err := runner.New(&runner.Config{ + Agent: myAgent, + AppName: appName, + SessionService: sessionService, + ArtifactService: artifactService, // Provide the service instance here + }) + if err != nil { + log.Fatalf("Failed to create runner: %v", err) + } + log.Printf("Runner created successfully: %v", r) +// --8<-- [end:prerequisite] +// --8<-- [end:configure-runner] +} + +// inMemoryServiceExample demonstrates how to set up an in-memory artifact service. +func inMemoryServiceExample() { + // --8<-- [start:in-memory-service] + // Simply instantiate the class + inMemoryService := artifactservice.Mem() + log.Printf("InMemoryArtifactService (Go) instantiated: %T", inMemoryService) + + // Use the service in your runner + // r, _ := runner.New(&runner.Config{ + // Agent: agent, + // AppName: "my_app", + // SessionService: sessionService, + // ArtifactService: artifactService, + // }) + + // --8<-- [end:in-memory-service] +} + +// --8<-- [start:loading-artifacts] +// loadArtifactsCallback is a BeforeModel callback that loads a specific artifact +// and adds its content to the LLM request. +func loadArtifactsCallback(ctx agent.Context, req *llm.Request) (*llm.Response, error) { + log.Println("[Callback] loadArtifactsCallback triggered.") + // In a real app, you would parse the user's request to find a filename. + // For this example, we'll hardcode a filename to demonstrate. + const filenameToLoad = "generated_report.pdf" + + // Load the artifact from the artifact service. + loadedPart, err := ctx.Artifacts().Load(filenameToLoad) + if err != nil { + log.Printf("Callback could not load artifact '%s': %v", filenameToLoad, err) + return nil, nil // File not found or error, continue to LLM. + } + + log.Printf("Callback successfully loaded artifact '%s'.", filenameToLoad) + // Add the loaded artifact to the request for the LLM. + if len(req.Contents) > 0 { + lastContent := req.Contents[len(req.Contents)-1] + lastContent.Parts = append(lastContent.Parts, &loadedPart) + log.Printf("Added artifact '%s' to LLM request.", filenameToLoad) + } + // Return nil to continue to the next callback or the LLM. + return nil, nil // Continue to next callback or LLM call +} +// --8<-- [end:loading-artifacts] + +// representation demonstrates how to manually construct an artifact. +func representation() { +// --8<-- [start:representation] + // Create a byte slice with the image data. + imageBytes := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A} + // Create a new artifact with the image data. + imageArtifact := &genai.Part{ + InlineData: &genai.Blob{ + MIMEType: "image/png", + Data: imageBytes, + }, + } + log.Printf("Artifact MIME Type: %s", imageArtifact.InlineData.MIMEType) + log.Printf("Artifact Data (first 8 bytes): %x...", imageArtifact.InlineData.Data[:8]) +// --8<-- [end:representation] +} + +// artifactData demonstrates how to create an artifact from a file. +func artifactData() { +// --8<-- [start:artifact-data] + // Load imageBytes from a file + imageBytes, err := os.ReadFile("image.png") + if err != nil { + log.Fatalf("Failed to read image file: %v", err) + } + + // genai.NewPartFromBytes is a convenience function that is a shorthand for + // creating a &genai.Part with the InlineData field populated. + // Create a new artifact from the image data. + imageArtifact := genai.NewPartFromBytes([]byte(imageBytes), "image/png") + + log.Printf("Artifact MIME Type: %s", imageArtifact.InlineData.MIMEType) +// --8<-- [end:artifact-data] +} + +// namespacing demonstrates the difference between session and user-scoped artifacts. +func namespacing() { +// --8<-- [start:namespacing] + // A session-scoped artifact is only available within the current session. + sessionReportFilename := "summary.txt" + // A user-scoped artifact is available across all sessions for the current user. + userConfigFilename := "user:settings.json" + + // When saving 'summary.txt' via ctx.Artifacts().Save, + // it's tied to the current app_name, user_id, and session_id. + // ctx.Artifacts().Save(sessionReportFilename, *artifact); + + // When saving 'user:settings.json' via ctx.Artifacts().Save, + // the ArtifactService implementation should recognize the "user:" prefix + // and scope it to app_name and user_id, making it accessible across sessions for that user. + // ctx.Artifacts().Save(userConfigFilename, *artifact); +// --8<-- [end:namespacing] + + log.Printf("Session filename: %s", sessionReportFilename) + log.Printf("User filename: %s", userConfigFilename) +} + +// --8<-- [start:saving-artifacts] +// saveReportCallback is a BeforeModel callback that saves a report from session state. +func saveReportCallback(ctx agent.Context, req *llm.Request) (*llm.Response, error) { + // Get the report data from the session state. + reportData, err := ctx.Session().State().Get("report_bytes") + if err != nil { + log.Printf("No report data found in session state: %v", err) + return nil, nil // No report to save, continue normally. + } + + // Check if the report data is in the expected format. + reportBytes, ok := reportData.([]byte) + if !ok { + log.Printf("Report data in session state was not in the expected byte format.") + return nil, nil + } + + // Create a new artifact with the report data. + reportArtifact := &genai.Part{ + InlineData: &genai.Blob{ + MIMEType: "application/pdf", + Data: reportBytes, + }, + } + // Set the filename for the artifact. + filename := "generated_report.pdf" + // Save the artifact to the artifact service. + err = ctx.Artifacts().Save(filename, *reportArtifact) + if err != nil { + log.Printf("An unexpected error occurred during Go artifact save: %v", err) + // Depending on requirements, you might want to return an error to the user. + return nil, nil + } + log.Printf("Successfully saved Go artifact '%s'.", filename) + // Return nil to continue to the next callback or the LLM. + return nil, nil +} +// --8<-- [end:saving-artifacts] + +// --8<-- [start:listing-artifacts] +// listUserFilesCallback is a BeforeModel callback that lists available artifacts +// and adds the list as context to the LLM request. +func listUserFilesCallback(ctx agent.Context, req *llm.Request) (*llm.Response, error) { + log.Println("[Callback] listUserFilesCallback triggered.") + // List the available artifacts from the artifact service. + availableFiles, err := ctx.Artifacts().List() + if err != nil { + log.Printf("An unexpected error occurred during Go artifact list: %v", err) + return nil, nil // Continue, but log the error. + } + // If there are available files, add them to the LLM request. + if len(availableFiles) > 0 { + var fileListStr strings.Builder + fileListStr.WriteString("SYSTEM: The following files are available:\n") + for _, fname := range availableFiles { + fileListStr.WriteString(fmt.Sprintf("- %s\n", fname)) + } + // Prepend this information to the user's request for the LLM. + if len(req.Contents) > 0 { + lastContent := req.Contents[len(req.Contents)-1] + if len(lastContent.Parts) > 0 { + lastContent.Parts[0] = genai.NewPartFromText(fileListStr.String() + lastContent.Parts[0].Text) + log.Println("Added file list to LLM request context.") + } + } + } + // Return nil to continue to the next callback or the LLM. + return nil, nil // Continue to next callback or LLM call +} +// --8<-- [end:listing-artifacts] + + + +// --- Local Implementations for Testing --- + +// localArtifacts implements the agent.Artifacts interface for testing. +type localArtifacts struct { + service artifactservice.Service + ctx context.Context + appName string + userID string + sessionID string +} + +func (m *localArtifacts) Load(filename string) (*genai.Part, error) { + res, err := m.service.Load(m.ctx, &artifactservice.LoadRequest{ + AppName: m.appName, + UserID: m.userID, + SessionID: m.sessionID, + FileName: filename, + }) + if err != nil { + return nil, err + } + return res.Part, nil +} + +func (m *localArtifacts) List() ([]string, error) { + res, err := m.service.List(m.ctx, &artifactservice.ListRequest{ + AppName: m.appName, + UserID: m.userID, + SessionID: m.sessionID, + }) + if err != nil { + return nil, err + } + return res.FileNames, nil +} + +// MockState implements the session.State interface for testing. +type MockState struct { + data map[string]any +} + +func (s *MockState) Get(key string) (any, error) { + val, ok := s.data[key] + if !ok { + return nil, fmt.Errorf("key not found") + } + return val, nil +} + +func (s *MockState) Set(key string, value any) error { + s.data[key] = value + return nil +} + +// MockSession implements the session.Session interface for testing. +type MockSession struct { + session.Session + mockState session.State +} + +func (s *MockSession) State() session.State { + return s.mockState +} + +// MockContext implements the agent.Context interface for testing. +type MockContext struct { + agent.Context + mockArtifacts agent.Artifacts + mockSession session.Session +} + +func (m *MockContext) Artifacts() agent.Artifacts { + return m.mockArtifacts +} + +func (m *MockContext) Session() session.Session { + return m.mockSession +} + +func main() { + log.Println("--- Running Snippets ---") + + // Call each standalone snippet function. + log.Println("\n--- representation ---") + representation() + + log.Println("\n--- artifactData ---") + artifactData() + + log.Println("\n--- namespacing ---") + namespacing() + + log.Println("\n--- configureRunner (demonstrates BeforeModelCallback for saving) ---") + configureRunner() + + log.Println("\n--- inMemoryServiceExample ---") + inMemoryServiceExample() + + log.Println("\n--- Running Agent with Multiple Callbacks ---") + // 1. Set up services + ctx := context.Background() + artifactService := artifactservice.Mem() + sessionService := sessionservice.Mem() + + // 2. Set up the agent with multiple callbacks + model, _ := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{}) + reportingAgent, _ := llmagent.New(llmagent.Config{ + Model: model, + Name: "reporting_agent", + Instruction: "You are a reporting agent. You can see available files and their contents if they are loaded for you. Summarize any provided files.", + BeforeModel: []llmagent.BeforeModelCallback{ + saveReportCallback, // Saves report from state + listUserFilesCallback, // Lists available files and adds to prompt + loadArtifactsCallback, // Loads a specific file and adds to prompt + }, + }) + + // 3. Create a session with some initial state to trigger `saveReportCallback` + initialState := map[string]any{ + "report_bytes": []byte("%PDF-1.4... This is a test PDF."), // Mock PDF data + } + userID := "test-user" + session, _ := sessionService.Create(ctx, &sessionservice.CreateRequest{ + AppName: "my_app", + UserID: userID, + SessionID: "test-session-callbacks", + State: initialState, + }) + + // 4. Create and run the runner + r, _ := runner.New(&runner.Config{ + Agent: reportingAgent, + AppName: "my_app", + SessionService: sessionService, + ArtifactService: artifactService, + }) + + log.Println("\n--- Agent Run 1: Triggering callbacks ---") + log.Println("This run will trigger `saveReportCallback` (from session state), `listUserFilesCallback` (will see the newly saved file), and `loadArtifactsCallback` (will load it).") + userInput := &genai.Content{Parts: []*genai.Part{genai.NewPartFromText("Please summarize the report for me.")}} + for event, err := range r.Run(ctx, session.Session.ID().UserID, session.Session.ID().SessionID, userInput, &runner.RunConfig{ + StreamingMode: runner.StreamingModeSSE, + }) { + if err != nil { + log.Printf("AGENT ERROR: %v\n", err) + } else if event.LLMResponse != nil && event.LLMResponse.Content != nil { + for _, p := range event.LLMResponse.Content.Parts { + fmt.Print(string(p.Text)) + } + } + } + fmt.Println() + + log.Println("\n--- Verifying artifacts after run ---") + // We can list artifacts directly from the service to see what the agent did. + listReq := &artifactservice.ListRequest{ + AppName: "my_app", + UserID: userID, + SessionID: "test-session-callbacks", + } + files, err := artifactService.List(ctx, listReq) + if err != nil { + log.Fatalf("Failed to list artifacts from service: %v", err) + } + log.Printf("Artifacts in service: %v", files) +} \ No newline at end of file From aa6521db02b975e568ee552ed6a3a4d4bb5b4b81 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Thu, 9 Oct 2025 15:30:03 -0400 Subject: [PATCH 071/125] Use story.pdf instead of random bytes --- examples/go/snippets/artifacts/main.go | 151 ++++++----------------- examples/go/snippets/artifacts/story.pdf | Bin 0 -> 53536 bytes 2 files changed, 41 insertions(+), 110 deletions(-) create mode 100644 examples/go/snippets/artifacts/story.pdf diff --git a/examples/go/snippets/artifacts/main.go b/examples/go/snippets/artifacts/main.go index e320992b..ff07ab1a 100644 --- a/examples/go/snippets/artifacts/main.go +++ b/examples/go/snippets/artifacts/main.go @@ -9,19 +9,18 @@ import ( "google.golang.org/adk/agent" "google.golang.org/adk/agent/llmagent" - "google.golang.org/adk/artifactservice" - "google.golang.org/adk/llm" - "google.golang.org/adk/llm/gemini" + "google.golang.org/adk/artifact" + "google.golang.org/adk/model" + "google.golang.org/adk/model/gemini" "google.golang.org/adk/runner" "google.golang.org/adk/session" - "google.golang.org/adk/sessionservice" "google.golang.org/genai" ) // This file contains snippets for the artifacts documentation. -// BeforeModelCallback saves any images from the user input before calling the LLM. -func BeforeModelCallback(ctx agent.Context, req *llm.Request) (*llm.Response, error) { +// BeforeModelCallback saves any images from the user input before calling the model. +func BeforeModelCallback(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) { log.Println("[Callback] BeforeModelCallback triggered.") // Get the artifact manager from the context. artifacts := ctx.Artifacts() @@ -47,7 +46,7 @@ func BeforeModelCallback(ctx agent.Context, req *llm.Request) (*llm.Response, er } } } - // Return nil to continue to the next callback or the LLM. + // Return nil to continue to the next callback or the model. return nil, nil // Continue to next callback or LLM call } @@ -79,12 +78,12 @@ func configureRunner() { } // Create a new in-memory artifact service. - artifactService := artifactservice.Mem() // In-memory ArtifactService + artifactService := artifact.InMemoryService() // Create a new in-memory session service. - sessionService := sessionservice.Mem() + sessionService := session.InMemoryService() // Create a new runner. - r, err := runner.New(&runner.Config{ + r, err := runner.New(runner.Config{ Agent: myAgent, AppName: appName, SessionService: sessionService, @@ -102,7 +101,7 @@ func configureRunner() { func inMemoryServiceExample() { // --8<-- [start:in-memory-service] // Simply instantiate the class - inMemoryService := artifactservice.Mem() + inMemoryService := artifact.InMemoryService() log.Printf("InMemoryArtifactService (Go) instantiated: %T", inMemoryService) // Use the service in your runner @@ -119,7 +118,7 @@ func inMemoryServiceExample() { // --8<-- [start:loading-artifacts] // loadArtifactsCallback is a BeforeModel callback that loads a specific artifact // and adds its content to the LLM request. -func loadArtifactsCallback(ctx agent.Context, req *llm.Request) (*llm.Response, error) { +func loadArtifactsCallback(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) { log.Println("[Callback] loadArtifactsCallback triggered.") // In a real app, you would parse the user's request to find a filename. // For this example, we'll hardcode a filename to demonstrate. @@ -129,17 +128,17 @@ func loadArtifactsCallback(ctx agent.Context, req *llm.Request) (*llm.Response, loadedPart, err := ctx.Artifacts().Load(filenameToLoad) if err != nil { log.Printf("Callback could not load artifact '%s': %v", filenameToLoad, err) - return nil, nil // File not found or error, continue to LLM. + return nil, nil // File not found or error, continue to model. } log.Printf("Callback successfully loaded artifact '%s'.", filenameToLoad) - // Add the loaded artifact to the request for the LLM. + // Add the loaded artifact to the request for the model. if len(req.Contents) > 0 { lastContent := req.Contents[len(req.Contents)-1] lastContent.Parts = append(lastContent.Parts, &loadedPart) log.Printf("Added artifact '%s' to LLM request.", filenameToLoad) } - // Return nil to continue to the next callback or the LLM. + // Return nil to continue to the next callback or the model. return nil, nil // Continue to next callback or LLM call } // --8<-- [end:loading-artifacts] @@ -148,7 +147,11 @@ func loadArtifactsCallback(ctx agent.Context, req *llm.Request) (*llm.Response, func representation() { // --8<-- [start:representation] // Create a byte slice with the image data. - imageBytes := []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A} + imageBytes, err := os.ReadFile("image.png") + if err != nil { + log.Fatalf("Failed to read image file: %v", err) + } + // Create a new artifact with the image data. imageArtifact := &genai.Part{ InlineData: &genai.Blob{ @@ -203,9 +206,9 @@ func namespacing() { // --8<-- [start:saving-artifacts] // saveReportCallback is a BeforeModel callback that saves a report from session state. -func saveReportCallback(ctx agent.Context, req *llm.Request) (*llm.Response, error) { +func saveReportCallback(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) { // Get the report data from the session state. - reportData, err := ctx.Session().State().Get("report_bytes") + reportData, err := ctx.State().Get("report_bytes") if err != nil { log.Printf("No report data found in session state: %v", err) return nil, nil // No report to save, continue normally. @@ -235,7 +238,7 @@ func saveReportCallback(ctx agent.Context, req *llm.Request) (*llm.Response, err return nil, nil } log.Printf("Successfully saved Go artifact '%s'.", filename) - // Return nil to continue to the next callback or the LLM. + // Return nil to continue to the next callback or the model. return nil, nil } // --8<-- [end:saving-artifacts] @@ -243,7 +246,7 @@ func saveReportCallback(ctx agent.Context, req *llm.Request) (*llm.Response, err // --8<-- [start:listing-artifacts] // listUserFilesCallback is a BeforeModel callback that lists available artifacts // and adds the list as context to the LLM request. -func listUserFilesCallback(ctx agent.Context, req *llm.Request) (*llm.Response, error) { +func listUserFilesCallback(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) { log.Println("[Callback] listUserFilesCallback triggered.") // List the available artifacts from the artifact service. availableFiles, err := ctx.Artifacts().List() @@ -251,6 +254,9 @@ func listUserFilesCallback(ctx agent.Context, req *llm.Request) (*llm.Response, log.Printf("An unexpected error occurred during Go artifact list: %v", err) return nil, nil // Continue, but log the error. } + + log.Printf("Found %d available files.", len(availableFiles)) + // If there are available files, add them to the LLM request. if len(availableFiles) > 0 { var fileListStr strings.Builder @@ -258,7 +264,7 @@ func listUserFilesCallback(ctx agent.Context, req *llm.Request) (*llm.Response, for _, fname := range availableFiles { fileListStr.WriteString(fmt.Sprintf("- %s\n", fname)) } - // Prepend this information to the user's request for the LLM. + // Prepend this information to the user's request for the model. if len(req.Contents) > 0 { lastContent := req.Contents[len(req.Contents)-1] if len(lastContent.Parts) > 0 { @@ -266,93 +272,17 @@ func listUserFilesCallback(ctx agent.Context, req *llm.Request) (*llm.Response, log.Println("Added file list to LLM request context.") } } + log.Printf("Available files:\n%s", fileListStr.String()) + } else { + log.Println("No available files found to list.") } - // Return nil to continue to the next callback or the LLM. + + // Return nil to continue to the next callback or the model. return nil, nil // Continue to next callback or LLM call } // --8<-- [end:listing-artifacts] - -// --- Local Implementations for Testing --- - -// localArtifacts implements the agent.Artifacts interface for testing. -type localArtifacts struct { - service artifactservice.Service - ctx context.Context - appName string - userID string - sessionID string -} - -func (m *localArtifacts) Load(filename string) (*genai.Part, error) { - res, err := m.service.Load(m.ctx, &artifactservice.LoadRequest{ - AppName: m.appName, - UserID: m.userID, - SessionID: m.sessionID, - FileName: filename, - }) - if err != nil { - return nil, err - } - return res.Part, nil -} - -func (m *localArtifacts) List() ([]string, error) { - res, err := m.service.List(m.ctx, &artifactservice.ListRequest{ - AppName: m.appName, - UserID: m.userID, - SessionID: m.sessionID, - }) - if err != nil { - return nil, err - } - return res.FileNames, nil -} - -// MockState implements the session.State interface for testing. -type MockState struct { - data map[string]any -} - -func (s *MockState) Get(key string) (any, error) { - val, ok := s.data[key] - if !ok { - return nil, fmt.Errorf("key not found") - } - return val, nil -} - -func (s *MockState) Set(key string, value any) error { - s.data[key] = value - return nil -} - -// MockSession implements the session.Session interface for testing. -type MockSession struct { - session.Session - mockState session.State -} - -func (s *MockSession) State() session.State { - return s.mockState -} - -// MockContext implements the agent.Context interface for testing. -type MockContext struct { - agent.Context - mockArtifacts agent.Artifacts - mockSession session.Session -} - -func (m *MockContext) Artifacts() agent.Artifacts { - return m.mockArtifacts -} - -func (m *MockContext) Session() session.Session { - return m.mockSession -} - func main() { log.Println("--- Running Snippets ---") @@ -375,8 +305,8 @@ func main() { log.Println("\n--- Running Agent with Multiple Callbacks ---") // 1. Set up services ctx := context.Background() - artifactService := artifactservice.Mem() - sessionService := sessionservice.Mem() + artifactService := artifact.InMemoryService() + sessionService := session.InMemoryService() // 2. Set up the agent with multiple callbacks model, _ := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{}) @@ -392,11 +322,12 @@ func main() { }) // 3. Create a session with some initial state to trigger `saveReportCallback` + reportBytes, _ := os.ReadFile("story.pdf") // Load a sample PDF file initialState := map[string]any{ - "report_bytes": []byte("%PDF-1.4... This is a test PDF."), // Mock PDF data + "report_bytes": reportBytes, } userID := "test-user" - session, _ := sessionService.Create(ctx, &sessionservice.CreateRequest{ + session, _ := sessionService.Create(ctx, &session.CreateRequest{ AppName: "my_app", UserID: userID, SessionID: "test-session-callbacks", @@ -404,7 +335,7 @@ func main() { }) // 4. Create and run the runner - r, _ := runner.New(&runner.Config{ + r, _ := runner.New(runner.Config{ Agent: reportingAgent, AppName: "my_app", SessionService: sessionService, @@ -414,8 +345,8 @@ func main() { log.Println("\n--- Agent Run 1: Triggering callbacks ---") log.Println("This run will trigger `saveReportCallback` (from session state), `listUserFilesCallback` (will see the newly saved file), and `loadArtifactsCallback` (will load it).") userInput := &genai.Content{Parts: []*genai.Part{genai.NewPartFromText("Please summarize the report for me.")}} - for event, err := range r.Run(ctx, session.Session.ID().UserID, session.Session.ID().SessionID, userInput, &runner.RunConfig{ - StreamingMode: runner.StreamingModeSSE, + for event, err := range r.Run(ctx, session.Session.UserID(), session.Session.ID(), userInput, &agent.RunConfig{ + StreamingMode: agent.StreamingModeSSE, }) { if err != nil { log.Printf("AGENT ERROR: %v\n", err) @@ -429,7 +360,7 @@ func main() { log.Println("\n--- Verifying artifacts after run ---") // We can list artifacts directly from the service to see what the agent did. - listReq := &artifactservice.ListRequest{ + listReq := &artifact.ListRequest{ AppName: "my_app", UserID: userID, SessionID: "test-session-callbacks", diff --git a/examples/go/snippets/artifacts/story.pdf b/examples/go/snippets/artifacts/story.pdf new file mode 100644 index 0000000000000000000000000000000000000000..6810fe4deb274555490bccedef49f15c93a75012 GIT binary patch literal 53536 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 Date: Thu, 9 Oct 2025 15:41:02 -0400 Subject: [PATCH 072/125] Fixed issue with artifact loading --- examples/go/snippets/artifacts/main.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/examples/go/snippets/artifacts/main.go b/examples/go/snippets/artifacts/main.go index ff07ab1a..819c71b5 100644 --- a/examples/go/snippets/artifacts/main.go +++ b/examples/go/snippets/artifacts/main.go @@ -132,12 +132,19 @@ func loadArtifactsCallback(ctx agent.CallbackContext, req *model.LLMRequest) (*m } log.Printf("Callback successfully loaded artifact '%s'.", filenameToLoad) - // Add the loaded artifact to the request for the model. - if len(req.Contents) > 0 { - lastContent := req.Contents[len(req.Contents)-1] - lastContent.Parts = append(lastContent.Parts, &loadedPart) - log.Printf("Added artifact '%s' to LLM request.", filenameToLoad) + + // Ensure there's at least one content in the request to append to. + if len(req.Contents) == 0 { + req.Contents = []*genai.Content{{Parts: []*genai.Part{ + genai.NewPartFromText("SYSTEM: The following file is provided for context:\n"), + }}} } + + // Add the loaded artifact to the request for the model. + lastContent := req.Contents[len(req.Contents)-1] + lastContent.Parts = append(lastContent.Parts, &loadedPart) + log.Printf("Added artifact '%s' to LLM request.", filenameToLoad) + // Return nil to continue to the next callback or the model. return nil, nil // Continue to next callback or LLM call } @@ -268,8 +275,9 @@ func listUserFilesCallback(ctx agent.CallbackContext, req *model.LLMRequest) (*m if len(req.Contents) > 0 { lastContent := req.Contents[len(req.Contents)-1] if len(lastContent.Parts) > 0 { - lastContent.Parts[0] = genai.NewPartFromText(fileListStr.String() + lastContent.Parts[0].Text) - log.Println("Added file list to LLM request context.") + fileListStr.WriteString("\n") // Add a newline for separation. + lastContent.Parts[0] = genai.NewPartFromText(fileListStr.String() + lastContent.Parts[0].Text) + log.Println("Added file list to LLM request context.") } } log.Printf("Available files:\n%s", fileListStr.String()) From 6f6179ec4a6d65af2b6ac0d898eeeec1d0cff6f9 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Thu, 9 Oct 2025 16:04:11 -0400 Subject: [PATCH 073/125] Cleanup unneeded fixes --- docs/artifacts/index.md | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/docs/artifacts/index.md b/docs/artifacts/index.md index c402154f..094fced7 100644 --- a/docs/artifacts/index.md +++ b/docs/artifacts/index.md @@ -89,7 +89,7 @@ While session `state` is suitable for storing small pieces of configuration or c 2. **Persisting Large Data:** Session state is generally not optimized for storing large amounts of data. Artifacts provide a dedicated mechanism for persisting larger blobs without cluttering the session state. 3. **User File Management:** Provide capabilities for users to upload files (which can be saved as artifacts) and retrieve or download files generated by the agent (loaded from artifacts). 4. **Sharing Outputs:** Enable tools or agents to generate binary outputs (like a PDF report or a generated image) that can be saved via `save_artifact` and later accessed by other parts of the application or even in subsequent sessions (if using user namespacing). -5. **Caching Binary Data:** Store the results of computationally expensive operations that produce binary data (e.g., rendering a complex chart image) as artifacts to avoid regenerating them on subsequent requests. +5. **Caching Binary Data:** Store the results of computationally expensive operations that produce binary data (e.g., rendering a complex chart image) as artifacts to avoid regenerating them on subsequent requests. In essence, whenever your agent needs to work with file-like binary data that needs to be persisted, versioned, or shared, Artifacts managed by an `ArtifactService` are the appropriate mechanism within ADK. @@ -103,7 +103,7 @@ Here are some typical scenarios where they prove valuable: * **Generated Reports/Files:** * A tool or agent generates a report (e.g., a PDF analysis, a CSV data export, an image chart). -* **Handling User Uploads:** +* **Handling User Uploads:** * A user uploads a file (e.g., an image for analysis, a document for summarization) through a front-end interface. @@ -592,9 +592,9 @@ The artifact interaction methods are available directly on instances of `Callbac public void onError(Throwable e) { // Handle potential storage errors or other exceptions System.err.println( - "An error occurred during Java artifact load for '" + "An error occurred during Java artifact load for '" + filename - + "': " + + "': " + e.getMessage()); } @@ -608,7 +608,8 @@ The artifact interaction methods are available directly on instances of `Callbac // Example: Load a specific version (e.g., version 0) /* artifactService.loadArtifact(appName, userId, sessionId, filename, Optional.of(0)) - .subscribe(part -> { + StringBuilder fileListStr + .subscribe(part -> { System.out.println("Loaded version 0 of Java artifact '" + filename + "'."); }, throwable -> { System.err.println("Error loading version 0 of '" + filename + "': " + throwable.getMessage()); @@ -713,7 +714,7 @@ The artifact interaction methods are available directly on instances of `Callbac + sessionId + " has no saved Java artifacts."); } else { - StringBuilder fileListStr = + StringBuilder fileListStr = new StringBuilder( "Here are the available Java artifacts for user " + userId From fe80d3bb51a8fb539ae0e5dfe86e16d49622660f Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Thu, 9 Oct 2025 16:11:00 -0400 Subject: [PATCH 074/125] Added back inadvertently removed GCS section --- docs/artifacts/index.md | 48 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 46 insertions(+), 2 deletions(-) diff --git a/docs/artifacts/index.md b/docs/artifacts/index.md index 094fced7..559f7149 100644 --- a/docs/artifacts/index.md +++ b/docs/artifacts/index.md @@ -608,7 +608,6 @@ The artifact interaction methods are available directly on instances of `Callbac // Example: Load a specific version (e.g., version 0) /* artifactService.loadArtifact(appName, userId, sessionId, filename, Optional.of(0)) - StringBuilder fileListStr .subscribe(part -> { System.out.println("Loaded version 0 of Java artifact '" + filename + "'."); }, throwable -> { @@ -832,6 +831,51 @@ ADK provides concrete implementations of the `BaseArtifactService` interface, of --8<-- "examples/go/snippets/artifacts/main.go:in-memory-service" ``` +### GcsArtifactService + + +* **Storage Mechanism:** Leverages Google Cloud Storage (GCS) for persistent artifact storage. Each version of an artifact is stored as a separate object (blob) within a specified GCS bucket. +* **Object Naming Convention:** It constructs GCS object names (blob names) using a hierarchical path structure. +* **Key Features:** + * **Persistence:** Artifacts stored in GCS persist across application restarts and deployments. + * **Scalability:** Leverages the scalability and durability of Google Cloud Storage. + * **Versioning:** Explicitly stores each version as a distinct GCS object. The `saveArtifact` method in `GcsArtifactService`. + * **Permissions Required:** The application environment needs appropriate credentials (e.g., Application Default Credentials) and IAM permissions to read from and write to the specified GCS bucket. +* **Use Cases:** + * Production environments requiring persistent artifact storage. + * Scenarios where artifacts need to be shared across different application instances or services (by accessing the same GCS bucket). + * Applications needing long-term storage and retrieval of user or session data. +* **Instantiation:** + + === "Python" + + ```python + from google.adk.artifacts import GcsArtifactService + + # Specify the GCS bucket name + gcs_bucket_name_py = "your-gcs-bucket-for-adk-artifacts" # Replace with your bucket name + + try: + gcs_service_py = GcsArtifactService(bucket_name=gcs_bucket_name_py) + print(f"Python GcsArtifactService initialized for bucket: {gcs_bucket_name_py}") + # Ensure your environment has credentials to access this bucket. + # e.g., via Application Default Credentials (ADC) + + # Then pass it to the Runner + # runner = Runner(..., artifact_service=gcs_service_py) + + except Exception as e: + # Catch potential errors during GCS client initialization (e.g., auth issues) + print(f"Error initializing Python GcsArtifactService: {e}") + # Handle the error appropriately - maybe fall back to InMemory or raise + ``` + + === "Java" + + ```java + --8<-- "examples/java/snippets/src/main/java/artifacts/GcsServiceSetup.java:full_code" + ``` + Choosing the appropriate `ArtifactService` implementation depends on your application's requirements for data persistence, scalability, and operational environment. ## Best Practices @@ -851,4 +895,4 @@ To use artifacts effectively and maintainably: * **Cleanup Strategy:** For persistent storage like `GcsArtifactService`, artifacts remain until explicitly deleted. If artifacts represent temporary data or have a limited lifespan, implement a strategy for cleanup. This might involve: * Using GCS lifecycle policies on the bucket. * Building specific tools or administrative functions that utilize the `artifact_service.delete_artifact` method (note: delete is *not* exposed via context objects for safety). - * Carefully managing filenames to allow pattern-based deletion if needed. \ No newline at end of file + * Carefully managing filenames to allow pattern-based deletion if needed. From 7119315b24fc20f51de4816256e45ff7e8110478 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 24 Oct 2025 13:50:20 -0400 Subject: [PATCH 075/125] Addressed PR comments --- examples/go/snippets/artifacts/main.go | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/examples/go/snippets/artifacts/main.go b/examples/go/snippets/artifacts/main.go index 819c71b5..53c701c3 100644 --- a/examples/go/snippets/artifacts/main.go +++ b/examples/go/snippets/artifacts/main.go @@ -74,7 +74,7 @@ func configureRunner() { }, }) if err != nil { - log.Fatalf("failed to create agent: %v", err) + log.Fatalf("Failed to create agent: %v", err) } // Create a new in-memory artifact service. @@ -100,17 +100,17 @@ func configureRunner() { // inMemoryServiceExample demonstrates how to set up an in-memory artifact service. func inMemoryServiceExample() { // --8<-- [start:in-memory-service] - // Simply instantiate the class - inMemoryService := artifact.InMemoryService() - log.Printf("InMemoryArtifactService (Go) instantiated: %T", inMemoryService) + // Simply instantiate the service + artifactService := artifact.InMemoryService() + log.Printf("InMemoryArtifactService (Go) instantiated: %T", artifactService) // Use the service in your runner - // r, _ := runner.New(&runner.Config{ + // r, _ := runner.New(runner.Config{ // Agent: agent, // AppName: "my_app", // SessionService: sessionService, // ArtifactService: artifactService, - // }) + // }) // --8<-- [end:in-memory-service] } @@ -192,6 +192,7 @@ func artifactData() { // namespacing demonstrates the difference between session and user-scoped artifacts. func namespacing() { // --8<-- [start:namespacing] + // Note: Namespacing is only supported when using the GCS ArtifactService implementation. // A session-scoped artifact is only available within the current session. sessionReportFilename := "summary.txt" // A user-scoped artifact is available across all sessions for the current user. From ac776f182a0104db82d3bcba76748c63fc48306d Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 29 Oct 2025 16:06:53 -0400 Subject: [PATCH 076/125] Ran linter --- examples/go/snippets/artifacts/main.go | 44 ++++++++++++++------------ 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/examples/go/snippets/artifacts/main.go b/examples/go/snippets/artifacts/main.go index 53c701c3..c2f34a34 100644 --- a/examples/go/snippets/artifacts/main.go +++ b/examples/go/snippets/artifacts/main.go @@ -52,8 +52,8 @@ func BeforeModelCallback(ctx agent.CallbackContext, req *model.LLMRequest) (*mod // configureRunner configures the runner with an in-memory artifact service. func configureRunner() { -// --8<-- [start:configure-runner] -// --8<-- [start:prerequisite] + // --8<-- [start:configure-runner] + // --8<-- [start:prerequisite] // Create a new context. ctx := context.Background() // Set the app name. @@ -93,8 +93,8 @@ func configureRunner() { log.Fatalf("Failed to create runner: %v", err) } log.Printf("Runner created successfully: %v", r) -// --8<-- [end:prerequisite] -// --8<-- [end:configure-runner] + // --8<-- [end:prerequisite] + // --8<-- [end:configure-runner] } // inMemoryServiceExample demonstrates how to set up an in-memory artifact service. @@ -110,8 +110,8 @@ func inMemoryServiceExample() { // AppName: "my_app", // SessionService: sessionService, // ArtifactService: artifactService, - // }) - + // }) + // --8<-- [end:in-memory-service] } @@ -136,7 +136,7 @@ func loadArtifactsCallback(ctx agent.CallbackContext, req *model.LLMRequest) (*m // Ensure there's at least one content in the request to append to. if len(req.Contents) == 0 { req.Contents = []*genai.Content{{Parts: []*genai.Part{ - genai.NewPartFromText("SYSTEM: The following file is provided for context:\n"), + genai.NewPartFromText("SYSTEM: The following file is provided for context:\n"), }}} } @@ -148,17 +148,18 @@ func loadArtifactsCallback(ctx agent.CallbackContext, req *model.LLMRequest) (*m // Return nil to continue to the next callback or the model. return nil, nil // Continue to next callback or LLM call } + // --8<-- [end:loading-artifacts] // representation demonstrates how to manually construct an artifact. func representation() { -// --8<-- [start:representation] + // --8<-- [start:representation] // Create a byte slice with the image data. imageBytes, err := os.ReadFile("image.png") if err != nil { log.Fatalf("Failed to read image file: %v", err) } - + // Create a new artifact with the image data. imageArtifact := &genai.Part{ InlineData: &genai.Blob{ @@ -168,12 +169,12 @@ func representation() { } log.Printf("Artifact MIME Type: %s", imageArtifact.InlineData.MIMEType) log.Printf("Artifact Data (first 8 bytes): %x...", imageArtifact.InlineData.Data[:8]) -// --8<-- [end:representation] + // --8<-- [end:representation] } // artifactData demonstrates how to create an artifact from a file. func artifactData() { -// --8<-- [start:artifact-data] + // --8<-- [start:artifact-data] // Load imageBytes from a file imageBytes, err := os.ReadFile("image.png") if err != nil { @@ -186,18 +187,18 @@ func artifactData() { imageArtifact := genai.NewPartFromBytes([]byte(imageBytes), "image/png") log.Printf("Artifact MIME Type: %s", imageArtifact.InlineData.MIMEType) -// --8<-- [end:artifact-data] + // --8<-- [end:artifact-data] } // namespacing demonstrates the difference between session and user-scoped artifacts. func namespacing() { -// --8<-- [start:namespacing] + // --8<-- [start:namespacing] // Note: Namespacing is only supported when using the GCS ArtifactService implementation. // A session-scoped artifact is only available within the current session. sessionReportFilename := "summary.txt" // A user-scoped artifact is available across all sessions for the current user. userConfigFilename := "user:settings.json" - + // When saving 'summary.txt' via ctx.Artifacts().Save, // it's tied to the current app_name, user_id, and session_id. // ctx.Artifacts().Save(sessionReportFilename, *artifact); @@ -206,7 +207,7 @@ func namespacing() { // the ArtifactService implementation should recognize the "user:" prefix // and scope it to app_name and user_id, making it accessible across sessions for that user. // ctx.Artifacts().Save(userConfigFilename, *artifact); -// --8<-- [end:namespacing] + // --8<-- [end:namespacing] log.Printf("Session filename: %s", sessionReportFilename) log.Printf("User filename: %s", userConfigFilename) @@ -249,6 +250,7 @@ func saveReportCallback(ctx agent.CallbackContext, req *model.LLMRequest) (*mode // Return nil to continue to the next callback or the model. return nil, nil } + // --8<-- [end:saving-artifacts] // --8<-- [start:listing-artifacts] @@ -285,12 +287,12 @@ func listUserFilesCallback(ctx agent.CallbackContext, req *model.LLMRequest) (*m } else { log.Println("No available files found to list.") } - + // Return nil to continue to the next callback or the model. return nil, nil // Continue to next callback or LLM call } -// --8<-- [end:listing-artifacts] +// --8<-- [end:listing-artifacts] func main() { log.Println("--- Running Snippets ---") @@ -324,9 +326,9 @@ func main() { Name: "reporting_agent", Instruction: "You are a reporting agent. You can see available files and their contents if they are loaded for you. Summarize any provided files.", BeforeModel: []llmagent.BeforeModelCallback{ - saveReportCallback, // Saves report from state - listUserFilesCallback, // Lists available files and adds to prompt - loadArtifactsCallback, // Loads a specific file and adds to prompt + saveReportCallback, // Saves report from state + listUserFilesCallback, // Lists available files and adds to prompt + loadArtifactsCallback, // Loads a specific file and adds to prompt }, }) @@ -379,4 +381,4 @@ func main() { log.Fatalf("Failed to list artifacts from service: %v", err) } log.Printf("Artifacts in service: %v", files) -} \ No newline at end of file +} From 857bd2b0f4bb655a0bfc0ed2da1bdf5c17c89c77 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 15 Oct 2025 13:57:46 -0400 Subject: [PATCH 077/125] feat: Added context snippets --- docs/context/index.md | 112 +++++++++- examples/go/snippets/context/main.go | 321 +++++++++++++++++++++++++++ 2 files changed, 431 insertions(+), 2 deletions(-) create mode 100644 examples/go/snippets/context/main.go diff --git a/docs/context/index.md b/docs/context/index.md index fdf46777..b2ef0b4e 100644 --- a/docs/context/index.md +++ b/docs/context/index.md @@ -75,6 +75,34 @@ The central piece holding all this information together for a single, complete u } ``` +=== "Go" + + ```go + // Conceptual Pseudocode: How the framework provides context (Internal Logic) + + // runner := runner.New(runner.Config{Agent: myRootAgent, SessionService: ...}) + // userMsg := &genai.Content{...} + // session, _ := sessionService.GetOrCreate(...) + + // --- Inside runner.Run(...) --- + // 1. Framework creates the main context for this specific run + // invocationContext := agent.NewInvocationContext(agent.InvocationContextConfig{ + // InvocationID: "unique-id-for-this-run", + // Session: session, + // UserContent: userMsg, + // Agent: myRootAgent, + // // ... other necessary fields ... + // }) + // + // 2. Framework calls the agent's run method, passing the context. + // for range myRootAgent.Run(invocationContext) { + // // ... + // } + // --- End Internal Logic --- + // + // As a developer, you work with the context objects provided in method arguments. + ``` + ## The Different types of Context While `InvocationContext` acts as the comprehensive internal container, ADK provides specialized context objects tailored to specific situations. This ensures you have the right tools and permissions for the task at hand without needing to handle the full complexity of the internal context everywhere. Here are the different "flavors" you'll encounter: @@ -163,6 +191,12 @@ While `InvocationContext` acts as the comprehensive internal container, ADK prov } ``` + === "Go" + + ```go + --8<-- "examples/go/snippets/context/main.go:invocation_context_agent" + ``` + 2. **`ReadonlyContext`** * **Where Used:** Provided in scenarios where only read access to basic information is needed and mutation is disallowed (e.g., `InstructionProvider` functions). It's also the base class for other contexts. * **Purpose:** Offers a safe, read-only view of fundamental contextual details. @@ -180,7 +214,7 @@ While `InvocationContext` acts as the comprehensive internal container, ADK prov # context.state['new_key'] = 'value' # This would typically cause an error or be ineffective return f"Process the request for a {user_tier} user." ``` - + === "Java" ```java @@ -194,6 +228,12 @@ While `InvocationContext` acts as the comprehensive internal container, ADK prov return "Process the request for a " + userTier + " user." } ``` + + === "Go" + + ```go + --8<-- "examples/go/snippets/context/main.go:readonly_context_instruction" + ``` 3. **`CallbackContext`** * **Where Used:** Passed as `callback_context` to agent lifecycle callbacks (`before_agent_callback`, `after_agent_callback`) and model interaction callbacks (`before_model_callback`, `after_model_callback`). @@ -244,6 +284,12 @@ While `InvocationContext` acts as the comprehensive internal container, ADK prov } ``` + === "Go" + + ```go + --8<-- "examples/go/snippets/context/main.go:callback_context_callback" + ``` + 4. **`ToolContext`** * **Where Used:** Passed as `tool_context` to the functions backing `FunctionTool`s and to tool execution callbacks (`before_tool_callback`, `after_tool_callback`). * **Purpose:** Provides everything `CallbackContext` does, plus specialized methods essential for tool execution, like handling authentication, searching memory, and listing artifacts. @@ -311,6 +357,12 @@ While `InvocationContext` acts as the comprehensive internal container, ADK prov } ``` + === "Go" + + ```go + --8<-- "examples/go/snippets/context/main.go:tool_context_tool" + ``` + Understanding these different context objects and when to use them is key to effectively managing state, accessing services, and controlling the flow of your ADK application. The next section will detail common tasks you can perform using these contexts. @@ -380,6 +432,16 @@ You'll frequently need to read information stored within the context. // ... callback logic ... ``` + === "Go" + + ```go + --8<-- "examples/go/snippets/context/main.go:accessing_state_tool" + ``` + + ```go + --8<-- "examples/go/snippets/context/main.go:accessing_state_callback" + ``` + * **Getting Current Identifiers:** Useful for logging or custom logic based on the current operation. === "Python" @@ -410,6 +472,12 @@ You'll frequently need to read information stored within the context. } ``` + === "Go" + + ```go + --8<-- "examples/go/snippets/context/main.go:accessing_ids" + ``` + * **Accessing the Initial User Input:** Refer back to the message that started the current invocation. === "Python" @@ -448,6 +516,12 @@ You'll frequently need to read information stored within the context. } } ``` + + === "Go" + + ```go + --8<-- "examples/go/snippets/context/main.go:accessing_user_content_agent" + ``` ### Managing State @@ -507,6 +581,16 @@ State is crucial for memory and data flow. When you modify state using `Callback } ``` + === "Go" + + ```go + --8<-- "examples/go/snippets/context/main.go:passing_data_tool1" + ``` + + ```go + --8<-- "examples/go/snippets/context/main.go:passing_data_tool2" + ``` + * **Updating User Preferences:** === "Python" @@ -538,6 +622,12 @@ State is crucial for memory and data flow. When you modify state using `Callback } ``` + === "Go" + + ```go + --8<-- "examples/go/snippets/context/main.go:updating_preferences" + ``` + * **State Prefixes:** While basic state is session-specific, prefixes like `app:` and `user:` can be used with persistent `SessionService` implementations (like `DatabaseSessionService` or `VertexAiSessionService`) to indicate broader scope (app-wide or user-wide across sessions). `temp:` can denote data only relevant within the current invocation. ### Working with Artifacts @@ -600,6 +690,12 @@ Use artifacts to handle files or large data blobs associated with the session. C // saveDocumentReference(context, "gs://my-bucket/docs/report.pdf") ``` + === "Go" + + ```go + --8<-- "examples/go/snippets/context/main.go:artifacts_save_ref" + ``` + 2. **Summarizer Tool:** Load the artifact to get the path/URI, read the actual document content using appropriate libraries, summarize, and return the result. === "Python" @@ -710,7 +806,13 @@ Use artifacts to handle files or large data blobs associated with the session. C } } ``` - + + === "Go" + + ```go + --8<-- "examples/go/snippets/context/main.go:artifacts_summarize" + ``` + * **Listing Artifacts:** Discover what files are available. === "Python" @@ -745,6 +847,12 @@ Use artifacts to handle files or large data blobs associated with the session. C } ``` + === "Go" + + ```go + --8<-- "examples/go/snippets/context/main.go:artifacts_list" + ``` + ### Handling Tool Authentication

diff --git a/examples/go/snippets/context/main.go b/examples/go/snippets/context/main.go new file mode 100644 index 00000000..87276daa --- /dev/null +++ b/examples/go/snippets/context/main.go @@ -0,0 +1,321 @@ +package main + +import ( + "context" + "fmt" + "iter" + "log" + + "google.golang.org/adk/agent" + "google.golang.org/adk/model" + "google.golang.org/adk/model/gemini" + "google.golang.org/adk/session" + "google.golang.org/adk/tool" + "google.golang.org/genai" +) + +// --- Conceptual Snippets for adk-docs/docs/context/index.md --- + +// --8<-- [start:invocation_context_agent] +// Pseudocode: Agent implementation receiving InvocationContext +type MyAgent struct { + agent.Agent // Embed the agent.Agent interface to satisfy it. +} + +// NewMyAgent creates a new MyAgent. +func NewMyAgent() (agent.Agent, error) { + a := &MyAgent{} + // Use agent.New to construct the base agent functionality. + baseAgent, err := agent.New(agent.Config{ + Name: "MyAgent", + Description: "An example agent.", + Run: a.Run, // Pass the Run method of our struct. + }) + if err != nil { + return nil, err + } + a.Agent = baseAgent + return a, nil +} + +func (a *MyAgent) Run(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] { + return func(yield func(*session.Event, error) bool) { + // Direct access example + agentName := ctx.Agent().Name() + sessionID := ctx.Session().ID() + fmt.Printf("Agent %s running in session %s for invocation %s\n", agentName, sessionID, ctx.InvocationID()) + // ... agent logic using ctx ... + yield(&session.Event{Author: a.Name()}, nil) + } +} + +// --8<-- [end:invocation_context_agent] + +// --8<-- [start:readonly_context_instruction] +// Pseudocode: Instruction provider receiving ReadonlyContext +func myInstructionProvider(ctx agent.ReadonlyContext) (string, error) { + // Read-only access example + userTier, err := ctx.ReadonlyState().Get("user_tier") + if err != nil { + userTier = "standard" // Default value + } + // ctx.State().Set("new_key", "value") // This would not be possible as State() is read-only. + return fmt.Sprintf("Process the request for a %v user.", userTier), nil +} + +// --8<-- [end:readonly_context_instruction] + +// --8<-- [start:callback_context_callback] +// Pseudocode: Callback receiving CallbackContext +func myBeforeModelCb(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) { + // Read/Write state example + callCount, err := ctx.State().Get("model_calls") + if err != nil { + callCount = 0 // Default value + } + newCount := callCount.(int) + 1 + if err := ctx.State().Set("model_calls", newCount); err != nil { + return nil, err + } + + // Optionally load an artifact + // configPart, err := ctx.Artifacts().Load("model_config.json") + fmt.Printf("Preparing model call #%d for invocation %s\n", newCount, ctx.InvocationID()) + return nil, nil // Allow model call to proceed +} + +// --8<-- [end:callback_context_callback] + +// --8<-- [start:tool_context_tool] +// Pseudocode: Tool function receiving ToolContext +type searchExternalAPIArgs struct { + Query string `json:"query"` +} + +func searchExternalAPI(tc tool.Context, args searchExternalAPIArgs) (map[string]any, error) { + apiKey, err := tc.State().Get("api_key") + if err != nil || apiKey == "" { + // In a real scenario, you would define and request credentials here. + // This is a conceptual placeholder. + return map[string]any{"status": "Auth Required"}, nil + } + + // Use the API key... + fmt.Printf("Tool executing for query '%s' using API key. Invocation: %s\n", args.Query, tc.InvocationID()) + + // Optionally search memory or list artifacts + // relevantDocs, _ := tc.Memory().Search(fmt.Sprintf("info related to %s", args.Query)) + // availableFiles, _ := tc.Artifacts().List() + + return map[string]any{"result": fmt.Sprintf("Data for %s fetched.", args.Query)}, nil +} + +// --8<-- [end:tool_context_tool] + +// --8<-- [start:accessing_state_tool] +// Pseudocode: In a Tool function +func myTool(tc tool.Context) error { + userPref, err := tc.State().Get("user_display_preference") + if err != nil { + userPref = "default_mode" + } + apiEndpoint, _ := tc.State().Get("app:api_endpoint") // Read app-level state + + if userPref == "dark_mode" { + // ... apply dark mode logic ... + } + fmt.Printf("Using API endpoint: %v\n", apiEndpoint) + // ... rest of tool logic ... + return nil +} + +// --8<-- [end:accessing_state_tool] + +// --8<-- [start:accessing_state_callback] +// Pseudocode: In a Callback function +func myCallback(ctx agent.CallbackContext) error { + lastToolResult, err := ctx.State().Get("temp:last_api_result") // Read temporary state + if err == nil { + fmt.Printf("Found temporary result from last tool: %v\n", lastToolResult) + } + // ... callback logic ... + return nil +} + +// --8<-- [end:accessing_state_callback] + +// --8<-- [start:accessing_ids] +// Pseudocode: In any context (ToolContext shown) +func logToolUsage(tc tool.Context) { + agentName := tc.AgentName() + invID := tc.InvocationID() + // funcCallID := tc.FunctionCallID() // This method may have been removed or changed. + + fmt.Printf("Log: Invocation=%s, Agent=%s - Tool Executed.\n", invID, agentName) +} + +// --8<-- [end:accessing_ids] + +// --8<-- [start:accessing_user_content_agent] +// Pseudocode: In a Callback +func checkInitialIntent(ctx agent.CallbackContext) { + initialText := "N/A" + userContent := ctx.UserContent() + if userContent != nil && len(userContent.Parts) > 0 { + // The API for Part content has changed from a type assertion to direct field access. + if text := userContent.Parts[0].Text; text != "" { + initialText = text + } else { + initialText = "Non-text input" + } + } + fmt.Printf("This invocation started with user input: '%s'\n", initialText) +} + +// Pseudocode: In an Agent's Run method +func (a *MyAgent) RunWithUserContent(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] { + return func(yield func(*session.Event, error) bool) { + userContent := ctx.UserContent() + if userContent != nil && len(userContent.Parts) > 0 { + if initialText := userContent.Parts[0].Text; initialText != "" { + fmt.Printf("Agent logic remembering initial query: %s\n", initialText) + } + } + yield(&session.Event{Author: a.Name()}, nil) + } +} + +// --8<-- [end:accessing_user_content_agent] + +// --8<-- [start:passing_data_tool1] +// Pseudocode: Tool 1 - Fetches user ID +type getUserProfileResult struct { + ProfileStatus string `json:"profile_status"` +} + +func getUserProfile(tc tool.Context) (*getUserProfileResult, error) { + userID := "user-12345" // Simulate fetching ID + // Save the ID to state for the next tool + if err := tc.State().Set("temp:current_user_id", userID); err != nil { + return nil, err + } + return &getUserProfileResult{ProfileStatus: "ID generated"}, nil +} + +// --8<-- [end:passing_data_tool1] + +// --8<-- [start:passing_data_tool2] +// Pseudocode: Tool 2 - Uses user ID from state +type getUserOrdersResult struct { + Orders []string `json:"orders,omitempty"` + Error string `json:"error,omitempty"` +} + +func getUserOrders(tc tool.Context) (*getUserOrdersResult, error) { + userID, err := tc.State().Get("temp:current_user_id") + if err != nil { + return &getUserOrdersResult{Error: "User ID not found in state"}, nil + } + + fmt.Printf("Fetching orders for user ID: %v\n", userID) + // ... logic to fetch orders using user_id ... + return &getUserOrdersResult{Orders: []string{"order123", "order456"}}, nil +} + +// --8<-- [end:passing_data_tool2] + +// --8<-- [start:updating_preferences] +// Pseudocode: Tool or Callback identifies a preference +type setUserPreferenceArgs struct { + Preference string `json:"preference"` + Value string `json:"value"` +} + +func setUserPreference(tc tool.Context, args setUserPreferenceArgs) (map[string]string, error) { + // Use 'user:' prefix for user-level state (if using a persistent SessionService) + stateKey := fmt.Sprintf("user:%s", args.Preference) + if err := tc.State().Set(stateKey, args.Value); err != nil { + return nil, err + } + fmt.Printf("Set user preference '%s' to '%s'\n", args.Preference, args.Value) + return map[string]string{"status": "Preference updated"}, nil +} + +// --8<-- [end:updating_preferences] + +// --8<-- [start:artifacts_save_ref] +// Pseudocode: In a callback or initial tool +func saveDocumentReference(ctx agent.CallbackContext, filePath string) error { + // Assume filePath is something like "gs://my-bucket/docs/report.pdf" + // Create a Part containing the path/URI text + artifactPart := genai.NewPartFromText(filePath) + err := ctx.Artifacts().Save("document_to_summarize.txt", *artifactPart) + if err != nil { + fmt.Printf("Error saving artifact: %v\n", err) + return err + } + fmt.Printf("Saved document reference '%s' as artifact\n", filePath) + // Store the filename in state if needed by other tools + return ctx.State().Set("temp:doc_artifact_name", "document_to_summarize.txt") +} + +// --8<-- [end:artifacts_save_ref] + +// --8<-- [start:artifacts_summarize] +// Pseudocode: In the Summarizer tool function +func summarizeDocumentTool(tc tool.Context) (map[string]string, error) { + artifactName, err := tc.State().Get("temp:doc_artifact_name") + if err != nil { + return map[string]string{"error": "Document artifact name not found in state."}, nil + } + + // 1. Load the artifact part containing the path/URI + artifactPart, err := tc.Artifacts().Load(artifactName.(string)) + if err != nil { + return nil, err + } + + if artifactPart.Text == "" { + return map[string]string{"error": "Could not load artifact or artifact has no text path."}, nil + } + filePath := artifactPart.Text + fmt.Printf("Loaded document reference: %s\n", filePath) + + // 2. Read the actual document content (outside ADK context) + // In a real implementation, you would use a GCS client or local file reader. + documentContent := "This is the fake content of the document at " + filePath + _ = documentContent // Avoid unused variable error. + + // 3. Summarize the content + summary := "Summary of content from " + filePath // Placeholder + + return map[string]string{"summary": summary}, nil +} + +// --8<-- [end:artifacts_summarize] + +// --8<-- [start:artifacts_list] +// Pseudocode: In a tool function +func checkAvailableDocs(tc tool.Context) (map[string][]string, error) { + artifactKeys, err := tc.Artifacts().List() + if err != nil { + return nil, err + } + fmt.Printf("Available artifacts: %v\n", artifactKeys) + return map[string][]string{"available_docs": artifactKeys}, nil +} + +// --8<-- [end:artifacts_list] + +// This main function is for compilation purposes and does not run the snippets. +func main() { + ctx := context.Background() + model, err := gemini.NewModel(ctx, "gemini-1.5-flash", &genai.ClientConfig{}) + if err != nil { + log.Fatalf("Failed to create model: %v", err) + } + _ = model // Avoid unused variable error + + // The functions defined above are conceptual and not called directly here. + // They are intended to be used as snippets in documentation. +} From b21077c2ea5a75d912cc166a5415ff86121e9df7 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 15 Oct 2025 21:56:12 -0400 Subject: [PATCH 078/125] Fixed callback signature --- docs/context/index.md | 5 +- examples/go/snippets/context/main.go | 320 +++++++++++++++++++++++---- 2 files changed, 272 insertions(+), 53 deletions(-) diff --git a/docs/context/index.md b/docs/context/index.md index b2ef0b4e..1a6ef943 100644 --- a/docs/context/index.md +++ b/docs/context/index.md @@ -436,9 +436,6 @@ You'll frequently need to read information stored within the context. ```go --8<-- "examples/go/snippets/context/main.go:accessing_state_tool" - ``` - - ```go --8<-- "examples/go/snippets/context/main.go:accessing_state_callback" ``` @@ -520,7 +517,7 @@ You'll frequently need to read information stored within the context. === "Go" ```go - --8<-- "examples/go/snippets/context/main.go:accessing_user_content_agent" + --8<-- "examples/go/snippets/context/main.go:accessing_initial_user_input" ``` ### Managing State diff --git a/examples/go/snippets/context/main.go b/examples/go/snippets/context/main.go index 87276daa..240d8b1b 100644 --- a/examples/go/snippets/context/main.go +++ b/examples/go/snippets/context/main.go @@ -7,21 +7,65 @@ import ( "log" "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" "google.golang.org/adk/model" "google.golang.org/adk/model/gemini" + "google.golang.org/adk/runner" "google.golang.org/adk/session" "google.golang.org/adk/tool" "google.golang.org/genai" ) // --- Conceptual Snippets for adk-docs/docs/context/index.md --- +const ( + modelName = "gemini-2.5-flash" + appName = "context_doc_app" + userID = "test_user_123" +) + +// Generic helper to run a single scenario. +func runScenario(ctx context.Context, r *runner.Runner, sessionService session.Service, appName, sessionID string, initialState map[string]any, prompt string) { + log.Printf("Running scenario for session: %s, initial state: %v", sessionID, initialState) + sessionResp, err := sessionService.Create(ctx, &session.CreateRequest{AppName: appName, UserID: userID, SessionID: sessionID, State: initialState}) + if err != nil { + log.Fatalf("FATAL: Failed to create session: %v", err) + } + + input := genai.NewContentFromText(prompt, genai.RoleUser) + events := r.Run(ctx, sessionResp.Session.UserID(), sessionResp.Session.ID(), input, &agent.RunConfig{}) + for event, err := range events { + if err != nil { + log.Printf("ERROR during agent execution: %v", err) + return + } + + // Print only the final output from the agent. + if event.LLMResponse != nil && event.LLMResponse.Content != nil && len(event.LLMResponse.Content.Parts) > 0 { + fmt.Printf("Final Output for %s: [%s] %s\n", sessionID, event.Author, event.LLMResponse.Content.Parts[0].Text) + } else { + log.Printf("Final response for %s received, but it has no content to display.", sessionID) + } + } +} // --8<-- [start:invocation_context_agent] // Pseudocode: Agent implementation receiving InvocationContext type MyAgent struct { - agent.Agent // Embed the agent.Agent interface to satisfy it. } +func (a *MyAgent) Run(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] { + return func(yield func(*session.Event, error) bool) { + // Direct access example + agentName := ctx.Agent().Name() + sessionID := ctx.Session().ID() + fmt.Printf("Agent %s running in session %s for invocation %s\n", agentName, sessionID, ctx.InvocationID()) + // ... agent logic using ctx ... + yield(&session.Event{Author: agentName}, nil) + } +} + +// --8<-- [end:invocation_context_agent] + // NewMyAgent creates a new MyAgent. func NewMyAgent() (agent.Agent, error) { a := &MyAgent{} @@ -34,22 +78,27 @@ func NewMyAgent() (agent.Agent, error) { if err != nil { return nil, err } - a.Agent = baseAgent - return a, nil + + return baseAgent, nil } -func (a *MyAgent) Run(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] { - return func(yield func(*session.Event, error) bool) { - // Direct access example - agentName := ctx.Agent().Name() - sessionID := ctx.Session().ID() - fmt.Printf("Agent %s running in session %s for invocation %s\n", agentName, sessionID, ctx.InvocationID()) - // ... agent logic using ctx ... - yield(&session.Event{Author: a.Name()}, nil) + +func runMyAgent() { + ctx := context.Background() + + testAgent, err := NewMyAgent() + if err != nil { + log.Fatalf("FATAL: Failed to create agent: %v", err) } -} -// --8<-- [end:invocation_context_agent] + sessionService := session.InMemoryService() + r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) + if err != nil { + log.Fatalf("FATAL: Failed to create runner: %v", err) + } + + runScenario(ctx, r, sessionService, appName, "session", nil, "Hello, world!") +} // --8<-- [start:readonly_context_instruction] // Pseudocode: Instruction provider receiving ReadonlyContext @@ -59,7 +108,7 @@ func myInstructionProvider(ctx agent.ReadonlyContext) (string, error) { if err != nil { userTier = "standard" // Default value } - // ctx.State().Set("new_key", "value") // This would not be possible as State() is read-only. + // ctx.ReadonlyState() has no Set method since State() is read-only. return fmt.Sprintf("Process the request for a %v user.", userTier), nil } @@ -84,37 +133,93 @@ func myBeforeModelCb(ctx agent.CallbackContext, req *model.LLMRequest) (*model.L return nil, nil // Allow model call to proceed } +func runBeforeAgentCallbackCheck() { + ctx := context.Background() + geminiModel, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + log.Fatalf("FATAL: Failed to create model: %v", err) + } + + // 3. Register the callback in the agent configuration. + llmCfg := llmagent.Config{ + Name: "agent", + BeforeModel: []llmagent.BeforeModelCallback{myBeforeModelCb}, + Model: geminiModel, + Instruction: "You are an assistant.", + } + testAgent, err := llmagent.New(llmCfg) + if err != nil { + log.Fatalf("FATAL: Failed to create agent: %v", err) + } + + sessionService := session.InMemoryService() + r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) + if err != nil { + log.Fatalf("FATAL: Failed to create runner: %v", err) + } + + runScenario(ctx, r, sessionService, appName, "session", nil, "Hello, world!") +} + // --8<-- [end:callback_context_callback] // --8<-- [start:tool_context_tool] // Pseudocode: Tool function receiving ToolContext type searchExternalAPIArgs struct { - Query string `json:"query"` + Query string } -func searchExternalAPI(tc tool.Context, args searchExternalAPIArgs) (map[string]any, error) { +type searchExternalAPIResults struct { + Result string + Status string +} + +func searchExternalAPI(tc tool.Context, input searchExternalAPIArgs) searchExternalAPIResults { apiKey, err := tc.State().Get("api_key") if err != nil || apiKey == "" { // In a real scenario, you would define and request credentials here. // This is a conceptual placeholder. - return map[string]any{"status": "Auth Required"}, nil + return searchExternalAPIResults{Status: "Auth Required"} } // Use the API key... - fmt.Printf("Tool executing for query '%s' using API key. Invocation: %s\n", args.Query, tc.InvocationID()) + fmt.Printf("Tool executing for query '%s' using API key. Invocation: %s\n", input.Query, tc.InvocationID()) // Optionally search memory or list artifacts - // relevantDocs, _ := tc.Memory().Search(fmt.Sprintf("info related to %s", args.Query)) + // relevantDocs, _ := tc.SearchMemory(tc, "info related to %s", input.Query)) // availableFiles, _ := tc.Artifacts().List() - return map[string]any{"result": fmt.Sprintf("Data for %s fetched.", args.Query)}, nil + return searchExternalAPIResults{Result: fmt.Sprintf("Data for %s fetched.", input.Query)} } // --8<-- [end:tool_context_tool] +func runSearchExternalAPIExample() { + myTool, err := tool.NewFunctionTool( + tool.FunctionToolConfig{ + Name: "search_external_api", + Description: "Searches an external API using a query string.", + }, + searchExternalAPI) + + if err != nil { + log.Fatal(err) + } + fmt.Printf("Tool created: %s\n", myTool.Name()) +} + // --8<-- [start:accessing_state_tool] // Pseudocode: In a Tool function -func myTool(tc tool.Context) error { +type toolArgs struct { + // Define tool-specific arguments here +} + +type toolResults struct { + // Define tool-specific results here +} + +// Example tool function demonstrating state access +func myTool(tc tool.Context, input toolArgs) toolResults { userPref, err := tc.State().Get("user_display_preference") if err != nil { userPref = "default_mode" @@ -126,39 +231,141 @@ func myTool(tc tool.Context) error { } fmt.Printf("Using API endpoint: %v\n", apiEndpoint) // ... rest of tool logic ... - return nil + return toolResults{} } // --8<-- [end:accessing_state_tool] +func runMyToolExample() { + myToolTool, err := tool.NewFunctionTool( + tool.FunctionToolConfig{ + Name: "my_tool", + Description: "A tool for doing something.", + }, + myTool) + + if err != nil { + log.Fatal(err) + } + + fmt.Printf("Tool created: %s\n", myToolTool.Name()) +} + // --8<-- [start:accessing_state_callback] // Pseudocode: In a Callback function -func myCallback(ctx agent.CallbackContext) error { +func myCallback(ctx agent.CallbackContext, event *session.Event, err error) (*genai.Content, error) { lastToolResult, err := ctx.State().Get("temp:last_api_result") // Read temporary state if err == nil { fmt.Printf("Found temporary result from last tool: %v\n", lastToolResult) + } else { + fmt.Println("No temporary result found.") } // ... callback logic ... - return nil -} + return nil, nil +} // --8<-- [end:accessing_state_callback] +func runMyCallbackExample() { + log.Println("\n--- Running Accessing State (Callback) Example ---") + ctx := context.Background() + geminiModel, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + log.Fatalf("FATAL: Failed to create model: %v", err) + } + + // Register myCallback as an AfterAgentCallback. + llmCfg := llmagent.Config{ + Name: "callbackAgent", + AfterAgent: []agent.AfterAgentCallback{myCallback}, + Model: geminiModel, + Instruction: "You are an assistant that does nothing.", + } + testAgent, err := llmagent.New(llmCfg) + if err != nil { + log.Fatalf("FATAL: Failed to create agent: %v", err) + } + + sessionService := session.InMemoryService() + r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) + if err != nil { + log.Fatalf("FATAL: Failed to create runner: %v", err) + } + + // Scenario 1: Run without the state key. + log.Println("Scenario 1: State key is NOT present.") + runScenario(ctx, r, sessionService, appName, "callback_session_1", nil, "Trigger callback") + + // Scenario 2: Run with the state key. + log.Println("Scenario 2: State key IS present.") + initialState := map[string]any{"temp:last_api_result": "Success from previous step"} + runScenario(ctx, r, sessionService, appName, "callback_session_2", initialState, "Trigger callback again") +} + // --8<-- [start:accessing_ids] // Pseudocode: In any context (ToolContext shown) -func logToolUsage(tc tool.Context) { +type logToolUsageArgs struct{} +type logToolUsageResult struct { + Status string +} + +func logToolUsage(tc tool.Context, args logToolUsageArgs) logToolUsageResult { agentName := tc.AgentName() invID := tc.InvocationID() - // funcCallID := tc.FunctionCallID() // This method may have been removed or changed. + funcCallID := tc.FunctionCallID() - fmt.Printf("Log: Invocation=%s, Agent=%s - Tool Executed.\n", invID, agentName) + fmt.Printf("Log: Invocation=%s, Agent=%s, FunctionCallID=%s - Tool Executed.\n", invID, agentName, funcCallID) + return logToolUsageResult{Status: "Logged successfully"} } // --8<-- [end:accessing_ids] +func runAccessIdsExample() { + log.Println("\n--- Running Accessing IDs Example ---") + ctx := context.Background() + + // 1. Create the tool. + loggingTool, err := tool.NewFunctionTool( + tool.FunctionToolConfig{ + Name: "log_tool_usage", + Description: "Logs the invocation and agent details.", + }, + logToolUsage, + ) + if err != nil { + log.Fatalf("FATAL: Failed to create tool: %v", err) + } + + // 2. Create an agent with the tool. + geminiModel, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + log.Fatalf("FATAL: Failed to create model: %v", err) + } + llmCfg := llmagent.Config{ + Name: "idAgent", + Model: geminiModel, + Instruction: "You are an assistant that uses the logging tool.", + Tools: []tool.Tool{loggingTool}, + } + testAgent, err := llmagent.New(llmCfg) + if err != nil { + log.Fatalf("FATAL: Failed to create agent: %v", err) + } + + // 3. Set up runner and session. + sessionService := session.InMemoryService() + r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) + if err != nil { + log.Fatalf("FATAL: Failed to create runner: %v", err) + } + + // 4. Run a scenario that will trigger the tool. + runScenario(ctx, r, sessionService, appName, "ids_session", nil, "Please log the current usage.") +} + // --8<-- [start:accessing_user_content_agent] // Pseudocode: In a Callback -func checkInitialIntent(ctx agent.CallbackContext) { +func checkInitialIntent(ctx agent.CallbackContext) (*genai.Content, error) { initialText := "N/A" userContent := ctx.UserContent() if userContent != nil && len(userContent.Parts) > 0 { @@ -170,22 +377,39 @@ func checkInitialIntent(ctx agent.CallbackContext) { } } fmt.Printf("This invocation started with user input: '%s'\n", initialText) + return nil, nil // No modification to the content } -// Pseudocode: In an Agent's Run method -func (a *MyAgent) RunWithUserContent(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] { - return func(yield func(*session.Event, error) bool) { - userContent := ctx.UserContent() - if userContent != nil && len(userContent.Parts) > 0 { - if initialText := userContent.Parts[0].Text; initialText != "" { - fmt.Printf("Agent logic remembering initial query: %s\n", initialText) - } - } - yield(&session.Event{Author: a.Name()}, nil) +// --8<-- [end:accessing_user_content_agent] + +func runInitialIntentCheck() { + ctx := context.Background() + geminiModel, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + log.Fatalf("FATAL: Failed to create model: %v", err) + } + + // 3. Register the callback in the agent configuration. + llmCfg := llmagent.Config{ + Name: "agent", + BeforeAgent: []agent.BeforeAgentCallback{checkInitialIntent}, + Model: geminiModel, + Instruction: "You are an assistant.", } + testAgent, err := llmagent.New(llmCfg) + if err != nil { + log.Fatalf("FATAL: Failed to create agent: %v", err) + } + + sessionService := session.InMemoryService() + r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) + if err != nil { + log.Fatalf("FATAL: Failed to create runner: %v", err) + } + + runScenario(ctx, r, sessionService, appName, "session", nil, "Hello, world!") } -// --8<-- [end:accessing_user_content_agent] // --8<-- [start:passing_data_tool1] // Pseudocode: Tool 1 - Fetches user ID @@ -309,13 +533,11 @@ func checkAvailableDocs(tc tool.Context) (map[string][]string, error) { // This main function is for compilation purposes and does not run the snippets. func main() { - ctx := context.Background() - model, err := gemini.NewModel(ctx, "gemini-1.5-flash", &genai.ClientConfig{}) - if err != nil { - log.Fatalf("Failed to create model: %v", err) - } - _ = model // Avoid unused variable error - - // The functions defined above are conceptual and not called directly here. - // They are intended to be used as snippets in documentation. + runInitialIntentCheck() + runMyAgent() + runBeforeAgentCallbackCheck() + runSearchExternalAPIExample() + runMyToolExample() + runMyCallbackExample() + runAccessIdsExample() } From 3144cf48b4d9c0c2c5d4f72b305fe66ad787b477 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Thu, 16 Oct 2025 09:57:19 -0400 Subject: [PATCH 079/125] Fixed additional callbacks --- docs/context/index.md | 2 - examples/go/snippets/context/main.go | 234 +++++++++++++++++++++++++-- 2 files changed, 218 insertions(+), 18 deletions(-) diff --git a/docs/context/index.md b/docs/context/index.md index 1a6ef943..20996d2f 100644 --- a/docs/context/index.md +++ b/docs/context/index.md @@ -582,9 +582,7 @@ State is crucial for memory and data flow. When you modify state using `Callback ```go --8<-- "examples/go/snippets/context/main.go:passing_data_tool1" - ``` - ```go --8<-- "examples/go/snippets/context/main.go:passing_data_tool2" ``` diff --git a/examples/go/snippets/context/main.go b/examples/go/snippets/context/main.go index 240d8b1b..28d76290 100644 --- a/examples/go/snippets/context/main.go +++ b/examples/go/snippets/context/main.go @@ -8,6 +8,7 @@ import ( "google.golang.org/adk/agent" "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/artifact" "google.golang.org/adk/model" "google.golang.org/adk/model/gemini" "google.golang.org/adk/runner" @@ -133,6 +134,8 @@ func myBeforeModelCb(ctx agent.CallbackContext, req *model.LLMRequest) (*model.L return nil, nil // Allow model call to proceed } +// --8<-- [end:callback_context_callback] + func runBeforeAgentCallbackCheck() { ctx := context.Background() geminiModel, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) @@ -161,8 +164,6 @@ func runBeforeAgentCallbackCheck() { runScenario(ctx, r, sessionService, appName, "session", nil, "Hello, world!") } -// --8<-- [end:callback_context_callback] - // --8<-- [start:tool_context_tool] // Pseudocode: Tool function receiving ToolContext type searchExternalAPIArgs struct { @@ -410,44 +411,152 @@ func runInitialIntentCheck() { runScenario(ctx, r, sessionService, appName, "session", nil, "Hello, world!") } +// --8<-- [start:accessing_initial_user_input] +// Pseudocode: In a Callback +func logInitialUserInput(ctx agent.CallbackContext) (*genai.Content, error) { + userContent := ctx.UserContent() + if userContent != nil && len(userContent.Parts) > 0 { + if text := userContent.Parts[0].Text; text != "" { + fmt.Printf("User's initial input for this turn: '%s'\n", text) + } + } + return nil, nil // No modification +} +// --8<-- [end:accessing_initial_user_input] + +func runAccessingInitialUserInputExample() { + log.Println("\n--- Running Accessing Initial User Input Example ---") + ctx := context.Background() + geminiModel, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + log.Fatalf("FATAL: Failed to create model: %v", err) + } + + llmCfg := llmagent.Config{ + Name: "userInputLoggerAgent", + BeforeAgent: []agent.BeforeAgentCallback{logInitialUserInput}, + Model: geminiModel, + Instruction: "You are an assistant.", + } + testAgent, err := llmagent.New(llmCfg) + if err != nil { + log.Fatalf("FATAL: Failed to create agent: %v", err) + } + + sessionService := session.InMemoryService() + r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) + if err != nil { + log.Fatalf("FATAL: Failed to create runner: %v", err) + } + + runScenario(ctx, r, sessionService, appName, "user_input_session", nil, "What is the weather in London?") +} + // --8<-- [start:passing_data_tool1] // Pseudocode: Tool 1 - Fetches user ID + +type GetUserProfileArgs struct { +} + type getUserProfileResult struct { ProfileStatus string `json:"profile_status"` + Error string } -func getUserProfile(tc tool.Context) (*getUserProfileResult, error) { - userID := "user-12345" // Simulate fetching ID +func getUserProfile(tc tool.Context, input GetUserProfileArgs) getUserProfileResult { + // A random user ID for demonstration purposes + userID := "random_user_456" + // Save the ID to state for the next tool if err := tc.State().Set("temp:current_user_id", userID); err != nil { - return nil, err + return getUserProfileResult{Error: "Failed to set user ID in state"} } - return &getUserProfileResult{ProfileStatus: "ID generated"}, nil + return getUserProfileResult{ProfileStatus: "ID generated"} } // --8<-- [end:passing_data_tool1] // --8<-- [start:passing_data_tool2] // Pseudocode: Tool 2 - Uses user ID from state +type GetUserOrdersArgs struct { +} + type getUserOrdersResult struct { - Orders []string `json:"orders,omitempty"` - Error string `json:"error,omitempty"` + Orders []string + Error string } -func getUserOrders(tc tool.Context) (*getUserOrdersResult, error) { +func getUserOrders(tc tool.Context, input GetUserOrdersArgs) getUserOrdersResult { userID, err := tc.State().Get("temp:current_user_id") if err != nil { - return &getUserOrdersResult{Error: "User ID not found in state"}, nil + return getUserOrdersResult{Error: "User ID not found in state"} } fmt.Printf("Fetching orders for user ID: %v\n", userID) // ... logic to fetch orders using user_id ... - return &getUserOrdersResult{Orders: []string{"order123", "order456"}}, nil + return getUserOrdersResult{Orders: []string{"order123", "order456"}} } // --8<-- [end:passing_data_tool2] +func runPassingDataExample() { + log.Println("\n--- Running Passing Data Between Tools Example ---") + ctx := context.Background() + + // 1. Create the tools. + getUserProfileTool, err := tool.NewFunctionTool( + tool.FunctionToolConfig{ + Name: "get_user_profile", + Description: "Gets the profile for a user.", + }, + getUserProfile, + ) + if err != nil { + log.Fatalf("FATAL: Failed to create getUserProfile tool: %v", err) + } + getUserOrdersTool, err := tool.NewFunctionTool( + tool.FunctionToolConfig{ + Name: "get_user_orders", + Description: "Gets the orders for a user.", + }, + getUserOrders, + ) + if err != nil { + log.Fatalf("FATAL: Failed to create getUserOrders tool: %v", err) + } + + // 2. Create an agent with the tools. + geminiModel, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + log.Fatalf("FATAL: Failed to create model: %v", err) + } + llmCfg := llmagent.Config{ + Name: "dataPassingAgent", + Model: geminiModel, + Instruction: "You are an assistant that first gets the user profile, then gets their orders.", + Tools: []tool.Tool{getUserProfileTool, getUserOrdersTool}, + } + testAgent, err := llmagent.New(llmCfg) + if err != nil { + log.Fatalf("FATAL: Failed to create agent: %v", err) + } + + // 3. Set up runner and session. + sessionService := session.InMemoryService() + initialState := map[string]any{ + "temp:current_user_id": userID, + } + + r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) + if err != nil { + log.Fatalf("FATAL: Failed to create runner: %v", err) + } + + // 4. Run a scenario that will trigger the tools. + runScenario(ctx, r, sessionService, appName, "passing_data_session", initialState, "Get my orders.") +} + // --8<-- [start:updating_preferences] // Pseudocode: Tool or Callback identifies a preference type setUserPreferenceArgs struct { @@ -485,22 +594,33 @@ func saveDocumentReference(ctx agent.CallbackContext, filePath string) error { // --8<-- [end:artifacts_save_ref] +func runSaveArtifactReferenceExample() { +} + // --8<-- [start:artifacts_summarize] // Pseudocode: In the Summarizer tool function -func summarizeDocumentTool(tc tool.Context) (map[string]string, error) { +type summarizeDocumentArgs struct { +} + +type summarizeDocumentResult struct { + Summary string + Error string +} + +func summarizeDocumentTool(tc tool.Context, input summarizeDocumentArgs) summarizeDocumentResult { artifactName, err := tc.State().Get("temp:doc_artifact_name") if err != nil { - return map[string]string{"error": "Document artifact name not found in state."}, nil + return summarizeDocumentResult{Error: "No document artifact name found in state"} } // 1. Load the artifact part containing the path/URI artifactPart, err := tc.Artifacts().Load(artifactName.(string)) if err != nil { - return nil, err + return summarizeDocumentResult{Error: err.Error()} } if artifactPart.Text == "" { - return map[string]string{"error": "Could not load artifact or artifact has no text path."}, nil + return summarizeDocumentResult{Error: "Could not load artifact or artifact has no text path."} } filePath := artifactPart.Text fmt.Printf("Loaded document reference: %s\n", filePath) @@ -513,7 +633,7 @@ func summarizeDocumentTool(tc tool.Context) (map[string]string, error) { // 3. Summarize the content summary := "Summary of content from " + filePath // Placeholder - return map[string]string{"summary": summary}, nil + return summarizeDocumentResult{Summary: summary} } // --8<-- [end:artifacts_summarize] @@ -531,6 +651,84 @@ func checkAvailableDocs(tc tool.Context) (map[string][]string, error) { // --8<-- [end:artifacts_list] +// Adapt the saveDocumentReference callback into a tool for this example. +type saveDocRefArgs struct { + FilePath string +} + +type saveDocRefResult struct { + Status string + Error string +} + +func saveDocRefToolFunc(tc tool.Context, args saveDocRefArgs) saveDocRefResult { + artifactPart := genai.NewPartFromText(args.FilePath) + err := tc.Artifacts().Save("document_to_summarize.txt", *artifactPart) + if err != nil { + return saveDocRefResult{"", err.Error()} + } + fmt.Printf("Saved document reference '%s' as artifact\n", args.FilePath) + if err := tc.State().Set("temp:doc_artifact_name", "document_to_summarize.txt"); err != nil { + return saveDocRefResult{"", err.Error()} + } + return saveDocRefResult{"Reference saved", ""} +} + +func runArtifactsExample() { + log.Println("\n--- Running Artifacts Example ---") + ctx := context.Background() + + + // 1. Create the tools. + saveRefTool, err := tool.NewFunctionTool( + tool.FunctionToolConfig{ + Name: "save_document_reference", + Description: "Saves a reference to a document path as an artifact.", + }, + saveDocRefToolFunc, + ) + if err != nil { + log.Fatalf("FATAL: Failed to create saveRefTool: %v", err) + } + summarizeTool, err := tool.NewFunctionTool( + tool.FunctionToolConfig{ + Name: "summarize_document", + Description: "Summarizes the document stored in artifacts.", + }, + summarizeDocumentTool, + ) + if err != nil { + log.Fatalf("FATAL: Failed to create summarizeTool: %v", err) + } + + // 2. Create an agent with the tools. + geminiModel, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + log.Fatalf("FATAL: Failed to create model: %v", err) + } + llmCfg := llmagent.Config{ + Name: "artifactAgent", + Model: geminiModel, + Instruction: "First save the document reference, then summarize it.", + Tools: []tool.Tool{saveRefTool, summarizeTool}, + } + testAgent, err := llmagent.New(llmCfg) + if err != nil { + log.Fatalf("FATAL: Failed to create agent: %v", err) + } + + // 3. Set up runner and session. + sessionService := session.InMemoryService() + artifactService := artifact.InMemoryService() + r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService, ArtifactService: artifactService }) + if err != nil { + log.Fatalf("FATAL: Failed to create runner: %v", err) + } + + // 4. Run a scenario that will trigger the tools. + runScenario(ctx, r, sessionService, appName, "artifacts_session", nil, "Save the doc at 'gs://my-bucket/report.pdf' and then summarize it.") +} + // This main function is for compilation purposes and does not run the snippets. func main() { runInitialIntentCheck() @@ -540,4 +738,8 @@ func main() { runMyToolExample() runMyCallbackExample() runAccessIdsExample() + runAccessingInitialUserInputExample() + runPassingDataExample() + runArtifactsExample() + runSaveArtifactReferenceExample } From 21c789602520666f0659a12e6a15e7bc6b009416 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Thu, 16 Oct 2025 14:06:43 -0400 Subject: [PATCH 080/125] Added remaining snippets --- examples/go/snippets/context/main.go | 80 +++++++++++++++++++--------- 1 file changed, 54 insertions(+), 26 deletions(-) diff --git a/examples/go/snippets/context/main.go b/examples/go/snippets/context/main.go index 28d76290..da0061ed 100644 --- a/examples/go/snippets/context/main.go +++ b/examples/go/snippets/context/main.go @@ -560,47 +560,72 @@ func runPassingDataExample() { // --8<-- [start:updating_preferences] // Pseudocode: Tool or Callback identifies a preference type setUserPreferenceArgs struct { - Preference string `json:"preference"` - Value string `json:"value"` + Preference string + Value string } -func setUserPreference(tc tool.Context, args setUserPreferenceArgs) (map[string]string, error) { +type setUserPreferenceResult struct { + Status string +} + +func setUserPreference(tc tool.Context, args setUserPreferenceArgs) setUserPreferenceResult { // Use 'user:' prefix for user-level state (if using a persistent SessionService) stateKey := fmt.Sprintf("user:%s", args.Preference) if err := tc.State().Set(stateKey, args.Value); err != nil { - return nil, err + return setUserPreferenceResult{Status: "Failed to set preference"} } fmt.Printf("Set user preference '%s' to '%s'\n", args.Preference, args.Value) - return map[string]string{"status": "Preference updated"}, nil + return setUserPreferenceResult{Status: "Preference updated"} } - // --8<-- [end:updating_preferences] -// --8<-- [start:artifacts_save_ref] -// Pseudocode: In a callback or initial tool -func saveDocumentReference(ctx agent.CallbackContext, filePath string) error { - // Assume filePath is something like "gs://my-bucket/docs/report.pdf" - // Create a Part containing the path/URI text - artifactPart := genai.NewPartFromText(filePath) - err := ctx.Artifacts().Save("document_to_summarize.txt", *artifactPart) +func runUpdatingPreferencesExample() { + log.Println("\n--- Running Updating Preferences Example ---") + ctx := context.Background() + + // 1. Create the tool. + setPrefTool, err := tool.NewFunctionTool( + tool.FunctionToolConfig{ + Name: "set_user_preference", + Description: "Sets a user's preference in the system.", + }, + setUserPreference, + ) if err != nil { - fmt.Printf("Error saving artifact: %v\n", err) - return err + log.Fatalf("FATAL: Failed to create setPrefTool: %v", err) } - fmt.Printf("Saved document reference '%s' as artifact\n", filePath) - // Store the filename in state if needed by other tools - return ctx.State().Set("temp:doc_artifact_name", "document_to_summarize.txt") -} -// --8<-- [end:artifacts_save_ref] + // 2. Create an agent with the tool. + geminiModel, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + log.Fatalf("FATAL: Failed to create model: %v", err) + } + llmCfg := llmagent.Config{ + Name: "preferenceAgent", + Model: geminiModel, + Instruction: "You are an assistant that helps users set their preferences.", + Tools: []tool.Tool{setPrefTool}, + } + testAgent, err := llmagent.New(llmCfg) + if err != nil { + log.Fatalf("FATAL: Failed to create agent: %v", err) + } -func runSaveArtifactReferenceExample() { + // 3. Set up runner and session. + sessionService := session.InMemoryService() + r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) + if err != nil { + log.Fatalf("FATAL: Failed to create runner: %v", err) + } + + // 4. Run a scenario that will trigger the tool. + runScenario(ctx, r, sessionService, appName, "preferences_session", nil, "Please set my theme preference to dark_mode.") } + // --8<-- [start:artifacts_summarize] // Pseudocode: In the Summarizer tool function -type summarizeDocumentArgs struct { -} +type summarizeDocumentToolArgs struct{} type summarizeDocumentResult struct { Summary string @@ -651,6 +676,7 @@ func checkAvailableDocs(tc tool.Context) (map[string][]string, error) { // --8<-- [end:artifacts_list] +// --8<-- [start:artifacts_save_ref] // Adapt the saveDocumentReference callback into a tool for this example. type saveDocRefArgs struct { FilePath string @@ -661,7 +687,7 @@ type saveDocRefResult struct { Error string } -func saveDocRefToolFunc(tc tool.Context, args saveDocRefArgs) saveDocRefResult { +func saveDocRef(tc tool.Context, args saveDocRefArgs) saveDocRefResult { artifactPart := genai.NewPartFromText(args.FilePath) err := tc.Artifacts().Save("document_to_summarize.txt", *artifactPart) if err != nil { @@ -674,6 +700,8 @@ func saveDocRefToolFunc(tc tool.Context, args saveDocRefArgs) saveDocRefResult { return saveDocRefResult{"Reference saved", ""} } +// --8<-- [end:artifacts_save_ref] + func runArtifactsExample() { log.Println("\n--- Running Artifacts Example ---") ctx := context.Background() @@ -685,7 +713,7 @@ func runArtifactsExample() { Name: "save_document_reference", Description: "Saves a reference to a document path as an artifact.", }, - saveDocRefToolFunc, + saveDocRef, ) if err != nil { log.Fatalf("FATAL: Failed to create saveRefTool: %v", err) @@ -741,5 +769,5 @@ func main() { runAccessingInitialUserInputExample() runPassingDataExample() runArtifactsExample() - runSaveArtifactReferenceExample + runUpdatingPreferencesExample() } From 758c7d9186308e9a60db54a27b2634a198b15bd0 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Thu, 16 Oct 2025 15:05:53 -0400 Subject: [PATCH 081/125] Added conceptual runner example --- docs/context/index.md | 71 ++++++++++++++++++---------- examples/go/snippets/context/main.go | 39 +++++++++++++++ 2 files changed, 86 insertions(+), 24 deletions(-) diff --git a/docs/context/index.md b/docs/context/index.md index 20996d2f..e6d9952b 100644 --- a/docs/context/index.md +++ b/docs/context/index.md @@ -78,29 +78,7 @@ The central piece holding all this information together for a single, complete u === "Go" ```go - // Conceptual Pseudocode: How the framework provides context (Internal Logic) - - // runner := runner.New(runner.Config{Agent: myRootAgent, SessionService: ...}) - // userMsg := &genai.Content{...} - // session, _ := sessionService.GetOrCreate(...) - - // --- Inside runner.Run(...) --- - // 1. Framework creates the main context for this specific run - // invocationContext := agent.NewInvocationContext(agent.InvocationContextConfig{ - // InvocationID: "unique-id-for-this-run", - // Session: session, - // UserContent: userMsg, - // Agent: myRootAgent, - // // ... other necessary fields ... - // }) - // - // 2. Framework calls the agent's run method, passing the context. - // for range myRootAgent.Run(invocationContext) { - // // ... - // } - // --- End Internal Logic --- - // - // As a developer, you work with the context objects provided in method arguments. + --8<-- "examples/go/snippets/context/main.go:conceptual_runner_example" ``` ## The Different types of Context @@ -194,6 +172,11 @@ While `InvocationContext` acts as the comprehensive internal container, ADK prov === "Go" ```go + import ( + "google.golang.org/adk/agent" + "google.golang.org/adk/session" + ) + --8<-- "examples/go/snippets/context/main.go:invocation_context_agent" ``` @@ -232,6 +215,8 @@ While `InvocationContext` acts as the comprehensive internal container, ADK prov === "Go" ```go + import "google.golang.org/adk/agent" + --8<-- "examples/go/snippets/context/main.go:readonly_context_instruction" ``` @@ -287,6 +272,11 @@ While `InvocationContext` acts as the comprehensive internal container, ADK prov === "Go" ```go + import ( + "google.golang.org/adk/agent" + "google.golang.org/adk/model" + ) + --8<-- "examples/go/snippets/context/main.go:callback_context_callback" ``` @@ -360,6 +350,8 @@ While `InvocationContext` acts as the comprehensive internal container, ADK prov === "Go" ```go + import "google.golang.org/adk/tool" + --8<-- "examples/go/snippets/context/main.go:tool_context_tool" ``` @@ -435,7 +427,16 @@ You'll frequently need to read information stored within the context. === "Go" ```go + import "google.golang.org/adk/tool" + --8<-- "examples/go/snippets/context/main.go:accessing_state_tool" + ```go + import ( + "google.golang.org/adk/agent" + "google.golang.org/adk/session" + "google.golang.org/genai" + ) + --8<-- "examples/go/snippets/context/main.go:accessing_state_callback" ``` @@ -472,6 +473,8 @@ You'll frequently need to read information stored within the context. === "Go" ```go + import "google.golang.org/adk/tool" + --8<-- "examples/go/snippets/context/main.go:accessing_ids" ``` @@ -517,6 +520,11 @@ You'll frequently need to read information stored within the context. === "Go" ```go + import ( + "google.golang.org/adk/agent" + "google.golang.org/genai" + ) + --8<-- "examples/go/snippets/context/main.go:accessing_initial_user_input" ``` @@ -581,8 +589,12 @@ State is crucial for memory and data flow. When you modify state using `Callback === "Go" ```go + import "google.golang.org/adk/tool" + --8<-- "examples/go/snippets/context/main.go:passing_data_tool1" - + ```go + import "google.golang.org/adk/tool" + --8<-- "examples/go/snippets/context/main.go:passing_data_tool2" ``` @@ -620,6 +632,8 @@ State is crucial for memory and data flow. When you modify state using `Callback === "Go" ```go + import "google.golang.org/adk/tool" + --8<-- "examples/go/snippets/context/main.go:updating_preferences" ``` @@ -688,6 +702,11 @@ Use artifacts to handle files or large data blobs associated with the session. C === "Go" ```go + import ( + "google.golang.org/adk/tool" + "google.golang.org/genai" + ) + --8<-- "examples/go/snippets/context/main.go:artifacts_save_ref" ``` @@ -805,6 +824,8 @@ Use artifacts to handle files or large data blobs associated with the session. C === "Go" ```go + import "google.golang.org/adk/tool" + --8<-- "examples/go/snippets/context/main.go:artifacts_summarize" ``` @@ -845,6 +866,8 @@ Use artifacts to handle files or large data blobs associated with the session. C === "Go" ```go + import "google.golang.org/adk/tool" + --8<-- "examples/go/snippets/context/main.go:artifacts_list" ``` diff --git a/examples/go/snippets/context/main.go b/examples/go/snippets/context/main.go index da0061ed..996a7bdc 100644 --- a/examples/go/snippets/context/main.go +++ b/examples/go/snippets/context/main.go @@ -49,6 +49,45 @@ func runScenario(ctx context.Context, r *runner.Runner, sessionService session.S } } +// --8<-- [start:conceptual_runner_example] +// Conceptual Pseudocode: How the framework provides context (Internal Logic) +func conceptualRunnerExample(ctx context.Context, agent agent.Agent) { + // The runner is the main entry point for the ADK. + r, err := runner.New(runner.Config{ + AppName: "my-app", + Agent: agent, + SessionService: session.InMemoryService(), + }) + if err != nil { + log.Fatalf("Failed to create runner: %v", err) + } + + // A session holds the state of a conversation. + session, err := r.SessionService().Create(ctx, &session.CreateRequest{ + AppName: r.AppName(), + UserID: "user123", + }) + if err != nil { + log.Fatalf("Failed to create session: %v", err) + } + + // --- Inside runner.Run(...) --- + // The runner takes the user's input and session details, creates an + // InvocationContext, and passes it to the agent's Run method. + userInput := genai.NewContentFromText("Hello, agent!", genai.RoleUser) + events := r.Run(ctx, session.Session.UserID(), session.Session.ID(), userInput, &agent.RunConfig{}) + + // As a developer, you work with the events returned by the agent. + for event, err := range events { + if err != nil { + log.Printf("Error during run: %v", err) + break + } + fmt.Printf("Event from agent %q\n", event.Author) + } +} +// --8<-- [end:conceptual_runner_example] + // --8<-- [start:invocation_context_agent] // Pseudocode: Agent implementation receiving InvocationContext type MyAgent struct { From 34497948f8c296f78d3ede5b58be1aa826a3a3ee Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Thu, 16 Oct 2025 15:25:22 -0400 Subject: [PATCH 082/125] Removed duplicate code --- examples/go/snippets/context/main.go | 165 ++++++++++++++------------- 1 file changed, 84 insertions(+), 81 deletions(-) diff --git a/examples/go/snippets/context/main.go b/examples/go/snippets/context/main.go index 996a7bdc..1ca54863 100644 --- a/examples/go/snippets/context/main.go +++ b/examples/go/snippets/context/main.go @@ -25,8 +25,23 @@ const ( ) // Generic helper to run a single scenario. -func runScenario(ctx context.Context, r *runner.Runner, sessionService session.Service, appName, sessionID string, initialState map[string]any, prompt string) { +func runScenario(ctx context.Context, agentToRun agent.Agent, sessionID string, initialState map[string]any, prompt string) { log.Printf("Running scenario for session: %s, initial state: %v", sessionID, initialState) + + sessionService := session.InMemoryService() + artifactService := artifact.InMemoryService() + rcfg := runner.Config{ + AppName: appName, + Agent: agentToRun, + SessionService: sessionService, + ArtifactService: artifactService, + } + + r, err := runner.New(rcfg) + if err != nil { + log.Fatalf("FATAL: Failed to create runner: %v", err) + } + sessionResp, err := sessionService.Create(ctx, &session.CreateRequest{AppName: appName, UserID: userID, SessionID: sessionID, State: initialState}) if err != nil { log.Fatalf("FATAL: Failed to create session: %v", err) @@ -51,20 +66,22 @@ func runScenario(ctx context.Context, r *runner.Runner, sessionService session.S // --8<-- [start:conceptual_runner_example] // Conceptual Pseudocode: How the framework provides context (Internal Logic) -func conceptualRunnerExample(ctx context.Context, agent agent.Agent) { +func conceptualRunnerExample(ctx context.Context, a agent.Agent) { // The runner is the main entry point for the ADK. r, err := runner.New(runner.Config{ AppName: "my-app", - Agent: agent, + Agent: a, SessionService: session.InMemoryService(), }) if err != nil { log.Fatalf("Failed to create runner: %v", err) } + sessionService := session.InMemoryService() + // A session holds the state of a conversation. - session, err := r.SessionService().Create(ctx, &session.CreateRequest{ - AppName: r.AppName(), + session, err := sessionService.Create(ctx, &session.CreateRequest{ + AppName: appName, UserID: "user123", }) if err != nil { @@ -131,13 +148,7 @@ func runMyAgent() { log.Fatalf("FATAL: Failed to create agent: %v", err) } - sessionService := session.InMemoryService() - r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) - if err != nil { - log.Fatalf("FATAL: Failed to create runner: %v", err) - } - - runScenario(ctx, r, sessionService, appName, "session", nil, "Hello, world!") + runScenario(ctx, testAgent, "session", nil, "Hello, world!") } // --8<-- [start:readonly_context_instruction] @@ -194,13 +205,7 @@ func runBeforeAgentCallbackCheck() { log.Fatalf("FATAL: Failed to create agent: %v", err) } - sessionService := session.InMemoryService() - r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) - if err != nil { - log.Fatalf("FATAL: Failed to create runner: %v", err) - } - - runScenario(ctx, r, sessionService, appName, "session", nil, "Hello, world!") + runScenario(ctx, testAgent, "session", nil, "Hello, world!") } // --8<-- [start:tool_context_tool] @@ -302,7 +307,7 @@ func myCallback(ctx agent.CallbackContext, event *session.Event, err error) (*ge } // ... callback logic ... return nil, nil -} +} // --8<-- [end:accessing_state_callback] @@ -326,20 +331,14 @@ func runMyCallbackExample() { log.Fatalf("FATAL: Failed to create agent: %v", err) } - sessionService := session.InMemoryService() - r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) - if err != nil { - log.Fatalf("FATAL: Failed to create runner: %v", err) - } - // Scenario 1: Run without the state key. log.Println("Scenario 1: State key is NOT present.") - runScenario(ctx, r, sessionService, appName, "callback_session_1", nil, "Trigger callback") + runScenario(ctx, testAgent, "callback_session_1", nil, "Trigger callback") // Scenario 2: Run with the state key. log.Println("Scenario 2: State key IS present.") initialState := map[string]any{"temp:last_api_result": "Success from previous step"} - runScenario(ctx, r, sessionService, appName, "callback_session_2", initialState, "Trigger callback again") + runScenario(ctx, testAgent, "callback_session_2", initialState, "Trigger callback again") } // --8<-- [start:accessing_ids] @@ -392,15 +391,8 @@ func runAccessIdsExample() { log.Fatalf("FATAL: Failed to create agent: %v", err) } - // 3. Set up runner and session. - sessionService := session.InMemoryService() - r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) - if err != nil { - log.Fatalf("FATAL: Failed to create runner: %v", err) - } - - // 4. Run a scenario that will trigger the tool. - runScenario(ctx, r, sessionService, appName, "ids_session", nil, "Please log the current usage.") + // 3. Run a scenario that will trigger the tool. + runScenario(ctx, testAgent, "ids_session", nil, "Please log the current usage.") } // --8<-- [start:accessing_user_content_agent] @@ -441,13 +433,7 @@ func runInitialIntentCheck() { log.Fatalf("FATAL: Failed to create agent: %v", err) } - sessionService := session.InMemoryService() - r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) - if err != nil { - log.Fatalf("FATAL: Failed to create runner: %v", err) - } - - runScenario(ctx, r, sessionService, appName, "session", nil, "Hello, world!") + runScenario(ctx, testAgent, "session", nil, "Hello, world!") } // --8<-- [start:accessing_initial_user_input] @@ -482,13 +468,7 @@ func runAccessingInitialUserInputExample() { log.Fatalf("FATAL: Failed to create agent: %v", err) } - sessionService := session.InMemoryService() - r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) - if err != nil { - log.Fatalf("FATAL: Failed to create runner: %v", err) - } - - runScenario(ctx, r, sessionService, appName, "user_input_session", nil, "What is the weather in London?") + runScenario(ctx, testAgent, "user_input_session", nil, "What is the weather in London?") } @@ -499,7 +479,7 @@ type GetUserProfileArgs struct { } type getUserProfileResult struct { - ProfileStatus string `json:"profile_status"` + ProfileStatus string Error string } @@ -582,18 +562,12 @@ func runPassingDataExample() { } // 3. Set up runner and session. - sessionService := session.InMemoryService() initialState := map[string]any{ "temp:current_user_id": userID, } - r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) - if err != nil { - log.Fatalf("FATAL: Failed to create runner: %v", err) - } - // 4. Run a scenario that will trigger the tools. - runScenario(ctx, r, sessionService, appName, "passing_data_session", initialState, "Get my orders.") + runScenario(ctx, testAgent, "passing_data_session", initialState, "Get my orders.") } // --8<-- [start:updating_preferences] @@ -650,21 +624,14 @@ func runUpdatingPreferencesExample() { log.Fatalf("FATAL: Failed to create agent: %v", err) } - // 3. Set up runner and session. - sessionService := session.InMemoryService() - r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService}) - if err != nil { - log.Fatalf("FATAL: Failed to create runner: %v", err) - } - - // 4. Run a scenario that will trigger the tool. - runScenario(ctx, r, sessionService, appName, "preferences_session", nil, "Please set my theme preference to dark_mode.") + // 3. Run a scenario that will trigger the tool. + runScenario(ctx, testAgent, "preferences_session", nil, "Please set my theme preference to dark_mode.") } // --8<-- [start:artifacts_summarize] // Pseudocode: In the Summarizer tool function -type summarizeDocumentToolArgs struct{} +type summarizeDocumentArgs struct{} type summarizeDocumentResult struct { Summary string @@ -704,13 +671,20 @@ func summarizeDocumentTool(tc tool.Context, input summarizeDocumentArgs) summari // --8<-- [start:artifacts_list] // Pseudocode: In a tool function -func checkAvailableDocs(tc tool.Context) (map[string][]string, error) { +type checkAvailableDocsArgs struct{} + +type checkAvailableDocsResult struct { + AvailableDocs []string + Error string +} + +func checkAvailableDocs(tc tool.Context, args checkAvailableDocsArgs) checkAvailableDocsResult { artifactKeys, err := tc.Artifacts().List() if err != nil { - return nil, err + return checkAvailableDocsResult{Error: err.Error()} } fmt.Printf("Available artifacts: %v\n", artifactKeys) - return map[string][]string{"available_docs": artifactKeys}, nil + return checkAvailableDocsResult{AvailableDocs: artifactKeys} } // --8<-- [end:artifacts_list] @@ -734,7 +708,7 @@ func saveDocRef(tc tool.Context, args saveDocRefArgs) saveDocRefResult { } fmt.Printf("Saved document reference '%s' as artifact\n", args.FilePath) if err := tc.State().Set("temp:doc_artifact_name", "document_to_summarize.txt"); err != nil { - return saveDocRefResult{"", err.Error()} + return saveDocRefResult{"", "Failed to set artifact name in state"} } return saveDocRefResult{"Reference saved", ""} } @@ -784,16 +758,44 @@ func runArtifactsExample() { log.Fatalf("FATAL: Failed to create agent: %v", err) } - // 3. Set up runner and session. - sessionService := session.InMemoryService() - artifactService := artifact.InMemoryService() - r, err := runner.New(runner.Config{AppName: appName, Agent: testAgent, SessionService: sessionService, ArtifactService: artifactService }) + // 3. Run a scenario that will trigger the tools. + runScenario(ctx, testAgent, "artifacts_session", nil, "Save the doc at 'gs://my-bucket/report.pdf' and then summarize it.") +} + +func runCheckAvailableDocsExample() { + log.Println("\n--- Running Check Available Docs Example ---") + ctx := context.Background() + + // 1. Create the tool. + checkDocsTool, err := tool.NewFunctionTool( + tool.FunctionToolConfig{ + Name: "check_available_docs", + Description: "Checks for available documents in artifacts.", + }, + checkAvailableDocs, + ) if err != nil { - log.Fatalf("FATAL: Failed to create runner: %v", err) + log.Fatalf("FATAL: Failed to create checkDocsTool: %v", err) } - // 4. Run a scenario that will trigger the tools. - runScenario(ctx, r, sessionService, appName, "artifacts_session", nil, "Save the doc at 'gs://my-bucket/report.pdf' and then summarize it.") + // 2. Create an agent with the tool. + geminiModel, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + log.Fatalf("FATAL: Failed to create model: %v", err) + } + llmCfg := llmagent.Config{ + Name: "docCheckerAgent", + Model: geminiModel, + Instruction: "You are an assistant that can check for available documents.", + Tools: []tool.Tool{checkDocsTool}, + } + testAgent, err := llmagent.New(llmCfg) + if err != nil { + log.Fatalf("FATAL: Failed to create agent: %v", err) + } + + // 3. Run a scenario that will trigger the tool. + runScenario(ctx, testAgent, "check_docs_session", nil, "Are there any documents available?") } // This main function is for compilation purposes and does not run the snippets. @@ -809,4 +811,5 @@ func main() { runPassingDataExample() runArtifactsExample() runUpdatingPreferencesExample() -} + runCheckAvailableDocsExample() +} \ No newline at end of file From bbeacaeaadf56e24e7ef05feb287180a1707d2d3 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Thu, 16 Oct 2025 16:11:06 -0400 Subject: [PATCH 083/125] Clean up tags and imports --- docs/context/index.md | 250 +++++++++++++-------------- examples/go/snippets/context/main.go | 82 ++++++--- 2 files changed, 179 insertions(+), 153 deletions(-) diff --git a/docs/context/index.md b/docs/context/index.md index e6d9952b..cf631c57 100644 --- a/docs/context/index.md +++ b/docs/context/index.md @@ -50,6 +50,13 @@ The central piece holding all this information together for a single, complete u # As a developer, you work with the context objects provided in method arguments. ``` +=== "Go" + + ```go + /* Conceptual Pseudocode: How the framework provides context (Internal Logic) */ + --8<-- "examples/go/snippets/context/main.go:conceptual_runner_example" + ``` + === "Java" ```java @@ -75,12 +82,6 @@ The central piece holding all this information together for a single, complete u } ``` -=== "Go" - - ```go - --8<-- "examples/go/snippets/context/main.go:conceptual_runner_example" - ``` - ## The Different types of Context While `InvocationContext` acts as the comprehensive internal container, ADK provides specialized context objects tailored to specific situations. This ensures you have the right tools and permissions for the task at hand without needing to handle the full complexity of the internal context everywhere. Here are the different "flavors" you'll encounter: @@ -110,6 +111,17 @@ While `InvocationContext` acts as the comprehensive internal container, ADK prov yield # ... event ... ``` + === "Go" + + ```go + import ( + "google.golang.org/adk/agent" + "google.golang.org/adk/session" + ) + + --8<-- "examples/go/snippets/context/main.go:invocation_context_agent" + ``` + === "Java" ```java @@ -169,17 +181,6 @@ While `InvocationContext` acts as the comprehensive internal container, ADK prov } ``` - === "Go" - - ```go - import ( - "google.golang.org/adk/agent" - "google.golang.org/adk/session" - ) - - --8<-- "examples/go/snippets/context/main.go:invocation_context_agent" - ``` - 2. **`ReadonlyContext`** * **Where Used:** Provided in scenarios where only read access to basic information is needed and mutation is disallowed (e.g., `InstructionProvider` functions). It's also the base class for other contexts. * **Purpose:** Offers a safe, read-only view of fundamental contextual details. @@ -198,6 +199,14 @@ While `InvocationContext` acts as the comprehensive internal container, ADK prov return f"Process the request for a {user_tier} user." ``` + === "Go" + + ```go + import "google.golang.org/adk/agent" + + --8<-- "examples/go/snippets/context/main.go:readonly_context_instruction" + ``` + === "Java" ```java @@ -211,14 +220,6 @@ While `InvocationContext` acts as the comprehensive internal container, ADK prov return "Process the request for a " + userTier + " user." } ``` - - === "Go" - - ```go - import "google.golang.org/adk/agent" - - --8<-- "examples/go/snippets/context/main.go:readonly_context_instruction" - ``` 3. **`CallbackContext`** * **Where Used:** Passed as `callback_context` to agent lifecycle callbacks (`before_agent_callback`, `after_agent_callback`) and model interaction callbacks (`before_model_callback`, `after_model_callback`). @@ -248,6 +249,17 @@ While `InvocationContext` acts as the comprehensive internal container, ADK prov return None # Allow model call to proceed ``` + === "Go" + + ```go + import ( + "google.golang.org/adk/agent" + "google.golang.org/adk/model" + ) + + --8<-- "examples/go/snippets/context/main.go:callback_context_callback" + ``` + === "Java" ```java @@ -269,17 +281,6 @@ While `InvocationContext` acts as the comprehensive internal container, ADK prov } ``` - === "Go" - - ```go - import ( - "google.golang.org/adk/agent" - "google.golang.org/adk/model" - ) - - --8<-- "examples/go/snippets/context/main.go:callback_context_callback" - ``` - 4. **`ToolContext`** * **Where Used:** Passed as `tool_context` to the functions backing `FunctionTool`s and to tool execution callbacks (`before_tool_callback`, `after_tool_callback`). * **Purpose:** Provides everything `CallbackContext` does, plus specialized methods essential for tool execution, like handling authentication, searching memory, and listing artifacts. @@ -318,6 +319,14 @@ While `InvocationContext` acts as the comprehensive internal container, ADK prov return {"result": f"Data for {query} fetched."} ``` + === "Go" + + ```go + import "google.golang.org/adk/tool" + + --8<-- "examples/go/snippets/context/main.go:tool_context_tool" + ``` + === "Java" ```java @@ -347,14 +356,6 @@ While `InvocationContext` acts as the comprehensive internal container, ADK prov } ``` - === "Go" - - ```go - import "google.golang.org/adk/tool" - - --8<-- "examples/go/snippets/context/main.go:tool_context_tool" - ``` - Understanding these different context objects and when to use them is key to effectively managing state, accessing services, and controlling the flow of your ADK application. The next section will detail common tasks you can perform using these contexts. @@ -394,6 +395,21 @@ You'll frequently need to read information stored within the context. # ... callback logic ... ``` + === "Go" + + ```go + import ( + "google.golang.org/adk/agent" + "google.golang.org/adk/session" + "google.golang.org/adk/tool" + "google.golang.org/genai" + ) + + --8<-- "examples/go/snippets/context/main.go:accessing_state_tool" + + --8<-- "examples/go/snippets/context/main.go:accessing_state_callback" + ``` + === "Java" ```java @@ -424,22 +440,6 @@ You'll frequently need to read information stored within the context. // ... callback logic ... ``` - === "Go" - - ```go - import "google.golang.org/adk/tool" - - --8<-- "examples/go/snippets/context/main.go:accessing_state_tool" - ```go - import ( - "google.golang.org/adk/agent" - "google.golang.org/adk/session" - "google.golang.org/genai" - ) - - --8<-- "examples/go/snippets/context/main.go:accessing_state_callback" - ``` - * **Getting Current Identifiers:** Useful for logging or custom logic based on the current operation. === "Python" @@ -456,6 +456,14 @@ You'll frequently need to read information stored within the context. print(f"Log: Invocation={inv_id}, Agent={agent_name}, FunctionCallID={func_call_id} - Tool Executed.") ``` + === "Go" + + ```go + import "google.golang.org/adk/tool" + + --8<-- "examples/go/snippets/context/main.go:accessing_ids" + ``` + === "Java" ```java @@ -470,14 +478,6 @@ You'll frequently need to read information stored within the context. } ``` - === "Go" - - ```go - import "google.golang.org/adk/tool" - - --8<-- "examples/go/snippets/context/main.go:accessing_ids" - ``` - * **Accessing the Initial User Input:** Refer back to the message that started the current invocation. === "Python" @@ -501,6 +501,17 @@ You'll frequently need to read information stored within the context. # ... ``` + === "Go" + + ```go + import ( + "google.golang.org/adk/agent" + "google.golang.org/genai" + ) + + --8<-- "examples/go/snippets/context/main.go:accessing_initial_user_input" + ``` + === "Java" ```java @@ -516,17 +527,6 @@ You'll frequently need to read information stored within the context. } } ``` - - === "Go" - - ```go - import ( - "google.golang.org/adk/agent" - "google.golang.org/genai" - ) - - --8<-- "examples/go/snippets/context/main.go:accessing_initial_user_input" - ``` ### Managing State @@ -560,6 +560,16 @@ State is crucial for memory and data flow. When you modify state using `Callback return {"orders": ["order123", "order456"]} ``` + === "Go" + + ```go + import "google.golang.org/adk/tool" + + --8<-- "examples/go/snippets/context/main.go:passing_data_tool1" + + --8<-- "examples/go/snippets/context/main.go:passing_data_tool2" + ``` + === "Java" ```java @@ -586,18 +596,6 @@ State is crucial for memory and data flow. When you modify state using `Callback } ``` - === "Go" - - ```go - import "google.golang.org/adk/tool" - - --8<-- "examples/go/snippets/context/main.go:passing_data_tool1" - ```go - import "google.golang.org/adk/tool" - - --8<-- "examples/go/snippets/context/main.go:passing_data_tool2" - ``` - * **Updating User Preferences:** === "Python" @@ -614,6 +612,14 @@ State is crucial for memory and data flow. When you modify state using `Callback return {"status": "Preference updated"} ``` + === "Go" + + ```go + import "google.golang.org/adk/tool" + + --8<-- "examples/go/snippets/context/main.go:updating_preferences" + ``` + === "Java" ```java @@ -629,14 +635,6 @@ State is crucial for memory and data flow. When you modify state using `Callback } ``` - === "Go" - - ```go - import "google.golang.org/adk/tool" - - --8<-- "examples/go/snippets/context/main.go:updating_preferences" - ``` - * **State Prefixes:** While basic state is session-specific, prefixes like `app:` and `user:` can be used with persistent `SessionService` implementations (like `DatabaseSessionService` or `VertexAiSessionService`) to indicate broader scope (app-wide or user-wide across sessions). `temp:` can denote data only relevant within the current invocation. ### Working with Artifacts @@ -672,6 +670,17 @@ Use artifacts to handle files or large data blobs associated with the session. C # save_document_reference(callback_context, "gs://my-bucket/docs/report.pdf") ``` + === "Go" + + ```go + import ( + "google.golang.org/adk/tool" + "google.golang.org/genai" + ) + + --8<-- "examples/go/snippets/context/main.go:artifacts_save_ref" + ``` + === "Java" ```java @@ -699,17 +708,6 @@ Use artifacts to handle files or large data blobs associated with the session. C // saveDocumentReference(context, "gs://my-bucket/docs/report.pdf") ``` - === "Go" - - ```go - import ( - "google.golang.org/adk/tool" - "google.golang.org/genai" - ) - - --8<-- "examples/go/snippets/context/main.go:artifacts_save_ref" - ``` - 2. **Summarizer Tool:** Load the artifact to get the path/URI, read the actual document content using appropriate libraries, summarize, and return the result. === "Python" @@ -769,6 +767,14 @@ Use artifacts to handle files or large data blobs associated with the session. C # return {"error": f"Error reading document {file_path}: {e}"} ``` + === "Go" + + ```go + import "google.golang.org/adk/tool" + + --8<-- "examples/go/snippets/context/main.go:artifacts_summarize" + ``` + === "Java" ```java @@ -820,14 +826,6 @@ Use artifacts to handle files or large data blobs associated with the session. C } } ``` - - === "Go" - - ```go - import "google.golang.org/adk/tool" - - --8<-- "examples/go/snippets/context/main.go:artifacts_summarize" - ``` * **Listing Artifacts:** Discover what files are available. @@ -846,6 +844,14 @@ Use artifacts to handle files or large data blobs associated with the session. C return {"error": f"Artifact service error: {e}"} ``` + === "Go" + + ```go + import "google.golang.org/adk/tool" + + --8<-- "examples/go/snippets/context/main.go:artifacts_list" + ``` + === "Java" ```java @@ -863,14 +869,6 @@ Use artifacts to handle files or large data blobs associated with the session. C } ``` - === "Go" - - ```go - import "google.golang.org/adk/tool" - - --8<-- "examples/go/snippets/context/main.go:artifacts_list" - ``` - ### Handling Tool Authentication
diff --git a/examples/go/snippets/context/main.go b/examples/go/snippets/context/main.go index 1ca54863..a705e34d 100644 --- a/examples/go/snippets/context/main.go +++ b/examples/go/snippets/context/main.go @@ -1,10 +1,13 @@ package main import ( + "bufio" "context" "fmt" "iter" "log" + "os" + "strings" "google.golang.org/adk/agent" "google.golang.org/adk/agent/llmagent" @@ -64,46 +67,72 @@ func runScenario(ctx context.Context, agentToRun agent.Agent, sessionID string, } } -// --8<-- [start:conceptual_runner_example] -// Conceptual Pseudocode: How the framework provides context (Internal Logic) -func conceptualRunnerExample(ctx context.Context, a agent.Agent) { - // The runner is the main entry point for the ADK. +func conceptualRunnerExample(ctx context.Context, myAgent agent.Agent) { + // --8<-- [start:conceptual_runner_example] + sessionService := session.InMemoryService() + r, err := runner.New(runner.Config{ - AppName: "my-app", - Agent: a, - SessionService: session.InMemoryService(), + AppName: appName, + Agent: myAgent, + SessionService: sessionService, }) if err != nil { log.Fatalf("Failed to create runner: %v", err) } - - sessionService := session.InMemoryService() - - // A session holds the state of a conversation. - session, err := sessionService.Create(ctx, &session.CreateRequest{ + + s, err := sessionService.Create(ctx, &session.CreateRequest{ AppName: appName, - UserID: "user123", + UserID: userID, }) if err != nil { - log.Fatalf("Failed to create session: %v", err) + log.Fatalf("FATAL: Failed to create session: %v", err) } - // --- Inside runner.Run(...) --- - // The runner takes the user's input and session details, creates an - // InvocationContext, and passes it to the agent's Run method. - userInput := genai.NewContentFromText("Hello, agent!", genai.RoleUser) - events := r.Run(ctx, session.Session.UserID(), session.Session.ID(), userInput, &agent.RunConfig{}) - - // As a developer, you work with the events returned by the agent. - for event, err := range events { - if err != nil { - log.Printf("Error during run: %v", err) + scanner := bufio.NewScanner(os.Stdin) + for { + fmt.Print("\nYou > ") + if !scanner.Scan() { break } - fmt.Printf("Event from agent %q\n", event.Author) + userInput := scanner.Text() + if strings.EqualFold(userInput, "quit") { + break + } + userMsg := genai.NewContentFromText(userInput, genai.RoleUser) + events := r.Run(ctx, s.Session.UserID(), s.Session.ID(), userMsg, &agent.RunConfig{ + StreamingMode: agent.StreamingModeNone, + }) + fmt.Print("\nAgent > ") + for event, err := range events { + if err != nil { + log.Printf("ERROR during agent execution: %v", err) + break + } + fmt.Print(event.Content.Parts[0].Text) + } + } + // --8<-- [end:conceptual_runner_example] +} + +func runConceptualExample() { + ctx := context.Background() + // 2. Create an agent with the tool. + geminiModel, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + log.Fatalf("FATAL: Failed to create model: %v", err) + } + llmCfg := llmagent.Config{ + Name: "agent", + Model: geminiModel, + Instruction: "You are an assistant", + } + testAgent, err := llmagent.New(llmCfg) + if err != nil { + log.Fatalf("FATAL: Failed to create agent: %v", err) } + + conceptualRunnerExample(ctx, testAgent) } -// --8<-- [end:conceptual_runner_example] // --8<-- [start:invocation_context_agent] // Pseudocode: Agent implementation receiving InvocationContext @@ -474,7 +503,6 @@ func runAccessingInitialUserInputExample() { // --8<-- [start:passing_data_tool1] // Pseudocode: Tool 1 - Fetches user ID - type GetUserProfileArgs struct { } From 755d85f7aae41c1acd41983d7f6ec09a06b88f2a Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 24 Oct 2025 14:22:58 -0400 Subject: [PATCH 084/125] Updated API --- examples/go/snippets/context/main.go | 70 +++++++++++++--------------- 1 file changed, 33 insertions(+), 37 deletions(-) diff --git a/examples/go/snippets/context/main.go b/examples/go/snippets/context/main.go index a705e34d..4a3fc730 100644 --- a/examples/go/snippets/context/main.go +++ b/examples/go/snippets/context/main.go @@ -17,6 +17,7 @@ import ( "google.golang.org/adk/runner" "google.golang.org/adk/session" "google.golang.org/adk/tool" + "google.golang.org/adk/tool/functiontool" "google.golang.org/genai" ) @@ -51,7 +52,7 @@ func runScenario(ctx context.Context, agentToRun agent.Agent, sessionID string, } input := genai.NewContentFromText(prompt, genai.RoleUser) - events := r.Run(ctx, sessionResp.Session.UserID(), sessionResp.Session.ID(), input, &agent.RunConfig{}) + events := r.Run(ctx, sessionResp.Session.UserID(), sessionResp.Session.ID(), input, agent.RunConfig{}) for event, err := range events { if err != nil { log.Printf("ERROR during agent execution: %v", err) @@ -59,7 +60,7 @@ func runScenario(ctx context.Context, agentToRun agent.Agent, sessionID string, } // Print only the final output from the agent. - if event.LLMResponse != nil && event.LLMResponse.Content != nil && len(event.LLMResponse.Content.Parts) > 0 { + if event != nil && event.Content != nil && len(event.Content.Parts) > 0 { fmt.Printf("Final Output for %s: [%s] %s\n", sessionID, event.Author, event.LLMResponse.Content.Parts[0].Text) } else { log.Printf("Final response for %s received, but it has no content to display.", sessionID) @@ -99,7 +100,7 @@ func conceptualRunnerExample(ctx context.Context, myAgent agent.Agent) { break } userMsg := genai.NewContentFromText(userInput, genai.RoleUser) - events := r.Run(ctx, s.Session.UserID(), s.Session.ID(), userMsg, &agent.RunConfig{ + events := r.Run(ctx, s.Session.UserID(), s.Session.ID(), userMsg, agent.RunConfig{ StreamingMode: agent.StreamingModeNone, }) fmt.Print("\nAgent > ") @@ -156,16 +157,11 @@ func (a *MyAgent) Run(ctx agent.InvocationContext) iter.Seq2[*session.Event, err func NewMyAgent() (agent.Agent, error) { a := &MyAgent{} // Use agent.New to construct the base agent functionality. - baseAgent, err := agent.New(agent.Config{ + return agent.New(agent.Config{ Name: "MyAgent", Description: "An example agent.", Run: a.Run, // Pass the Run method of our struct. }) - if err != nil { - return nil, err - } - - return baseAgent, nil } @@ -225,7 +221,7 @@ func runBeforeAgentCallbackCheck() { // 3. Register the callback in the agent configuration. llmCfg := llmagent.Config{ Name: "agent", - BeforeModel: []llmagent.BeforeModelCallback{myBeforeModelCb}, + BeforeModelCallbacks: []llmagent.BeforeModelCallback{myBeforeModelCb}, Model: geminiModel, Instruction: "You are an assistant.", } @@ -269,8 +265,8 @@ func searchExternalAPI(tc tool.Context, input searchExternalAPIArgs) searchExter // --8<-- [end:tool_context_tool] func runSearchExternalAPIExample() { - myTool, err := tool.NewFunctionTool( - tool.FunctionToolConfig{ + myTool, err := functiontool.New( + functiontool.Config{ Name: "search_external_api", Description: "Searches an external API using a query string.", }, @@ -311,8 +307,8 @@ func myTool(tc tool.Context, input toolArgs) toolResults { // --8<-- [end:accessing_state_tool] func runMyToolExample() { - myToolTool, err := tool.NewFunctionTool( - tool.FunctionToolConfig{ + myToolTool, err := functiontool.New( + functiontool.Config{ Name: "my_tool", Description: "A tool for doing something.", }, @@ -351,7 +347,7 @@ func runMyCallbackExample() { // Register myCallback as an AfterAgentCallback. llmCfg := llmagent.Config{ Name: "callbackAgent", - AfterAgent: []agent.AfterAgentCallback{myCallback}, + AfterAgentCallbacks: []agent.AfterAgentCallback{myCallback}, Model: geminiModel, Instruction: "You are an assistant that does nothing.", } @@ -393,8 +389,8 @@ func runAccessIdsExample() { ctx := context.Background() // 1. Create the tool. - loggingTool, err := tool.NewFunctionTool( - tool.FunctionToolConfig{ + loggingTool, err := functiontool.New( + functiontool.Config{ Name: "log_tool_usage", Description: "Logs the invocation and agent details.", }, @@ -453,7 +449,7 @@ func runInitialIntentCheck() { // 3. Register the callback in the agent configuration. llmCfg := llmagent.Config{ Name: "agent", - BeforeAgent: []agent.BeforeAgentCallback{checkInitialIntent}, + BeforeAgentCallbacks: []agent.BeforeAgentCallback{checkInitialIntent}, Model: geminiModel, Instruction: "You are an assistant.", } @@ -488,7 +484,7 @@ func runAccessingInitialUserInputExample() { llmCfg := llmagent.Config{ Name: "userInputLoggerAgent", - BeforeAgent: []agent.BeforeAgentCallback{logInitialUserInput}, + BeforeAgentCallbacks: []agent.BeforeAgentCallback{logInitialUserInput}, Model: geminiModel, Instruction: "You are an assistant.", } @@ -552,8 +548,8 @@ func runPassingDataExample() { ctx := context.Background() // 1. Create the tools. - getUserProfileTool, err := tool.NewFunctionTool( - tool.FunctionToolConfig{ + getUserProfileTool, err := functiontool.New( + functiontool.Config{ Name: "get_user_profile", Description: "Gets the profile for a user.", }, @@ -562,8 +558,8 @@ func runPassingDataExample() { if err != nil { log.Fatalf("FATAL: Failed to create getUserProfile tool: %v", err) } - getUserOrdersTool, err := tool.NewFunctionTool( - tool.FunctionToolConfig{ + getUserOrdersTool, err := functiontool.New( + functiontool.Config{ Name: "get_user_orders", Description: "Gets the orders for a user.", }, @@ -625,8 +621,8 @@ func runUpdatingPreferencesExample() { ctx := context.Background() // 1. Create the tool. - setPrefTool, err := tool.NewFunctionTool( - tool.FunctionToolConfig{ + setPrefTool, err := functiontool.New( + functiontool.Config{ Name: "set_user_preference", Description: "Sets a user's preference in the system.", }, @@ -673,15 +669,15 @@ func summarizeDocumentTool(tc tool.Context, input summarizeDocumentArgs) summari } // 1. Load the artifact part containing the path/URI - artifactPart, err := tc.Artifacts().Load(artifactName.(string)) + artifactPart, err := tc.Artifacts().Load(tc, artifactName.(string)) if err != nil { return summarizeDocumentResult{Error: err.Error()} } - if artifactPart.Text == "" { + if artifactPart.Part.Text == "" { return summarizeDocumentResult{Error: "Could not load artifact or artifact has no text path."} } - filePath := artifactPart.Text + filePath := artifactPart.Part.Text fmt.Printf("Loaded document reference: %s\n", filePath) // 2. Read the actual document content (outside ADK context) @@ -707,12 +703,12 @@ type checkAvailableDocsResult struct { } func checkAvailableDocs(tc tool.Context, args checkAvailableDocsArgs) checkAvailableDocsResult { - artifactKeys, err := tc.Artifacts().List() + artifactKeys, err := tc.Artifacts().List(tc) if err != nil { return checkAvailableDocsResult{Error: err.Error()} } fmt.Printf("Available artifacts: %v\n", artifactKeys) - return checkAvailableDocsResult{AvailableDocs: artifactKeys} + return checkAvailableDocsResult{AvailableDocs: artifactKeys.FileNames} } // --8<-- [end:artifacts_list] @@ -730,7 +726,7 @@ type saveDocRefResult struct { func saveDocRef(tc tool.Context, args saveDocRefArgs) saveDocRefResult { artifactPart := genai.NewPartFromText(args.FilePath) - err := tc.Artifacts().Save("document_to_summarize.txt", *artifactPart) + _, err := tc.Artifacts().Save(tc, "document_to_summarize.txt", artifactPart) if err != nil { return saveDocRefResult{"", err.Error()} } @@ -749,8 +745,8 @@ func runArtifactsExample() { // 1. Create the tools. - saveRefTool, err := tool.NewFunctionTool( - tool.FunctionToolConfig{ + saveRefTool, err := functiontool.New( + functiontool.Config{ Name: "save_document_reference", Description: "Saves a reference to a document path as an artifact.", }, @@ -759,8 +755,8 @@ func runArtifactsExample() { if err != nil { log.Fatalf("FATAL: Failed to create saveRefTool: %v", err) } - summarizeTool, err := tool.NewFunctionTool( - tool.FunctionToolConfig{ + summarizeTool, err := functiontool.New( + functiontool.Config{ Name: "summarize_document", Description: "Summarizes the document stored in artifacts.", }, @@ -795,8 +791,8 @@ func runCheckAvailableDocsExample() { ctx := context.Background() // 1. Create the tool. - checkDocsTool, err := tool.NewFunctionTool( - tool.FunctionToolConfig{ + checkDocsTool, err := functiontool.New( + functiontool.Config{ Name: "check_available_docs", Description: "Checks for available documents in artifacts.", }, From 7f05ee532534be86b275b407e7868af70edcec4d Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 24 Oct 2025 14:24:57 -0400 Subject: [PATCH 085/125] Twaked text --- docs/tools/built-in-tools.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tools/built-in-tools.md b/docs/tools/built-in-tools.md index 1b9ac18a..0374a364 100644 --- a/docs/tools/built-in-tools.md +++ b/docs/tools/built-in-tools.md @@ -18,7 +18,7 @@ tool when the agent calls it. Important: check the ***Limitations*** section of ## Available Built-in tools Note: Java only supports Google Search and Code Execution tools currently. -Note: Go only supports the Google Search tool currently. +Note: Go supports the Google Search tool and other built-in tools via the `geminitool` package. ### Google Search From ac8bee7ec1ca43c4c39ffa28b6da716aa6f28e7a Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 29 Oct 2025 16:09:15 -0400 Subject: [PATCH 086/125] Ran linter --- examples/go/snippets/context/main.go | 52 +++++++++++++--------------- 1 file changed, 25 insertions(+), 27 deletions(-) diff --git a/examples/go/snippets/context/main.go b/examples/go/snippets/context/main.go index 4a3fc730..ba2b66a9 100644 --- a/examples/go/snippets/context/main.go +++ b/examples/go/snippets/context/main.go @@ -35,9 +35,9 @@ func runScenario(ctx context.Context, agentToRun agent.Agent, sessionID string, sessionService := session.InMemoryService() artifactService := artifact.InMemoryService() rcfg := runner.Config{ - AppName: appName, - Agent: agentToRun, - SessionService: sessionService, + AppName: appName, + Agent: agentToRun, + SessionService: sessionService, ArtifactService: artifactService, } @@ -80,7 +80,7 @@ func conceptualRunnerExample(ctx context.Context, myAgent agent.Agent) { if err != nil { log.Fatalf("Failed to create runner: %v", err) } - + s, err := sessionService.Create(ctx, &session.CreateRequest{ AppName: appName, UserID: userID, @@ -116,7 +116,7 @@ func conceptualRunnerExample(ctx context.Context, myAgent agent.Agent) { } func runConceptualExample() { - ctx := context.Background() + ctx := context.Background() // 2. Create an agent with the tool. geminiModel, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) if err != nil { @@ -164,7 +164,6 @@ func NewMyAgent() (agent.Agent, error) { }) } - func runMyAgent() { ctx := context.Background() @@ -220,10 +219,10 @@ func runBeforeAgentCallbackCheck() { // 3. Register the callback in the agent configuration. llmCfg := llmagent.Config{ - Name: "agent", + Name: "agent", BeforeModelCallbacks: []llmagent.BeforeModelCallback{myBeforeModelCb}, - Model: geminiModel, - Instruction: "You are an assistant.", + Model: geminiModel, + Instruction: "You are an assistant.", } testAgent, err := llmagent.New(llmCfg) if err != nil { @@ -271,7 +270,7 @@ func runSearchExternalAPIExample() { Description: "Searches an external API using a query string.", }, searchExternalAPI) - + if err != nil { log.Fatal(err) } @@ -346,10 +345,10 @@ func runMyCallbackExample() { // Register myCallback as an AfterAgentCallback. llmCfg := llmagent.Config{ - Name: "callbackAgent", - AfterAgentCallbacks: []agent.AfterAgentCallback{myCallback}, - Model: geminiModel, - Instruction: "You are an assistant that does nothing.", + Name: "callbackAgent", + AfterAgentCallbacks: []agent.AfterAgentCallback{myCallback}, + Model: geminiModel, + Instruction: "You are an assistant that does nothing.", } testAgent, err := llmagent.New(llmCfg) if err != nil { @@ -448,10 +447,10 @@ func runInitialIntentCheck() { // 3. Register the callback in the agent configuration. llmCfg := llmagent.Config{ - Name: "agent", + Name: "agent", BeforeAgentCallbacks: []agent.BeforeAgentCallback{checkInitialIntent}, - Model: geminiModel, - Instruction: "You are an assistant.", + Model: geminiModel, + Instruction: "You are an assistant.", } testAgent, err := llmagent.New(llmCfg) if err != nil { @@ -472,6 +471,7 @@ func logInitialUserInput(ctx agent.CallbackContext) (*genai.Content, error) { } return nil, nil // No modification } + // --8<-- [end:accessing_initial_user_input] func runAccessingInitialUserInputExample() { @@ -483,10 +483,10 @@ func runAccessingInitialUserInputExample() { } llmCfg := llmagent.Config{ - Name: "userInputLoggerAgent", + Name: "userInputLoggerAgent", BeforeAgentCallbacks: []agent.BeforeAgentCallback{logInitialUserInput}, - Model: geminiModel, - Instruction: "You are an assistant.", + Model: geminiModel, + Instruction: "You are an assistant.", } testAgent, err := llmagent.New(llmCfg) if err != nil { @@ -496,7 +496,6 @@ func runAccessingInitialUserInputExample() { runScenario(ctx, testAgent, "user_input_session", nil, "What is the weather in London?") } - // --8<-- [start:passing_data_tool1] // Pseudocode: Tool 1 - Fetches user ID type GetUserProfileArgs struct { @@ -504,13 +503,13 @@ type GetUserProfileArgs struct { type getUserProfileResult struct { ProfileStatus string - Error string + Error string } func getUserProfile(tc tool.Context, input GetUserProfileArgs) getUserProfileResult { // A random user ID for demonstration purposes userID := "random_user_456" - + // Save the ID to state for the next tool if err := tc.State().Set("temp:current_user_id", userID); err != nil { return getUserProfileResult{Error: "Failed to set user ID in state"} @@ -589,7 +588,7 @@ func runPassingDataExample() { initialState := map[string]any{ "temp:current_user_id": userID, } - + // 4. Run a scenario that will trigger the tools. runScenario(ctx, testAgent, "passing_data_session", initialState, "Get my orders.") } @@ -614,6 +613,7 @@ func setUserPreference(tc tool.Context, args setUserPreferenceArgs) setUserPrefe fmt.Printf("Set user preference '%s' to '%s'\n", args.Preference, args.Value) return setUserPreferenceResult{Status: "Preference updated"} } + // --8<-- [end:updating_preferences] func runUpdatingPreferencesExample() { @@ -652,7 +652,6 @@ func runUpdatingPreferencesExample() { runScenario(ctx, testAgent, "preferences_session", nil, "Please set my theme preference to dark_mode.") } - // --8<-- [start:artifacts_summarize] // Pseudocode: In the Summarizer tool function type summarizeDocumentArgs struct{} @@ -743,7 +742,6 @@ func runArtifactsExample() { log.Println("\n--- Running Artifacts Example ---") ctx := context.Background() - // 1. Create the tools. saveRefTool, err := functiontool.New( functiontool.Config{ @@ -836,4 +834,4 @@ func main() { runArtifactsExample() runUpdatingPreferencesExample() runCheckAvailableDocsExample() -} \ No newline at end of file +} From 8751885205db28550454ef04d4e73c0703758fd3 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Fri, 31 Oct 2025 11:41:16 -0400 Subject: [PATCH 087/125] Fixed broken API issues --- .../go/snippets/tools/built-in-tools/google_search.go | 2 +- examples/go/snippets/tools/function-tools/func_tool.go | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/go/snippets/tools/built-in-tools/google_search.go b/examples/go/snippets/tools/built-in-tools/google_search.go index bad89c34..000c75a9 100644 --- a/examples/go/snippets/tools/built-in-tools/google_search.go +++ b/examples/go/snippets/tools/built-in-tools/google_search.go @@ -63,7 +63,7 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) error { // The r.Run method streams events and errors. // The loop iterates over the results, handling them as they arrive. - for event, err := range r.Run(ctx, userID, sessionID, userMsg, &agent.RunConfig{ + for event, err := range r.Run(ctx, userID, sessionID, userMsg, agent.RunConfig{ StreamingMode: agent.StreamingModeSSE, }) { if err != nil { diff --git a/examples/go/snippets/tools/function-tools/func_tool.go b/examples/go/snippets/tools/function-tools/func_tool.go index d6f5ed1b..8b74921d 100644 --- a/examples/go/snippets/tools/function-tools/func_tool.go +++ b/examples/go/snippets/tools/function-tools/func_tool.go @@ -12,6 +12,7 @@ import ( "google.golang.org/adk/runner" "google.golang.org/adk/session" "google.golang.org/adk/tool" + "google.golang.org/adk/tool/functiontool" "google.golang.org/genai" ) @@ -20,7 +21,7 @@ import ( // to simulate a real-world stock data API. This allows the example to // demonstrate tool functionality without making external network calls. var mockStockPrices = map[string]float64{ - "GOOG": 600.6, + "GOOG": 300.6, "AAPL": 123.4, "MSFT": 234.5, } @@ -57,8 +58,8 @@ func getStockPrice(ctx tool.Context, input getStockPriceArgs) getStockPriceResul // on how to respond to user queries about stock prices. It uses the // Gemini model to understand user intent and decide when to use its tools. func createStockAgent(ctx context.Context) (agent.Agent, error) { - stockPriceTool, err := tool.NewFunctionTool( - tool.FunctionToolConfig{ + stockPriceTool, err := functiontool.New( + functiontool.Config{ Name: "get_stock_price", Description: "Retrieves the current stock price for a given symbol.", }, @@ -129,7 +130,7 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) { Role: string(genai.RoleUser), } - for event, err := range r.Run(ctx, userID, sessionID, userMsg, &agent.RunConfig{ + for event, err := range r.Run(ctx, userID, sessionID, userMsg, agent.RunConfig{ StreamingMode: agent.StreamingModeSSE, }) { if err != nil { From 9d14bde0cc56182344990c6fe9a1626f94d6c0b4 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Mon, 3 Nov 2025 09:40:56 -0500 Subject: [PATCH 088/125] Updated to latest Go ADK API --- examples/go/snippets/artifacts/main.go | 24 +++++++++++-------- .../callbacks/types_of_callbacks/main.go | 7 +----- examples/go/snippets/context/main.go | 2 +- 3 files changed, 16 insertions(+), 17 deletions(-) diff --git a/examples/go/snippets/artifacts/main.go b/examples/go/snippets/artifacts/main.go index c2f34a34..d4380047 100644 --- a/examples/go/snippets/artifacts/main.go +++ b/examples/go/snippets/artifacts/main.go @@ -37,7 +37,7 @@ func BeforeModelCallback(ctx agent.CallbackContext, req *model.LLMRequest) (*mod // Create a unique filename for the image. fileName := fmt.Sprintf("user_image_%d.%s", i, strings.Split(part.InlineData.MIMEType, "/")[1]) // Save the image as an artifact. - if err := artifacts.Save(fileName, *part); err != nil { + if _, err := artifacts.Save(ctx, fileName, part); err != nil { log.Printf("[WARN] Failed to save user image: %v\n", err) } else { log.Printf("[INFO] Saved user image artifact: %s\n", fileName) @@ -69,7 +69,7 @@ func configureRunner() { Model: model, Name: "artifact_user_agent", Instruction: "You are an agent that describes images.", - BeforeModel: []llmagent.BeforeModelCallback{ + BeforeModelCallbacks: []llmagent.BeforeModelCallback{ BeforeModelCallback, }, }) @@ -125,12 +125,14 @@ func loadArtifactsCallback(ctx agent.CallbackContext, req *model.LLMRequest) (*m const filenameToLoad = "generated_report.pdf" // Load the artifact from the artifact service. - loadedPart, err := ctx.Artifacts().Load(filenameToLoad) + loadedPartResponse, err := ctx.Artifacts().Load(ctx, filenameToLoad) if err != nil { log.Printf("Callback could not load artifact '%s': %v", filenameToLoad, err) return nil, nil // File not found or error, continue to model. } + loadedPart := loadedPartResponse.Part + log.Printf("Callback successfully loaded artifact '%s'.", filenameToLoad) // Ensure there's at least one content in the request to append to. @@ -142,7 +144,7 @@ func loadArtifactsCallback(ctx agent.CallbackContext, req *model.LLMRequest) (*m // Add the loaded artifact to the request for the model. lastContent := req.Contents[len(req.Contents)-1] - lastContent.Parts = append(lastContent.Parts, &loadedPart) + lastContent.Parts = append(lastContent.Parts, loadedPart) log.Printf("Added artifact '%s' to LLM request.", filenameToLoad) // Return nil to continue to the next callback or the model. @@ -240,7 +242,7 @@ func saveReportCallback(ctx agent.CallbackContext, req *model.LLMRequest) (*mode // Set the filename for the artifact. filename := "generated_report.pdf" // Save the artifact to the artifact service. - err = ctx.Artifacts().Save(filename, *reportArtifact) + _, err = ctx.Artifacts().Save(ctx, filename, reportArtifact) if err != nil { log.Printf("An unexpected error occurred during Go artifact save: %v", err) // Depending on requirements, you might want to return an error to the user. @@ -259,12 +261,14 @@ func saveReportCallback(ctx agent.CallbackContext, req *model.LLMRequest) (*mode func listUserFilesCallback(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) { log.Println("[Callback] listUserFilesCallback triggered.") // List the available artifacts from the artifact service. - availableFiles, err := ctx.Artifacts().List() + listResponse, err := ctx.Artifacts().List(ctx) if err != nil { log.Printf("An unexpected error occurred during Go artifact list: %v", err) return nil, nil // Continue, but log the error. } + availableFiles := listResponse.FileNames + log.Printf("Found %d available files.", len(availableFiles)) // If there are available files, add them to the LLM request. @@ -325,7 +329,7 @@ func main() { Model: model, Name: "reporting_agent", Instruction: "You are a reporting agent. You can see available files and their contents if they are loaded for you. Summarize any provided files.", - BeforeModel: []llmagent.BeforeModelCallback{ + BeforeModelCallbacks: []llmagent.BeforeModelCallback{ saveReportCallback, // Saves report from state listUserFilesCallback, // Lists available files and adds to prompt loadArtifactsCallback, // Loads a specific file and adds to prompt @@ -356,13 +360,13 @@ func main() { log.Println("\n--- Agent Run 1: Triggering callbacks ---") log.Println("This run will trigger `saveReportCallback` (from session state), `listUserFilesCallback` (will see the newly saved file), and `loadArtifactsCallback` (will load it).") userInput := &genai.Content{Parts: []*genai.Part{genai.NewPartFromText("Please summarize the report for me.")}} - for event, err := range r.Run(ctx, session.Session.UserID(), session.Session.ID(), userInput, &agent.RunConfig{ + for event, err := range r.Run(ctx, session.Session.UserID(), session.Session.ID(), userInput, agent.RunConfig{ StreamingMode: agent.StreamingModeSSE, }) { if err != nil { log.Printf("AGENT ERROR: %v\n", err) - } else if event.LLMResponse != nil && event.LLMResponse.Content != nil { - for _, p := range event.LLMResponse.Content.Parts { + } else if event != nil && event.Content != nil { + for _, p := range event.Content.Parts { fmt.Print(string(p.Text)) } } diff --git a/examples/go/snippets/callbacks/types_of_callbacks/main.go b/examples/go/snippets/callbacks/types_of_callbacks/main.go index 040b0570..b487425e 100644 --- a/examples/go/snippets/callbacks/types_of_callbacks/main.go +++ b/examples/go/snippets/callbacks/types_of_callbacks/main.go @@ -81,7 +81,7 @@ func runBeforeAgentExample() { // --8<-- [end:before_agent_example] // --8<-- [start:after_agent_example] -func onAfterAgent(ctx agent.CallbackContext, finalEvent *session.Event, runErr error) (*genai.Content, error) { +func onAfterAgent(ctx agent.CallbackContext) (*genai.Content, error) { agentName := ctx.AgentName() invocationID := ctx.InvocationID() state := ctx.State() @@ -89,11 +89,6 @@ func onAfterAgent(ctx agent.CallbackContext, finalEvent *session.Event, runErr e log.Printf("\n[Callback] Exiting agent: %s (Inv: %s)", agentName, invocationID) log.Printf("[Callback] Current State: %v", state) - if runErr != nil { - log.Printf("[Callback] Agent run produced an error: %v. Passing through.", runErr) - return nil, runErr - } - if addNote, _ := state.Get("add_concluding_note"); addNote == true { log.Printf("[Callback] State condition 'add_concluding_note=True' met: Replacing agent %s's output.", agentName) return genai.NewContentFromText( diff --git a/examples/go/snippets/context/main.go b/examples/go/snippets/context/main.go index ba2b66a9..86e662c7 100644 --- a/examples/go/snippets/context/main.go +++ b/examples/go/snippets/context/main.go @@ -322,7 +322,7 @@ func runMyToolExample() { // --8<-- [start:accessing_state_callback] // Pseudocode: In a Callback function -func myCallback(ctx agent.CallbackContext, event *session.Event, err error) (*genai.Content, error) { +func myCallback(ctx agent.CallbackContext) (*genai.Content, error) { lastToolResult, err := ctx.State().Get("temp:last_api_result") // Read temporary state if err == nil { fmt.Printf("Found temporary result from last tool: %v\n", lastToolResult) From 9ce514b7e8f6bf5a640b4dd5d0c6b48392672833 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 22 Oct 2025 21:25:46 -0400 Subject: [PATCH 089/125] Added func param info --- docs/tools/function-tools.md | 254 ++++++++++++++++++++++++++--------- 1 file changed, 194 insertions(+), 60 deletions(-) diff --git a/docs/tools/function-tools.md b/docs/tools/function-tools.md index cc942eed..be443e60 100644 --- a/docs/tools/function-tools.md +++ b/docs/tools/function-tools.md @@ -27,73 +27,207 @@ A well-defined function signature is crucial for the LLM to use your tool correc #### Parameters -You can define functions with required parameters, optional parameters, and variadic arguments. Here’s how each is handled: -##### Required Parameters -A parameter is considered **required** if it has a type hint but **no default value**. The LLM must provide a value for this argument when it calls the tool. -???+ "Example: Required Parameters" - === "Python" - ```python - def get_weather(city: str, unit: str): - """ - Retrieves the weather for a city in the specified unit. - - Args: - city (str): The city name. - unit (str): The temperature unit, either 'Celsius' or 'Fahrenheit'. - """ - # ... function logic ... - return {"status": "success", "report": f"Weather for {city} is sunny."} - ``` - In this example, both `city` and `unit` are mandatory. If the LLM tries to call `get_weather` without one of them, the ADK will return an error to the LLM, prompting it to correct the call. +=== "Python" -##### Optional Parameters with Default Values -A parameter is considered **optional** if you provide a **default value**. This is the standard Python way to define optional arguments. The ADK correctly interprets these and does not list them in the `required` field of the tool schema sent to the LLM. -???+ "Example: Optional Parameter with Default Value" - === "Python" - ```python - def search_flights(destination: str, departure_date: str, flexible_days: int = 0): - """ - Searches for flights. - - Args: - destination (str): The destination city. - departure_date (str): The desired departure date. - flexible_days (int, optional): Number of flexible days for the search. Defaults to 0. - """ - # ... function logic ... - if flexible_days > 0: - return {"status": "success", "report": f"Found flexible flights to {destination}."} - return {"status": "success", "report": f"Found flights to {destination} on {departure_date}."} - ``` - Here, `flexible_days` is optional. The LLM can choose to provide it, but it's not required. -##### Optional Parameters with `typing.Optional` -You can also mark a parameter as optional using `typing.Optional[SomeType]` or the `| None` syntax (Python 3.10+). This signals that the parameter can be `None`. When combined with a default value of `None`, it behaves as a standard optional parameter. + You can define functions with required parameters, optional parameters, and variadic arguments. Here’s how each is handled: -???+ "Example: `typing.Optional`" - === "Python" - ```python - from typing import Optional - - def create_user_profile(username: str, bio: Optional[str] = None): - """ - Creates a new user profile. - - Args: - username (str): The user's unique username. - bio (str, optional): A short biography for the user. Defaults to None. - """ - # ... function logic ... - if bio: - return {"status": "success", "message": f"Profile for {username} created with a bio."} - return {"status": "success", "message": f"Profile for {username} created."} - ``` -##### Variadic Parameters (`*args` and `**kwargs`) -While you can include `*args` (variable positional arguments) and `**kwargs` (variable keyword arguments) in your function signature for other purposes, they are **ignored by the ADK framework** when generating the tool schema for the LLM. The LLM will not be aware of them and cannot pass arguments to them. It's best to rely on explicitly defined parameters for all data you expect from the LLM. + + ##### Required Parameters + + A parameter is considered **required** if it has a type hint but **no default value**. The LLM must provide a value for this argument when it calls the tool. + + + + ???+ "Example: Required Parameters" + + === "Python" + + ```python + + def get_weather(city: str, unit: str): + + """ + + Retrieves the weather for a city in the specified unit. + + + + Args: + + city (str): The city name. + + unit (str): The temperature unit, either 'Celsius' or 'Fahrenheit'. + + """ + + # ... function logic ... + + return {"status": "success", "report": f"Weather for {city} is sunny."} + + ``` + + In this example, both `city` and `unit` are mandatory. If the LLM tries to call `get_weather` without one of them, the ADK will return an error to the LLM, prompting it to correct the call. + + + + ##### Optional Parameters with Default Values + + A parameter is considered **optional** if you provide a **default value**. This is the standard Python way to define optional arguments. The ADK correctly interprets these and does not list them in the `required` field of the tool schema sent to the LLM. + + + + ???+ "Example: Optional Parameter with Default Value" + + === "Python" + + ```python + + def search_flights(destination: str, departure_date: str, flexible_days: int = 0): + + """ + + Searches for flights. + + + + Args: + + destination (str): The destination city. + + departure_date (str): The desired departure date. + + flexible_days (int, optional): Number of flexible days for the search. Defaults to 0. + + """ + + # ... function logic ... + + if flexible_days > 0: + + return {"status": "success", "report": f"Found flexible flights to {destination}."} + + return {"status": "success", "report": f"Found flights to {destination} on {departure_date}."} + + ``` + + Here, `flexible_days` is optional. The LLM can choose to provide it, but it's not required. + + + + ##### Optional Parameters with `typing.Optional` + + You can also mark a parameter as optional using `typing.Optional[SomeType]` or the `| None` syntax (Python 3.10+). This signals that the parameter can be `None`. When combined with a default value of `None`, it behaves as a standard optional parameter. + + + + ???+ "Example: `typing.Optional`" + + === "Python" + + ```python + + from typing import Optional + + + + def create_user_profile(username: str, bio: Optional[str] = None): + + """ + + Creates a new user profile. + + + + Args: + + username (str): The user's unique username. + + bio (str, optional): A short biography for the user. Defaults to None. + + """ + + # ... function logic ... + + if bio: + + return {"status": "success", "message": f"Profile for {username} created with a bio."} + + return {"status": "success", "message": f"Profile for {username} created."} + + ``` + + + + ##### Variadic Parameters (`*args` and `**kwargs`) + + While you can include `*args` (variable positional arguments) and `**kwargs` (variable keyword arguments) in your function signature for other purposes, they are **ignored by the ADK framework** when generating the tool schema for the LLM. The LLM will not be aware of them and cannot pass arguments to them. It's best to rely on explicitly defined parameters for all data you expect from the LLM. + + + +=== "Go" + + In Go, you define a tool's parameters using a `struct`. The fields of the struct become the arguments for the tool. You use struct tags to provide details like name, description, and whether a parameter is required. + + The two primary tags you will use are `json` and `jsonschema`. + + - The `json` tag defines the parameter name and its optionality. + - The `jsonschema` tag provides the description for the parameter. + + ##### Required Parameters + A parameter is considered **required** if its struct field does **not** have the `omitempty` or `omitzero` option in its `json` tag. The LLM must provide a value for this argument when it calls the tool. + + ???+ "Example: Required Parameters" + === "Go" + ```go + // GetWeatherParams defines the arguments for the getWeather tool. + type GetWeatherParams struct { + // Location is a required parameter. + Location string `json:"location" jsonschema:"The city and state, e.g., San Francisco, CA"` + // Unit is also required. + Unit string `json:"unit" jsonschema:"The temperature unit, either 'celsius' or 'fahrenheit'"` + } + ``` + In this example, both `location` and `unit` are mandatory. + + ##### Optional Parameters + A parameter is considered **optional** if its struct field has the `omitempty` or `omitzero` option in its `json` tag. + + - `omitempty`: Use for strings, slices, maps, and other reference types. The field is omitted from the JSON if its value is the zero value for its type (e.g., `""` for string, `nil` for slices/maps). + - `omitzero`: Use for numeric types (like `int` or `float`). This is a convention to signal that `0` is a value to be omitted, though `omitempty` with Go 1.20+ works for numbers too. + + ???+ "Example: Optional Parameters" + === "Go" + ```go + // GetWeatherParams defines the arguments for the getWeather tool. + type GetWeatherParams struct { + // Location is required. + Location string `json:"location" jsonschema:"The city and state, e.g., San Francisco, CA"` + + // Unit is optional. + Unit string `json:"unit,omitempty" jsonschema:"The temperature unit, either 'celsius' or 'fahrenheit'"` + + // Days is optional. + Days int `json:"days,omitzero" jsonschema:"The number of forecast days to return (defaults to 1)"` + } + ``` + Here, `unit` and `days` are optional. The LLM can choose to provide them, but they are not required. + + ##### Variadic Parameters + Go does not have a direct equivalent to Python's `*args` and `**kwargs` that can be represented in the tool's JSON schema. To accept a variable number of inputs of the same type, you can use a slice. + + ???+ "Example: Slice for multiple values" + === "Go" + ```go + type CreateUsersParams struct { + Usernames []string `json:"usernames" jsonschema:"A list of usernames to create"` + } + ``` + #### Return Type From df9f83f15fe625df5bd92c954a6ffaeea1d34085 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 22 Oct 2025 21:42:24 -0400 Subject: [PATCH 090/125] Interleaved Py and Go --- docs/tools/function-tools.md | 271 ++++++++++++----------------------- 1 file changed, 92 insertions(+), 179 deletions(-) diff --git a/docs/tools/function-tools.md b/docs/tools/function-tools.md index be443e60..cf1e8087 100644 --- a/docs/tools/function-tools.md +++ b/docs/tools/function-tools.md @@ -27,206 +27,119 @@ A well-defined function signature is crucial for the LLM to use your tool correc #### Parameters - +##### Required Parameters === "Python" - - - - You can define functions with required parameters, optional parameters, and variadic arguments. Here’s how each is handled: - - - - ##### Required Parameters - - A parameter is considered **required** if it has a type hint but **no default value**. The LLM must provide a value for this argument when it calls the tool. - - + A parameter is considered **required** if it has a type hint but **no default value**. The LLM must provide a value for this argument when it calls the tool. The parameter's description is taken from the function's docstring. ???+ "Example: Required Parameters" + ```python + def get_weather(city: str, unit: str): + """ + Retrieves the weather for a city in the specified unit. + + Args: + city (str): The city name. + unit (str): The temperature unit, either 'Celsius' or 'Fahrenheit'. + """ + # ... function logic ... + return {"status": "success", "report": f"Weather for {city} is sunny."} + ``` + In this example, both `city` and `unit` are mandatory. If the LLM tries to call `get_weather` without one of them, the ADK will return an error to the LLM, prompting it to correct the call. - === "Python" - - ```python - - def get_weather(city: str, unit: str): - - """ - - Retrieves the weather for a city in the specified unit. - - - - Args: - - city (str): The city name. - - unit (str): The temperature unit, either 'Celsius' or 'Fahrenheit'. - - """ - - # ... function logic ... - - return {"status": "success", "report": f"Weather for {city} is sunny."} - - ``` - - In this example, both `city` and `unit` are mandatory. If the LLM tries to call `get_weather` without one of them, the ADK will return an error to the LLM, prompting it to correct the call. - - - - ##### Optional Parameters with Default Values - - A parameter is considered **optional** if you provide a **default value**. This is the standard Python way to define optional arguments. The ADK correctly interprets these and does not list them in the `required` field of the tool schema sent to the LLM. - - - - ???+ "Example: Optional Parameter with Default Value" - - === "Python" - - ```python - - def search_flights(destination: str, departure_date: str, flexible_days: int = 0): - - """ - - Searches for flights. - - - - Args: - - destination (str): The destination city. - - departure_date (str): The desired departure date. - - flexible_days (int, optional): Number of flexible days for the search. Defaults to 0. - - """ - - # ... function logic ... - - if flexible_days > 0: - - return {"status": "success", "report": f"Found flexible flights to {destination}."} - - return {"status": "success", "report": f"Found flights to {destination} on {departure_date}."} - - ``` - - Here, `flexible_days` is optional. The LLM can choose to provide it, but it's not required. - - - - ##### Optional Parameters with `typing.Optional` - - You can also mark a parameter as optional using `typing.Optional[SomeType]` or the `| None` syntax (Python 3.10+). This signals that the parameter can be `None`. When combined with a default value of `None`, it behaves as a standard optional parameter. - - - - ???+ "Example: `typing.Optional`" - - === "Python" - - ```python - - from typing import Optional - - - - def create_user_profile(username: str, bio: Optional[str] = None): - - """ - - Creates a new user profile. - - - - Args: - - username (str): The user's unique username. - - bio (str, optional): A short biography for the user. Defaults to None. - - """ - - # ... function logic ... - - if bio: - - return {"status": "success", "message": f"Profile for {username} created with a bio."} - - return {"status": "success", "message": f"Profile for {username} created."} - - ``` +=== "Go" + In Go, you use struct tags to control the JSON schema. The two primary tags are `json` and `jsonschema`. + A parameter is considered **required** if its struct field does **not** have the `omitempty` or `omitzero` option in its `json` tag. + The `jsonschema` tag is used to provide the argument's description. This is crucial for the LLM to understand what the argument is for. - ##### Variadic Parameters (`*args` and `**kwargs`) + ???+ "Example: Required Parameters" + ```go + // GetWeatherParams defines the arguments for the getWeather tool. + type GetWeatherParams struct { + // This field is REQUIRED (no "omitempty"). + // The jsonschema tag provides the description. + Location string `json:"location" jsonschema:"The city and state, e.g., San Francisco, CA"` + + // This field is also REQUIRED. + Unit string `json:"unit" jsonschema:"The temperature unit, either 'celsius' or 'fahrenheit'"` + } + ``` + In this example, both `location` and `unit` are mandatory. - While you can include `*args` (variable positional arguments) and `**kwargs` (variable keyword arguments) in your function signature for other purposes, they are **ignored by the ADK framework** when generating the tool schema for the LLM. The LLM will not be aware of them and cannot pass arguments to them. It's best to rely on explicitly defined parameters for all data you expect from the LLM. +##### Optional Parameters +=== "Python" + A parameter is considered **optional** if you provide a **default value**. This is the standard Python way to define optional arguments. You can also mark a parameter as optional using `typing.Optional[SomeType]` or the `| None` syntax (Python 3.10+). + ???+ "Example: Optional Parameters" + ```python + from typing import Optional + + def search_flights(destination: str, departure_date: str, flexible_days: int = 0, bio: Optional[str] = None): + """ + Searches for flights. + + Args: + destination (str): The destination city. + departure_date (str): The desired departure date. + flexible_days (int, optional): Number of flexible days for the search. Defaults to 0. + bio (str, optional): A short biography for the user. Defaults to None. + """ + # ... function logic ... + if flexible_days > 0: + return {"status": "success", "report": f"Found flexible flights to {destination}."} + return {"status": "success", "report": f"Found flights to {destination} on {departure_date}."} + ``` + Here, `flexible_days` and `bio` are optional. The LLM can choose to provide them, but it's not required. === "Go" + A parameter is considered **optional** if its struct field has the `omitempty` or `omitzero` option in its `json` tag. - In Go, you define a tool's parameters using a `struct`. The fields of the struct become the arguments for the tool. You use struct tags to provide details like name, description, and whether a parameter is required. - - The two primary tags you will use are `json` and `jsonschema`. + ???+ "Example: Optional Parameters" + ```go + // GetWeatherParams defines the arguments for the getWeather tool. + type GetWeatherParams struct { + // Location is required. + Location string `json:"location" jsonschema:"The city and state, e.g., San Francisco, CA"` - - The `json` tag defines the parameter name and its optionality. - - The `jsonschema` tag provides the description for the parameter. + // Unit is optional. + Unit string `json:"unit,omitempty" jsonschema:"The temperature unit, either 'celsius' or 'fahrenheit'"` - ##### Required Parameters - A parameter is considered **required** if its struct field does **not** have the `omitempty` or `omitzero` option in its `json` tag. The LLM must provide a value for this argument when it calls the tool. + // Days is optional. + Days int `json:"days,omitzero" jsonschema:"The number of forecast days to return (defaults to 1)"` + } + ``` + Here, `unit` and `days` are optional. The LLM can choose to provide them, but they are not required. - ???+ "Example: Required Parameters" - === "Go" - ```go - // GetWeatherParams defines the arguments for the getWeather tool. - type GetWeatherParams struct { - // Location is a required parameter. - Location string `json:"location" jsonschema:"The city and state, e.g., San Francisco, CA"` - // Unit is also required. - Unit string `json:"unit" jsonschema:"The temperature unit, either 'celsius' or 'fahrenheit'"` - } - ``` - In this example, both `location` and `unit` are mandatory. - - ##### Optional Parameters - A parameter is considered **optional** if its struct field has the `omitempty` or `omitzero` option in its `json` tag. +##### Variadic Parameters - - `omitempty`: Use for strings, slices, maps, and other reference types. The field is omitted from the JSON if its value is the zero value for its type (e.g., `""` for string, `nil` for slices/maps). - - `omitzero`: Use for numeric types (like `int` or `float`). This is a convention to signal that `0` is a value to be omitted, though `omitempty` with Go 1.20+ works for numbers too. +=== "Python" + While you can include `*args` (variable positional arguments) and `**kwargs` (variable keyword arguments) in your function signature for other purposes, they are **ignored by the ADK framework** when generating the tool schema for the LLM. The LLM will not be aware of them and cannot pass arguments to them. It's best to rely on explicitly defined parameters for all data you expect from the LLM. To accept a variable number of inputs of the same type, you can use a list. + + ???+ "Example: Variadic Parameters" + ```python + def create_users(usernames: list[str]): + """ + Creates multiple user profiles. + + Args: + usernames (list[str]): A list of usernames to create. + """ + # ... function logic ... + return {"status": "success", "message": f"{len(usernames)} users created."} + ``` - ???+ "Example: Optional Parameters" - === "Go" - ```go - // GetWeatherParams defines the arguments for the getWeather tool. - type GetWeatherParams struct { - // Location is required. - Location string `json:"location" jsonschema:"The city and state, e.g., San Francisco, CA"` - - // Unit is optional. - Unit string `json:"unit,omitempty" jsonschema:"The temperature unit, either 'celsius' or 'fahrenheit'"` - - // Days is optional. - Days int `json:"days,omitzero" jsonschema:"The number of forecast days to return (defaults to 1)"` - } - ``` - Here, `unit` and `days` are optional. The LLM can choose to provide them, but they are not required. - - ##### Variadic Parameters +=== "Go" Go does not have a direct equivalent to Python's `*args` and `**kwargs` that can be represented in the tool's JSON schema. To accept a variable number of inputs of the same type, you can use a slice. - ???+ "Example: Slice for multiple values" - === "Go" - ```go - type CreateUsersParams struct { - Usernames []string `json:"usernames" jsonschema:"A list of usernames to create"` - } - ``` + ???+ "Example: Variadic Parameters" + ```go + type CreateUsersParams struct { + Usernames []string `json:"usernames" jsonschema:"A list of usernames to create"` + } + ``` #### Return Type From 07df4ea21ffb85383073eb529616c182dc7d9306 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 5 Nov 2025 00:30:11 -0500 Subject: [PATCH 091/125] Revert mistakes --- docs/tools/function-tools.md | 40 +++++++++++++++--------------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/docs/tools/function-tools.md b/docs/tools/function-tools.md index cf1e8087..28fe0894 100644 --- a/docs/tools/function-tools.md +++ b/docs/tools/function-tools.md @@ -75,9 +75,7 @@ A well-defined function signature is crucial for the LLM to use your tool correc ???+ "Example: Optional Parameters" ```python - from typing import Optional - - def search_flights(destination: str, departure_date: str, flexible_days: int = 0, bio: Optional[str] = None): + def search_flights(destination: str, departure_date: str, flexible_days: int = 0): """ Searches for flights. @@ -85,14 +83,13 @@ A well-defined function signature is crucial for the LLM to use your tool correc destination (str): The destination city. departure_date (str): The desired departure date. flexible_days (int, optional): Number of flexible days for the search. Defaults to 0. - bio (str, optional): A short biography for the user. Defaults to None. """ # ... function logic ... if flexible_days > 0: return {"status": "success", "report": f"Found flexible flights to {destination}."} return {"status": "success", "report": f"Found flights to {destination} on {departure_date}."} ``` - Here, `flexible_days` and `bio` are optional. The LLM can choose to provide them, but it's not required. + Here, `flexible_days` is optional. The LLM can choose to provide it, but it's not required. === "Go" A parameter is considered **optional** if its struct field has the `omitempty` or `omitzero` option in its `json` tag. @@ -113,33 +110,30 @@ A well-defined function signature is crucial for the LLM to use your tool correc ``` Here, `unit` and `days` are optional. The LLM can choose to provide them, but they are not required. -##### Variadic Parameters +##### Optional Parameters with `typing.Optional` +You can also mark a parameter as optional using `typing.Optional[SomeType]` or the `| None` syntax (Python 3.10+). This signals that the parameter can be `None`. When combined with a default value of `None`, it behaves as a standard optional parameter. -=== "Python" - While you can include `*args` (variable positional arguments) and `**kwargs` (variable keyword arguments) in your function signature for other purposes, they are **ignored by the ADK framework** when generating the tool schema for the LLM. The LLM will not be aware of them and cannot pass arguments to them. It's best to rely on explicitly defined parameters for all data you expect from the LLM. To accept a variable number of inputs of the same type, you can use a list. - - ???+ "Example: Variadic Parameters" +???+ "Example: `typing.Optional`" + === "Python" ```python - def create_users(usernames: list[str]): + from typing import Optional + + def create_user_profile(username: str, bio: Optional[str] = None): """ - Creates multiple user profiles. + Creates a new user profile. Args: - usernames (list[str]): A list of usernames to create. + username (str): The user's unique username. + bio (str, optional): A short biography for the user. Defaults to None. """ # ... function logic ... - return {"status": "success", "message": f"{len(usernames)} users created."} + if bio: + return {"status": "success", "message": f"Profile for {username} created with a bio."} + return {"status": "success", "message": f"Profile for {username} created."} ``` -=== "Go" - Go does not have a direct equivalent to Python's `*args` and `**kwargs` that can be represented in the tool's JSON schema. To accept a variable number of inputs of the same type, you can use a slice. - - ???+ "Example: Variadic Parameters" - ```go - type CreateUsersParams struct { - Usernames []string `json:"usernames" jsonschema:"A list of usernames to create"` - } - ``` +##### Variadic Parameters (`*args` and `**kwargs`) +While you can include `*args` (variable positional arguments) and `**kwargs` (variable keyword arguments) in your function signature for other purposes, they are **ignored by the ADK framework** when generating the tool schema for the LLM. The LLM will not be aware of them and cannot pass arguments to them. It's best to rely on explicitly defined parameters for all data you expect from the LLM. #### Return Type From 95785f53434c392324242aaec3f6f4d5cdfe0086 Mon Sep 17 00:00:00 2001 From: ivanmkc Date: Wed, 5 Nov 2025 01:19:45 -0500 Subject: [PATCH 092/125] Removed extra newline --- docs/tools/function-tools.md | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/tools/function-tools.md b/docs/tools/function-tools.md index 28fe0894..586efcc6 100644 --- a/docs/tools/function-tools.md +++ b/docs/tools/function-tools.md @@ -135,7 +135,6 @@ You can also mark a parameter as optional using `typing.Optional[SomeType]` or t ##### Variadic Parameters (`*args` and `**kwargs`) While you can include `*args` (variable positional arguments) and `**kwargs` (variable keyword arguments) in your function signature for other purposes, they are **ignored by the ADK framework** when generating the tool schema for the LLM. The LLM will not be aware of them and cannot pass arguments to them. It's best to rely on explicitly defined parameters for all data you expect from the LLM. - #### Return Type The preferred return type for a Function Tool is a **dictionary** in Python or **Map** in Java. This allows you to structure the response with key-value pairs, providing context and clarity to the LLM. If your function returns a type other than a dictionary, the framework automatically wraps it into a dictionary with a single key named **"result"**. From aedf4d5eb8adf4027c353a146c824aa254082eb6 Mon Sep 17 00:00:00 2001 From: Ivan Cheung Date: Wed, 5 Nov 2025 15:53:03 -0500 Subject: [PATCH 093/125] Added example cloud run code and update md with Go (#41) * Added example cloud run code and update md with Go * Added a2a_agent_url and fixed future tense * Moved Go before Java --------- Co-authored-by: ivanmkc --- docs/deploy/cloud-run.md | 137 +++++++++++++++++++++++++++++++++- examples/go/cloud-run/main.go | 101 +++++++++++++++++++++++++ 2 files changed, 235 insertions(+), 3 deletions(-) create mode 100644 examples/go/cloud-run/main.go diff --git a/docs/deploy/cloud-run.md b/docs/deploy/cloud-run.md index e92b1df2..9fa47f62 100644 --- a/docs/deploy/cloud-run.md +++ b/docs/deploy/cloud-run.md @@ -1,7 +1,7 @@ # Deploy to Cloud Run
- Supported in ADKPythonJava + Supported in ADKPythonGoJava
[Cloud Run](https://cloud.google.com/run) @@ -22,6 +22,19 @@ To proceed, confirm that your agent code is configured as follows: 3. `__init__.py` is within your agent directory and contains `from . import agent`. 4. Your `requirements.txt` file is present in the agent directory. +=== "Go" + + 1. Your application's entry point (the main package and main() function) is in a + single Go file. Using main.go is a strong convention. + 2. Your agent instance is passed to a launcher configuration, typically using + services.NewSingleAgentLoader(agent). The adkgo tool uses this launcher to start + your agent with the correct services. + 3. Your go.mod and go.sum files are present in your project directory to manage + dependencies. + + Refer to the following section for more details. You can also find a [sample app](https://github.com/google/adk-docs/tree/main/examples/go/cloud-run) in the Github + repo. + === "Java" 1. Agent code is in a file called `CapitalAgent.java` within your agent directory. @@ -52,6 +65,31 @@ export GOOGLE_API_KEY=your-api-key ``` *(Replace `your-project-id` with your actual GCP project ID and `your-api-key` with your actual API key from AI Studio)* +## Prerequisites + +1. You should have a Google Cloud project. You need to know your: + 1. Project name (i.e. "my-project") + 1. Project location (i.e. "us-central1") + 1. Service account (i.e. "1234567890-compute@developer.gserviceaccount.com") + 1. GOOGLE_API_KEY + +## Secret + +Please make sure you have created a secret which can be read by your service account. + +### Entry for GOOGLE_API_KEY secret + +You can create your secret manually or use CLI: +```bash +echo "<>" | gcloud secrets create GOOGLE_API_KEY --project=my-project --data-file=- +``` + +### Permissions to read +You should give appropiate permissision for you service account to read this secret. +```bash +gcloud secrets add-iam-policy-binding GOOGLE_API_KEY --member="serviceAccount:1234567890-compute@developer.gserviceaccount.com" --role="roles/secretmanager.secretAccessor" --project=my-project +``` + ## Deployment payload {#payload} When you deploy your ADK agent workflow to the Google Cloud Run, @@ -141,7 +179,7 @@ unless you specify it as deployment setting, such as the `--with_ui` option for * Enter `y` to allow public access to your agent's API endpoint without authentication. * Enter `N` (or press Enter for the default) to require authentication (e.g., using an identity token as shown in the "Testing your agent" section). - Upon successful execution, the command will deploy your agent to Cloud Run and provide the URL of the deployed service. + Upon successful execution, the command deploys your agent to Cloud Run and provide the URL of the deployed service. === "Python - gcloud CLI" @@ -376,6 +414,99 @@ unless you specify it as deployment setting, such as the `--with_ui` option for For a full list of deployment options, see the [`gcloud run deploy` reference documentation](https://cloud.google.com/sdk/gcloud/reference/run/deploy). +=== "Go - adkgo CLI" + + ### adk CLI + + The adkgo command is located in the google/adk-go repository under cmd/adkgo. Before using it, you need to build it from the root of the adk-go repository: + + `go build ./cmd/adkgo` + + The adkgo deploy cloudrun command automates the deployment of your application. You do not need to provide your own Dockerfile. + + #### Agent Code Structure + + When using the adkgo tool, your main.go file must use the launcher framework. This is because the tool compiles your code and then runs the resulting executable with specific command-line arguments (like web, api, a2a) to start the required services. The launcher is designed to parse these arguments correctly. + + Your main.go should look like this: + + ```go title="main.go" + --8<-- "examples/go/cloud-run/main.go" + ``` + + #### How it Works + 1. The adkgo tool compiles your main.go into a statically linked binary for Linux. + 2. It generates a Dockerfile that copies this binary into a minimal container. + 3. It uses gcloud to build and deploy this container to Cloud Run. + 4. After deployment, it starts a local proxy that securely connects to your new + service. + + Ensure you have authenticated with Google Cloud (`gcloud auth login` and `gcloud config set project `). + + #### Setup environment variables + + Optional but recommended: Setting environment variables can make the deployment commands cleaner. + + ```bash + # Set your Google Cloud Project ID + export GOOGLE_CLOUD_PROJECT="your-gcp-project-id" + + # Set your desired Google Cloud Location + export GOOGLE_CLOUD_LOCATION="us-central1" + + # Set the path to your agent's main Go file + export AGENT_PATH="./examples/go/cloud-run/main.go" + + # Set a name for your Cloud Run service + export SERVICE_NAME="capital-agent-service" + ``` + + #### Command usage + + ```bash + ./adkgo deploy cloudrun \ + -p $GOOGLE_CLOUD_PROJECT \ + -r $GOOGLE_CLOUD_LOCATION \ + -s $SERVICE_NAME \ + --proxy_port=8081 \ + --server_port=8080 \ + -e $AGENT_PATH \ + --a2a --api --webui + ``` + + ##### Arguments + + * -p, --project_name TEXT: (Required) Your Google Cloud project ID (e.g., + $GOOGLE_CLOUD_PROJECT). + * -r, --region TEXT: (Required) The Google Cloud location for deployment (e.g., + $GOOGLE_CLOUD_LOCATION, us-central1). + * -s, --service_name TEXT: (Required) The name for the Cloud Run service (e.g., + $SERVICE_NAME). + * -e, --entry_point_path TEXT: (Required) Path to the main Go file containing your + agent's source code (e.g., $AGENT_PATH). + + ##### Options + + * --proxy_port INTEGER: (Optional) The local port for the authenticating proxy to + listen on. Defaults to 8081. + * --server_port INTEGER: (Optional) The port number the server will listen on + within the Cloud Run container. Defaults to 8080. + * --a2a: (Optional) If included, enables Agent-to-Agent communication. Enabled by + default. + * --a2a_agent_url: (Optional) A2A agent card URL as advertised in the public agent card. This flag is only valid when + used with the --a2a flag. + * --api: (Optional) If included, deploys the ADK API server. Enabled by default. + * --webui: (Optional) If included, deploys the ADK dev UI alongside the agent API + server. Enabled by default. + * --temp_dir TEXT: (Optional) Temp directory for build artifacts. Defaults to + os.TempDir(). + * --help: Show the help message and exit. + + ##### Authenticated access + The service is deployed with --no-allow-unauthenticated by default. + + Upon successful execution, the command deploys your agent to Cloud Run and provide a local URL to access the service through the proxy. + ## Testing your agent Once your agent is deployed to Cloud Run, you can interact with it via the deployed UI (if enabled) or directly with its API endpoints using tools like `curl`. You'll need the service URL provided after deployment. @@ -386,7 +517,7 @@ Once your agent is deployed to Cloud Run, you can interact with it via the deplo If you deployed your agent with the UI enabled: - * **adk CLI:** You included the `--with_ui` flag during deployment. + * **adk CLI:** You included the `--webui` flag during deployment. * **gcloud CLI:** You set `SERVE_WEB_INTERFACE = True` in your `main.py`. You can test your agent by simply navigating to the Cloud Run service URL provided after deployment in your web browser. diff --git a/examples/go/cloud-run/main.go b/examples/go/cloud-run/main.go new file mode 100644 index 00000000..cbba5928 --- /dev/null +++ b/examples/go/cloud-run/main.go @@ -0,0 +1,101 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "fmt" + "log" + "os" + "strings" + + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/cmd/launcher/adk" + "google.golang.org/adk/cmd/launcher/full" + "google.golang.org/adk/server/restapi/services" + + "google.golang.org/adk/model/gemini" + "google.golang.org/adk/tool" + "google.golang.org/adk/tool/functiontool" + "google.golang.org/genai" +) + +type getCapitalCityArgs struct { + Country string `json:"country"` +} + +type getCapitalCityResult struct { + Result string `json:"result,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` +} + +func getCapitalCity(ctx tool.Context, args getCapitalCityArgs) getCapitalCityResult { + capitals := map[string]string{ + "united states": "Washington, D.C.", + "canada": "Ottawa", + "france": "Paris", + "japan": "Tokyo", + } + capital, ok := capitals[strings.ToLower(args.Country)] + if !ok { + result := fmt.Sprintf("Sorry, I couldn't find the capital for %s.", args.Country) + return getCapitalCityResult{ErrorMessage: result} + } + + return getCapitalCityResult{Result: capital} +} + +func main() { + ctx := context.Background() + + model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{ + APIKey: os.Getenv("GOOGLE_API_KEY"), + }) + if err != nil { + log.Fatalf("Failed to create model: %v", err) + } + + capitalTool, err := functiontool.New( + functiontool.Config{ + Name: "get_capital_city", + Description: "Retrieves the capital city for a given country.", + }, + getCapitalCity, + ) + if err != nil { + log.Fatalf("Failed to create function tool: %v", err) + } + + agent, err := llmagent.New(llmagent.Config{ + Name: "capital_agent", + Model: model, + Description: "Agent to find the capital city of a country.", + Instruction: "I can answer your questions about the capital city of a country.", + Tools: []tool.Tool{capitalTool}, + }) + if err != nil { + log.Fatalf("Failed to create agent: %v", err) + } + + config := &adk.Config{ + AgentLoader: services.NewSingleAgentLoader(agent), + } + + l := full.NewLauncher() + err = l.Execute(ctx, config, os.Args[1:]) + if err != nil { + log.Fatalf("run failed: %v\n\n%s", err, l.CommandLineSyntax()) + } +} From b3710469528c5d09386d21a6e84d49d7e86170bc Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Tue, 4 Nov 2025 01:11:38 +0000 Subject: [PATCH 094/125] adding go snippets to runconfig docs --- docs/runtime/runconfig.md | 159 +++++++++++++++++++++++++++++++------- 1 file changed, 132 insertions(+), 27 deletions(-) diff --git a/docs/runtime/runconfig.md b/docs/runtime/runconfig.md index 93c6f3c1..61bd7b37 100644 --- a/docs/runtime/runconfig.md +++ b/docs/runtime/runconfig.md @@ -21,16 +21,18 @@ The `RunConfig` class holds configuration parameters for an agent's runtime beha - Java ADK typically uses immutable data classes. +- Go ADK has mutable structs by default. + === "Python" ```python class RunConfig(BaseModel): """Configs for runtime behavior of agents.""" - + model_config = ConfigDict( extra='forbid', ) - + speech_config: Optional[types.SpeechConfig] = None response_modalities: Optional[list[str]] = None save_input_blobs_as_artifacts: bool = False @@ -44,38 +46,83 @@ The `RunConfig` class holds configuration parameters for an agent's runtime beha ```java public abstract class RunConfig { - + public enum StreamingMode { NONE, SSE, BIDI } - + public abstract @Nullable SpeechConfig speechConfig(); - + public abstract ImmutableList responseModalities(); - + public abstract boolean saveInputBlobsAsArtifacts(); - + public abstract @Nullable AudioTranscriptionConfig outputAudioTranscription(); - + public abstract int maxLlmCalls(); - + // ... } ``` +=== "Go" + + ```go + import "google.golang.org/genai" + + type StreamingMode string + + const ( + StreamingModeNone StreamingMode = "none" + StreamingModeSSE StreamingMode = "sse" + StreamingModeBidi StreamingMode = "bidi" + ) + + // RunConfig controls runtime behavior. + type RunConfig struct { + // Speech configuration for the live agent. + SpeechConfig *genai.SpeechConfig + // Output transcription for live agents with audio response. + OutputAudioTranscriptionConfig *genai.AudioTranscriptionConfig + // The output modalities. If not set, it defaults to AUDIO. + ResponseModalities []string + // Streaming mode, None or StreamingMode.SSE or StreamingMode.BIDI. + StreamingMode StreamingMode + // Whether or not to save the input blobs as artifacts + SaveInputBlobsAsArtifacts bool + + // Whether to support CFC (Compositional Function Calling). Only applicable for + // StreamingModeSSE. If it's true. the LIVE API will be invoked since only LIVE + // API supports CFC. + // + // .. warning:: + // This feature is **experimental** and its API or behavior may change + // in future releases. + SupportCFC bool + + // A limit on the total number of llm calls for a given run. + // + // Valid Values: + // - More than 0 and less than sys.maxsize: The bound on the number of llm + // calls is enforced, if the value is set in this range. + // - Less than or equal to 0: This allows for unbounded number of llm calls. + MaxLLMCalls int + } + ``` + ## Runtime Parameters -| Parameter | Python Type | Java Type | Default (Py / Java) | Description | -| :------------------------------ | :------------------------------------------- |:------------------------------------------------------|:----------------------------------|:-----------------------------------------------------------------------------------------------------------------------------| -| `speech_config` | `Optional[types.SpeechConfig]` | `SpeechConfig` (nullable via `@Nullable`) | `None` / `null` | Configures speech synthesis (voice, language) using the `SpeechConfig` type. | -| `response_modalities` | `Optional[list[str]]` | `ImmutableList` | `None` / Empty `ImmutableList` | List of desired output modalities (e.g., Python: `["TEXT", "AUDIO"]`; Java: uses structured `Modality` objects). | -| `save_input_blobs_as_artifacts` | `bool` | `boolean` | `False` / `false` | If `true`, saves input blobs (e.g., uploaded files) as run artifacts for debugging/auditing. | -| `streaming_mode` | `StreamingMode` | *Currently not supported* | `StreamingMode.NONE` / N/A | Sets the streaming behavior: `NONE` (default), `SSE` (server-sent events), or `BIDI` (bidirectional). | -| `output_audio_transcription` | `Optional[types.AudioTranscriptionConfig]` | `AudioTranscriptionConfig` (nullable via `@Nullable`) | `None` / `null` | Configures transcription of generated audio output using the `AudioTranscriptionConfig` type. | -| `max_llm_calls` | `int` | `int` | `500` / `500` | Limits total LLM calls per run. `0` or negative means unlimited (warned); `sys.maxsize` raises `ValueError`. | -| `support_cfc` | `bool` | *Currently not supported* | `False` / N/A | **Python:** Enables Compositional Function Calling. Requires `streaming_mode=SSE` and uses the LIVE API. **Experimental.** | +| Parameter | Python Type | Java Type | Go Type | Default (Py / Java / Go) | Description | +| :------------------------------ | :------------------------------------------- |:------------------------------------------------------|:--------------------------------------|:---------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------| +| `speech_config` | `Optional[types.SpeechConfig]` | `SpeechConfig` (nullable via `@Nullable`) | `*genai.SpeechConfig` | `None` / `null` / `nil` | Configures speech synthesis (voice, language) using the `SpeechConfig` type. | +| `response_modalities` | `Optional[list[str]]` | `ImmutableList` | `[]string` | `None` / Empty `ImmutableList` / `AUDIO` | List of desired output modalities (e.g., Python: `["TEXT", "AUDIO"]`; Java: uses structured `Modality` objects). | +| `save_input_blobs_as_artifacts` | `bool` | `boolean` | `bool` | `False` / `false` / `false` | If `true`, saves input blobs (e.g., uploaded files) as run artifacts for debugging/auditing. | +| `streaming_mode` | `StreamingMode` | `StreamingMode` | `StreamingMode` | `StreamingMode.NONE` / `StreamingMode.NONE` / `agent.StreamingModeNone` | Sets the streaming behavior: `NONE` (default), `SSE` (server-sent events), or `BIDI` (bidirectional). | +| `output_audio_transcription` | `Optional[types.AudioTranscriptionConfig]` | `AudioTranscriptionConfig` (nullable via `@Nullable`) | `*genai.AudioTranscriptionConfig` | `None` / `null` / `nil` | Configures transcription of generated audio output using the `AudioTranscriptionConfig` type. | +| `max_llm_calls` | `int` | `int` | `int` | `500` / `500` / `0` (unlimited) | Limits total LLM calls per run. `0` or negative means unlimited (warned); `sys.maxsize` raises `ValueError`. | +| `support_cfc` | `bool` | `bool` | `bool` | `False` / `false` / `false` | **Python/Go:** Enables Compositional Function Calling. Requires `streaming_mode=SSE` and uses the LIVE API. **Experimental.** | ### `speech_config` @@ -150,7 +197,7 @@ Enables Compositional Function Calling (CFC) support. Only applicable when using StreamingMode.SSE. When enabled, the LIVE API will be invoked as only it supports CFC functionality. -!!! example "Experimental release" +!!! example "Experimental release" The `support_cfc` feature is experimental and its API or behavior might change in future releases. @@ -199,7 +246,7 @@ For the `max_llm_calls` parameter specifically: ```python from google.genai.adk import RunConfig, StreamingMode - + config = RunConfig( streaming_mode=StreamingMode.NONE, max_llm_calls=100 @@ -211,13 +258,24 @@ For the `max_llm_calls` parameter specifically: ```java import com.google.adk.agents.RunConfig; import com.google.adk.agents.RunConfig.StreamingMode; - + RunConfig config = RunConfig.builder() .setStreamingMode(StreamingMode.NONE) .setMaxLlmCalls(100) .build(); ``` +=== "Go" + + ```go + import "google.golang.org/adk/agent" + + config := agent.RunConfig{ + StreamingMode: agent.StreamingModeNone, + MaxLLMCalls: 100, + } + ``` + This configuration creates a non-streaming agent with a limit of 100 LLM calls, suitable for simple task-oriented agents where complete responses are preferable. @@ -228,7 +286,7 @@ preferable. ```python from google.genai.adk import RunConfig, StreamingMode - + config = RunConfig( streaming_mode=StreamingMode.SSE, max_llm_calls=200 @@ -240,13 +298,24 @@ preferable. ```java import com.google.adk.agents.RunConfig; import com.google.adk.agents.RunConfig.StreamingMode; - + RunConfig config = RunConfig.builder() .setStreamingMode(StreamingMode.SSE) .setMaxLlmCalls(200) .build(); ``` +=== "Go" + + ```go + import "google.golang.org/adk/agent" + + config := agent.RunConfig{ + StreamingMode: agent.StreamingModeSSE, + MaxLLMCalls: 200, + } + ``` + Using SSE streaming allows users to see responses as they're generated, providing a more responsive feel for chatbots and assistants. @@ -257,7 +326,7 @@ providing a more responsive feel for chatbots and assistants. ```python from google.genai.adk import RunConfig, StreamingMode from google.genai import types - + config = RunConfig( speech_config=types.SpeechConfig( language_code="en-US", @@ -287,7 +356,7 @@ providing a more responsive feel for chatbots and assistants. import com.google.genai.types.PrebuiltVoiceConfig; import com.google.genai.types.SpeechConfig; import com.google.genai.types.VoiceConfig; - + RunConfig runConfig = RunConfig.builder() .setStreamingMode(StreamingMode.SSE) @@ -306,19 +375,43 @@ providing a more responsive feel for chatbots and assistants. .build(); ``` +=== "Go" + + ```go + import ( + "google.golang.org/adk/agent" + "google.golang.org/genai" + ) + + config := agent.RunConfig{ + SpeechConfig: &genai.SpeechConfig{ + LanguageCode: "en-US", + VoiceConfig: &genai.VoiceConfig{ + PrebuiltVoiceConfig: &genai.PrebuiltVoiceConfig{ + VoiceName: "Kore", + }, + }, + }, + ResponseModalities: []string{"AUDIO", "TEXT"}, + SaveInputBlobsAsArtifacts: true, + SupportCFC: true, + StreamingMode: agent.StreamingModeSSE, + MaxLLMCalls: 1000, + } + ``` + This comprehensive example configures an agent with: * Speech capabilities using the "Kore" voice (US English) * Both audio and text output modalities * Artifact saving for input blobs (useful for debugging) -* Experimental CFC support enabled **(Python only)** * SSE streaming for responsive interaction * A limit of 1000 LLM calls ### Enabling Experimental CFC Support
- Supported in ADKPython v0.1.0Experimental + Supported in ADKPython v0.1.0Go v0.1.0Experimental
```python @@ -331,6 +424,18 @@ config = RunConfig( ) ``` +=== "Go" + + ```go + import "google.golang.org/adk/agent" + + config := agent.RunConfig{ + StreamingMode: agent.StreamingModeSSE, + SupportCFC: true, + MaxLLMCalls: 150, + } + ``` + Enabling Compositional Function Calling creates an agent that can dynamically execute functions based on model outputs, powerful for applications requiring complex workflows. From c11472d88823c625a12b9a009149182f99c7adf4 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Wed, 5 Nov 2025 01:23:38 +0000 Subject: [PATCH 095/125] realigning runconfig go examples with current state of adk-go. --- docs/runtime/runconfig.md | 102 ++++++++++++++------------------------ 1 file changed, 36 insertions(+), 66 deletions(-) diff --git a/docs/runtime/runconfig.md b/docs/runtime/runconfig.md index 61bd7b37..07e456e6 100644 --- a/docs/runtime/runconfig.md +++ b/docs/runtime/runconfig.md @@ -70,8 +70,6 @@ The `RunConfig` class holds configuration parameters for an agent's runtime beha === "Go" ```go - import "google.golang.org/genai" - type StreamingMode string const ( @@ -82,12 +80,6 @@ The `RunConfig` class holds configuration parameters for an agent's runtime beha // RunConfig controls runtime behavior. type RunConfig struct { - // Speech configuration for the live agent. - SpeechConfig *genai.SpeechConfig - // Output transcription for live agents with audio response. - OutputAudioTranscriptionConfig *genai.AudioTranscriptionConfig - // The output modalities. If not set, it defaults to AUDIO. - ResponseModalities []string // Streaming mode, None or StreamingMode.SSE or StreamingMode.BIDI. StreamingMode StreamingMode // Whether or not to save the input blobs as artifacts @@ -101,31 +93,26 @@ The `RunConfig` class holds configuration parameters for an agent's runtime beha // This feature is **experimental** and its API or behavior may change // in future releases. SupportCFC bool - - // A limit on the total number of llm calls for a given run. - // - // Valid Values: - // - More than 0 and less than sys.maxsize: The bound on the number of llm - // calls is enforced, if the value is set in this range. - // - Less than or equal to 0: This allows for unbounded number of llm calls. - MaxLLMCalls int } ``` ## Runtime Parameters -| Parameter | Python Type | Java Type | Go Type | Default (Py / Java / Go) | Description | -| :------------------------------ | :------------------------------------------- |:------------------------------------------------------|:--------------------------------------|:---------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------| -| `speech_config` | `Optional[types.SpeechConfig]` | `SpeechConfig` (nullable via `@Nullable`) | `*genai.SpeechConfig` | `None` / `null` / `nil` | Configures speech synthesis (voice, language) using the `SpeechConfig` type. | -| `response_modalities` | `Optional[list[str]]` | `ImmutableList` | `[]string` | `None` / Empty `ImmutableList` / `AUDIO` | List of desired output modalities (e.g., Python: `["TEXT", "AUDIO"]`; Java: uses structured `Modality` objects). | -| `save_input_blobs_as_artifacts` | `bool` | `boolean` | `bool` | `False` / `false` / `false` | If `true`, saves input blobs (e.g., uploaded files) as run artifacts for debugging/auditing. | -| `streaming_mode` | `StreamingMode` | `StreamingMode` | `StreamingMode` | `StreamingMode.NONE` / `StreamingMode.NONE` / `agent.StreamingModeNone` | Sets the streaming behavior: `NONE` (default), `SSE` (server-sent events), or `BIDI` (bidirectional). | -| `output_audio_transcription` | `Optional[types.AudioTranscriptionConfig]` | `AudioTranscriptionConfig` (nullable via `@Nullable`) | `*genai.AudioTranscriptionConfig` | `None` / `null` / `nil` | Configures transcription of generated audio output using the `AudioTranscriptionConfig` type. | -| `max_llm_calls` | `int` | `int` | `int` | `500` / `500` / `0` (unlimited) | Limits total LLM calls per run. `0` or negative means unlimited (warned); `sys.maxsize` raises `ValueError`. | -| `support_cfc` | `bool` | `bool` | `bool` | `False` / `false` / `false` | **Python/Go:** Enables Compositional Function Calling. Requires `streaming_mode=SSE` and uses the LIVE API. **Experimental.** | +| Parameter | Python Type | Java Type | Go Type | Default (Py / Java / Go) | Description | +| :------------------------------ | :------------------------------------------- |:------------------------------------------------------|:----------------|:---------------------------------------|:-----------------------------------------------------------------------------------------------------------------------------| +| `speech_config` | `Optional[types.SpeechConfig]` | `SpeechConfig` (nullable via `@Nullable`) | N/A | `None` / `null` / N/A | Configures speech synthesis (voice, language) using the `SpeechConfig` type. | +| `response_modalities` | `Optional[list[str]]` | `ImmutableList` | N/A | `None` / Empty `ImmutableList` / N/A | List of desired output modalities (e.g., Python: `["TEXT", "AUDIO"]`; Java: uses structured `Modality` objects). | +| `save_input_blobs_as_artifacts` | `bool` | `boolean` | `bool` | `False` / `false` / `false` | If `true`, saves input blobs (e.g., uploaded files) as run artifacts for debugging/auditing. | +| `streaming_mode` | `StreamingMode` | `StreamingMode` | `StreamingMode` | `StreamingMode.NONE` / `StreamingMode.NONE` / `agent.StreamingModeNone` | Sets the streaming behavior: `NONE` (default), `SSE` (server-sent events), or `BIDI` (bidirectional). | +| `output_audio_transcription` | `Optional[types.AudioTranscriptionConfig]` | `AudioTranscriptionConfig` (nullable via `@Nullable`) | N/A | `None` / `null` / N/A | Configures transcription of generated audio output using the `AudioTranscriptionConfig` type. | +| `max_llm_calls` | `int` | `int` | N/A | `500` / `500` / N/A | Limits total LLM calls per run. `0` or negative means unlimited (warned); `sys.maxsize` raises `ValueError`. | +| `support_cfc` | `bool` | `bool` | `bool` | `False` / `false` / `false` | **Python/Go:** Enables Compositional Function Calling. Requires `streaming_mode=SSE` and uses the LIVE API. **Experimental.** | ### `speech_config` +!!! note "Go ADK" + This feature is not available in the Go ADK. + !!! Note The interface or definition of `SpeechConfig` is the same, irrespective of the language. @@ -181,6 +168,9 @@ how your agent sounds when speaking. ### `response_modalities` +!!! note "Go ADK" + This feature is not available in the Go ADK. + Defines the output modalities for the agent. If not set, defaults to AUDIO. Response modalities determine how the agent communicates with users through various channels (e.g., text, audio). @@ -214,12 +204,18 @@ Streaming modes affect both performance and user experience. SSE streaming lets ### `output_audio_transcription` +!!! note "Go ADK" + This feature is not available in the Go ADK. + Configuration for transcribing audio outputs from live agents with audio response capability. This enables automatic transcription of audio responses for accessibility, record-keeping, and multi-modal applications. ### `max_llm_calls` +!!! note "Go ADK" + This feature is not available in the Go ADK. + Sets a limit on the total number of LLM calls for a given agent run. * Values greater than 0 and less than `sys.maxsize`: Enforces a bound on LLM calls @@ -272,7 +268,6 @@ For the `max_llm_calls` parameter specifically: config := agent.RunConfig{ StreamingMode: agent.StreamingModeNone, - MaxLLMCalls: 100, } ``` @@ -312,7 +307,6 @@ preferable. config := agent.RunConfig{ StreamingMode: agent.StreamingModeSSE, - MaxLLMCalls: 200, } ``` @@ -375,30 +369,7 @@ providing a more responsive feel for chatbots and assistants. .build(); ``` -=== "Go" - - ```go - import ( - "google.golang.org/adk/agent" - "google.golang.org/genai" - ) - config := agent.RunConfig{ - SpeechConfig: &genai.SpeechConfig{ - LanguageCode: "en-US", - VoiceConfig: &genai.VoiceConfig{ - PrebuiltVoiceConfig: &genai.PrebuiltVoiceConfig{ - VoiceName: "Kore", - }, - }, - }, - ResponseModalities: []string{"AUDIO", "TEXT"}, - SaveInputBlobsAsArtifacts: true, - SupportCFC: true, - StreamingMode: agent.StreamingModeSSE, - MaxLLMCalls: 1000, - } - ``` This comprehensive example configures an agent with: @@ -414,27 +385,26 @@ This comprehensive example configures an agent with: Supported in ADKPython v0.1.0Go v0.1.0Experimental
-```python -from google.genai.adk import RunConfig, StreamingMode + ```python + from google.genai.adk import RunConfig, StreamingMode -config = RunConfig( - streaming_mode=StreamingMode.SSE, - support_cfc=True, - max_llm_calls=150 -) -``` + config = RunConfig( + streaming_mode=StreamingMode.SSE, + support_cfc=True, + max_llm_calls=150 + ) + ``` === "Go" - ```go - import "google.golang.org/adk/agent" + ```go + import "google.golang.org/adk/agent" - config := agent.RunConfig{ - StreamingMode: agent.StreamingModeSSE, - SupportCFC: true, - MaxLLMCalls: 150, - } - ``` + config := agent.RunConfig{ + StreamingMode: agent.StreamingModeSSE, + SupportCFC: true, + } + ``` Enabling Compositional Function Calling creates an agent that can dynamically execute functions based on model outputs, powerful for applications requiring From 3f0fd7944d06e6cfa67390ac6bec807dfd88f99e Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Wed, 5 Nov 2025 20:48:42 +0000 Subject: [PATCH 096/125] adding language support tags to individual runtime config fields --- docs/runtime/runconfig.md | 32 ++++++++++++++++++++++++-------- 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/runtime/runconfig.md b/docs/runtime/runconfig.md index 07e456e6..b38ff71b 100644 --- a/docs/runtime/runconfig.md +++ b/docs/runtime/runconfig.md @@ -110,8 +110,9 @@ The `RunConfig` class holds configuration parameters for an agent's runtime beha ### `speech_config` -!!! note "Go ADK" - This feature is not available in the Go ADK. +
+ Supported in ADKPython v0.1.0Java v0.1.0 +
!!! Note The interface or definition of `SpeechConfig` is the same, irrespective of the language. @@ -168,8 +169,9 @@ how your agent sounds when speaking. ### `response_modalities` -!!! note "Go ADK" - This feature is not available in the Go ADK. +
+ Supported in ADKPython v0.1.0Java v0.1.0 +
Defines the output modalities for the agent. If not set, defaults to AUDIO. Response modalities determine how the agent communicates with users through @@ -177,12 +179,20 @@ various channels (e.g., text, audio). ### `save_input_blobs_as_artifacts` +
+ Supported in ADKPython v0.1.0Java v0.1.0Go v0.1.0 +
+ When enabled, input blobs will be saved as artifacts during agent execution. This is useful for debugging and audit purposes, allowing developers to review the exact data received by agents. ### `support_cfc` +
+ Supported in ADKPython v0.1.0Go v0.1.0Experimental +
+ Enables Compositional Function Calling (CFC) support. Only applicable when using StreamingMode.SSE. When enabled, the LIVE API will be invoked as only it supports CFC functionality. @@ -194,6 +204,10 @@ supports CFC functionality. ### `streaming_mode` +
+ Supported in ADKPython v0.1.0Go v0.1.0 +
+ Configures the streaming behavior of the agent. Possible values: * `StreamingMode.NONE`: No streaming; responses delivered as complete units @@ -204,8 +218,9 @@ Streaming modes affect both performance and user experience. SSE streaming lets ### `output_audio_transcription` -!!! note "Go ADK" - This feature is not available in the Go ADK. +
+ Supported in ADKPython v0.1.0Java v0.1.0 +
Configuration for transcribing audio outputs from live agents with audio response capability. This enables automatic transcription of audio responses for @@ -213,8 +228,9 @@ accessibility, record-keeping, and multi-modal applications. ### `max_llm_calls` -!!! note "Go ADK" - This feature is not available in the Go ADK. +
+ Supported in ADKPython v0.1.0Java v0.1.0 +
Sets a limit on the total number of LLM calls for a given agent run. From 5dcb277eeeb24ea593887d676eeccb3a60e446d6 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Wed, 5 Nov 2025 21:11:52 +0000 Subject: [PATCH 097/125] removing unsupported supportcfc go examples --- docs/runtime/runconfig.md | 26 +++----------------------- 1 file changed, 3 insertions(+), 23 deletions(-) diff --git a/docs/runtime/runconfig.md b/docs/runtime/runconfig.md index b38ff71b..149d3410 100644 --- a/docs/runtime/runconfig.md +++ b/docs/runtime/runconfig.md @@ -84,15 +84,6 @@ The `RunConfig` class holds configuration parameters for an agent's runtime beha StreamingMode StreamingMode // Whether or not to save the input blobs as artifacts SaveInputBlobsAsArtifacts bool - - // Whether to support CFC (Compositional Function Calling). Only applicable for - // StreamingModeSSE. If it's true. the LIVE API will be invoked since only LIVE - // API supports CFC. - // - // .. warning:: - // This feature is **experimental** and its API or behavior may change - // in future releases. - SupportCFC bool } ``` @@ -106,7 +97,7 @@ The `RunConfig` class holds configuration parameters for an agent's runtime beha | `streaming_mode` | `StreamingMode` | `StreamingMode` | `StreamingMode` | `StreamingMode.NONE` / `StreamingMode.NONE` / `agent.StreamingModeNone` | Sets the streaming behavior: `NONE` (default), `SSE` (server-sent events), or `BIDI` (bidirectional). | | `output_audio_transcription` | `Optional[types.AudioTranscriptionConfig]` | `AudioTranscriptionConfig` (nullable via `@Nullable`) | N/A | `None` / `null` / N/A | Configures transcription of generated audio output using the `AudioTranscriptionConfig` type. | | `max_llm_calls` | `int` | `int` | N/A | `500` / `500` / N/A | Limits total LLM calls per run. `0` or negative means unlimited (warned); `sys.maxsize` raises `ValueError`. | -| `support_cfc` | `bool` | `bool` | `bool` | `False` / `false` / `false` | **Python/Go:** Enables Compositional Function Calling. Requires `streaming_mode=SSE` and uses the LIVE API. **Experimental.** | +| `support_cfc` | `bool` | `bool` | N/A | `False` / `false` / N/A | **Python:** Enables Compositional Function Calling. Requires `streaming_mode=SSE` and uses the LIVE API. **Experimental.** | ### `speech_config` @@ -190,7 +181,7 @@ the exact data received by agents. ### `support_cfc`
- Supported in ADKPython v0.1.0Go v0.1.0Experimental + Supported in ADKPython v0.1.0Experimental
Enables Compositional Function Calling (CFC) support. Only applicable when using @@ -398,7 +389,7 @@ This comprehensive example configures an agent with: ### Enabling Experimental CFC Support
- Supported in ADKPython v0.1.0Go v0.1.0Experimental + Supported in ADKPython v0.1.0Experimental
```python @@ -411,17 +402,6 @@ This comprehensive example configures an agent with: ) ``` -=== "Go" - - ```go - import "google.golang.org/adk/agent" - - config := agent.RunConfig{ - StreamingMode: agent.StreamingModeSSE, - SupportCFC: true, - } - ``` - Enabling Compositional Function Calling creates an agent that can dynamically execute functions based on model outputs, powerful for applications requiring complex workflows. From ba8e54c109dac0880733b92b83afdebf102ff705 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Wed, 5 Nov 2025 21:27:29 +0000 Subject: [PATCH 098/125] removing unsupported streamingmode.bidi go snippets --- docs/runtime/runconfig.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/runtime/runconfig.md b/docs/runtime/runconfig.md index 149d3410..92bfc04e 100644 --- a/docs/runtime/runconfig.md +++ b/docs/runtime/runconfig.md @@ -75,12 +75,11 @@ The `RunConfig` class holds configuration parameters for an agent's runtime beha const ( StreamingModeNone StreamingMode = "none" StreamingModeSSE StreamingMode = "sse" - StreamingModeBidi StreamingMode = "bidi" ) // RunConfig controls runtime behavior. type RunConfig struct { - // Streaming mode, None or StreamingMode.SSE or StreamingMode.BIDI. + // Streaming mode, None or StreamingMode.SSE. StreamingMode StreamingMode // Whether or not to save the input blobs as artifacts SaveInputBlobsAsArtifacts bool @@ -94,7 +93,7 @@ The `RunConfig` class holds configuration parameters for an agent's runtime beha | `speech_config` | `Optional[types.SpeechConfig]` | `SpeechConfig` (nullable via `@Nullable`) | N/A | `None` / `null` / N/A | Configures speech synthesis (voice, language) using the `SpeechConfig` type. | | `response_modalities` | `Optional[list[str]]` | `ImmutableList` | N/A | `None` / Empty `ImmutableList` / N/A | List of desired output modalities (e.g., Python: `["TEXT", "AUDIO"]`; Java: uses structured `Modality` objects). | | `save_input_blobs_as_artifacts` | `bool` | `boolean` | `bool` | `False` / `false` / `false` | If `true`, saves input blobs (e.g., uploaded files) as run artifacts for debugging/auditing. | -| `streaming_mode` | `StreamingMode` | `StreamingMode` | `StreamingMode` | `StreamingMode.NONE` / `StreamingMode.NONE` / `agent.StreamingModeNone` | Sets the streaming behavior: `NONE` (default), `SSE` (server-sent events), or `BIDI` (bidirectional). | +| `streaming_mode` | `StreamingMode` | `StreamingMode` | `StreamingMode` | `StreamingMode.NONE` / `StreamingMode.NONE` / `agent.StreamingModeNone` | Sets the streaming behavior: `NONE` (default), `SSE` (server-sent events), or `BIDI` (bidirectional) (**Python/Java**). | | `output_audio_transcription` | `Optional[types.AudioTranscriptionConfig]` | `AudioTranscriptionConfig` (nullable via `@Nullable`) | N/A | `None` / `null` / N/A | Configures transcription of generated audio output using the `AudioTranscriptionConfig` type. | | `max_llm_calls` | `int` | `int` | N/A | `500` / `500` / N/A | Limits total LLM calls per run. `0` or negative means unlimited (warned); `sys.maxsize` raises `ValueError`. | | `support_cfc` | `bool` | `bool` | N/A | `False` / `false` / N/A | **Python:** Enables Compositional Function Calling. Requires `streaming_mode=SSE` and uses the LIVE API. **Experimental.** | From c5c82e659c3def675e005b100034436c5164fafa Mon Sep 17 00:00:00 2001 From: Ivan Cheung Date: Wed, 5 Nov 2025 17:53:10 -0500 Subject: [PATCH 099/125] Added AgentTool examples (#31) * Added AgentTool example * Ran formatter --------- Co-authored-by: ivanmkc --- docs/tools/function-tools.md | 42 ++++++++++ .../tools/function-tools/func_tool.go | 82 +++++++++++++++++-- 2 files changed, 119 insertions(+), 5 deletions(-) diff --git a/docs/tools/function-tools.md b/docs/tools/function-tools.md index 586efcc6..1147e984 100644 --- a/docs/tools/function-tools.md +++ b/docs/tools/function-tools.md @@ -193,6 +193,17 @@ A tool can write data to a `temp:` variable, and a subsequent tool can read it. This tool retrieves the mocked value of a stock price. ```go + import ( + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/model/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/session" + "google.golang.org/adk/tool" + "google.golang.org/adk/tool/functiontool" + "google.golang.org/genai" + ) + --8<-- "examples/go/snippets/tools/function-tools/func_tool.go" ``` @@ -298,6 +309,15 @@ Define your tool function and wrap it using the `LongRunningFunctionTool` class: === "Go" ```go + import ( + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/model/gemini" + "google.golang.org/adk/tool" + "google.golang.org/adk/tool/functiontool" + "google.golang.org/genai" + ) + --8<-- "examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go:create_long_running_tool" ``` @@ -413,6 +433,13 @@ To use an agent as a tool, wrap the agent with the AgentTool class. AgentTool.create(agent) ``` +=== "Go" + + ```go + agenttool.New(agent, &agenttool.Config{...}) + ``` + + ### Customization The `AgentTool` class provides the following attributes for customizing its behavior: @@ -433,6 +460,21 @@ The `AgentTool` class provides the following attributes for customizing its beha --8<-- "examples/java/snippets/src/main/java/tools/AgentToolCustomization.java:full_code" ``` + === "Go" + + ```go + import ( + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/model/gemini" + "google.golang.org/adk/tool" + "google.golang.org/adk/tool/agenttool" + "google.golang.org/genai" + ) + + --8<-- "examples/go/snippets/tools/function-tools/func_tool.go:agent_tool_example" + ``` + ### How it works 1. When the `main_agent` receives the long text, its instruction tells it to use the 'summarize' tool for long texts. diff --git a/examples/go/snippets/tools/function-tools/func_tool.go b/examples/go/snippets/tools/function-tools/func_tool.go index 8b74921d..4238b710 100644 --- a/examples/go/snippets/tools/function-tools/func_tool.go +++ b/examples/go/snippets/tools/function-tools/func_tool.go @@ -12,6 +12,7 @@ import ( "google.golang.org/adk/runner" "google.golang.org/adk/session" "google.golang.org/adk/tool" + "google.golang.org/adk/tool/agenttool" "google.golang.org/adk/tool/functiontool" "google.golang.org/genai" @@ -74,10 +75,9 @@ func createStockAgent(ctx context.Context) (agent.Agent, error) { log.Fatalf("Failed to create model: %v", err) } - return llmagent.New(llmagent.Config{ - Name: "stock_agent", - Model: model, + Name: "stock_agent", + Model: model, Instruction: "You are an agent who retrieves stock prices. If a ticker symbol is provided, fetch the current price. If only a company name is given, first perform a Google search to find the correct ticker symbol before retrieving the stock price. If the provided ticker symbol is invalid or data cannot be retrieved, inform the user that the stock price could not be found.", Description: "This agent specializes in retrieving real-time stock prices. Given a stock ticker symbol (e.g., AAPL, GOOG, MSFT) or the stock name, use the tools and reliable data sources to provide the most up-to-date price.", Tools: []tool.Tool{ @@ -131,11 +131,11 @@ func callAgent(ctx context.Context, a agent.Agent, prompt string) { } for event, err := range r.Run(ctx, userID, sessionID, userMsg, agent.RunConfig{ - StreamingMode: agent.StreamingModeSSE, + StreamingMode: agent.StreamingModeNone, }) { if err != nil { fmt.Printf("\nAGENT_ERROR: %v\n", err) - } else if event.Partial { + } else { for _, p := range event.Content.Parts { fmt.Print(p.Text) } @@ -171,7 +171,79 @@ func RunAgentSimulation() { } } +// --8<-- [start:agent_tool_example] +// createSummarizerAgent creates an agent whose sole purpose is to summarize text. +func createSummarizerAgent(ctx context.Context) (agent.Agent, error) { + model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{}) + if err != nil { + return nil, err + } + return llmagent.New(llmagent.Config{ + Name: "SummarizerAgent", + Model: model, + Instruction: "You are an expert at summarizing text. Take the user's input and provide a concise summary.", + Description: "An agent that summarizes text.", + }) +} + +// createMainAgent creates the primary agent that will use the summarizer agent as a tool. +func createMainAgent(ctx context.Context, tools ...tool.Tool) (agent.Agent, error) { + model, err := gemini.NewModel(ctx, "gemini-2.5-flash", &genai.ClientConfig{}) + if err != nil { + return nil, err + } + return llmagent.New(llmagent.Config{ + Name: "MainAgent", + Model: model, + Instruction: "You are a helpful assistant. If you are asked to summarize a long text, use the 'summarize' tool. " + + "After getting the summary, present it to the user by saying 'Here is a summary of the text:'.", + Description: "The main agent that can delegate tasks.", + Tools: tools, + }) +} + +func RunAgentAsToolSimulation() { + ctx := context.Background() + + // 1. Create the Tool Agent (Summarizer) + summarizerAgent, err := createSummarizerAgent(ctx) + if err != nil { + log.Fatalf("Failed to create summarizer agent: %v", err) + } + + // 2. Wrap the Tool Agent in an AgentTool + summarizeTool := agenttool.New(summarizerAgent, &agenttool.Config{ + SkipSummarization: true, + }) + + // 3. Create the Main Agent and provide it with the AgentTool + mainAgent, err := createMainAgent(ctx, summarizeTool) + if err != nil { + log.Fatalf("Failed to create main agent: %v", err) + } + + // 4. Run the main agent + prompt := ` + Please summarize this text for me: + Quantum computing represents a fundamentally different approach to computation, + leveraging the bizarre principles of quantum mechanics to process information. Unlike classical computers + that rely on bits representing either 0 or 1, quantum computers use qubits which can exist in a state of superposition - effectively + being 0, 1, or a combination of both simultaneously. Furthermore, qubits can become entangled, + meaning their fates are intertwined regardless of distance, allowing for complex correlations. This parallelism and + interconnectedness grant quantum computers the potential to solve specific types of incredibly complex problems - such + as drug discovery, materials science, complex system optimization, and breaking certain types of cryptography - far + faster than even the most powerful classical supercomputers could ever achieve, although the technology is still largely in its developmental stages. + ` + fmt.Printf("\nPrompt: %s\nResponse: ", prompt) + callAgent(context.Background(), mainAgent, prompt) + fmt.Println("\n---") +} + +// --8<-- [end:agent_tool_example] + func main() { fmt.Println("Attempting to run the agent simulation...") RunAgentSimulation() + fmt.Println("\nAttempting to run the agent-as-a-tool simulation...") + RunAgentAsToolSimulation() } From fde04934cc432500aa3e62911b585c8a174c2910 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Thu, 16 Oct 2025 22:59:27 +0000 Subject: [PATCH 100/125] adding initial go sessions.sessions snippets --- docs/sessions/session.md | 18 +++ .../session_management_example/go.mod | 35 ++++++ .../session_management_example/go.sum | 73 ++++++++++++ .../session_management_example.go | 104 ++++++++++++++++++ 4 files changed, 230 insertions(+) create mode 100644 examples/go/snippets/sessions/session_management_example/go.mod create mode 100644 examples/go/snippets/sessions/session_management_example/go.sum create mode 100644 examples/go/snippets/sessions/session_management_example/session_management_example.go diff --git a/docs/sessions/session.md b/docs/sessions/session.md index b038358a..1f0f9167 100644 --- a/docs/sessions/session.md +++ b/docs/sessions/session.md @@ -94,6 +94,12 @@ are its key properties: var unused = exampleSessionService.deleteSession(appName, userId, sessionId); ``` +=== "Go" + + ```go + // --8<-- "examples/go/snippets/sessions/session_management_example/session_management_example.go:examine_session" + ``` + *(**Note:** The state shown above is only the initial state. State updates happen via events, as discussed in the State section.)* @@ -145,6 +151,12 @@ the storage backend that best suits your needs: InMemorySessionService exampleSessionService = new InMemorySessionService(); ``` + === "Go" + + ```go + // --8<-- "examples/go/snippets/sessions/session_management_example/session_management_example.go:in_memory_service" + ``` + 2. **`VertexAiSessionService`** * **How it works:** Uses Google Cloud Vertex AI infrastructure via API @@ -202,6 +214,12 @@ the storage backend that best suits your needs: .blockingGet(); ``` + === "Go" + + ```go + // --8<-- "examples/go/snippets/sessions/session_management_example/session_management_example.go:vertexai_service" + ``` + 3. **`DatabaseSessionService`**
diff --git a/examples/go/snippets/sessions/session_management_example/go.mod b/examples/go/snippets/sessions/session_management_example/go.mod new file mode 100644 index 00000000..39db7102 --- /dev/null +++ b/examples/go/snippets/sessions/session_management_example/go.mod @@ -0,0 +1,35 @@ +module session_management_example + +go 1.24.7 + +require google.golang.org/adk v0.0.0-20251015134637-7cc254810953 + +require ( + cloud.google.com/go v0.121.6 // indirect + cloud.google.com/go/auth v0.16.5 // indirect + cloud.google.com/go/compute/metadata v0.8.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.36.0 // indirect + go.opentelemetry.io/otel/metric v1.36.0 // indirect + go.opentelemetry.io/otel/trace v1.36.0 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.29.0 // indirect + google.golang.org/genai v1.20.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect + google.golang.org/grpc v1.75.0-dev // indirect + google.golang.org/protobuf v1.36.10 // indirect + rsc.io/omap v1.2.0 // indirect + rsc.io/ordered v1.1.1 // indirect +) diff --git a/examples/go/snippets/sessions/session_management_example/go.sum b/examples/go/snippets/sessions/session_management_example/go.sum new file mode 100644 index 00000000..9fefded0 --- /dev/null +++ b/examples/go/snippets/sessions/session_management_example/go.sum @@ -0,0 +1,73 @@ +cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= +cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= +cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI= +cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= +cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA= +cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +google.golang.org/adk v0.0.0-20251015134637-7cc254810953 h1:RGIoN4hXlJchW5mef+6+J+Ym9/FwTeF7WC70LonFKxU= +google.golang.org/adk v0.0.0-20251015134637-7cc254810953/go.mod h1:hfcsmlJhB3RRqBmoiLDZ1PwxbIcjtJIamk70gPp6GcE= +google.golang.org/genai v1.20.0 h1:nmDZSJjXwBvSXcdOohz7pzTVGP9yuNITY8kZ2Ta24xY= +google.golang.org/genai v1.20.0/go.mod h1:QPj5NGJw+3wEOHg+PrsWwJKvG6UC84ex5FR7qAYsN/M= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/grpc v1.75.0-dev h1:3GnKkkh9RI6YGGw8/Zu3WDlX4+lexwzdKZlrtlo9RCc= +google.golang.org/grpc v1.75.0-dev/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/omap v1.2.0 h1:c1M8jchnHbzmJALzGLclfH3xDWXrPxSUHXzH5C+8Kdw= +rsc.io/omap v1.2.0/go.mod h1:C8pkI0AWexHopQtZX+qiUeJGzvc8HkdgnsWK4/mAa00= +rsc.io/ordered v1.1.1 h1:1kZM6RkTmceJgsFH/8DLQvkCVEYomVDJfBRLT595Uak= +rsc.io/ordered v1.1.1/go.mod h1:evAi8739bWVBRG9aaufsjVc202+6okf8u2QeVL84BCM= diff --git a/examples/go/snippets/sessions/session_management_example/session_management_example.go b/examples/go/snippets/sessions/session_management_example/session_management_example.go new file mode 100644 index 00000000..7a134283 --- /dev/null +++ b/examples/go/snippets/sessions/session_management_example/session_management_example.go @@ -0,0 +1,104 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "fmt" + "log" + + "google.golang.org/adk/session" +) + +// This example demonstrates session management in the Go ADK, covering: +// 1. Initializing different SessionService implementations. +// 2. Creating a session and examining its properties. + +func main() { + ctx := context.Background() + + // --- SessionService Implementations --- + + // --8<-- [start:in_memory_service] + // 1. InMemorySessionService + // Stores all session data directly in the application's memory. + // All conversation data is lost if the application restarts. + inMemoryService := session.InMemoryService() + fmt.Println("Initialized InMemorySessionService.") + // --8<-- [end:in_memory_service] + + // --8<-- [start:vertexai_service] + // 2. VertexAiSessionService + // Uses Google Cloud Vertex AI for persistent, scalable session management. + // Requires a Google Cloud project and an Agent Engine ID. + // Before running, ensure your environment is authenticated and variables are set: + // export GOOGLE_API_KEY="your-express-mode-api-key" + // export GOOGLE_CLOUD_PROJECT="your-gcp-project-id" + // export GOOGLE_CLOUD_LOCATION="your-gcp-location" + agentEngineID := "your-reasoning-engine-id" // Replace with your actual Agent Engine ID + vertexService, err := session.VertexAIService(ctx, agentEngineID) + if err != nil { + log.Printf("Could not initialize VertexAiSessionService (this is expected if GOOGLE_API_KEY is not set): %v", err) + } else { + fmt.Println("Successfully initialized VertexAiSessionService.") + } + // --8<-- [end:vertexai_service] + _ = vertexService // Avoid unused variable error if initialization fails. + + // --- Examining Session Properties --- + // We'll use the InMemorySessionService for this demonstration. + // --8<-- [start:examine_session] + appName := "my_go_app" + userID := "example_go_user" + initialState := map[string]any{"initial_key": "initial_value"} + + // Create a session to examine its properties. + createResp, err := inMemoryService.Create(ctx, &session.CreateRequest{ + AppName: appName, + UserID: userID, + State: initialState, + }) + if err != nil { + log.Fatalf("Failed to create session: %v", err) + } + exampleSession := createResp.Session + + fmt.Println("\n--- Examining Session Properties ---") + fmt.Printf("ID (`ID()`): %s\n", exampleSession.ID()) + fmt.Printf("Application Name (`AppName()`): %s\n", exampleSession.AppName()) + fmt.Printf("User ID (`UserID()`): %s\n", exampleSession.UserID()) + + // To access state, you get a state object and then call Get(). + stateObj := exampleSession.State() + val, _ := stateObj.Get("initial_key") + fmt.Printf("State (`State().Get()`): initial_key = %v\n", val) + + // Events are initially empty. + fmt.Printf("Events (`Events().Len()`): %d\n", exampleSession.Events().Len()) + fmt.Printf("Last Update (`LastUpdateTime()`): %s\n", exampleSession.LastUpdateTime().Format("2006-01-02 15:04:05")) + fmt.Println("---------------------------------") + + // Clean up the session. + err = inMemoryService.Delete(ctx, &session.DeleteRequest{ + AppName: exampleSession.AppName(), + UserID: exampleSession.UserID(), + SessionID: exampleSession.ID(), + }) + if err != nil { + log.Fatalf("Failed to delete session: %v", err) + } + fmt.Println("Session deleted successfully.") + // --8<-- [end:examine_session] +} From f2ae8b8f0bdda7404da9cbfd2bb8a67dbd94ae99 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Tue, 4 Nov 2025 04:50:49 +0000 Subject: [PATCH 101/125] updating go session docs + session examples --- docs/sessions/session.md | 30 ++++---- .../session_management_example/go.sum | 73 ------------------- .../session_management_example.go | 22 +++--- 3 files changed, 26 insertions(+), 99 deletions(-) delete mode 100644 examples/go/snippets/sessions/session_management_example/go.sum diff --git a/docs/sessions/session.md b/docs/sessions/session.md index 1f0f9167..3ddca55f 100644 --- a/docs/sessions/session.md +++ b/docs/sessions/session.md @@ -20,7 +20,7 @@ are its key properties: * **Identification (`id`, `appName`, `userId`):** Unique labels for the conversation. * `id`: A unique identifier for *this specific* conversation thread, essential for retrieving it later. A SessionService object can handle multiple `Session`(s). This field identifies which particular session object are we referring to. For example, "test_id_modification". - * `app_name`: Identifies which agent application this conversation belongs to. For example, "id_modifier_workflow". + * `app_name`: Identifies which agent application this conversation belongs to. For example, "id_modifier_workflow". * `userId`: Links the conversation to a particular user. * **History (`events`):** A chronological sequence of all interactions (`Event` objects – user messages, agent responses, tool actions) that have @@ -39,7 +39,7 @@ are its key properties: ```py from google.adk.sessions import InMemorySessionService, Session - + # Create a simple session to examine its properties temp_service = InMemorySessionService() example_session = await temp_service.create_session( @@ -70,26 +70,26 @@ are its key properties: import com.google.adk.sessions.Session; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.ConcurrentHashMap; - + String sessionId = "123"; String appName = "example-app"; // Example app name String userId = "example-user"; // Example user id ConcurrentMap initialState = new ConcurrentHashMap<>(Map.of("newKey", "newValue")); InMemorySessionService exampleSessionService = new InMemorySessionService(); - + // Create Session Session exampleSession = exampleSessionService.createSession( appName, userId, initialState, Optional.of(sessionId)).blockingGet(); System.out.println("Session created successfully."); - + System.out.println("--- Examining Session Properties ---"); System.out.printf("ID (`id`): %s%n", exampleSession.id()); System.out.printf("Application Name (`appName`): %s%n", exampleSession.appName()); System.out.printf("User ID (`userId`): %s%n", exampleSession.userId()); System.out.printf("State (`state`): %s%n", exampleSession.state()); System.out.println("------------------------------------"); - - + + // Clean up (optional for this example) var unused = exampleSessionService.deleteSession(appName, userId, sessionId); ``` @@ -97,7 +97,7 @@ are its key properties: === "Go" ```go - // --8<-- "examples/go/snippets/sessions/session_management_example/session_management_example.go:examine_session" + --8<-- "examples/go/snippets/sessions/session_management_example/session_management_example.go:examine_session" ``` *(**Note:** The state shown above is only the initial state. State updates @@ -139,13 +139,13 @@ the storage backend that best suits your needs: where long-term persistence isn't required. === "Python" - + ```py from google.adk.sessions import InMemorySessionService session_service = InMemorySessionService() ``` === "Java" - + ```java import com.google.adk.sessions.InMemorySessionService; InMemorySessionService exampleSessionService = new InMemorySessionService(); @@ -154,7 +154,7 @@ the storage backend that best suits your needs: === "Go" ```go - // --8<-- "examples/go/snippets/sessions/session_management_example/session_management_example.go:in_memory_service" + --8<-- "examples/go/snippets/sessions/session_management_example/session_management_example.go:in_memory_service" ``` 2. **`VertexAiSessionService`** @@ -174,7 +174,7 @@ the storage backend that best suits your needs: especially when integrating with other Vertex AI features. === "Python" - + ```py # Requires: pip install google-adk[vertexai] # Plus GCP setup and authentication @@ -189,9 +189,9 @@ the storage backend that best suits your needs: # Use REASONING_ENGINE_APP_NAME when calling service methods, e.g.: # session_service = await session_service.create_session(app_name=REASONING_ENGINE_APP_NAME, ...) ``` - + === "Java" - + ```java // Please look at the set of requirements above, consequently export the following in your bashrc file: // export GOOGLE_CLOUD_PROJECT=my_gcp_project @@ -217,7 +217,7 @@ the storage backend that best suits your needs: === "Go" ```go - // --8<-- "examples/go/snippets/sessions/session_management_example/session_management_example.go:vertexai_service" + --8<-- "examples/go/snippets/sessions/session_management_example/session_management_example.go:vertexai_service" ``` 3. **`DatabaseSessionService`** diff --git a/examples/go/snippets/sessions/session_management_example/go.sum b/examples/go/snippets/sessions/session_management_example/go.sum deleted file mode 100644 index 9fefded0..00000000 --- a/examples/go/snippets/sessions/session_management_example/go.sum +++ /dev/null @@ -1,73 +0,0 @@ -cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= -cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= -cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI= -cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= -cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA= -cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= -github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= -github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= -github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= -go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= -go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= -go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= -go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= -go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= -go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -google.golang.org/adk v0.0.0-20251015134637-7cc254810953 h1:RGIoN4hXlJchW5mef+6+J+Ym9/FwTeF7WC70LonFKxU= -google.golang.org/adk v0.0.0-20251015134637-7cc254810953/go.mod h1:hfcsmlJhB3RRqBmoiLDZ1PwxbIcjtJIamk70gPp6GcE= -google.golang.org/genai v1.20.0 h1:nmDZSJjXwBvSXcdOohz7pzTVGP9yuNITY8kZ2Ta24xY= -google.golang.org/genai v1.20.0/go.mod h1:QPj5NGJw+3wEOHg+PrsWwJKvG6UC84ex5FR7qAYsN/M= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.75.0-dev h1:3GnKkkh9RI6YGGw8/Zu3WDlX4+lexwzdKZlrtlo9RCc= -google.golang.org/grpc v1.75.0-dev/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -rsc.io/omap v1.2.0 h1:c1M8jchnHbzmJALzGLclfH3xDWXrPxSUHXzH5C+8Kdw= -rsc.io/omap v1.2.0/go.mod h1:C8pkI0AWexHopQtZX+qiUeJGzvc8HkdgnsWK4/mAa00= -rsc.io/ordered v1.1.1 h1:1kZM6RkTmceJgsFH/8DLQvkCVEYomVDJfBRLT595Uak= -rsc.io/ordered v1.1.1/go.mod h1:evAi8739bWVBRG9aaufsjVc202+6okf8u2QeVL84BCM= diff --git a/examples/go/snippets/sessions/session_management_example/session_management_example.go b/examples/go/snippets/sessions/session_management_example/session_management_example.go index 7a134283..efb5a819 100644 --- a/examples/go/snippets/sessions/session_management_example/session_management_example.go +++ b/examples/go/snippets/sessions/session_management_example/session_management_example.go @@ -32,33 +32,33 @@ func main() { // --- SessionService Implementations --- // --8<-- [start:in_memory_service] - // 1. InMemorySessionService + // 1. InMemoryService // Stores all session data directly in the application's memory. // All conversation data is lost if the application restarts. inMemoryService := session.InMemoryService() - fmt.Println("Initialized InMemorySessionService.") + fmt.Println("Initialized InMemoryService.") // --8<-- [end:in_memory_service] // --8<-- [start:vertexai_service] - // 2. VertexAiSessionService + // 2. VertexAIService // Uses Google Cloud Vertex AI for persistent, scalable session management. - // Requires a Google Cloud project and an Agent Engine ID. - // Before running, ensure your environment is authenticated and variables are set: - // export GOOGLE_API_KEY="your-express-mode-api-key" + // Requires a Google Cloud project. + // Before running, ensure your environment is authenticated: + // gcloud auth application-default login // export GOOGLE_CLOUD_PROJECT="your-gcp-project-id" // export GOOGLE_CLOUD_LOCATION="your-gcp-location" - agentEngineID := "your-reasoning-engine-id" // Replace with your actual Agent Engine ID - vertexService, err := session.VertexAIService(ctx, agentEngineID) + modelName := "gemini-1.5-flash-001" // Replace with your desired model + vertexService, err := session.VertexAIService(ctx, modelName) if err != nil { - log.Printf("Could not initialize VertexAiSessionService (this is expected if GOOGLE_API_KEY is not set): %v", err) + log.Printf("Could not initialize VertexAIService (this is expected if the gcloud project is not set): %v", err) } else { - fmt.Println("Successfully initialized VertexAiSessionService.") + fmt.Println("Successfully initialized VertexAIService.") } // --8<-- [end:vertexai_service] _ = vertexService // Avoid unused variable error if initialization fails. // --- Examining Session Properties --- - // We'll use the InMemorySessionService for this demonstration. + // We'll use the InMemoryService for this demonstration. // --8<-- [start:examine_session] appName := "my_go_app" userID := "example_go_user" From 1b38e405815bd8622952e4d76499dcb83fc7ac30 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Wed, 5 Nov 2025 03:31:52 +0000 Subject: [PATCH 102/125] update go session snippets --- docs/sessions/session.md | 24 +++++++++++++++---- .../session_management_example.go | 11 +++------ 2 files changed, 23 insertions(+), 12 deletions(-) diff --git a/docs/sessions/session.md b/docs/sessions/session.md index 3ddca55f..cfaec41b 100644 --- a/docs/sessions/session.md +++ b/docs/sessions/session.md @@ -154,7 +154,9 @@ the storage backend that best suits your needs: === "Go" ```go - --8<-- "examples/go/snippets/sessions/session_management_example/session_management_example.go:in_memory_service" + import "google.golang.org/adk/session" + + inMemoryService := session.InMemoryService() ``` 2. **`VertexAiSessionService`** @@ -216,9 +218,23 @@ the storage backend that best suits your needs: === "Go" - ```go - --8<-- "examples/go/snippets/sessions/session_management_example/session_management_example.go:vertexai_service" - ``` + ```go + import "google.golang.org/adk/session" + + // 2. VertexAIService + // Before running, ensure your environment is authenticated: + // gcloud auth application-default login + // export GOOGLE_CLOUD_PROJECT="your-gcp-project-id" + // export GOOGLE_CLOUD_LOCATION="your-gcp-location" + + modelName := "gemini-1.5-flash-001" // Replace with your desired model + vertexService, err := session.VertexAIService(ctx, modelName) + if err != nil { + log.Printf("Could not initialize VertexAIService (this is expected if the gcloud project is not set): %v", err) + } else { + fmt.Println("Successfully initialized VertexAIService.") + } + ``` 3. **`DatabaseSessionService`** diff --git a/examples/go/snippets/sessions/session_management_example/session_management_example.go b/examples/go/snippets/sessions/session_management_example/session_management_example.go index efb5a819..a03b642e 100644 --- a/examples/go/snippets/sessions/session_management_example/session_management_example.go +++ b/examples/go/snippets/sessions/session_management_example/session_management_example.go @@ -31,13 +31,11 @@ func main() { // --- SessionService Implementations --- - // --8<-- [start:in_memory_service] // 1. InMemoryService // Stores all session data directly in the application's memory. // All conversation data is lost if the application restarts. inMemoryService := session.InMemoryService() fmt.Println("Initialized InMemoryService.") - // --8<-- [end:in_memory_service] // --8<-- [start:vertexai_service] // 2. VertexAIService @@ -76,13 +74,10 @@ func main() { exampleSession := createResp.Session fmt.Println("\n--- Examining Session Properties ---") - fmt.Printf("ID (`ID()`): %s\n", exampleSession.ID()) + fmt.Printf("ID (`ID()`): %s\n", exampleSession.ID()) fmt.Printf("Application Name (`AppName()`): %s\n", exampleSession.AppName()) - fmt.Printf("User ID (`UserID()`): %s\n", exampleSession.UserID()) - - // To access state, you get a state object and then call Get(). - stateObj := exampleSession.State() - val, _ := stateObj.Get("initial_key") + // To access state, you call Get(). + val, _ := exampleSession.State().Get("initial_key") fmt.Printf("State (`State().Get()`): initial_key = %v\n", val) // Events are initially empty. From f8cc678bd6628522a64b0a5e702d3e56fabd1789 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Wed, 5 Nov 2025 06:40:29 +0000 Subject: [PATCH 103/125] updating DatabaseSessionService language support tag --- docs/sessions/session.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sessions/session.md b/docs/sessions/session.md index cfaec41b..e95cbd86 100644 --- a/docs/sessions/session.md +++ b/docs/sessions/session.md @@ -239,7 +239,7 @@ the storage backend that best suits your needs: 3. **`DatabaseSessionService`**
- Supported in ADKPython v0.1.0 + Supported in ADKPython v0.1.0Go v0.1.0
* **How it works:** Connects to a relational database (e.g., PostgreSQL, From 7a18c1f720b2c1eecb685834126b7990192da364 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Wed, 5 Nov 2025 21:20:57 +0000 Subject: [PATCH 104/125] cleaning up go vertex ai comments --- .../session_management_example/session_management_example.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/go/snippets/sessions/session_management_example/session_management_example.go b/examples/go/snippets/sessions/session_management_example/session_management_example.go index a03b642e..e9292dd2 100644 --- a/examples/go/snippets/sessions/session_management_example/session_management_example.go +++ b/examples/go/snippets/sessions/session_management_example/session_management_example.go @@ -39,8 +39,6 @@ func main() { // --8<-- [start:vertexai_service] // 2. VertexAIService - // Uses Google Cloud Vertex AI for persistent, scalable session management. - // Requires a Google Cloud project. // Before running, ensure your environment is authenticated: // gcloud auth application-default login // export GOOGLE_CLOUD_PROJECT="your-gcp-project-id" From 1e201ddf2f5c678a43f3daeae7625e725e0fab23 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Wed, 5 Nov 2025 23:46:34 +0000 Subject: [PATCH 105/125] removing go.mod for session_management_example --- .../session_management_example/go.mod | 35 ------------------- 1 file changed, 35 deletions(-) delete mode 100644 examples/go/snippets/sessions/session_management_example/go.mod diff --git a/examples/go/snippets/sessions/session_management_example/go.mod b/examples/go/snippets/sessions/session_management_example/go.mod deleted file mode 100644 index 39db7102..00000000 --- a/examples/go/snippets/sessions/session_management_example/go.mod +++ /dev/null @@ -1,35 +0,0 @@ -module session_management_example - -go 1.24.7 - -require google.golang.org/adk v0.0.0-20251015134637-7cc254810953 - -require ( - cloud.google.com/go v0.121.6 // indirect - cloud.google.com/go/auth v0.16.5 // indirect - cloud.google.com/go/compute/metadata v0.8.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/google/s2a-go v0.1.9 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect - github.com/googleapis/gax-go/v2 v2.15.0 // indirect - github.com/gorilla/websocket v1.5.3 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel v1.36.0 // indirect - go.opentelemetry.io/otel/metric v1.36.0 // indirect - go.opentelemetry.io/otel/trace v1.36.0 // indirect - golang.org/x/crypto v0.41.0 // indirect - golang.org/x/net v0.43.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.29.0 // indirect - google.golang.org/genai v1.20.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect - google.golang.org/grpc v1.75.0-dev // indirect - google.golang.org/protobuf v1.36.10 // indirect - rsc.io/omap v1.2.0 // indirect - rsc.io/ordered v1.1.1 // indirect -) From 40eae7b65d419270186682cac74d8c3c9ec1c2c2 Mon Sep 17 00:00:00 2001 From: Joe Fernandez Date: Tue, 4 Nov 2025 23:09:08 +0000 Subject: [PATCH 106/125] docs: add language support tags for Go --- docs/agents/custom-agents.md | 2 +- docs/agents/index.md | 2 +- docs/agents/llm-agents.md | 2 +- docs/agents/models.md | 2 +- docs/agents/multi-agents.md | 2 +- docs/agents/workflow-agents/index.md | 2 +- docs/agents/workflow-agents/loop-agents.md | 2 +- docs/agents/workflow-agents/parallel-agents.md | 2 +- docs/agents/workflow-agents/sequential-agents.md | 2 +- docs/artifacts/index.md | 2 +- docs/callbacks/index.md | 2 +- docs/context/index.md | 2 +- docs/events/index.md | 2 +- docs/mcp/index.md | 2 +- docs/observability/logging.md | 2 +- docs/runtime/api-server.md | 2 +- docs/runtime/index.md | 2 +- docs/runtime/runconfig.md | 2 +- docs/sessions/index.md | 2 +- docs/sessions/memory.md | 2 +- docs/sessions/session.md | 2 +- docs/sessions/state.md | 2 +- docs/tools-custom/index.md | 2 +- docs/tools/function-tools.md | 2 +- docs/tools/mcp-tools.md | 2 +- 25 files changed, 25 insertions(+), 25 deletions(-) diff --git a/docs/agents/custom-agents.md b/docs/agents/custom-agents.md index 46e74e91..bf736dd8 100644 --- a/docs/agents/custom-agents.md +++ b/docs/agents/custom-agents.md @@ -5,7 +5,7 @@ # Custom agents
- Supported in ADKPython v0.1.0Java v0.2.0 + Supported in ADKPython v0.1.0Java v0.2.0Go v0.1.0
Custom agents provide the ultimate flexibility in ADK, allowing you to define **arbitrary orchestration logic** by inheriting directly from `BaseAgent` and implementing your own control flow. This goes beyond the predefined patterns of `SequentialAgent`, `LoopAgent`, and `ParallelAgent`, enabling you to build highly specific and complex agentic workflows. diff --git a/docs/agents/index.md b/docs/agents/index.md index 3ad9f3fd..e07d1d0f 100644 --- a/docs/agents/index.md +++ b/docs/agents/index.md @@ -1,7 +1,7 @@ # Agents
- Supported in ADKPythonJava + Supported in ADKPythonJavaGo
In the Agent Development Kit (ADK), an **Agent** is a self-contained execution unit designed to act autonomously to achieve specific goals. Agents can perform tasks, interact with users, utilize external tools, and coordinate with other agents. diff --git a/docs/agents/llm-agents.md b/docs/agents/llm-agents.md index 239fa795..43762ba2 100644 --- a/docs/agents/llm-agents.md +++ b/docs/agents/llm-agents.md @@ -1,7 +1,7 @@ # LLM Agent
- Supported in ADKPython v0.1.0Java v0.1.0 + Supported in ADKPython v0.1.0Java v0.1.0Go v0.1.0
The `LlmAgent` (often aliased simply as `Agent`) is a core component in ADK, diff --git a/docs/agents/models.md b/docs/agents/models.md index 05decd44..891a80b0 100644 --- a/docs/agents/models.md +++ b/docs/agents/models.md @@ -1,7 +1,7 @@ # Using Different Models with ADK
- Supported in ADKPython v0.1.0Java v0.1.0 + Supported in ADKPython v0.1.0Java v0.1.0Go v0.1.0
The Agent Development Kit (ADK) is designed for flexibility, allowing you to diff --git a/docs/agents/multi-agents.md b/docs/agents/multi-agents.md index fa67eaa1..2cbfe2c9 100644 --- a/docs/agents/multi-agents.md +++ b/docs/agents/multi-agents.md @@ -1,7 +1,7 @@ # Multi-Agent Systems in ADK
- Supported in ADKPython v0.1.0Java v0.2.0 + Supported in ADKPython v0.1.0Java v0.2.0Go v0.1.0
As agentic applications grow in complexity, structuring them as a single, monolithic agent can become challenging to develop, maintain, and reason about. The Agent Development Kit (ADK) supports building sophisticated applications by composing multiple, distinct `BaseAgent` instances into a **Multi-Agent System (MAS)**. diff --git a/docs/agents/workflow-agents/index.md b/docs/agents/workflow-agents/index.md index 589ab723..d61545c4 100644 --- a/docs/agents/workflow-agents/index.md +++ b/docs/agents/workflow-agents/index.md @@ -1,7 +1,7 @@ # Workflow Agents
- Supported in ADKPythonJava + Supported in ADKPythonJavaGo
This section introduces "*workflow agents*" - **specialized agents that control the execution flow of its sub-agents**. diff --git a/docs/agents/workflow-agents/loop-agents.md b/docs/agents/workflow-agents/loop-agents.md index db97d9cb..d023a4ae 100644 --- a/docs/agents/workflow-agents/loop-agents.md +++ b/docs/agents/workflow-agents/loop-agents.md @@ -1,7 +1,7 @@ # Loop agents
- Supported in ADKPython v0.1.0Java v0.2.0 + Supported in ADKPython v0.1.0Java v0.2.0Go v0.1.0
The `LoopAgent` is a workflow agent that executes its sub-agents in a loop (i.e. iteratively). It **_repeatedly runs_ a sequence of agents** for a specified number of iterations or until a termination condition is met. diff --git a/docs/agents/workflow-agents/parallel-agents.md b/docs/agents/workflow-agents/parallel-agents.md index 5ad8a8c2..168d46cd 100644 --- a/docs/agents/workflow-agents/parallel-agents.md +++ b/docs/agents/workflow-agents/parallel-agents.md @@ -1,7 +1,7 @@ # Parallel agents
- Supported in ADKPython v0.1.0Java v0.2.0 + Supported in ADKPython v0.1.0Java v0.2.0Go v0.1.0
The `ParallelAgent` is a [workflow agent](index.md) that executes its sub-agents *concurrently*. This dramatically speeds up workflows where tasks can be performed independently. diff --git a/docs/agents/workflow-agents/sequential-agents.md b/docs/agents/workflow-agents/sequential-agents.md index ce41243e..721e2574 100644 --- a/docs/agents/workflow-agents/sequential-agents.md +++ b/docs/agents/workflow-agents/sequential-agents.md @@ -1,7 +1,7 @@ # Sequential agents
- Supported in ADKPython v0.1.0Java v0.2.0 + Supported in ADKPython v0.1.0Java v0.2.0Go v0.1.0
The `SequentialAgent` is a [workflow agent](index.md) that executes its sub-agents in the order they are specified in the list. diff --git a/docs/artifacts/index.md b/docs/artifacts/index.md index 559f7149..2cce9f69 100644 --- a/docs/artifacts/index.md +++ b/docs/artifacts/index.md @@ -1,7 +1,7 @@ # Artifacts
- Supported in ADKPython v0.1.0Java v0.1.0 + Supported in ADKPython v0.1.0Java v0.1.0Go v0.1.0
In ADK, **Artifacts** represent a crucial mechanism for managing named, versioned binary data associated either with a specific user interaction session or persistently with a user across multiple sessions. They allow your agents and tools to handle data beyond simple text strings, enabling richer interactions involving files, images, audio, and other binary formats. diff --git a/docs/callbacks/index.md b/docs/callbacks/index.md index cbb3502b..e4f60f57 100644 --- a/docs/callbacks/index.md +++ b/docs/callbacks/index.md @@ -1,7 +1,7 @@ # Callbacks: Observe, Customize, and Control Agent Behavior
- Supported in ADKPython v0.1.0Java v0.1.0 + Supported in ADKPython v0.1.0Java v0.1.0Go v0.1.0
Callbacks are a cornerstone feature of ADK, providing a powerful mechanism to hook into an agent's execution process. They allow you to observe, customize, and even control the agent's behavior at specific, predefined points without modifying the core ADK framework code. diff --git a/docs/context/index.md b/docs/context/index.md index cf631c57..9d2af834 100644 --- a/docs/context/index.md +++ b/docs/context/index.md @@ -1,7 +1,7 @@ # Context
- Supported in ADKPython v0.1.0Java v0.1.0 + Supported in ADKPython v0.1.0Java v0.1.0Go v0.1.0
In the Agent Development Kit (ADK), "context" refers to the crucial bundle of information available to your agent and its tools during specific operations. Think of it as the necessary background knowledge and resources needed to handle a current task or conversation turn effectively. diff --git a/docs/events/index.md b/docs/events/index.md index 3254384e..760b8b6c 100644 --- a/docs/events/index.md +++ b/docs/events/index.md @@ -1,7 +1,7 @@ # Events
- Supported in ADKPython v0.1.0Java v0.1.0 + Supported in ADKPython v0.1.0Java v0.1.0Go v0.1.0
Events are the fundamental units of information flow within the Agent Development Kit (ADK). They represent every significant occurrence during an agent's interaction lifecycle, from initial user input to the final response and all the steps in between. Understanding events is crucial because they are the primary way components communicate, state is managed, and control flow is directed. diff --git a/docs/mcp/index.md b/docs/mcp/index.md index b220f509..95d4c636 100644 --- a/docs/mcp/index.md +++ b/docs/mcp/index.md @@ -1,7 +1,7 @@ # Model Context Protocol (MCP)
- Supported in ADKPythonJava + Supported in ADKPythonJavaGo
The diff --git a/docs/observability/logging.md b/docs/observability/logging.md index 0b295757..80149c9b 100644 --- a/docs/observability/logging.md +++ b/docs/observability/logging.md @@ -1,7 +1,7 @@ # Logging in the Agent Development Kit (ADK)
- Supported in ADKPython v0.1.0Java v0.1.0 + Supported in ADKPython v0.1.0Java v0.1.0Go v0.1.0
The Agent Development Kit (ADK) uses Python's standard `logging` module to provide flexible and powerful logging capabilities. Understanding how to configure and interpret these logs is crucial for monitoring agent behavior and debugging issues effectively. diff --git a/docs/runtime/api-server.md b/docs/runtime/api-server.md index 37ce2036..ba3c636c 100644 --- a/docs/runtime/api-server.md +++ b/docs/runtime/api-server.md @@ -1,7 +1,7 @@ # Use the API Server
- Supported in ADKPython v0.1.0Java v0.1.0 + Supported in ADKPython v0.1.0Java v0.1.0Go v0.1.0
Before you deploy your agent, you should test it to ensure that it is working as diff --git a/docs/runtime/index.md b/docs/runtime/index.md index 05aa2bbe..291f1adb 100644 --- a/docs/runtime/index.md +++ b/docs/runtime/index.md @@ -1,7 +1,7 @@ # Runtime
- Supported in ADKPython v0.1.0Java v0.1.0 + Supported in ADKPython v0.1.0Java v0.1.0Go v0.1.0
The ADK Runtime is the underlying engine that powers your agent application during user interactions. It's the system that takes your defined agents, tools, and callbacks and orchestrates their execution in response to user input, managing the flow of information, state changes, and interactions with external services like LLMs or storage. diff --git a/docs/runtime/runconfig.md b/docs/runtime/runconfig.md index 92bfc04e..535692a5 100644 --- a/docs/runtime/runconfig.md +++ b/docs/runtime/runconfig.md @@ -1,7 +1,7 @@ # Runtime Configuration
- Supported in ADKPython v0.1.0Java v0.1.0 + Supported in ADKPython v0.1.0Java v0.1.0Go v0.1.0
`RunConfig` defines runtime behavior and options for agents in the ADK. It diff --git a/docs/sessions/index.md b/docs/sessions/index.md index ef3e83c5..a6ac0a70 100644 --- a/docs/sessions/index.md +++ b/docs/sessions/index.md @@ -1,7 +1,7 @@ # Introduction to Conversational Context: Session, State, and Memory
- Supported in ADKPythonJava + Supported in ADKPythonJavaGo
Meaningful, multi-turn conversations require agents to understand context. Just diff --git a/docs/sessions/memory.md b/docs/sessions/memory.md index 51d846b7..e6647a75 100644 --- a/docs/sessions/memory.md +++ b/docs/sessions/memory.md @@ -1,7 +1,7 @@ # Memory: Long-Term Knowledge with `MemoryService`
- Supported in ADKPython v0.1.0Java v0.2.0 + Supported in ADKPython v0.1.0Java v0.2.0Go v0.1.0
We've seen how `Session` tracks the history (`events`) and temporary data (`state`) for a *single, ongoing conversation*. But what if an agent needs to recall information from *past* conversations? This is where the concept of **Long-Term Knowledge** and the **`MemoryService`** come into play. diff --git a/docs/sessions/session.md b/docs/sessions/session.md index e95cbd86..56bfcaea 100644 --- a/docs/sessions/session.md +++ b/docs/sessions/session.md @@ -1,7 +1,7 @@ # Session: Tracking Individual Conversations
- Supported in ADKPython v0.1.0Java v0.1.0 + Supported in ADKPython v0.1.0Java v0.1.0Go v0.1.0
Following our Introduction, let's dive into the `Session`. Think back to the diff --git a/docs/sessions/state.md b/docs/sessions/state.md index 3bc0e501..9dca0c98 100644 --- a/docs/sessions/state.md +++ b/docs/sessions/state.md @@ -1,7 +1,7 @@ # State: The Session's Scratchpad
- Supported in ADKPython v0.1.0Java v0.1.0 + Supported in ADKPython v0.1.0Java v0.1.0Go v0.1.0
Within each `Session` (our conversation thread), the **`state`** attribute acts like the agent's dedicated scratchpad for that specific interaction. While `session.events` holds the full history, `session.state` is where the agent stores and updates dynamic details needed *during* the conversation. diff --git a/docs/tools-custom/index.md b/docs/tools-custom/index.md index d7e4b551..24a01a63 100644 --- a/docs/tools-custom/index.md +++ b/docs/tools-custom/index.md @@ -1,7 +1,7 @@ # Custom Tools for ADK
- Supported in ADKPython v0.1.0Java v0.1.0 + Supported in ADKPython v0.1.0Java v0.1.0Go v0.1.0
In an ADK agent workflow, Tools are programming functions with structured input diff --git a/docs/tools/function-tools.md b/docs/tools/function-tools.md index 1147e984..7219fe2b 100644 --- a/docs/tools/function-tools.md +++ b/docs/tools/function-tools.md @@ -1,7 +1,7 @@ # Function tools
- Supported in ADKPython v0.1.0Java v0.1.0 + Supported in ADKPython v0.1.0Java v0.1.0Go v0.1.0
When pre-built ADK tools don't meet your requirements, you can create custom *function tools*. Building function tools allows you to create tailored functionality, such as connecting to proprietary databases or implementing unique algorithms. diff --git a/docs/tools/mcp-tools.md b/docs/tools/mcp-tools.md index a3eb5b11..8e3ca0ce 100644 --- a/docs/tools/mcp-tools.md +++ b/docs/tools/mcp-tools.md @@ -1,7 +1,7 @@ # Model Context Protocol Tools
- Supported in ADKPython v0.1.0Java v0.1.0 + Supported in ADKPython v0.1.0Java v0.1.0Go v0.1.0
This guide walks you through two ways of integrating Model Context Protocol (MCP) with ADK. From 7530e92fd409b5354a59771e7678f8cd3164cbc4 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Fri, 10 Oct 2025 15:48:21 +0000 Subject: [PATCH 107/125] initial go runtime sample updates --- docs/runtime/index.md | 168 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 168 insertions(+) diff --git a/docs/runtime/index.md b/docs/runtime/index.md index 291f1adb..4e371f8e 100644 --- a/docs/runtime/index.md +++ b/docs/runtime/index.md @@ -100,6 +100,58 @@ The `Runner` acts as the central coordinator for a single user invocation. Its r }); } ``` +=== "Go" + ```go + // Simplified conceptual view of the Runner's main loop logic in Go. + func (r *Runner) RunConceptual(ctx context.Context, session *session.Session, newQuery *genai.Content) (<-chan *Event, <-chan error) { + events := make(chan *Event) + errs := make(chan error, 1) + + go func() { + defer close(events) + defer close(errs) + + // 1. Append new_query to session event history (via SessionService) + // ... + if _, err := r.sessionService.Append(ctx, &session.AppendRequest{Event: userEvent}); err != nil { + errs <- err + return + } + + // 2. Kick off event stream by calling the agent + agentEvents, agentErrs := r.agent.Run(ctx, &agent.RunRequest{Session: session, Input: newQuery}) + + for { + select { + case event, ok := <-agentEvents: + if !ok { + return // Agent finished + } + // 3. Process the generated event and commit changes + if _, err := r.sessionService.Append(ctx, &session.AppendRequest{Event: event}); err != nil { + errs <- err + return + } + // memory_service.update_memory(...) // If applicable + // artifact_service might have already been called via context during agent run + + // 4. Yield event for upstream processing + events <- event + case err, ok := <-agentErrs: + if ok { + errs <- err + } + return // Agent finished with an error + case <-ctx.Done(): + errs <- ctx.Err() + return + } + } + }() + + return events, errs + } + ``` ### Execution Logic's Role (Agent, Tool, Callback) @@ -219,6 +271,82 @@ Your code within agents, tools, and callbacks is responsible for the actual comp // ... subsequent code continues ... // If this subsequent code needs to yield another event, it would do so here. ``` +=== "Go" + ```go + // Simplified view of logic inside Agent.Run, callbacks, or tools + + // ... previous code runs based on current state ... + + // 1. Determine a change or output is needed, construct the event + // Example: Updating state + updateData := map[string]interface{}{"field_1": "value_2"} + eventWithStateChange := &Event{ + Author: self.Name(), + Actions: &EventActions{StateDelta: updateData}, + Content: genai.NewContentFromText("State updated.", "model"), + // ... other event fields ... + } + + // 2. Yield the event to the Runner for processing & commit + // In Go, this is done by sending the event to a channel. + eventsChan <- eventWithStateChange + // <<<<<<<<<<<< EXECUTION PAUSES HERE (conceptually) >>>>>>>>>>>> + // The Runner on the other side of the channel will receive and process the event. + // The agent's goroutine might continue, but the logical flow waits for the next input or step. + + // <<<<<<<<<<<< RUNNER PROCESSES & COMMITS THE EVENT >>>>>>>>>>>> + + // 3. Resume execution ONLY after Runner is done processing the above event. + // In a real Go implementation, this would likely be handled by the agent receiving + // a new RunRequest or context indicating the next step. The updated state + // would be part of the session object in that new request. + // For this conceptual example, we'll just check the state. + val := ctx.Session.State["field_1"] + // here `val` is guaranteed to be "value_2" because the Runner would have + // updated the session state before calling the agent again. + fmt.Printf("Resumed execution. Value of field_1 is now: %v\n", val) + + // ... subsequent code continues ... + // Maybe send another event to the channel later... + ``` +=== "Go" + ```go + // Simplified view of logic inside Agent.Run, callbacks, or tools + + // ... previous code runs based on current state ... + + // 1. Determine a change or output is needed, construct the event + // Example: Updating state + updateData := map[string]interface{}{"field_1": "value_2"} + eventWithStateChange := &Event{ + Author: self.Name(), + Actions: &EventActions{StateDelta: updateData}, + Content: genai.NewContentFromText("State updated.", "model"), + // ... other event fields ... + } + + // 2. Yield the event to the Runner for processing & commit + // In Go, this is done by sending the event to a channel. + eventsChan <- eventWithStateChange + // <<<<<<<<<<<< EXECUTION PAUSES HERE (conceptually) >>>>>>>>>>>> + // The Runner on the other side of the channel will receive and process the event. + // The agent's goroutine might continue, but the logical flow waits for the next input or step. + + // <<<<<<<<<<<< RUNNER PROCESSES & COMMITS THE EVENT >>>>>>>>>>>> + + // 3. Resume execution ONLY after Runner is done processing the above event. + // In a real Go implementation, this would likely be handled by the agent receiving + // a new RunRequest or context indicating the next step. The updated state + // would be part of the session object in that new request. + // For this conceptual example, we'll just check the state. + val := ctx.Session.State["field_1"] + // here `val` is guaranteed to be "value_2" because the Runner would have + // updated the session state before calling the agent again. + fmt.Printf("Resumed execution. Value of field_1 is now: %v\n", val) + + // ... subsequent code continues ... + // Maybe send another event to the channel later... + ``` This cooperative yield/pause/resume cycle between the `Runner` and your Execution Logic, mediated by `Event` objects, forms the core of the ADK Runtime. @@ -363,6 +491,28 @@ Understanding a few key aspects of how the ADK Runtime handles state, streaming, // ... subsequent agent logic might involve further reactive operators // or emitting more events based on the now-updated `ctx.session().state()`. ``` +=== "Go" + ```go + // Inside agent logic (conceptual) + + // 1. Modify state + // In Go, state modifications are staged and then sent as part of an event. + updateData := map[string]interface{}{"status": "processing"} + event1 := &Event{ + Actions: &EventActions{StateDelta: updateData}, + // ... other event fields + } + + // 2. Yield event with the delta by sending it to the channel + eventsChan <- event1 + // --- PAUSE --- Runner processes event1, SessionService commits 'status' = 'processing' --- + + // 3. Resume execution + // In a real Go app, the agent would receive a new context with the updated session. + // We can now safely rely on the committed state. + currentStatus := ctx.Session.State["status"] // Guaranteed to be 'processing' + fmt.Printf("Status after resuming: %v\n", currentStatus) + ``` ### "Dirty Reads" of Session State @@ -403,6 +553,24 @@ Understanding a few key aspects of how the ADK Runtime handles state, streaming, // Assume the event carrying the state_delta={'field_1': 'value_1'} // is yielded *after* this tool runs and is processed by the Runner. ``` +=== "Go" + ```go + // Code in before_agent_callback + // The callback would modify the context's session state directly. + // This change is local to the current invocation context. + callback_context.Session.State["field_1"] = "value_1" + // State is locally set to 'value_1', but not yet committed by Runner + + // ... agent runs ... + + // Code in a tool called later *within the same invocation* + // Readable (dirty read), but 'value_1' isn't guaranteed persistent yet. + val := tool_context.Session.State["field_1"] // 'val' will likely be 'value_1' here + fmt.Printf("Dirty read value in tool: %v\n", val) + + // Assume the event carrying the state_delta={'field_1': 'value_1'} + // is yielded *after* this tool runs and is processed by the Runner. + ``` * **Implications:** * **Benefit:** Allows different parts of your logic within a single complex step (e.g., multiple callbacks or tool calls before the next LLM turn) to coordinate using state without waiting for a full yield/commit cycle. From de36d0f09e0563669d863f71b98991ee566dbed5 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Mon, 13 Oct 2025 19:36:06 +0000 Subject: [PATCH 108/125] fixing runtime go snippet formatting --- docs/runtime/index.md | 123 +++++++++++++++--------------------------- 1 file changed, 44 insertions(+), 79 deletions(-) diff --git a/docs/runtime/index.md b/docs/runtime/index.md index 4e371f8e..eca52e95 100644 --- a/docs/runtime/index.md +++ b/docs/runtime/index.md @@ -52,16 +52,16 @@ The `Runner` acts as the central coordinator for a single user invocation. Its r def run(new_query, ...) -> Generator[Event]: # 1. Append new_query to session event history (via SessionService) session_service.append_event(session, Event(author='user', content=new_query)) - + # 2. Kick off event loop by calling the agent agent_event_generator = agent_to_run.run_async(context) - + async for event in agent_event_generator: # 3. Process the generated event and commit changes session_service.append_event(session, event) # Commits state/artifact deltas etc. # memory_service.update_memory(...) # If applicable # artifact_service might have already been called via context during agent run - + # 4. Yield event for upstream processing (e.g., UI rendering) yield event # Runner implicitly signals agent generator can continue after yielding @@ -72,28 +72,28 @@ The `Runner` acts as the central coordinator for a single user invocation. Its r ```java // Simplified conceptual view of the Runner's main loop logic in Java. public Flowable runConceptual( - Session session, - InvocationContext invocationContext, - Content newQuery + Session session, + InvocationContext invocationContext, + Content newQuery ) { - + // 1. Append new_query to session event history (via SessionService) // ... sessionService.appendEvent(session, userEvent).blockingGet(); - + // 2. Kick off event stream by calling the agent Flowable agentEventStream = agentToRun.runAsync(invocationContext); - + // 3. Process each generated event, commit changes, and "yield" or "emit" return agentEventStream.map(event -> { // This mutates the session object (adds event, applies stateDelta). // The return value of appendEvent (a Single) is conceptually // just the event itself after processing. sessionService.appendEvent(session, event).blockingGet(); // Simplified blocking call - + // memory_service.update_memory(...) // If applicable - conceptual // artifact_service might have already been called via context during agent run - + // 4. "Yield" event for upstream processing // In RxJava, returning the event in map effectively yields it to the next operator or subscriber. return event; @@ -101,6 +101,7 @@ The `Runner` acts as the central coordinator for a single user invocation. Its r } ``` === "Go" + ```go // Simplified conceptual view of the Runner's main loop logic in Go. func (r *Runner) RunConceptual(ctx context.Context, session *session.Session, newQuery *genai.Content) (<-chan *Event, <-chan error) { @@ -169,9 +170,9 @@ Your code within agents, tools, and callbacks is responsible for the actual comp ```py # Simplified view of logic inside Agent.run_async, callbacks, or tools - + # ... previous code runs based on current state ... - + # 1. Determine a change or output is needed, construct the event # Example: Updating state update_data = {'field_1': 'value_2'} @@ -181,20 +182,20 @@ Your code within agents, tools, and callbacks is responsible for the actual comp content=types.Content(parts=[types.Part(text="State updated.")]) # ... other event fields ... ) - + # 2. Yield the event to the Runner for processing & commit yield event_with_state_change # <<<<<<<<<<<< EXECUTION PAUSES HERE >>>>>>>>>>>> - + # <<<<<<<<<<<< RUNNER PROCESSES & COMMITS THE EVENT >>>>>>>>>>>> - + # 3. Resume execution ONLY after Runner is done processing the above event. # Now, the state committed by the Runner is reliably reflected. # Subsequent code can safely assume the change from the yielded event happened. val = ctx.session.state['field_1'] # here `val` is guaranteed to be "value_2" (assuming Runner committed successfully) print(f"Resumed execution. Value of field_1 is now: {val}") - + # ... subsequent code continues ... # Maybe yield another event later... ``` @@ -204,37 +205,37 @@ Your code within agents, tools, and callbacks is responsible for the actual comp ```java // Simplified view of logic inside Agent.runAsync, callbacks, or tools // ... previous code runs based on current state ... - + // 1. Determine a change or output is needed, construct the event // Example: Updating state ConcurrentMap updateData = new ConcurrentHashMap<>(); updateData.put("field_1", "value_2"); - + EventActions actions = EventActions.builder().stateDelta(updateData).build(); Content eventContent = Content.builder().parts(Part.fromText("State updated.")).build(); - + Event eventWithStateChange = Event.builder() .author(self.name()) .actions(actions) .content(Optional.of(eventContent)) // ... other event fields ... .build(); - + // 2. "Yield" the event. In RxJava, this means emitting it into the stream. // The Runner (or upstream consumer) will subscribe to this Flowable. // When the Runner receives this event, it will process it (e.g., call sessionService.appendEvent). // The 'appendEvent' in Java ADK mutates the 'Session' object held within 'ctx' (InvocationContext). - + // <<<<<<<<<<<< CONCEPTUAL PAUSE POINT >>>>>>>>>>>> // In RxJava, the emission of 'eventWithStateChange' happens, and then the stream // might continue with a 'flatMap' or 'concatMap' operator that represents // the logic *after* the Runner has processed this event. - + // To model the "resume execution ONLY after Runner is done processing": // The Runner's `appendEvent` is usually an async operation itself (returns Single). // The agent's flow needs to be structured such that subsequent logic // that depends on the committed state runs *after* that `appendEvent` completes. - + // This is how the Runner typically orchestrates it: // Runner: // agent.runAsync(ctx) @@ -243,14 +244,14 @@ Your code within agents, tools, and callbacks is responsible for the actual comp // .toFlowable() // Emits the event after it's processed // ) // .subscribe(processedEvent -> { /* UI renders processedEvent */ }); - + // So, within the agent's own logic, if it needs to do something *after* an event it yielded // has been processed and its state changes are reflected in ctx.session().state(), // that subsequent logic would typically be in another step of its reactive chain. - + // For this conceptual example, we'll emit the event, and then simulate the "resume" // as a subsequent operation in the Flowable chain. - + return Flowable.just(eventWithStateChange) // Step 2: Yield the event .concatMap(yieldedEvent -> { // <<<<<<<<<<<< RUNNER CONCEPTUALLY PROCESSES & COMMITS THE EVENT >>>>>>>>>>>> @@ -258,58 +259,21 @@ Your code within agents, tools, and callbacks is responsible for the actual comp // by the Runner, and ctx.session().state() would be updated. // Since we are *inside* the agent's conceptual logic trying to model this, // we assume the Runner's action has implicitly updated our 'ctx.session()'. - + // 3. Resume execution. // Now, the state committed by the Runner (via sessionService.appendEvent) // is reliably reflected in ctx.session().state(). Object val = ctx.session().state().get("field_1"); // here `val` is guaranteed to be "value_2" because the `sessionService.appendEvent` // called by the Runner would have updated the session state within the `ctx` object. - + System.out.println("Resumed execution. Value of field_1 is now: " + val); - + // ... subsequent code continues ... // If this subsequent code needs to yield another event, it would do so here. ``` === "Go" - ```go - // Simplified view of logic inside Agent.Run, callbacks, or tools - - // ... previous code runs based on current state ... - - // 1. Determine a change or output is needed, construct the event - // Example: Updating state - updateData := map[string]interface{}{"field_1": "value_2"} - eventWithStateChange := &Event{ - Author: self.Name(), - Actions: &EventActions{StateDelta: updateData}, - Content: genai.NewContentFromText("State updated.", "model"), - // ... other event fields ... - } - - // 2. Yield the event to the Runner for processing & commit - // In Go, this is done by sending the event to a channel. - eventsChan <- eventWithStateChange - // <<<<<<<<<<<< EXECUTION PAUSES HERE (conceptually) >>>>>>>>>>>> - // The Runner on the other side of the channel will receive and process the event. - // The agent's goroutine might continue, but the logical flow waits for the next input or step. - // <<<<<<<<<<<< RUNNER PROCESSES & COMMITS THE EVENT >>>>>>>>>>>> - - // 3. Resume execution ONLY after Runner is done processing the above event. - // In a real Go implementation, this would likely be handled by the agent receiving - // a new RunRequest or context indicating the next step. The updated state - // would be part of the session object in that new request. - // For this conceptual example, we'll just check the state. - val := ctx.Session.State["field_1"] - // here `val` is guaranteed to be "value_2" because the Runner would have - // updated the session state before calling the agent again. - fmt.Printf("Resumed execution. Value of field_1 is now: %v\n", val) - - // ... subsequent code continues ... - // Maybe send another event to the channel later... - ``` -=== "Go" ```go // Simplified view of logic inside Agent.Run, callbacks, or tools @@ -439,15 +403,15 @@ Understanding a few key aspects of how the ADK Runtime handles state, streaming, ```py # Inside agent logic (conceptual) - + # 1. Modify state ctx.session.state['status'] = 'processing' event1 = Event(..., actions=EventActions(state_delta={'status': 'processing'})) - + # 2. Yield event with the delta yield event1 # --- PAUSE --- Runner processes event1, SessionService commits 'status' = 'processing' --- - + # 3. Resume execution # Now it's safe to rely on the committed state current_status = ctx.session.state['status'] # Guaranteed to be 'processing' @@ -459,19 +423,19 @@ Understanding a few key aspects of how the ADK Runtime handles state, streaming, ```java // Inside agent logic (conceptual) // ... previous code runs based on current state ... - + // 1. Prepare state modification and construct the event ConcurrentHashMap stateChanges = new ConcurrentHashMap<>(); stateChanges.put("status", "processing"); - + EventActions actions = EventActions.builder().stateDelta(stateChanges).build(); Content content = Content.builder().parts(Part.fromText("Status update: processing")).build(); - + Event event1 = Event.builder() .actions(actions) // ... .build(); - + // 2. Yield event with the delta return Flowable.just(event1) .map( @@ -481,17 +445,18 @@ Understanding a few key aspects of how the ADK Runtime handles state, streaming, // Now it's safe to rely on the committed state. String currentStatus = (String) ctx.session().state().get("status"); System.out.println("Status after resuming (inside agent logic): " + currentStatus); // Guaranteed to be 'processing' - + // The event itself (event1) is passed on. // If subsequent logic within this agent step produced *another* event, // you'd use concatMap to emit that new event. return emittedEvent; }); - + // ... subsequent agent logic might involve further reactive operators // or emitting more events based on the now-updated `ctx.session().state()`. ``` === "Go" + ```go // Inside agent logic (conceptual) @@ -525,14 +490,14 @@ Understanding a few key aspects of how the ADK Runtime handles state, streaming, # Code in before_agent_callback callback_context.state['field_1'] = 'value_1' # State is locally set to 'value_1', but not yet committed by Runner - + # ... agent runs ... - + # Code in a tool called later *within the same invocation* # Readable (dirty read), but 'value_1' isn't guaranteed persistent yet. val = tool_context.state['field_1'] # 'val' will likely be 'value_1' here print(f"Dirty read value in tool: {val}") - + # Assume the event carrying the state_delta={'field_1': 'value_1'} # is yielded *after* this tool runs and is processed by the Runner. ``` From 060312a980878fae9f524fc84ba6862ecec01e08 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Mon, 20 Oct 2025 19:47:18 +0000 Subject: [PATCH 109/125] formatting updates to runtime go snippets --- docs/runtime/index.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/runtime/index.md b/docs/runtime/index.md index eca52e95..77053467 100644 --- a/docs/runtime/index.md +++ b/docs/runtime/index.md @@ -303,7 +303,7 @@ Your code within agents, tools, and callbacks is responsible for the actual comp // a new RunRequest or context indicating the next step. The updated state // would be part of the session object in that new request. // For this conceptual example, we'll just check the state. - val := ctx.Session.State["field_1"] + val := ctx.State.Get("field_1") // here `val` is guaranteed to be "value_2" because the Runner would have // updated the session state before calling the agent again. fmt.Printf("Resumed execution. Value of field_1 is now: %v\n", val) @@ -475,7 +475,7 @@ Understanding a few key aspects of how the ADK Runtime handles state, streaming, // 3. Resume execution // In a real Go app, the agent would receive a new context with the updated session. // We can now safely rely on the committed state. - currentStatus := ctx.Session.State["status"] // Guaranteed to be 'processing' + currentStatus := ctx.State.Set("status") // Guaranteed to be 'processing' fmt.Printf("Status after resuming: %v\n", currentStatus) ``` @@ -523,14 +523,14 @@ Understanding a few key aspects of how the ADK Runtime handles state, streaming, // Code in before_agent_callback // The callback would modify the context's session state directly. // This change is local to the current invocation context. - callback_context.Session.State["field_1"] = "value_1" + ctx.State.Set("field_1", "value_1") // State is locally set to 'value_1', but not yet committed by Runner // ... agent runs ... // Code in a tool called later *within the same invocation* // Readable (dirty read), but 'value_1' isn't guaranteed persistent yet. - val := tool_context.Session.State["field_1"] // 'val' will likely be 'value_1' here + val := ctx.State.Get("field_1") // 'val' will likely be 'value_1' here fmt.Printf("Dirty read value in tool: %v\n", val) // Assume the event carrying the state_delta={'field_1': 'value_1'} From d0aaa0a35182769f942971484d30b77475661580 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Thu, 6 Nov 2025 01:05:09 +0000 Subject: [PATCH 110/125] updating runtime go examples to streamline to newer go features --- docs/runtime/index.md | 127 ++++++++++++++++++++++++------------------ 1 file changed, 74 insertions(+), 53 deletions(-) diff --git a/docs/runtime/index.md b/docs/runtime/index.md index 77053467..404f998a 100644 --- a/docs/runtime/index.md +++ b/docs/runtime/index.md @@ -29,7 +29,7 @@ This event-driven loop is the fundamental pattern governing how ADK executes you The Event Loop is the core operational pattern defining the interaction between the `Runner` and your custom code (Agents, Tools, Callbacks, collectively referred to as "Execution Logic" or "Logic Components" in the design document). It establishes a clear division of responsibilities: !!! Note - The specific method names and parameter names may vary slightly by SDK language (e.g., `agent_to_run.runAsync(...)` in Java, `agent_to_run.run_async(...)` in Python). Refer to the language-specific API documentation for details. + The specific method names and parameter names may vary slightly by SDK language (e.g., `agent_to_run.run_async(...)` in Python, `agent.Run(...)` in Go, `agent_to_run.runAsync(...)` in Java ). Refer to the language-specific API documentation for details. ### Runner's Role (Orchestrator) @@ -103,55 +103,52 @@ The `Runner` acts as the central coordinator for a single user invocation. Its r === "Go" ```go - // Simplified conceptual view of the Runner's main loop logic in Go. - func (r *Runner) RunConceptual(ctx context.Context, session *session.Session, newQuery *genai.Content) (<-chan *Event, <-chan error) { - events := make(chan *Event) - errs := make(chan error, 1) - - go func() { - defer close(events) - defer close(errs) - + // Simplified conceptual view of the Runner's main loop logic in Go + func (r *Runner) RunConceptual(ctx context.Context, session *session.Session, newQuery *genai.Content) iter.Seq2[*Event, error] { + return func(yield func(*Event, error) bool) { // 1. Append new_query to session event history (via SessionService) // ... + userEvent := session.NewEvent(ctx.InvocationID()) // Simplified for conceptual view + userEvent.Author = "user" + userEvent.LLMResponse = model.LLMResponse{Content: newQuery} + if _, err := r.sessionService.Append(ctx, &session.AppendRequest{Event: userEvent}); err != nil { - errs <- err + yield(nil, err) return } // 2. Kick off event stream by calling the agent - agentEvents, agentErrs := r.agent.Run(ctx, &agent.RunRequest{Session: session, Input: newQuery}) + // Assuming agent.Run also returns iter.Seq2[*Event, error] + agentEventsAndErrs := r.agent.Run(ctx, &agent.RunRequest{Session: session, Input: newQuery}) - for { - select { - case event, ok := <-agentEvents: - if !ok { - return // Agent finished + for event, err := range agentEventsAndErrs { + if err != nil { + if !yield(event, err) { // Yield event even if there's an error, then stop + return } - // 3. Process the generated event and commit changes + return // Agent finished with an error + } + + // 3. Process the generated event and commit changes + // Only commit non-partial event to a session service (as seen in actual code) + if !event.LLMResponse.Partial { if _, err := r.sessionService.Append(ctx, &session.AppendRequest{Event: event}); err != nil { - errs <- err + yield(nil, err) return } - // memory_service.update_memory(...) // If applicable - // artifact_service might have already been called via context during agent run - - // 4. Yield event for upstream processing - events <- event - case err, ok := <-agentErrs: - if ok { - errs <- err - } - return // Agent finished with an error - case <-ctx.Done(): - errs <- ctx.Err() - return } - } - }() + // memory_service.update_memory(...) // If applicable + // artifact_service might have already been called via context during agent run - return events, errs + // 4. Yield event for upstream processing + if !yield(event, nil) { + return // Upstream consumer stopped + } + } + // Agent finished successfully + } } + ``` ### Execution Logic's Role (Agent, Tool, Callback) @@ -458,25 +455,48 @@ Understanding a few key aspects of how the ADK Runtime handles state, streaming, === "Go" ```go - // Inside agent logic (conceptual) - - // 1. Modify state - // In Go, state modifications are staged and then sent as part of an event. - updateData := map[string]interface{}{"status": "processing"} - event1 := &Event{ - Actions: &EventActions{StateDelta: updateData}, - // ... other event fields + // Inside agent logic (conceptual) + + func (a *Agent) RunConceptual(ctx agent.InvocationContext) iter.Seq2[*session.Event, error] { + // The entire logic is wrapped in a function that will be returned as an iterator. + return func(yield func(*session.Event, error) bool) { + // ... previous code runs based on current state from the input `ctx` ... + // e.g., val := ctx.State().Get("field_1") might return "value_1" here. + + // 1. Determine a change or output is needed, construct the event + updateData := map[string]interface{}{"field_1": "value_2"} + eventWithStateChange := session.NewEvent(ctx.InvocationID()) + eventWithStateChange.Author = a.Name() + eventWithStateChange.Actions = &session.EventActions{StateDelta: updateData} + // ... other event fields ... + + + // 2. Yield the event to the Runner for processing & commit. + // The agent's execution continues immediately after this call. + if !yield(eventWithStateChange, nil) { + // If yield returns false, it means the consumer (the Runner) + // has stopped listening, so we should stop producing events. + return + } + + // <<<<<<<<<<<< RUNNER PROCESSES & COMMITS THE EVENT >>>>>>>>>>>> + // This happens outside the agent, after the agent's iterator has + // produced the event. + + // 3. The agent CANNOT immediately see the state change it just yielded. + // The state is immutable within a single `Run` invocation. + val := ctx.State().Get("field_1") + // `val` here is STILL "value_1" (or whatever it was at the start). + // The updated state ("value_2") will only be available in the `ctx` + // of the *next* `Run` invocation in a subsequent turn. + + // ... subsequent code continues, potentially yielding more events ... + finalEvent := session.NewEvent(ctx.InvocationID()) + finalEvent.Author = a.Name() + // ... + yield(finalEvent, nil) + } } - - // 2. Yield event with the delta by sending it to the channel - eventsChan <- event1 - // --- PAUSE --- Runner processes event1, SessionService commits 'status' = 'processing' --- - - // 3. Resume execution - // In a real Go app, the agent would receive a new context with the updated session. - // We can now safely rely on the committed state. - currentStatus := ctx.State.Set("status") // Guaranteed to be 'processing' - fmt.Printf("Status after resuming: %v\n", currentStatus) ``` ### "Dirty Reads" of Session State @@ -519,6 +539,7 @@ Understanding a few key aspects of how the ADK Runtime handles state, streaming, // is yielded *after* this tool runs and is processed by the Runner. ``` === "Go" + ```go // Code in before_agent_callback // The callback would modify the context's session state directly. From 994b87e85562624d172f63972467254e12a556e5 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Mon, 20 Oct 2025 19:36:01 +0000 Subject: [PATCH 111/125] adding go snippets for events --- docs/events/index.md | 296 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 274 insertions(+), 22 deletions(-) diff --git a/docs/events/index.md b/docs/events/index.md index 760b8b6c..6120950d 100644 --- a/docs/events/index.md +++ b/docs/events/index.md @@ -57,6 +57,33 @@ An `Event` in ADK is an immutable record representing a specific point in the ag // } ``` +=== "Go" + In Go, this is a struct of type `google.golang.org/adk/session.Event`. + + ```go + // Conceptual Structure of an Event (Go - See session/session.go) + // Simplified view based on the session.Event struct + type Event struct { + // --- Fields analogous to LlmResponse --- + LLMResponse *llm.Response + + // --- ADK specific additions --- + Author string // 'user' or agent name + InvocationID string // ID for the whole interaction run + ID string // Unique ID for this specific event + Timestamp time.Time // Creation time + Actions *Actions // Important for side-effects & control + Branch string // Hierarchy path + // ... other fields + } + + // llm.Response contains the Content field + type Response struct { + Content *genai.Content + // ... other fields + } + ``` + Events are central to ADK's operation for several key reasons: 1. **Communication:** They serve as the standard message format between the user interface, the `Runner`, agents, the LLM, and tools. Everything flows as an `Event`. @@ -95,6 +122,7 @@ Quickly determine what an event represents by checking: * `False` or `None`/`Optional.empty()`: This part of the content is complete (though the overall turn might not be finished if `turn_complete` is also false). === "Python" + ```python # Pseudocode: Basic event identification (Python) # async for event in runner.run_async(...): @@ -119,6 +147,7 @@ Quickly determine what an event represents by checking: ``` === "Java" + ```java // Pseudocode: Basic event identification (Java) // import com.google.genai.types.Content; @@ -154,6 +183,70 @@ Quickly determine what an event represents by checking: // }); ``` +=== "Go" + + ```go + // Pseudocode: Basic event identification (Go) + import ( + "fmt" + "google.golang.org/adk/session" + "google.golang.org/genai" + ) + + func hasFunctionCalls(content *genai.Content) bool { + if content == nil { + return false + } + for _, part := range content.Parts { + if part.FunctionCall != nil { + return true + } + } + return false + } + + func hasFunctionResponses(content *genai.Content) bool { + if content == nil { + return false + } + for _, part := range content.Parts { + if part.FunctionResponse != nil { + return true + } + } + return false + } + + func processEvents(events <-chan *session.Event) { + for event := range events { + fmt.Printf("Event from: %s\n", event.Author) + + if event.LLMResponse != nil && event.LLMResponse.Content != nil { + if hasFunctionCalls(event.LLMResponse.Content) { + fmt.Println(" Type: Tool Call Request") + } else if hasFunctionResponses(event.LLMResponse.Content) { + fmt.Println(" Type: Tool Result") + } else if len(event.LLMResponse.Content.Parts) > 0 { + if event.LLMResponse.Content.Parts[0].Text != "" { + if event.LLMResponse.Partial { + fmt.Println(" Type: Streaming Text Chunk") + } else { + fmt.Println(" Type: Complete Text Message") + } + } else { + fmt.Println(" Type: Other Content (e.g., code result)") + } + } + } else if &event.Actions != nil && (len(event.Actions.StateDelta) > 0) { + fmt.Println(" Type: State Update") + } else { + fmt.Println(" Type: Control Signal or Other") + } + } + } + + ``` + ### Extracting Key Information Once you know the event type, access the relevant data: @@ -162,7 +255,7 @@ Once you know the event type, access the relevant data: Always check for the presence of content and parts before accessing text. In Python its `text = event.content.parts[0].text`. * **Function Call Details:** - + === "Python" ```python calls = event.get_function_calls() @@ -179,7 +272,7 @@ Once you know the event type, access the relevant data: import com.google.genai.types.FunctionCall; import com.google.common.collect.ImmutableList; import java.util.Map; - + ImmutableList calls = event.functionCalls(); // from Event.java if (!calls.isEmpty()) { for (FunctionCall call : calls) { @@ -192,9 +285,35 @@ Once you know the event type, access the relevant data: } ``` + === "Go" + + ```go + import ( + "fmt" + "google.golang.org/adk/session" + "google.golang.org/genai" + ) + + func handleFunctionCalls(event *session.Event) { + if event.LLMResponse == nil { + return + } + calls := event.LLMResponse.FunctionCalls() + if len(calls) > 0 { + for _, call := range calls { + toolName := call.Name + arguments := call.Args + fmt.Printf(" Tool: %s, Args: %v\n", toolName, arguments) + // Application might dispatch execution based on this + } + } + } + ``` + * **Function Response Details:** - + === "Python" + ```python responses = event.get_function_responses() if responses: @@ -208,7 +327,7 @@ Once you know the event type, access the relevant data: ```java import com.google.genai.types.FunctionResponse; import com.google.common.collect.ImmutableList; - import java.util.Map; + import java.util.Map; ImmutableList responses = event.functionResponses(); // from Event.java if (!responses.isEmpty()) { @@ -220,6 +339,30 @@ Once you know the event type, access the relevant data: } ``` + === "Go" + + ```go + import ( + "fmt" + "google.golang.org/adk/session" + "google.golang.org/genai" + ) + + func handleFunctionResponses(event *session.Event) { + if event.LLMResponse == nil { + return + } + responses := event.LLMResponse.FunctionResponses() + if len(responses) > 0 { + for _, response := range responses { + toolName := response.Name + result := response.Response + fmt.Printf(" Tool Result: %s -> %v\n", toolName, result) + } + } + } + ``` + * **Identifiers:** * `event.id`: Unique ID for this specific event instance. * `event.invocation_id`: ID for the entire user-request-to-final-response cycle this event belongs to. Useful for logging and tracing. @@ -229,7 +372,7 @@ Once you know the event type, access the relevant data: The `event.actions` object signals changes that occurred or should occur. Always check if `event.actions` and it's fields/ methods exists before accessing them. * **State Changes:** Gives you a collection of key-value pairs that were modified in the session state during the step that produced this event. - + === "Python" `delta = event.actions.state_delta` (a dictionary of `{key: value}` pairs). ```python @@ -238,22 +381,38 @@ The `event.actions` object signals changes that occurred or should occur. Always # Update local UI or application state if necessary ``` === "Java" - `ConcurrentMap delta = event.actions().stateDelta();` + `ConcurrentMap delta = event.actions().stateDelta();` - ```java - import java.util.concurrent.ConcurrentMap; - import com.google.adk.events.EventActions; + ```java + import java.util.concurrent.ConcurrentMap; + import com.google.adk.events.EventActions; - EventActions actions = event.actions(); // Assuming event.actions() is not null - if (actions != null && actions.stateDelta() != null && !actions.stateDelta().isEmpty()) { - ConcurrentMap stateChanges = actions.stateDelta(); - System.out.println(" State changes: " + stateChanges); - // Update local UI or application state if necessary + EventActions actions = event.actions(); // Assuming event.actions() is not null + if (actions != null && actions.stateDelta() != null && !actions.stateDelta().isEmpty()) { + ConcurrentMap stateChanges = actions.stateDelta(); + System.out.println(" State changes: " + stateChanges); + // Update local UI or application state if necessary + } + ``` + + === "Go" + `delta := event.Actions.StateDelta` (a `map[string]any`) + ```go + import ( + "fmt" + "google.golang.org/adk/session" + ) + + func handleStateChanges(event *session.Event) { + if event.Actions != nil && len(event.Actions.StateDelta) > 0 { + fmt.Printf(" State changes: %v\n", event.Actions.StateDelta) + // Update local UI or application state if necessary + } } ``` * **Artifact Saves:** Gives you a collection indicating which artifacts were saved and their new version number (or relevant `Part` information). - + === "Python" `artifact_changes = event.actions.artifact_delta` (a dictionary of `{filename: version}`). ```python @@ -263,7 +422,7 @@ The `event.actions` object signals changes that occurred or should occur. Always ``` === "Java" `ConcurrentMap artifactChanges = event.actions().artifactDelta();` - + ```java import java.util.concurrent.ConcurrentMap; import com.google.genai.types.Part; @@ -278,8 +437,24 @@ The `event.actions` object signals changes that occurred or should occur. Always } ``` + === "Go" + `artifactChanges := event.Actions.ArtifactDelta` (a `map[string]genai.Part`) + ```go + import ( + "fmt" + "google.golang.org/adk/session" + ) + + func handleArtifactChanges(event *session.Event) { + if event.Actions != nil && len(event.Actions.ArtifactDelta) > 0 { + fmt.Printf(" Artifacts saved: %v\n", event.Actions.ArtifactDelta) + // UI might refresh an artifact list + } + } + ``` + * **Control Flow Signals:** Check boolean flags or string values: - + === "Python" * `event.actions.transfer_to_agent` (string): Control should pass to the named agent. * `event.actions.escalate` (bool): A loop should terminate. @@ -320,6 +495,30 @@ The `event.actions` object signals changes that occurred or should occur. Always } } ``` + === "Go" + * `event.Actions.TransferToAgent` (string): Control should pass to the named agent. + * `event.Actions.Escalate` (bool): A loop should terminate. + * `event.Actions.SkipSummarization` (bool): A tool result should not be summarized by the LLM. + ```go + import ( + "fmt" + "google.golang.org/adk/session" + ) + + func handleControlFlow(event *session.Event) { + if event.Actions != nil { + if event.Actions.TransferToAgent != "" { + fmt.Printf(" Signal: Transfer to %s\n", event.Actions.TransferToAgent) + } + if event.Actions.Escalate { + fmt.Println(" Signal: Escalate (terminate loop)") + } + if event.Actions.SkipSummarization { + fmt.Println(" Signal: Skip summarization for tool result") + } + } + } + ``` ### Determining if an Event is a "Final" Response @@ -328,7 +527,7 @@ Use the built-in helper method `event.is_final_response()` to identify events su * **Purpose:** Filters out intermediate steps (like tool calls, partial streaming text, internal state updates) from the final user-facing message(s). * **When `True`?** 1. The event contains a tool result (`function_response`) and `skip_summarization` is `True`. - 2. The event contains a tool call (`function_call`) for a tool marked as `is_long_running=True`. In Java, check if the `longRunningToolIds` list is empty: + 2. The event contains a tool call (`function_call`) for a tool marked as `is_long_running=True`. In Java, check if the `longRunningToolIds` list is empty: * `event.longRunningToolIds().isPresent() && !event.longRunningToolIds().get().isEmpty()` is `true`. 3. OR, **all** of the following are met: * No function calls (`get_function_calls()` is empty). @@ -382,7 +581,7 @@ Use the built-in helper method `event.is_final_response()` to identify events su } }); } - + // Check if it's a final, displayable event if (event.finalResponse()) { // Using the method from Event.java System.out.println("\n--- Final Output Detected ---"); @@ -393,7 +592,7 @@ Use the built-in helper method `event.is_final_response()` to identify events su String finalText = fullResponseText.toString() + (event.partial().orElse(false) ? "" : eventText); System.out.println("Display to user: " + finalText.trim()); fullResponseText.setLength(0); // Reset accumulator - } else if (event.actions() != null && event.actions().skipSummarization().orElse(false) + } else if (event.actions() != nil && event.actions().skipSummarization().orElse(false) && !event.functionResponses().isEmpty()) { // Handle displaying the raw tool result if needed, // especially if finalResponse() was true due to other conditions @@ -411,6 +610,56 @@ Use the built-in helper method `event.is_final_response()` to identify events su }); ``` + === "Go" + + ```go + // Pseudocode: Handling final responses in application (Go) + import ( + "fmt" + "strings" + "google.golang.org/adk/session" + "google.golang.org/genai" + ) + + func handleFinalResponses() { + // Pseudocode: Handling final responses in application (Go) + var fullResponseText strings.Builder + // for event := range runner.Run(...) { + // // Accumulate streaming text if needed... + // if event.LLMResponse != nil && event.LLMResponse.IsPartial && event.LLMResponse.Content != nil { + // if part, ok := event.LLMResponse.Content.Parts[0].(genai.Text); ok { + // fullResponseText.WriteString(string(part)) + // } + // } + // + // // Check if it's a final, displayable event + // if event.IsFinalResponse() { + // fmt.Println("\n--- Final Output Detected ---") + // if event.LLMResponse != nil && event.LLMResponse.Content != nil { + // if part, ok := event.LLMResponse.Content.Parts[0].(genai.Text); ok { + // // If it's the final part of a stream, use accumulated text + // finalText := fullResponseText.String() + // if !event.LLMResponse.IsPartial { + // finalText += string(part) + // } + // fmt.Printf("Display to user: %s\n", strings.TrimSpace(finalText)) + // fullResponseText.Reset() // Reset accumulator + // } + // } else if event.Actions != nil && event.Actions.SkipSummarization && len(event.LLMResponse.FunctionResponses()) > 0 { + // // Handle displaying the raw tool result if needed + // responseData := event.LLMResponse.FunctionResponses()[0].Response + // fmt.Printf("Display raw tool result: %v\n", responseData) + // } else if len(event.LongRunningToolIDs) > 0 { + // fmt.Println("Display message: Tool is running in background...") + // } else { + // // Handle other types of final responses if applicable + // fmt.Println("Display: Final non-textual response or signal.") + // } + // } + // } + } + ``` + By carefully examining these aspects of an event, you can build robust applications that react appropriately to the rich information flowing through the ADK system. ## How Events Flow: Generation and Processing @@ -566,16 +815,19 @@ These details provide a more complete picture for advanced use cases involving t To use events effectively in your ADK applications: * **Clear Authorship:** When building custom agents, ensure correct attribution for agent actions in the history. The framework generally handles authorship correctly for LLM/tool events. - + === "Python" Use `yield Event(author=self.name, ...)` in `BaseAgent` subclasses. === "Java" When constructing an `Event` in your custom agent logic, set the author, for example: `Event.builder().author(this.getAgentName()) // ... .build();` + === "Go" + In custom agent `Run` methods, the framework typically handles authorship. If creating an event manually, set the author: `yield(&session.Event{Author: a.name, ...}, nil)` + * **Semantic Content & Actions:** Use `event.content` for the core message/data (text, function call/response). Use `event.actions` specifically for signaling side effects (state/artifact deltas) or control flow (`transfer`, `escalate`, `skip_summarization`). * **Idempotency Awareness:** Understand that the `SessionService` is responsible for applying the state/artifact changes signaled in `event.actions`. While ADK services aim for consistency, consider potential downstream effects if your application logic re-processes events. * **Use `is_final_response()`:** Rely on this helper method in your application/UI layer to identify complete, user-facing text responses. Avoid manually replicating its logic. * **Leverage History:** The session's event list is your primary debugging tool. Examine the sequence of authors, content, and actions to trace execution and diagnose issues. * **Use Metadata:** Use `invocation_id` to correlate all events within a single user interaction. Use `event.id` to reference specific, unique occurrences. -Treating events as structured messages with clear purposes for their content and actions is key to building, debugging, and managing complex agent behaviors in ADK. \ No newline at end of file +Treating events as structured messages with clear purposes for their content and actions is key to building, debugging, and managing complex agent behaviors in ADK. From 02a47814995771f3d203ac7122dd92a1009db96a Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Thu, 23 Oct 2025 03:58:34 +0000 Subject: [PATCH 112/125] updating go snippets for events --- docs/events/index.md | 108 +++++++++++++++++++++++-------------------- 1 file changed, 57 insertions(+), 51 deletions(-) diff --git a/docs/events/index.md b/docs/events/index.md index 6120950d..5dfa58f5 100644 --- a/docs/events/index.md +++ b/docs/events/index.md @@ -64,21 +64,21 @@ An `Event` in ADK is an immutable record representing a specific point in the ag // Conceptual Structure of an Event (Go - See session/session.go) // Simplified view based on the session.Event struct type Event struct { - // --- Fields analogous to LlmResponse --- - LLMResponse *llm.Response + // --- Fields from embedded model.LLMResponse --- + model.LLMResponse // --- ADK specific additions --- - Author string // 'user' or agent name - InvocationID string // ID for the whole interaction run - ID string // Unique ID for this specific event - Timestamp time.Time // Creation time - Actions *Actions // Important for side-effects & control - Branch string // Hierarchy path + Author string // 'user' or agent name + InvocationID string // ID for the whole interaction run + ID string // Unique ID for this specific event + Timestamp time.Time // Creation time + Actions EventActions // Important for side-effects & control + Branch string // Hierarchy path // ... other fields } - // llm.Response contains the Content field - type Response struct { + // model.LLMResponse contains the Content field + type LLMResponse struct { Content *genai.Content // ... other fields } @@ -237,7 +237,7 @@ Quickly determine what an event represents by checking: fmt.Println(" Type: Other Content (e.g., code result)") } } - } else if &event.Actions != nil && (len(event.Actions.StateDelta) > 0) { + } else if len(event.Actions.StateDelta) > 0 { fmt.Println(" Type: State Update") } else { fmt.Println(" Type: Control Signal or Other") @@ -295,10 +295,10 @@ Once you know the event type, access the relevant data: ) func handleFunctionCalls(event *session.Event) { - if event.LLMResponse == nil { + if event.LLMResponse == nil || event.LLMResponse.Content == nil { return } - calls := event.LLMResponse.FunctionCalls() + calls := event.LLMResponse.Content.FunctionCalls() if len(calls) > 0 { for _, call := range calls { toolName := call.Name @@ -349,10 +349,10 @@ Once you know the event type, access the relevant data: ) func handleFunctionResponses(event *session.Event) { - if event.LLMResponse == nil { + if event.LLMResponse == nil || event.LLMResponse.Content == nil { return } - responses := event.LLMResponse.FunctionResponses() + responses := event.LLMResponse.Content.FunctionResponses() if len(responses) > 0 { for _, response := range responses { toolName := response.Name @@ -404,7 +404,7 @@ The `event.actions` object signals changes that occurred or should occur. Always ) func handleStateChanges(event *session.Event) { - if event.Actions != nil && len(event.Actions.StateDelta) > 0 { + if len(event.Actions.StateDelta) > 0 { fmt.Printf(" State changes: %v\n", event.Actions.StateDelta) // Update local UI or application state if necessary } @@ -438,20 +438,8 @@ The `event.actions` object signals changes that occurred or should occur. Always ``` === "Go" - `artifactChanges := event.Actions.ArtifactDelta` (a `map[string]genai.Part`) - ```go - import ( - "fmt" - "google.golang.org/adk/session" - ) - - func handleArtifactChanges(event *session.Event) { - if event.Actions != nil && len(event.Actions.ArtifactDelta) > 0 { - fmt.Printf(" Artifacts saved: %v\n", event.Actions.ArtifactDelta) - // UI might refresh an artifact list - } - } - ``` + !!! warning "Not Yet Available" + Artifact change tracking via `event.Actions.ArtifactDelta` is not yet implemented in the Go ADK. * **Control Flow Signals:** Check boolean flags or string values: @@ -506,16 +494,14 @@ The `event.actions` object signals changes that occurred or should occur. Always ) func handleControlFlow(event *session.Event) { - if event.Actions != nil { - if event.Actions.TransferToAgent != "" { - fmt.Printf(" Signal: Transfer to %s\n", event.Actions.TransferToAgent) - } - if event.Actions.Escalate { - fmt.Println(" Signal: Escalate (terminate loop)") - } - if event.Actions.SkipSummarization { - fmt.Println(" Signal: Skip summarization for tool result") - } + if event.Actions.TransferToAgent != "" { + fmt.Printf(" Signal: Transfer to %s\n", event.Actions.TransferToAgent) + } + if event.Actions.Escalate { + fmt.Println(" Signal: Escalate (terminate loop)") + } + if event.Actions.SkipSummarization { + fmt.Println(" Signal: Skip summarization for tool result") } } ``` @@ -621,33 +607,53 @@ Use the built-in helper method `event.is_final_response()` to identify events su "google.golang.org/genai" ) + // isFinalResponse checks if an event is a final response suitable for display. + func isFinalResponse(event *session.Event) bool { + if event.LLMResponse != nil { + // Condition 1: Tool result with skip summarization. + if event.LLMResponse.Content != nil && len(event.LLMResponse.Content.FunctionResponses()) > 0 && event.Actions.SkipSummarization { + return true + } + // Condition 2: Long-running tool call. + if len(event.LongRunningToolIDs) > 0 { + return true + } + // Condition 3: A complete message without tool calls or responses. + if (event.LLMResponse.Content == nil || + (len(event.LLMResponse.Content.FunctionCalls()) == 0 && len(event.LLMResponse.Content.FunctionResponses()) == 0)) && + !event.LLMResponse.Partial { + return true + } + } + return false + } + func handleFinalResponses() { - // Pseudocode: Handling final responses in application (Go) var fullResponseText strings.Builder - // for event := range runner.Run(...) { + // for event := range runner.Run(...) { // Example loop // // Accumulate streaming text if needed... - // if event.LLMResponse != nil && event.LLMResponse.IsPartial && event.LLMResponse.Content != nil { - // if part, ok := event.LLMResponse.Content.Parts[0].(genai.Text); ok { - // fullResponseText.WriteString(string(part)) + // if event.LLMResponse != nil && event.LLMResponse.Partial && event.LLMResponse.Content != nil { + // if len(event.LLMResponse.Content.Parts) > 0 && event.LLMResponse.Content.Parts[0].Text != "" { + // fullResponseText.WriteString(event.LLMResponse.Content.Parts[0].Text) // } // } // // // Check if it's a final, displayable event - // if event.IsFinalResponse() { + // if isFinalResponse(event) { // fmt.Println("\n--- Final Output Detected ---") // if event.LLMResponse != nil && event.LLMResponse.Content != nil { - // if part, ok := event.LLMResponse.Content.Parts[0].(genai.Text); ok { + // if len(event.LLMResponse.Content.Parts) > 0 && event.LLMResponse.Content.Parts[0].Text != "" { // // If it's the final part of a stream, use accumulated text // finalText := fullResponseText.String() - // if !event.LLMResponse.IsPartial { - // finalText += string(part) + // if !event.LLMResponse.Partial { + // finalText += event.LLMResponse.Content.Parts[0].Text // } // fmt.Printf("Display to user: %s\n", strings.TrimSpace(finalText)) // fullResponseText.Reset() // Reset accumulator // } - // } else if event.Actions != nil && event.Actions.SkipSummarization && len(event.LLMResponse.FunctionResponses()) > 0 { + // } else if event.Actions.SkipSummarization && event.LLMResponse.Content != nil && len(event.LLMResponse.Content.FunctionResponses()) > 0 { // // Handle displaying the raw tool result if needed - // responseData := event.LLMResponse.FunctionResponses()[0].Response + // responseData := event.LLMResponse.Content.FunctionResponses()[0].Response // fmt.Printf("Display raw tool result: %v\n", responseData) // } else if len(event.LongRunningToolIDs) > 0 { // fmt.Println("Display message: Tool is running in background...") From 5e6a8e32d80283570a19795bfe7efdfb3e73331e Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Mon, 3 Nov 2025 02:32:27 +0000 Subject: [PATCH 113/125] adding artifactdelta snippet for go --- docs/events/index.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/docs/events/index.md b/docs/events/index.md index 5dfa58f5..62b7adb0 100644 --- a/docs/events/index.md +++ b/docs/events/index.md @@ -438,8 +438,25 @@ The `event.actions` object signals changes that occurred or should occur. Always ``` === "Go" - !!! warning "Not Yet Available" - Artifact change tracking via `event.Actions.ArtifactDelta` is not yet implemented in the Go ADK. + `artifactChanges := event.Actions.ArtifactDelta` (a `map[string]artifact.Artifact`) + ```go + import ( + "fmt" + "google.golang.org/adk/artifact" + "google.golang.org/adk/session" + ) + + func handleArtifactChanges(event *session.Event) { + if len(event.Actions.ArtifactDelta) > 0 { + fmt.Printf(" Artifacts saved: %v\n", event.Actions.ArtifactDelta) + // UI might refresh an artifact list + // Iterate through event.Actions.ArtifactDelta to get filename and artifact.Artifact details + for filename, art := range event.Actions.ArtifactDelta { + fmt.Printf(" Filename: %s, Version: %d, MIMEType: %s\n", filename, art.Version, art.MIMEType) + } + } + } + ``` * **Control Flow Signals:** Check boolean flags or string values: From efbfc572b88628e066c71171be780a944a447511 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Thu, 6 Nov 2025 01:37:15 +0000 Subject: [PATCH 114/125] minor java fixes, cleaning up go event.content --- docs/events/index.md | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/docs/events/index.md b/docs/events/index.md index 62b7adb0..5d119402 100644 --- a/docs/events/index.md +++ b/docs/events/index.md @@ -298,7 +298,7 @@ Once you know the event type, access the relevant data: if event.LLMResponse == nil || event.LLMResponse.Content == nil { return } - calls := event.LLMResponse.Content.FunctionCalls() + calls := event.Content.FunctionCalls() if len(calls) > 0 { for _, call := range calls { toolName := call.Name @@ -352,7 +352,7 @@ Once you know the event type, access the relevant data: if event.LLMResponse == nil || event.LLMResponse.Content == nil { return } - responses := event.LLMResponse.Content.FunctionResponses() + responses := event.Content.FunctionResponses() if len(responses) > 0 { for _, response := range responses { toolName := response.Name @@ -381,19 +381,19 @@ The `event.actions` object signals changes that occurred or should occur. Always # Update local UI or application state if necessary ``` === "Java" - `ConcurrentMap delta = event.actions().stateDelta();` + `ConcurrentMap delta = event.actions().stateDelta();` - ```java - import java.util.concurrent.ConcurrentMap; - import com.google.adk.events.EventActions; + ```java + import java.util.concurrent.ConcurrentMap; + import com.google.adk.events.EventActions; - EventActions actions = event.actions(); // Assuming event.actions() is not null - if (actions != null && actions.stateDelta() != null && !actions.stateDelta().isEmpty()) { - ConcurrentMap stateChanges = actions.stateDelta(); - System.out.println(" State changes: " + stateChanges); - // Update local UI or application state if necessary - } - ``` + EventActions actions = event.actions(); // Assuming event.actions() is not null + if (actions != null && actions.stateDelta() != null && !actions.stateDelta().isEmpty()) { + ConcurrentMap stateChanges = actions.stateDelta(); + System.out.println(" State changes: " + stateChanges); + // Update local UI or application state if necessary + } + ``` === "Go" `delta := event.Actions.StateDelta` (a `map[string]any`) @@ -595,7 +595,7 @@ Use the built-in helper method `event.is_final_response()` to identify events su String finalText = fullResponseText.toString() + (event.partial().orElse(false) ? "" : eventText); System.out.println("Display to user: " + finalText.trim()); fullResponseText.setLength(0); // Reset accumulator - } else if (event.actions() != nil && event.actions().skipSummarization().orElse(false) + } else if (event.actions() != null && event.actions().skipSummarization().orElse(false) && !event.functionResponses().isEmpty()) { // Handle displaying the raw tool result if needed, // especially if finalResponse() was true due to other conditions From 154a9f67b3013b663031f7c6596930c2d8189acc Mon Sep 17 00:00:00 2001 From: Ivan Cheung Date: Wed, 5 Nov 2025 21:22:11 -0500 Subject: [PATCH 115/125] Custom tools: Merging again (#44) * Added missing custom-tools snippets * Split user_preference runner code * Split more files * Tweaks * Split session into 1 and 2 and added artifact saving * Fixed conflict --------- Co-authored-by: ivanmkc --- docs/tools-custom/index.md | 32 +++- docs/tools/function-tools.md | 4 +- .../customer_support_agent/main.go | 115 +++++++++++++ .../tools-custom/doc_analysis/doc_analysis.go | 71 ++++++++ .../tools-custom/doc_analysis/main.go | 151 ++++++++++++++++++ .../tools-custom/order_status/main.go | 87 ++++++++++ .../tools-custom/order_status/order_status.go | 51 ++++++ .../tools-custom/user_preference/main.go | 87 ++++++++++ .../user_preference/user_preference.go | 42 +++++ .../tools-custom/weather_sentiment/main.go | 139 ++++++++++++++++ 10 files changed, 776 insertions(+), 3 deletions(-) create mode 100644 examples/go/snippets/tools-custom/customer_support_agent/main.go create mode 100644 examples/go/snippets/tools-custom/doc_analysis/doc_analysis.go create mode 100644 examples/go/snippets/tools-custom/doc_analysis/main.go create mode 100644 examples/go/snippets/tools-custom/order_status/main.go create mode 100644 examples/go/snippets/tools-custom/order_status/order_status.go create mode 100644 examples/go/snippets/tools-custom/user_preference/main.go create mode 100644 examples/go/snippets/tools-custom/user_preference/user_preference.go create mode 100644 examples/go/snippets/tools-custom/weather_sentiment/main.go diff --git a/docs/tools-custom/index.md b/docs/tools-custom/index.md index 24a01a63..3c289431 100644 --- a/docs/tools-custom/index.md +++ b/docs/tools-custom/index.md @@ -97,6 +97,12 @@ The following example showcases how an agent can use tools by **referencing thei --8<-- "examples/java/snippets/src/main/java/tools/WeatherSentimentAgentApp.java:full_code" ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/tools-custom/weather_sentiment/main.go" + ``` + ## Tool Context For more advanced scenarios, ADK allows you to access additional contextual information within your tool function by including the special parameter `tool_context: ToolContext`. By including this in the function signature, ADK will **automatically** provide an **instance of the ToolContext** class when your tool is called during agent execution. @@ -168,6 +174,12 @@ The `tool_context.state` attribute provides direct read and write access to the } ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/tools-custom/user_preference/user_preference.go:example" + ``` + ### **Controlling Agent Flow** The `tool_context.actions` attribute (`ToolContext.actions()` in Java) holds an **EventActions** object. Modifying attributes on this object allows your tool to influence what the agent or framework does after the tool finishes execution. @@ -192,6 +204,12 @@ The `tool_context.actions` attribute (`ToolContext.actions()` in Java) holds an --8<-- "examples/java/snippets/src/main/java/tools/CustomerSupportAgentApp.java:full_code" ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/tools-custom/customer_support_agent/main.go" + ``` + ##### Explanation * We define two agents: `main_agent` and `support_agent`. The `main_agent` is designed to be the initial point of contact. @@ -229,7 +247,7 @@ These methods provide convenient ways for your tool to interact with persistent * **`save_artifact(filename: str, artifact: types.Part)`**: Saves a new version of an artifact to the artifact_service. Returns the new version number (starting from 0). -* **`search_memory(query: str)`**: (Python only feature) +* **`search_memory(query: str)`**: (Python and Go only feature) Queries the user's long-term memory using the configured `memory_service`. This is useful for retrieving relevant information from past interactions or stored knowledge. The structure of the **SearchMemoryResponse** depends on the specific memory service implementation but typically contains relevant text snippets or conversation excerpts. #### Example @@ -294,6 +312,12 @@ These methods provide convenient ways for your tool to interact with persistent // LlmAgent agent = LlmAgent().builder().tools(processDocumentTool).build(); ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/tools-custom/doc_analysis/doc_analysis.go" + ``` + By leveraging the **ToolContext**, developers can create more sophisticated and context-aware custom tools that seamlessly integrate with ADK's architecture and enhance the overall capabilities of their agents. ## Defining Effective Tool Functions @@ -395,6 +419,12 @@ Here are key guidelines for defining effective tool functions: } ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/tools-custom/order_status/order_status.go:snippet" + ``` + * **Simplicity and Focus:** * **Keep Tools Focused:** Each tool should ideally perform one well-defined task. * **Fewer Parameters are Better:** Models generally handle tools with fewer, clearly defined parameters more reliably than those with many optional or complex ones. diff --git a/docs/tools/function-tools.md b/docs/tools/function-tools.md index 7219fe2b..e2184813 100644 --- a/docs/tools/function-tools.md +++ b/docs/tools/function-tools.md @@ -207,10 +207,10 @@ A tool can write data to a `temp:` variable, and a subsequent tool can read it. --8<-- "examples/go/snippets/tools/function-tools/func_tool.go" ``` - The return value from this tool will be a `map[string]any` marshalled into a JSON object. + The return value from this tool will be a `getStockPriceResults` instance. ```json - For input `{"symbol": "GOOG"}`: {"price":1,"symbol":"GOOG"} + For input `{"symbol": "GOOG"}`: {"price":300.6,"symbol":"GOOG"} ``` ### Best Practices diff --git a/examples/go/snippets/tools-custom/customer_support_agent/main.go b/examples/go/snippets/tools-custom/customer_support_agent/main.go new file mode 100644 index 00000000..1c0c44cb --- /dev/null +++ b/examples/go/snippets/tools-custom/customer_support_agent/main.go @@ -0,0 +1,115 @@ +package main + +import ( + "context" + "fmt" + "log" + "strings" + + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/model/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/session" + "google.golang.org/adk/tool" + "google.golang.org/adk/tool/functiontool" + "google.golang.org/genai" +) + +type checkAndTransferArgs struct { + Query string `json:"query"` +} + +type checkAndTransferResult struct { + Status string `json:"status"` +} + +func checkAndTransfer(ctx tool.Context, args checkAndTransferArgs) checkAndTransferResult { + if strings.Contains(strings.ToLower(args.Query), "urgent") { + fmt.Println("Tool: Detected urgency, transferring to the support agent.") + ctx.Actions().TransferToAgent = "support_agent" + return checkAndTransferResult{Status: "Transferring to the support agent..."} + } + return checkAndTransferResult{Status: fmt.Sprintf("Processed query: '%s'. No further action needed.", args.Query)} +} + +func main() { + ctx := context.Background() + model, err := gemini.NewModel(ctx, "gemini-2.0-flash", &genai.ClientConfig{}) + if err != nil { + log.Fatal(err) + } + + supportAgent, err := llmagent.New(llmagent.Config{ + Name: "support_agent", + Model: model, + Instruction: "You are the dedicated support agent. Mentioned you are a support handler and please help the user with their urgent issue.", + }) + if err != nil { + log.Fatal(err) + } + + checkAndTransferTool, err := functiontool.New( + functiontool.Config{ + Name: "check_and_transfer", + Description: "Checks if the query requires escalation and transfers to another agent if needed.", + }, + checkAndTransfer, + ) + if err != nil { + log.Fatal(err) + } + + mainAgent, err := llmagent.New(llmagent.Config{ + Name: "main_agent", + Model: model, + Instruction: "You are the first point of contact for customer support of an analytics tool. Answer general queries. If the user indicates urgency, use the 'check_and_transfer' tool.", + Tools: []tool.Tool{checkAndTransferTool}, + SubAgents: []agent.Agent{supportAgent}, + }) + if err != nil { + log.Fatal(err) + } + + sessionService := session.InMemoryService() + runner, err := runner.New(runner.Config{ + AppName: "customer_support_agent", + Agent: mainAgent, + SessionService: sessionService, + }) + if err != nil { + log.Fatal(err) + } + + session, err := sessionService.Create(ctx, &session.CreateRequest{ + AppName: "customer_support_agent", + UserID: "user1234", + }) + if err != nil { + log.Fatal(err) + } + + run(ctx, runner, session.Session.ID(), "this is urgent, i cant login") +} + +func run(ctx context.Context, r *runner.Runner, sessionID string, prompt string) { + fmt.Printf("\n> %s\n", prompt) + events := r.Run( + ctx, + "user1234", + sessionID, + genai.NewContentFromText(prompt, genai.RoleUser), + agent.RunConfig{ + StreamingMode: agent.StreamingModeNone, + }, + ) + for event, err := range events { + if err != nil { + log.Fatalf("ERROR during agent execution: %v", err) + } + + if event.Content.Parts[0].Text != "" { + fmt.Printf("Agent Response: %s\n", event.Content.Parts[0].Text) + } + } +} diff --git a/examples/go/snippets/tools-custom/doc_analysis/doc_analysis.go b/examples/go/snippets/tools-custom/doc_analysis/doc_analysis.go new file mode 100644 index 00000000..763a06f9 --- /dev/null +++ b/examples/go/snippets/tools-custom/doc_analysis/doc_analysis.go @@ -0,0 +1,71 @@ +package main + +import ( + "fmt" + + "google.golang.org/adk/tool" + "google.golang.org/genai" +) + +type processDocumentArgs struct { + DocumentName string `json:"document_name"` + AnalysisQuery string `json:"analysis_query"` +} + +type processDocumentResult struct { + Status string `json:"status"` + AnalysisArtifact string `json:"analysis_artifact,omitempty"` + Version int64 `json:"version,omitempty"` + Message string `json:"message,omitempty"` +} + +func processDocument(ctx tool.Context, args processDocumentArgs) processDocumentResult { + fmt.Printf("Tool: Attempting to load artifact: %s\n", args.DocumentName) + + // List all artifacts + listResponse, err := ctx.Artifacts().List(ctx) + if err != nil { + return processDocumentResult{Status: "error", Message: "Failed to list artifacts."} + } + + fmt.Println("Tool: Available artifacts:") + for _, file := range listResponse.FileNames { + fmt.Printf(" - %s\n", file) + } + + documentPart, err := ctx.Artifacts().Load(ctx, args.DocumentName) + if err != nil { + return processDocumentResult{Status: "error", Message: fmt.Sprintf("Document '%s' not found.", args.DocumentName)} + } + + fmt.Printf("Tool: Loaded document '%s' of size %d bytes.\n", args.DocumentName, len(documentPart.Part.InlineData.Data)) + + // 3. Search memory for related context + fmt.Printf("Tool: Searching memory for context related to: '%s'\n", args.AnalysisQuery) + memoryResp, err := ctx.SearchMemory(ctx, args.AnalysisQuery) + if err != nil { + fmt.Printf("Tool: Error searching memory: %v\n", err) + } + memoryResultCount := 0 + if memoryResp != nil { + memoryResultCount = len(memoryResp.Memories) + } + fmt.Printf("Tool: Found %d memory results.\n", memoryResultCount) + + analysisResult := fmt.Sprintf("Analysis of '%s' regarding '%s' using memory context: [Placeholder Analysis Result]", args.DocumentName, args.AnalysisQuery) + fmt.Println("Tool: Performed analysis.") + + analysisPart := genai.NewPartFromText(analysisResult) + newArtifactName := fmt.Sprintf("analysis_%s", args.DocumentName) + version, err := ctx.Artifacts().Save(ctx, newArtifactName, analysisPart) + if err != nil { + return processDocumentResult{Status: "error", Message: "Failed to save artifact."} + } + fmt.Printf("Tool: Saved analysis result as '%s' version %d.\n", newArtifactName, version.Version) + + return processDocumentResult{ + Status: "success", + AnalysisArtifact: newArtifactName, + Version: version.Version, + } +} diff --git a/examples/go/snippets/tools-custom/doc_analysis/main.go b/examples/go/snippets/tools-custom/doc_analysis/main.go new file mode 100644 index 00000000..6097d6e5 --- /dev/null +++ b/examples/go/snippets/tools-custom/doc_analysis/main.go @@ -0,0 +1,151 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/artifact" + "google.golang.org/adk/memory" + "google.golang.org/adk/model" + "google.golang.org/adk/model/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/session" + "google.golang.org/adk/tool" + "google.golang.org/adk/tool/functiontool" + "google.golang.org/genai" +) + +func saveStoryBytes(ctx agent.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) { + // Get the report data from the session state. + storyData, err := ctx.State().Get("story_bytes") + if err != nil { + log.Printf("No report data found in session state: %v", err) + return nil, nil // No report to save, continue normally. + } + + // Check if the report data is in the expected format. + storyBytes, ok := storyData.([]byte) + if !ok { + log.Printf("Report data in session state was not in the expected byte format.") + return nil, nil + } + + // Create a new artifact with the report data. + documentArtifact := &genai.Part{ + InlineData: &genai.Blob{ + MIMEType: "application/pdf", + Data: storyBytes, + }, + } + // Set the filename for the artifact. + filename := "my_document.pdf" + // Save the artifact to the artifact service. + _, err = ctx.Artifacts().Save(ctx, filename, documentArtifact) + if err != nil { + log.Printf("An unexpected error occurred during Go artifact save: %v", err) + // Depending on requirements, you might want to return an error to the user. + return nil, nil + } + log.Printf("Successfully saved Go artifact '%s'.", filename) + + // Return nil to continue to the next callback or the model. + return nil, nil +} + +func main() { + ctx := context.Background() + model, err := gemini.NewModel(ctx, "gemini-2.0-flash", &genai.ClientConfig{}) + if err != nil { + log.Fatal(err) + } + + docAnalysisTool, err := functiontool.New( + functiontool.Config{ + Name: "process_document", + Description: "Analyzes a document using context from memory.", + }, + processDocument, + ) + if err != nil { + log.Fatal(err) + } + + mainAgent, err := llmagent.New(llmagent.Config{ + Name: "main_agent", + Model: model, + Instruction: "You are an agent that can process documents.", + Tools: []tool.Tool{docAnalysisTool}, + BeforeModelCallbacks: []llmagent.BeforeModelCallback{saveStoryBytes}, + }) + if err != nil { + log.Fatal(err) + } + + sessionService := session.InMemoryService() + artifactService := artifact.InMemoryService() + memoryService := memory.InMemoryService() + runner, err := runner.New(runner.Config{ + AppName: "doc_analysis", + Agent: mainAgent, + SessionService: sessionService, + ArtifactService: artifactService, + MemoryService: memoryService, + }) + if err != nil { + log.Fatal(err) + } + + session1, err := sessionService.Create(ctx, &session.CreateRequest{ + AppName: "doc_analysis", + UserID: "user1234", + }) + if err != nil { + log.Fatal(err) + } + + storyBytes, _ := os.ReadFile("story.pdf") // Load a sample PDF file + initialState := map[string]any{ + "story_bytes": storyBytes, + } + + session2, err := sessionService.Create(ctx, &session.CreateRequest{ + AppName: "doc_analysis", + UserID: "user1234", + State: initialState, + }) + if err != nil { + log.Fatal(err) + } + + // First run to populate memory. The agent will respond, and the runner will + // automatically add the interaction to the memory service. + run(ctx, runner, session1.Session.ID(), "I am very interested in positive sentiment analysis.") + // Second run that uses the tool to search the memory populated by the first run. + run(ctx, runner, session2.Session.ID(), "process the document named 'my_document.pdf' and analyze it for 'sentiment'") +} + +func run(ctx context.Context, r *runner.Runner, sessionID string, prompt string) { + fmt.Printf("\n> %s\n", prompt) + events := r.Run( + ctx, + "user1234", + sessionID, + genai.NewContentFromText(prompt, genai.RoleUser), + agent.RunConfig{ + StreamingMode: agent.StreamingModeNone, + }, + ) + for event, err := range events { + if err != nil { + log.Fatalf("ERROR during agent execution: %v", err) + } + + if event.Content.Parts[0].Text != "" { + fmt.Printf("Agent Response: %s\n", event.Content.Parts[0].Text) + } + } +} diff --git a/examples/go/snippets/tools-custom/order_status/main.go b/examples/go/snippets/tools-custom/order_status/main.go new file mode 100644 index 00000000..237cdb38 --- /dev/null +++ b/examples/go/snippets/tools-custom/order_status/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "context" + "fmt" + "log" + + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/model/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/session" + "google.golang.org/adk/tool" + "google.golang.org/adk/tool/functiontool" + "google.golang.org/genai" +) + +func main() { + ctx := context.Background() + model, err := gemini.NewModel(ctx, "gemini-2.0-flash", &genai.ClientConfig{}) + if err != nil { + log.Fatal(err) + } + + orderStatusTool, err := functiontool.New( + functiontool.Config{ + Name: "lookup_order_status", + Description: "Fetches the current status of a customer's order using its ID.", + }, + lookupOrderStatus, + ) + if err != nil { + log.Fatal(err) + } + + mainAgent, err := llmagent.New(llmagent.Config{ + Name: "main_agent", + Model: model, + Instruction: "You are an agent that can lookup order status.", + Tools: []tool.Tool{orderStatusTool}, + }) + if err != nil { + log.Fatal(err) + } + + sessionService := session.InMemoryService() + runner, err := runner.New(runner.Config{ + AppName: "order_status", + Agent: mainAgent, + SessionService: sessionService, + }) + if err != nil { + log.Fatal(err) + } + + session, err := sessionService.Create(ctx, &session.CreateRequest{ + AppName: "order_status", + UserID: "user1234", + }) + if err != nil { + log.Fatal(err) + } + + run(ctx, runner, session.Session.ID(), "what is the status of order 12345?") +} + +func run(ctx context.Context, r *runner.Runner, sessionID string, prompt string) { + fmt.Printf("\n> %s\n", prompt) + events := r.Run( + ctx, + "user1234", + sessionID, + genai.NewContentFromText(prompt, genai.RoleUser), + agent.RunConfig{ + StreamingMode: agent.StreamingModeNone, + }, + ) + for event, err := range events { + if err != nil { + log.Fatalf("ERROR during agent execution: %v", err) + } + + if event.Content.Parts[0].Text != "" { + fmt.Printf("Agent Response: %s\n", event.Content.Parts[0].Text) + } + } +} diff --git a/examples/go/snippets/tools-custom/order_status/order_status.go b/examples/go/snippets/tools-custom/order_status/order_status.go new file mode 100644 index 00000000..26c22f66 --- /dev/null +++ b/examples/go/snippets/tools-custom/order_status/order_status.go @@ -0,0 +1,51 @@ +package main + +// --8<-- [start:snippet] +import ( + "fmt" + + "google.golang.org/adk/tool" +) + +type lookupOrderStatusArgs struct { + OrderID string `json:"order_id"` +} + +type order struct { + State string `json:"state"` + TrackingNumber string `json:"tracking_number"` +} + +type lookupOrderStatusResult struct { + Status string `json:"status"` + Order order `json:"order,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` +} + +func lookupOrderStatus(ctx tool.Context, args lookupOrderStatusArgs) lookupOrderStatusResult { + // ... function implementation to fetch status ... + if statusDetails, ok := fetchStatusFromBackend(args.OrderID); ok { + return lookupOrderStatusResult{ + Status: "success", + Order: order{ + State: statusDetails.State, + TrackingNumber: statusDetails.Tracking, + }, + } + } + return lookupOrderStatusResult{Status: "error", ErrorMessage: fmt.Sprintf("Order ID %s not found.", args.OrderID)} +} + +// --8<-- [end:snippet] + +type statusDetails struct { + State string + Tracking string +} + +func fetchStatusFromBackend(orderID string) (statusDetails, bool) { + if orderID == "12345" { + return statusDetails{State: "shipped", Tracking: "1Z9..."}, true + } + return statusDetails{}, false +} diff --git a/examples/go/snippets/tools-custom/user_preference/main.go b/examples/go/snippets/tools-custom/user_preference/main.go new file mode 100644 index 00000000..e51be9d5 --- /dev/null +++ b/examples/go/snippets/tools-custom/user_preference/main.go @@ -0,0 +1,87 @@ +package main + +import ( + "context" + "fmt" + "log" + + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/model/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/session" + "google.golang.org/adk/tool" + "google.golang.org/adk/tool/functiontool" + "google.golang.org/genai" +) + +func main() { + ctx := context.Background() + model, err := gemini.NewModel(ctx, "gemini-2.0-flash", &genai.ClientConfig{}) + if err != nil { + log.Fatal(err) + } + + userPreferenceTool, err := functiontool.New( + functiontool.Config{ + Name: "update_user_preference", + Description: "Updates a user-specific preference.", + }, + updateUserPreference, + ) + if err != nil { + log.Fatal(err) + } + + mainAgent, err := llmagent.New(llmagent.Config{ + Name: "main_agent", + Model: model, + Instruction: "You are an agent that can update user preferences. When a user asks to set a preference, identify the preference key and the desired value. For example, in 'set my theme to dark', the key is 'theme' and the value is 'dark'. Then, call the 'update_user_preference' tool with these values.", + Tools: []tool.Tool{userPreferenceTool}, + }) + if err != nil { + log.Fatal(err) + } + + sessionService := session.InMemoryService() + runner, err := runner.New(runner.Config{ + AppName: "user_preference", + Agent: mainAgent, + SessionService: sessionService, + }) + if err != nil { + log.Fatal(err) + } + + session, err := sessionService.Create(ctx, &session.CreateRequest{ + AppName: "user_preference", + UserID: "user1234", + }) + if err != nil { + log.Fatal(err) + } + + run(ctx, runner, session.Session.ID(), "set my theme to dark") +} + +func run(ctx context.Context, r *runner.Runner, sessionID string, prompt string) { + fmt.Printf("\n> %s\n", prompt) + events := r.Run( + ctx, + "user1234", + sessionID, + genai.NewContentFromText(prompt, genai.RoleUser), + agent.RunConfig{ + StreamingMode: agent.StreamingModeNone, + }, + ) + for event, err := range events { + if err != nil { + log.Fatalf("ERROR during agent execution: %v", err) + } + + if event.Content.Parts[0].Text != "" { + fmt.Printf("Agent Response: %s\n", event.Content.Parts[0].Text) + } + } +} diff --git a/examples/go/snippets/tools-custom/user_preference/user_preference.go b/examples/go/snippets/tools-custom/user_preference/user_preference.go new file mode 100644 index 00000000..da6a7a52 --- /dev/null +++ b/examples/go/snippets/tools-custom/user_preference/user_preference.go @@ -0,0 +1,42 @@ +package main + +// --8<-- [start:example] +import ( + "fmt" + + "google.golang.org/adk/tool" +) + +type updateUserPreferenceArgs struct { + Preference string `json:"preference"` + Value string `json:"value"` +} + +type updateUserPreferenceResult struct { + Status string `json:"status"` + UpdatedPreference string `json:"updated_preference"` +} + +func updateUserPreference(ctx tool.Context, args updateUserPreferenceArgs) updateUserPreferenceResult { + userPrefsKey := "user:preferences" + val, err := ctx.State().Get(userPrefsKey) + if err != nil { + val = make(map[string]any) + } + + preferencesMap, ok := val.(map[string]any) + if !ok { + preferencesMap = make(map[string]any) + } + + preferencesMap[args.Preference] = args.Value + + if err := ctx.State().Set(userPrefsKey, preferencesMap); err != nil { + return updateUserPreferenceResult{Status: "error"} + } + + fmt.Printf("Tool: Updated user preference '%s' to '%s'\n", args.Preference, args.Value) + return updateUserPreferenceResult{Status: "success", UpdatedPreference: args.Preference} +} + +// --8<-- [end:example] diff --git a/examples/go/snippets/tools-custom/weather_sentiment/main.go b/examples/go/snippets/tools-custom/weather_sentiment/main.go new file mode 100644 index 00000000..1154aaa2 --- /dev/null +++ b/examples/go/snippets/tools-custom/weather_sentiment/main.go @@ -0,0 +1,139 @@ +package main + +import ( + "context" + "fmt" + "log" + "strings" + + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/model/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/session" + "google.golang.org/adk/tool" + "google.golang.org/adk/tool/functiontool" + "google.golang.org/genai" +) + +type getWeatherReportArgs struct { + City string `json:"city"` +} + +type getWeatherReportResult struct { + Status string `json:"status"` + Report string `json:"report,omitempty"` + ErrorMessage string `json:"error_message,omitempty"` +} + +func getWeatherReport(ctx tool.Context, args getWeatherReportArgs) getWeatherReportResult { + if strings.ToLower(args.City) == "london" { + return getWeatherReportResult{Status: "success", Report: "The current weather in London is cloudy with a temperature of 18 degrees Celsius and a chance of rain."} + } + if strings.ToLower(args.City) == "paris" { + return getWeatherReportResult{Status: "success", Report: "The weather in Paris is sunny with a temperature of 25 degrees Celsius."} + } + return getWeatherReportResult{Status: "error", ErrorMessage: fmt.Sprintf("Weather information for '%s' is not available.", args.City)} +} + +type analyzeSentimentArgs struct { + Text string `json:"text"` +} + +type analyzeSentimentResult struct { + Sentiment string `json:"sentiment"` + Confidence float64 `json:"confidence"` +} + +func analyzeSentiment(ctx tool.Context, args analyzeSentimentArgs) analyzeSentimentResult { + if strings.Contains(strings.ToLower(args.Text), "good") || strings.Contains(strings.ToLower(args.Text), "sunny") { + return analyzeSentimentResult{Sentiment: "positive", Confidence: 0.8} + } + if strings.Contains(strings.ToLower(args.Text), "rain") || strings.Contains(strings.ToLower(args.Text), "bad") { + return analyzeSentimentResult{Sentiment: "negative", Confidence: 0.7} + } + return analyzeSentimentResult{Sentiment: "neutral", Confidence: 0.6} +} + +func main() { + ctx := context.Background() + model, err := gemini.NewModel(ctx, "gemini-2.0-flash", &genai.ClientConfig{}) + if err != nil { + log.Fatal(err) + } + + weatherTool, err := functiontool.New( + functiontool.Config{ + Name: "get_weather_report", + Description: "Retrieves the current weather report for a specified city.", + }, + getWeatherReport, + ) + if err != nil { + log.Fatal(err) + } + + sentimentTool, err := functiontool.New( + functiontool.Config{ + Name: "analyze_sentiment", + Description: "Analyzes the sentiment of the given text.", + }, + analyzeSentiment, + ) + if err != nil { + log.Fatal(err) + } + + weatherSentimentAgent, err := llmagent.New(llmagent.Config{ + Name: "weather_sentiment_agent", + Model: model, + Instruction: "You are a helpful assistant that provides weather information and analyzes the sentiment of user feedback. **If the user asks about the weather in a specific city, use the 'get_weather_report' tool to retrieve the weather details.** **If the 'get_weather_report' tool returns a 'success' status, provide the weather report to the user.** **If the 'get_weather_report' tool returns an 'error' status, inform the user that the weather information for the specified city is not available and ask if they have another city in mind.** **After providing a weather report, if the user gives feedback on the weather (e.g., 'That's good' or 'I don't like rain'), use the 'analyze_sentiment' tool to understand their sentiment.** Then, briefly acknowledge their sentiment. You can handle these tasks sequentially if needed.", + Tools: []tool.Tool{weatherTool, sentimentTool}, + }) + if err != nil { + log.Fatal(err) + } + + sessionService := session.InMemoryService() + runner, err := runner.New(runner.Config{ + AppName: "weather_sentiment_agent", + Agent: weatherSentimentAgent, + SessionService: sessionService, + }) + if err != nil { + log.Fatal(err) + } + + session, err := sessionService.Create(ctx, &session.CreateRequest{ + AppName: "weather_sentiment_agent", + UserID: "user1234", + }) + if err != nil { + log.Fatal(err) + } + + run(ctx, runner, session.Session.ID(), "weather in london?") + run(ctx, runner, session.Session.ID(), "I don't like rain.") +} + +func run(ctx context.Context, r *runner.Runner, sessionID string, prompt string) { + fmt.Printf("\n> %s\n", prompt) + events := r.Run( + ctx, + "user1234", + sessionID, + genai.NewContentFromText(prompt, genai.RoleUser), + agent.RunConfig{ + StreamingMode: agent.StreamingModeNone, + }, + ) + for event, err := range events { + if err != nil { + log.Fatalf("ERROR during agent execution: %v", err) + } + + if event.Content.Parts[0].Text != "" { + fmt.Printf("Agent Response: %s\n", event.Content.Parts[0].Text) + } + } +} From a0d8bb81aa8d78f6908bd2073aadf7fa11206b0b Mon Sep 17 00:00:00 2001 From: Ivan Cheung Date: Wed, 5 Nov 2025 21:48:28 -0500 Subject: [PATCH 116/125] Fixed tab issue with cloud-run md (#45) Co-authored-by: ivanmkc --- docs/deploy/cloud-run.md | 180 ++++++++++++++++++--------------------- 1 file changed, 85 insertions(+), 95 deletions(-) diff --git a/docs/deploy/cloud-run.md b/docs/deploy/cloud-run.md index 9fa47f62..5948359a 100644 --- a/docs/deploy/cloud-run.md +++ b/docs/deploy/cloud-run.md @@ -32,8 +32,7 @@ To proceed, confirm that your agent code is configured as follows: 3. Your go.mod and go.sum files are present in your project directory to manage dependencies. - Refer to the following section for more details. You can also find a [sample app](https://github.com/google/adk-docs/tree/main/examples/go/cloud-run) in the Github - repo. + Refer to the following section for more details. You can also find a [sample app](https://github.com/google/adk-docs/tree/main/examples/go/cloud-run) in the Github repo. === "Java" @@ -43,6 +42,7 @@ To proceed, confirm that your agent code is configured as follows: Refer to the following section for more details. You can also find a [sample app](https://github.com/google/adk-docs/tree/main/examples/java/cloud-run) in the Github repo. + ## Environment variables Set your environment variables as described in the [Setup and Installation](../get-started/installation.md) guide. @@ -317,6 +317,89 @@ unless you specify it as deployment setting, such as the `--with_ui` option for For a full list of deployment options, see the [`gcloud run deploy` reference documentation](https://cloud.google.com/sdk/gcloud/reference/run/deploy). +=== "Go - adkgo CLI" + + ### adk CLI + + The adkgo command is located in the google/adk-go repository under cmd/adkgo. Before using it, you need to build it from the root of the adk-go repository: + + `go build ./cmd/adkgo` + + The adkgo deploy cloudrun command automates the deployment of your application. You do not need to provide your own Dockerfile. + + #### Agent Code Structure + + When using the adkgo tool, your main.go file must use the launcher framework. This is because the tool compiles your code and then runs the resulting executable with specific command-line arguments (like web, api, a2a) to start the required services. The launcher is designed to parse these arguments correctly. + + Your main.go should look like this: + + ```go title="main.go" + --8<-- "examples/go/cloud-run/main.go" + ``` + + #### How it Works + 1. The adkgo tool compiles your main.go into a statically linked binary for Linux. + 2. It generates a Dockerfile that copies this binary into a minimal container. + 3. It uses gcloud to build and deploy this container to Cloud Run. + 4. After deployment, it starts a local proxy that securely connects to your new + service. + + Ensure you have authenticated with Google Cloud (`gcloud auth login` and `gcloud config set project `). + + #### Setup environment variables + + Optional but recommended: Setting environment variables can make the deployment commands cleaner. + + ```bash + # Set your Google Cloud Project ID + export GOOGLE_CLOUD_PROJECT="your-gcp-project-id" + + # Set your desired Google Cloud Location + export GOOGLE_CLOUD_LOCATION="us-central1" + + # Set the path to your agent's main Go file + export AGENT_PATH="./examples/go/cloud-run/main.go" + + # Set a name for your Cloud Run service + export SERVICE_NAME="capital-agent-service" + ``` + + #### Command usage + + ```bash + ./adkgo deploy cloudrun \ + -p $GOOGLE_CLOUD_PROJECT \ + -r $GOOGLE_CLOUD_LOCATION \ + -s $SERVICE_NAME \ + --proxy_port=8081 \ + --server_port=8080 \ + -e $AGENT_PATH \ + --a2a --api --webui + ``` + + ##### Required + + * `-p, --project_name`: Your Google Cloud project ID (e.g., $GOOGLE_CLOUD_PROJECT). + * `-r, --region`: The Google Cloud location for deployment (e.g., $GOOGLE_CLOUD_LOCATION, us-central1). + * `-s, --service_name`: The name for the Cloud Run service (e.g., $SERVICE_NAME). + * `-e, --entry_point_path`: Path to the main Go file containing your agent's source code (e.g., $AGENT_PATH). + + ##### Optional + + * `--proxy_port`: The local port for the authenticating proxy to listen on. Defaults to 8081. + * `--server_port`: The port number the server will listen on within the Cloud Run container. Defaults to 8080. + * `--a2a`: If included, enables Agent-to-Agent communication. Enabled by default. + * `--a2a_agent_url`: A2A agent card URL as advertised in the public agent card. This flag is only valid when used with the --a2a flag. + * `--api`: If included, deploys the ADK API server. Enabled by default. + * `--webui`: If included, deploys the ADK dev UI alongside the agent API server. Enabled by default. + * `--temp_dir`: Temp directory for build artifacts. Defaults to os.TempDir(). + * `--help`: Show the help message and exit. + + ##### Authenticated access + The service is deployed with --no-allow-unauthenticated by default. + + Upon successful execution, the command deploys your agent to Cloud Run and provide a local URL to access the service through the proxy. + === "Java - gcloud CLI" ### gcloud CLI for Java @@ -414,99 +497,6 @@ unless you specify it as deployment setting, such as the `--with_ui` option for For a full list of deployment options, see the [`gcloud run deploy` reference documentation](https://cloud.google.com/sdk/gcloud/reference/run/deploy). -=== "Go - adkgo CLI" - - ### adk CLI - - The adkgo command is located in the google/adk-go repository under cmd/adkgo. Before using it, you need to build it from the root of the adk-go repository: - - `go build ./cmd/adkgo` - - The adkgo deploy cloudrun command automates the deployment of your application. You do not need to provide your own Dockerfile. - - #### Agent Code Structure - - When using the adkgo tool, your main.go file must use the launcher framework. This is because the tool compiles your code and then runs the resulting executable with specific command-line arguments (like web, api, a2a) to start the required services. The launcher is designed to parse these arguments correctly. - - Your main.go should look like this: - - ```go title="main.go" - --8<-- "examples/go/cloud-run/main.go" - ``` - - #### How it Works - 1. The adkgo tool compiles your main.go into a statically linked binary for Linux. - 2. It generates a Dockerfile that copies this binary into a minimal container. - 3. It uses gcloud to build and deploy this container to Cloud Run. - 4. After deployment, it starts a local proxy that securely connects to your new - service. - - Ensure you have authenticated with Google Cloud (`gcloud auth login` and `gcloud config set project `). - - #### Setup environment variables - - Optional but recommended: Setting environment variables can make the deployment commands cleaner. - - ```bash - # Set your Google Cloud Project ID - export GOOGLE_CLOUD_PROJECT="your-gcp-project-id" - - # Set your desired Google Cloud Location - export GOOGLE_CLOUD_LOCATION="us-central1" - - # Set the path to your agent's main Go file - export AGENT_PATH="./examples/go/cloud-run/main.go" - - # Set a name for your Cloud Run service - export SERVICE_NAME="capital-agent-service" - ``` - - #### Command usage - - ```bash - ./adkgo deploy cloudrun \ - -p $GOOGLE_CLOUD_PROJECT \ - -r $GOOGLE_CLOUD_LOCATION \ - -s $SERVICE_NAME \ - --proxy_port=8081 \ - --server_port=8080 \ - -e $AGENT_PATH \ - --a2a --api --webui - ``` - - ##### Arguments - - * -p, --project_name TEXT: (Required) Your Google Cloud project ID (e.g., - $GOOGLE_CLOUD_PROJECT). - * -r, --region TEXT: (Required) The Google Cloud location for deployment (e.g., - $GOOGLE_CLOUD_LOCATION, us-central1). - * -s, --service_name TEXT: (Required) The name for the Cloud Run service (e.g., - $SERVICE_NAME). - * -e, --entry_point_path TEXT: (Required) Path to the main Go file containing your - agent's source code (e.g., $AGENT_PATH). - - ##### Options - - * --proxy_port INTEGER: (Optional) The local port for the authenticating proxy to - listen on. Defaults to 8081. - * --server_port INTEGER: (Optional) The port number the server will listen on - within the Cloud Run container. Defaults to 8080. - * --a2a: (Optional) If included, enables Agent-to-Agent communication. Enabled by - default. - * --a2a_agent_url: (Optional) A2A agent card URL as advertised in the public agent card. This flag is only valid when - used with the --a2a flag. - * --api: (Optional) If included, deploys the ADK API server. Enabled by default. - * --webui: (Optional) If included, deploys the ADK dev UI alongside the agent API - server. Enabled by default. - * --temp_dir TEXT: (Optional) Temp directory for build artifacts. Defaults to - os.TempDir(). - * --help: Show the help message and exit. - - ##### Authenticated access - The service is deployed with --no-allow-unauthenticated by default. - - Upon successful execution, the command deploys your agent to Cloud Run and provide a local URL to access the service through the proxy. - ## Testing your agent Once your agent is deployed to Cloud Run, you can interact with it via the deployed UI (if enabled) or directly with its API endpoints using tools like `curl`. You'll need the service URL provided after deployment. From 4a67c15267861d40c136e20d562e764abbd2d307 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Wed, 15 Oct 2025 00:17:09 +0000 Subject: [PATCH 117/125] adding initial go snippets for memory services --- docs/sessions/memory.md | 37 ++- .../snippets/sessions/memory_example/go.mod | 40 ++++ .../snippets/sessions/memory_example/go.sum | 77 ++++++ .../sessions/memory_example/memory_example.go | 220 ++++++++++++++++++ 4 files changed, 364 insertions(+), 10 deletions(-) create mode 100644 examples/go/snippets/sessions/memory_example/go.mod create mode 100644 examples/go/snippets/sessions/memory_example/go.sum create mode 100644 examples/go/snippets/sessions/memory_example/memory_example.go diff --git a/docs/sessions/memory.md b/docs/sessions/memory.md index e6647a75..c4e5b19d 100644 --- a/docs/sessions/memory.md +++ b/docs/sessions/memory.md @@ -8,14 +8,14 @@ We've seen how `Session` tracks the history (`events`) and temporary data (`stat Think of it this way: -* **`Session` / `State`:** Like your short-term memory during one specific chat. +* **`Session` / `State`:** Like your short-term memory during one specific chat. * **Long-Term Knowledge (`MemoryService`)**: Like a searchable archive or knowledge library the agent can consult, potentially containing information from many past chats or other sources. ## The `MemoryService` Role The `BaseMemoryService` defines the interface for managing this searchable, long-term knowledge store. Its primary responsibilities are: -1. **Ingesting Information (`add_session_to_memory`):** Taking the contents of a (usually completed) `Session` and adding relevant information to the long-term knowledge store. +1. **Ingesting Information (`add_session_to_memory`):** Taking the contents of a (usually completed) `Session` and adding relevant information to the long-term knowledge store. 2. **Searching Information (`search_memory`):** Allowing an agent (typically via a `Tool`) to query the knowledge store and retrieve relevant snippets or context based on a search query. ## Choosing the Right Memory Service @@ -45,7 +45,7 @@ memory_service = InMemoryMemoryService() This example demonstrates the basic flow using the `InMemoryMemoryService` for simplicity. -??? "Full Code" +=== "Python" ```py import asyncio @@ -140,6 +140,23 @@ This example demonstrates the basic flow using the `InMemoryMemoryService` for s # await run_scenario() ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/sessions/memory_example/memory_example.go:full_example" + ``` + + +### Searching Memory Within a Tool + +You can also search memory from within a custom tool by using the `tool.Context`. + +=== "Go" + + ```go + --8<-- "examples/go/snippets/sessions/memory_example/memory_example.go:tool_search" + ``` + ## Vertex AI Memory Bank The `VertexAiMemoryBankService` connects your agent to [Vertex AI Memory Bank](https://cloud.google.com/vertex-ai/generative-ai/docs/agent-engine/memory-bank/overview), a fully managed Google Cloud service that provides sophisticated, persistent memory capabilities for conversational agents. @@ -192,7 +209,7 @@ runner = adk.Runner( ... memory_service=memory_service ) -``` +``` ## Using Memory in Your Agent @@ -239,12 +256,12 @@ agent = Agent( The memory workflow internally involves these steps: -1. **Session Interaction:** A user interacts with an agent via a `Session`, managed by a `SessionService`. Events are added, and state might be updated. -2. **Ingestion into Memory:** At some point (often when a session is considered complete or has yielded significant information), your application calls `memory_service.add_session_to_memory(session)`. This extracts relevant information from the session's events and adds it to the long-term knowledge store (in-memory dictionary or Agent Engine Memory Bank). -3. **Later Query:** In a *different* (or the same) session, the user might ask a question requiring past context (e.g., "What did we discuss about project X last week?"). -4. **Agent Uses Memory Tool:** An agent equipped with a memory-retrieval tool (like the built-in `load_memory` tool) recognizes the need for past context. It calls the tool, providing a search query (e.g., "discussion project X last week"). -5. **Search Execution:** The tool internally calls `memory_service.search_memory(app_name, user_id, query)`. -6. **Results Returned:** The `MemoryService` searches its store (using keyword matching or semantic search) and returns relevant snippets as a `SearchMemoryResponse` containing a list of `MemoryResult` objects (each potentially holding events from a relevant past session). +1. **Session Interaction:** A user interacts with an agent via a `Session`, managed by a `SessionService`. Events are added, and state might be updated. +2. **Ingestion into Memory:** At some point (often when a session is considered complete or has yielded significant information), your application calls `memory_service.add_session_to_memory(session)`. This extracts relevant information from the session's events and adds it to the long-term knowledge store (in-memory dictionary or Agent Engine Memory Bank). +3. **Later Query:** In a *different* (or the same) session, the user might ask a question requiring past context (e.g., "What did we discuss about project X last week?"). +4. **Agent Uses Memory Tool:** An agent equipped with a memory-retrieval tool (like the built-in `load_memory` tool) recognizes the need for past context. It calls the tool, providing a search query (e.g., "discussion project X last week"). +5. **Search Execution:** The tool internally calls `memory_service.search_memory(app_name, user_id, query)`. +6. **Results Returned:** The `MemoryService` searches its store (using keyword matching or semantic search) and returns relevant snippets as a `SearchMemoryResponse` containing a list of `MemoryResult` objects (each potentially holding events from a relevant past session). 7. **Agent Uses Results:** The tool returns these results to the agent, usually as part of the context or function response. The agent can then use this retrieved information to formulate its final answer to the user. ### Can an agent have access to more than one memory service? diff --git a/examples/go/snippets/sessions/memory_example/go.mod b/examples/go/snippets/sessions/memory_example/go.mod new file mode 100644 index 00000000..84a8b652 --- /dev/null +++ b/examples/go/snippets/sessions/memory_example/go.mod @@ -0,0 +1,40 @@ +module memory_example + +go 1.24.7 + +require ( + google.golang.org/adk v0.0.0-20251014172420-92bd90d12719 + google.golang.org/genai v1.30.0 +) + +require ( + cloud.google.com/go v0.121.6 // indirect + cloud.google.com/go/auth v0.16.5 // indirect + cloud.google.com/go/compute/metadata v0.8.0 // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/jsonschema-go v0.3.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + go.opentelemetry.io/auto/sdk v1.1.0 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect + go.opentelemetry.io/otel v1.36.0 // indirect + go.opentelemetry.io/otel/metric v1.36.0 // indirect + go.opentelemetry.io/otel/sdk v1.36.0 // indirect + go.opentelemetry.io/otel/trace v1.36.0 // indirect + golang.org/x/crypto v0.41.0 // indirect + golang.org/x/net v0.43.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.35.0 // indirect + golang.org/x/text v0.29.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect + google.golang.org/grpc v1.75.0-dev // indirect + google.golang.org/protobuf v1.36.10 // indirect + rsc.io/omap v1.2.0 // indirect + rsc.io/ordered v1.1.1 // indirect +) diff --git a/examples/go/snippets/sessions/memory_example/go.sum b/examples/go/snippets/sessions/memory_example/go.sum new file mode 100644 index 00000000..b00352e2 --- /dev/null +++ b/examples/go/snippets/sessions/memory_example/go.sum @@ -0,0 +1,77 @@ +cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= +cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= +cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI= +cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= +cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA= +cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q= +github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= +github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= +github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= +github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= +github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= +github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= +go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= +go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= +go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= +go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= +go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= +go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= +go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= +go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= +go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= +go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= +golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= +golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= +golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= +golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= +golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= +golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= +google.golang.org/adk v0.0.0-20251014172420-92bd90d12719 h1:SL9rad8eE9HwuP7b++he9vcfvAuJfmDsVYrEohI36vg= +google.golang.org/adk v0.0.0-20251014172420-92bd90d12719/go.mod h1:hfcsmlJhB3RRqBmoiLDZ1PwxbIcjtJIamk70gPp6GcE= +google.golang.org/genai v1.30.0 h1:7021aneIvl24nEBLbtQFEWleHsMbjzpcQvkT4WcJ1dc= +google.golang.org/genai v1.30.0/go.mod h1:7pAilaICJlQBonjKKJNhftDFv3SREhZcTe9F6nRcjbg= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4= +google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= +google.golang.org/grpc v1.75.0-dev h1:3GnKkkh9RI6YGGw8/Zu3WDlX4+lexwzdKZlrtlo9RCc= +google.golang.org/grpc v1.75.0-dev/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/omap v1.2.0 h1:c1M8jchnHbzmJALzGLclfH3xDWXrPxSUHXzH5C+8Kdw= +rsc.io/omap v1.2.0/go.mod h1:C8pkI0AWexHopQtZX+qiUeJGzvc8HkdgnsWK4/mAa00= +rsc.io/ordered v1.1.1 h1:1kZM6RkTmceJgsFH/8DLQvkCVEYomVDJfBRLT595Uak= +rsc.io/ordered v1.1.1/go.mod h1:evAi8739bWVBRG9aaufsjVc202+6okf8u2QeVL84BCM= diff --git a/examples/go/snippets/sessions/memory_example/memory_example.go b/examples/go/snippets/sessions/memory_example/memory_example.go new file mode 100644 index 00000000..64d584db --- /dev/null +++ b/examples/go/snippets/sessions/memory_example/memory_example.go @@ -0,0 +1,220 @@ +// Copyright 2025 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "context" + "fmt" + "log" + + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/memory" + "google.golang.org/adk/model" + "google.golang.org/adk/model/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/session" + "google.golang.org/adk/tool" + "google.golang.org/genai" +) + +const ( + appName = "go_memory_example_app" + userID = "go_mem_user" + modelID = "gemini-2.0-flash" // Replace with a valid model name +) + +// This example demonstrates how to use the MemoryService in the Go ADK. +// It covers two main scenarios: +// 1. Adding a completed session to memory and recalling it in a new session. +// 2. Searching memory from within a custom tool using the tool.Context. + +// --8<-- [start:full_example] +func main() { + ctx := context.Background() + + // --- Services --- + // Services must be shared across runners to share state and memory. + sessionService := session.InMemoryService() + memoryService := memory.InMemoryService() // Use in-memory for this demo. + + // --- Scenario 1: Capture information in one session --- + fmt.Println("--- Turn 1: Capturing Information ---") + infoCaptureAgent := must(llmagent.New(llmagent.Config{ + Name: "InfoCaptureAgent", + Model: must(gemini.NewModel(ctx, modelID, nil)), + Instruction: "Acknowledge the user's statement.", + })) + + runner1 := must(runner.New(runner.Config{ + AppName: appName, + Agent: infoCaptureAgent, + SessionService: sessionService, + MemoryService: memoryService, // Provide the memory service to the Runner + })) + + session1ID := "session_info" + must(sessionService.Create(ctx, &session.CreateRequest{AppName: appName, UserID: userID, SessionID: session1ID})) + + userInput1 := genai.NewContentFromText("My favorite project is Project Alpha.", "user") + var finalResponseText string + for event, err := range runner1.Run(ctx, userID, session1ID, userInput1, &agent.RunConfig{}) { + if err != nil { + log.Printf("Agent 1 Error: %v", err) + } + if isFinalResponse(event) { + texts := textParts(event.LLMResponse.Content) + if len(texts) > 0 { + finalResponseText = texts[0] + } + } + } + fmt.Printf("Agent 1 Response: %s\n", finalResponseText) + + // Add the completed session to the Memory Service + fmt.Println("\n--- Adding Session 1 to Memory ---") + completedSession := must(sessionService.Get(ctx, &session.GetRequest{AppName: appName, UserID: userID, SessionID: session1ID})).Session + if err := memoryService.AddSession(ctx, completedSession); err != nil { + log.Fatalf("Failed to add session to memory: %v", err) + } + fmt.Println("Session added to memory.") + + // --- Scenario 2: Recall the information in a new session using a tool --- + fmt.Println("\n--- Turn 2: Recalling Information ---") + + // --8<-- [start:tool_search] + // Define a tool that can search memory. + memorySearchTool := must(tool.NewFunctionTool[struct{ Query string `json:"query"` }, struct{ Results []string `json:"results"` }]( + tool.FunctionToolConfig{ + Name: "search_past_conversations", + Description: "Searches past conversations for relevant information.", + }, + // This function demonstrates accessing memory via tool.Context. + func(tctx tool.Context, args struct{ Query string `json:"query"` }) struct{ Results []string `json:"results"` } { + fmt.Printf("Tool: Searching memory for query: '%s'\n", args.Query) + // The SearchMemory function is available on the context. + searchResults, err := tctx.SearchMemory(context.Background(), args.Query) + if err != nil { + log.Printf("Error searching memory: %v", err) + return struct{ Results []string `json:"results"` }{Results: []string{"Error searching memory."}} + } + + var results []string + for _, res := range searchResults { + if res.Content != nil { + results = append(results, textParts(res.Content)...) + } + } + return struct{ Results []string `json:"results"` }{Results: results} + }, + )) + // --8<-- [end:tool_search] + + memoryRecallAgent := must(llmagent.New(llmagent.Config{ + Name: "MemoryRecallAgent", + Model: must(gemini.NewModel(ctx, modelID, nil)), + Instruction: "Answer the user's question. Use the 'search_past_conversations' tool if the answer might be in past conversations.", + Tools: []tool.Tool{memorySearchTool}, // Give the agent the tool + })) + + runner2 := must(runner.New(runner.Config{ + Agent: memoryRecallAgent, + AppName: appName, + SessionService: sessionService, + MemoryService: memoryService, + })) + + session2ID := "session_recall" + must(sessionService.Create(ctx, &session.CreateRequest{AppName: appName, UserID: userID, SessionID: session2ID})) + userInput2 := genai.NewContentFromText("What is my favorite project?", "user") + + var finalResponseText2 string + for event, err := range runner2.Run(ctx, userID, session2ID, userInput2, &agent.RunConfig{}) { + if err != nil { + log.Printf("Agent 2 Error: %v", err) + } + if isFinalResponse(event) { + texts := textParts(event.LLMResponse.Content) + if len(texts) > 0 { + finalResponseText2 = texts[0] + } + } + } + fmt.Printf("Agent 2 Response: %s\n", finalResponseText2) +} +// --8<-- [end:full_example] + +// --- Helper Functions --- + +func must[T any](v T, err error) T { + if err != nil { + log.Fatalf("Setup failed: %v", err) + } + return v +} + +func isFinalResponse(ev *session.Event) bool { + if ev.Actions.SkipSummarization || len(ev.LongRunningToolIDs) > 0 { + return true + } + if ev.LLMResponse == nil { + return true + } + return !hasFunctionCalls(ev.LLMResponse) && !hasFunctionResponses(ev.LLMResponse) && !ev.LLMResponse.Partial && !hasTrailingCodeExecutionResult(ev.LLMResponse) +} + +func hasFunctionCalls(resp *model.LLMResponse) bool { + if resp == nil || resp.Content == nil { + return false + } + for _, part := range resp.Content.Parts { + if part.FunctionCall != nil { + return true + } + } + return false +} + +func hasFunctionResponses(resp *model.LLMResponse) bool { + if resp == nil || resp.Content == nil { + return false + } + for _, part := range resp.Content.Parts { + if part.FunctionResponse != nil { + return true + } + } + return false +} + +func hasTrailingCodeExecutionResult(resp *model.LLMResponse) bool { + if resp == nil || resp.Content == nil || len(resp.Content.Parts) == 0 { + return false + } + lastPart := resp.Content.Parts[len(resp.Content.Parts)-1] + return lastPart.CodeExecutionResult != nil +} + +func textParts(c *genai.Content) (ret []string) { + if c == nil { + return nil + } + for _, p := range c.Parts { + if p.Text != "" { + ret = append(ret, p.Text) + } + } + return ret +} From de31b722298ad3a0addd66b5d5da6fd70052620d Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Mon, 3 Nov 2025 18:50:10 +0000 Subject: [PATCH 118/125] updating go memory sample, adding python formatting in docs to clarify current support. --- docs/sessions/memory.md | 30 ++++---- .../snippets/sessions/memory_example/go.mod | 41 +++++----- .../sessions/memory_example/memory_example.go | 74 ++++--------------- 3 files changed, 54 insertions(+), 91 deletions(-) diff --git a/docs/sessions/memory.md b/docs/sessions/memory.md index c4e5b19d..40184af0 100644 --- a/docs/sessions/memory.md +++ b/docs/sessions/memory.md @@ -194,22 +194,23 @@ adk web path/to/your/agents_dir --memory_service_uri="agentengine://1234567890" Or, you can configure your agent to use the Memory Bank by manually instantiating the `VertexAiMemoryBankService` and passing it to the `Runner`. -```py -from google.adk.memory import VertexAiMemoryBankService +=== "Python" + ```py + from google.adk.memory import VertexAiMemoryBankService -agent_engine_id = agent_engine.api_resource.name.split("/")[-1] + agent_engine_id = agent_engine.api_resource.name.split("/")[-1] -memory_service = VertexAiMemoryBankService( - project="PROJECT_ID", - location="LOCATION", - agent_engine_id=agent_engine_id -) + memory_service = VertexAiMemoryBankService( + project="PROJECT_ID", + location="LOCATION", + agent_engine_id=agent_engine_id + ) -runner = adk.Runner( - ... - memory_service=memory_service -) -``` + runner = adk.Runner( + ... + memory_service=memory_service + ) + ``` ## Using Memory in Your Agent @@ -220,6 +221,7 @@ When a memory service is configured, your agent can use a tool or callback to re **Example:** +=== "Python" ```python from google.adk.agents import Agent from google.adk.tools.preload_memory_tool import PreloadMemoryTool @@ -234,6 +236,7 @@ agent = Agent( To extract memories from your session, you need to call `add_session_to_memory`. For example, you can automate this via a callback: +=== "Python" ```python from google import adk @@ -276,6 +279,7 @@ For example, your agent could use the framework-configured `VertexAiMemoryBankSe Here’s how you could implement that in your agent's code: +=== "Python" ```python from google.adk.agents import Agent from google.adk.memory import InMemoryMemoryService, VertexAiMemoryBankService diff --git a/examples/go/snippets/sessions/memory_example/go.mod b/examples/go/snippets/sessions/memory_example/go.mod index 84a8b652..bb9efa48 100644 --- a/examples/go/snippets/sessions/memory_example/go.mod +++ b/examples/go/snippets/sessions/memory_example/go.mod @@ -1,16 +1,20 @@ module memory_example -go 1.24.7 +go 1.24.4 + +toolchain go1.24.8 + +replace google.golang.org/adk => ../../../../../../adk-go require ( - google.golang.org/adk v0.0.0-20251014172420-92bd90d12719 - google.golang.org/genai v1.30.0 + google.golang.org/adk v0.0.0-00010101000000-000000000000 + google.golang.org/genai v1.33.0 ) require ( - cloud.google.com/go v0.121.6 // indirect - cloud.google.com/go/auth v0.16.5 // indirect - cloud.google.com/go/compute/metadata v0.8.0 // indirect + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.17.0 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/stdr v1.2.2 // indirect @@ -21,19 +25,18 @@ require ( github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect github.com/gorilla/websocket v1.5.3 // indirect - go.opentelemetry.io/auto/sdk v1.1.0 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 // indirect - go.opentelemetry.io/otel v1.36.0 // indirect - go.opentelemetry.io/otel/metric v1.36.0 // indirect - go.opentelemetry.io/otel/sdk v1.36.0 // indirect - go.opentelemetry.io/otel/trace v1.36.0 // indirect - golang.org/x/crypto v0.41.0 // indirect - golang.org/x/net v0.43.0 // indirect - golang.org/x/sync v0.17.0 // indirect - golang.org/x/sys v0.35.0 // indirect - golang.org/x/text v0.29.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 // indirect - google.golang.org/grpc v1.75.0-dev // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f // indirect + google.golang.org/grpc v1.76.0 // indirect google.golang.org/protobuf v1.36.10 // indirect rsc.io/omap v1.2.0 // indirect rsc.io/ordered v1.1.1 // indirect diff --git a/examples/go/snippets/sessions/memory_example/memory_example.go b/examples/go/snippets/sessions/memory_example/memory_example.go index 64d584db..bd7ce72c 100644 --- a/examples/go/snippets/sessions/memory_example/memory_example.go +++ b/examples/go/snippets/sessions/memory_example/memory_example.go @@ -18,22 +18,23 @@ import ( "context" "fmt" "log" + "strings" "google.golang.org/adk/agent" "google.golang.org/adk/agent/llmagent" "google.golang.org/adk/memory" - "google.golang.org/adk/model" "google.golang.org/adk/model/gemini" "google.golang.org/adk/runner" "google.golang.org/adk/session" "google.golang.org/adk/tool" + "google.golang.org/adk/tool/functiontool" "google.golang.org/genai" ) const ( appName = "go_memory_example_app" userID = "go_mem_user" - modelID = "gemini-2.0-flash" // Replace with a valid model name + modelID = "gemini-2.5-pro" ) // This example demonstrates how to use the MemoryService in the Go ADK. @@ -70,15 +71,13 @@ func main() { userInput1 := genai.NewContentFromText("My favorite project is Project Alpha.", "user") var finalResponseText string - for event, err := range runner1.Run(ctx, userID, session1ID, userInput1, &agent.RunConfig{}) { + for event, err := range runner1.Run(ctx, userID, session1ID, userInput1, agent.RunConfig{}) { if err != nil { log.Printf("Agent 1 Error: %v", err) + continue } - if isFinalResponse(event) { - texts := textParts(event.LLMResponse.Content) - if len(texts) > 0 { - finalResponseText = texts[0] - } + if event.LLMResponse.Content != nil && !event.LLMResponse.Partial { + finalResponseText = strings.Join(textParts(event.LLMResponse.Content), "") } } fmt.Printf("Agent 1 Response: %s\n", finalResponseText) @@ -96,8 +95,8 @@ func main() { // --8<-- [start:tool_search] // Define a tool that can search memory. - memorySearchTool := must(tool.NewFunctionTool[struct{ Query string `json:"query"` }, struct{ Results []string `json:"results"` }]( - tool.FunctionToolConfig{ + memorySearchTool := must(functiontool.New[struct{ Query string `json:"query"` }, struct{ Results []string `json:"results"` }]( + functiontool.Config{ Name: "search_past_conversations", Description: "Searches past conversations for relevant information.", }, @@ -112,7 +111,7 @@ func main() { } var results []string - for _, res := range searchResults { + for _, res := range searchResults.Memories { if res.Content != nil { results = append(results, textParts(res.Content)...) } @@ -141,19 +140,18 @@ func main() { userInput2 := genai.NewContentFromText("What is my favorite project?", "user") var finalResponseText2 string - for event, err := range runner2.Run(ctx, userID, session2ID, userInput2, &agent.RunConfig{}) { + for event, err := range runner2.Run(ctx, userID, session2ID, userInput2, agent.RunConfig{}) { if err != nil { log.Printf("Agent 2 Error: %v", err) + continue } - if isFinalResponse(event) { - texts := textParts(event.LLMResponse.Content) - if len(texts) > 0 { - finalResponseText2 = texts[0] - } + if event.LLMResponse.Content != nil && !event.LLMResponse.Partial { + finalResponseText2 = strings.Join(textParts(event.LLMResponse.Content), "") } } fmt.Printf("Agent 2 Response: %s\n", finalResponseText2) } + // --8<-- [end:full_example] // --- Helper Functions --- @@ -165,48 +163,6 @@ func must[T any](v T, err error) T { return v } -func isFinalResponse(ev *session.Event) bool { - if ev.Actions.SkipSummarization || len(ev.LongRunningToolIDs) > 0 { - return true - } - if ev.LLMResponse == nil { - return true - } - return !hasFunctionCalls(ev.LLMResponse) && !hasFunctionResponses(ev.LLMResponse) && !ev.LLMResponse.Partial && !hasTrailingCodeExecutionResult(ev.LLMResponse) -} - -func hasFunctionCalls(resp *model.LLMResponse) bool { - if resp == nil || resp.Content == nil { - return false - } - for _, part := range resp.Content.Parts { - if part.FunctionCall != nil { - return true - } - } - return false -} - -func hasFunctionResponses(resp *model.LLMResponse) bool { - if resp == nil || resp.Content == nil { - return false - } - for _, part := range resp.Content.Parts { - if part.FunctionResponse != nil { - return true - } - } - return false -} - -func hasTrailingCodeExecutionResult(resp *model.LLMResponse) bool { - if resp == nil || resp.Content == nil || len(resp.Content.Parts) == 0 { - return false - } - lastPart := resp.Content.Parts[len(resp.Content.Parts)-1] - return lastPart.CodeExecutionResult != nil -} - func textParts(c *genai.Content) (ret []string) { if c == nil { return nil From 2b40d97e05fd9743e2de5f125194f294daab63d7 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Mon, 3 Nov 2025 18:51:35 +0000 Subject: [PATCH 119/125] removing go replace for memory example --- examples/go/snippets/sessions/memory_example/go.mod | 2 -- 1 file changed, 2 deletions(-) diff --git a/examples/go/snippets/sessions/memory_example/go.mod b/examples/go/snippets/sessions/memory_example/go.mod index bb9efa48..1886c82f 100644 --- a/examples/go/snippets/sessions/memory_example/go.mod +++ b/examples/go/snippets/sessions/memory_example/go.mod @@ -4,8 +4,6 @@ go 1.24.4 toolchain go1.24.8 -replace google.golang.org/adk => ../../../../../../adk-go - require ( google.golang.org/adk v0.0.0-00010101000000-000000000000 google.golang.org/genai v1.33.0 From 52b3447a0adc42cb82b2485a9858e36752015238 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Mon, 3 Nov 2025 18:54:35 +0000 Subject: [PATCH 120/125] removing go.sum from memory_example --- .../snippets/sessions/memory_example/go.sum | 77 ------------------- 1 file changed, 77 deletions(-) delete mode 100644 examples/go/snippets/sessions/memory_example/go.sum diff --git a/examples/go/snippets/sessions/memory_example/go.sum b/examples/go/snippets/sessions/memory_example/go.sum deleted file mode 100644 index b00352e2..00000000 --- a/examples/go/snippets/sessions/memory_example/go.sum +++ /dev/null @@ -1,77 +0,0 @@ -cloud.google.com/go v0.121.6 h1:waZiuajrI28iAf40cWgycWNgaXPO06dupuS+sgibK6c= -cloud.google.com/go v0.121.6/go.mod h1:coChdst4Ea5vUpiALcYKXEpR1S9ZgXbhEzzMcMR66vI= -cloud.google.com/go/auth v0.16.5 h1:mFWNQ2FEVWAliEQWpAdH80omXFokmrnbDhUS9cBywsI= -cloud.google.com/go/auth v0.16.5/go.mod h1:utzRfHMP+Vv0mpOkTRQoWD2q3BatTOoWbA7gCc2dUhQ= -cloud.google.com/go/compute/metadata v0.8.0 h1:HxMRIbao8w17ZX6wBnjhcDkW6lTFpgcaobyVfZWqRLA= -cloud.google.com/go/compute/metadata v0.8.0/go.mod h1:sYOGTp851OV9bOFJ9CH7elVvyzopvWQFNNghtDQ/Biw= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= -github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= -github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= -github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= -github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= -github.com/google/jsonschema-go v0.3.0 h1:6AH2TxVNtk3IlvkkhjrtbUc4S8AvO0Xii0DxIygDg+Q= -github.com/google/jsonschema-go v0.3.0/go.mod h1:r5quNTdLOYEz95Ru18zA0ydNbBuYoo9tgaYcxEYhJVE= -github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= -github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= -github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= -github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.6 h1:GW/XbdyBFQ8Qe+YAmFU9uHLo7OnF5tL52HFAgMmyrf4= -github.com/googleapis/enterprise-certificate-proxy v0.3.6/go.mod h1:MkHOF77EYAE7qfSuSS9PU6g4Nt4e11cnsDUowfwewLA= -github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81vgd/bo= -github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= -github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= -github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= -go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6hwnZ41NSFTO5q4LYDtJRXBf2PD0rNBkeB/lus= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= -go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg= -go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E= -go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE= -go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs= -go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs= -go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY= -go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis= -go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4= -go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w= -go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA= -go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= -go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4= -golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc= -golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE= -golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg= -golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= -golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= -golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= -golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= -golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= -google.golang.org/adk v0.0.0-20251014172420-92bd90d12719 h1:SL9rad8eE9HwuP7b++he9vcfvAuJfmDsVYrEohI36vg= -google.golang.org/adk v0.0.0-20251014172420-92bd90d12719/go.mod h1:hfcsmlJhB3RRqBmoiLDZ1PwxbIcjtJIamk70gPp6GcE= -google.golang.org/genai v1.30.0 h1:7021aneIvl24nEBLbtQFEWleHsMbjzpcQvkT4WcJ1dc= -google.golang.org/genai v1.30.0/go.mod h1:7pAilaICJlQBonjKKJNhftDFv3SREhZcTe9F6nRcjbg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4 h1:i8QOKZfYg6AbGVZzUAY3LrNWCKF8O6zFisU9Wl9RER4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20250929231259-57b25ae835d4/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= -google.golang.org/grpc v1.75.0-dev h1:3GnKkkh9RI6YGGw8/Zu3WDlX4+lexwzdKZlrtlo9RCc= -google.golang.org/grpc v1.75.0-dev/go.mod h1:NZUaK8dAMUfzhK6uxZ+9511LtOrk73UGWOFoNvz7z+s= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -rsc.io/omap v1.2.0 h1:c1M8jchnHbzmJALzGLclfH3xDWXrPxSUHXzH5C+8Kdw= -rsc.io/omap v1.2.0/go.mod h1:C8pkI0AWexHopQtZX+qiUeJGzvc8HkdgnsWK4/mAa00= -rsc.io/ordered v1.1.1 h1:1kZM6RkTmceJgsFH/8DLQvkCVEYomVDJfBRLT595Uak= -rsc.io/ordered v1.1.1/go.mod h1:evAi8739bWVBRG9aaufsjVc202+6okf8u2QeVL84BCM= From 6eea1a63e1e816bd17b568833a95faec8573a836 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Mon, 3 Nov 2025 19:18:37 +0000 Subject: [PATCH 121/125] fixing go formatting for memory example --- .../sessions/memory_example/memory_example.go | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/examples/go/snippets/sessions/memory_example/memory_example.go b/examples/go/snippets/sessions/memory_example/memory_example.go index bd7ce72c..badf6879 100644 --- a/examples/go/snippets/sessions/memory_example/memory_example.go +++ b/examples/go/snippets/sessions/memory_example/memory_example.go @@ -95,19 +95,29 @@ func main() { // --8<-- [start:tool_search] // Define a tool that can search memory. - memorySearchTool := must(functiontool.New[struct{ Query string `json:"query"` }, struct{ Results []string `json:"results"` }]( + memorySearchTool := must(functiontool.New[struct { + Query string `json:"query"` + }, struct { + Results []string `json:"results"` + }]( functiontool.Config{ Name: "search_past_conversations", Description: "Searches past conversations for relevant information.", }, // This function demonstrates accessing memory via tool.Context. - func(tctx tool.Context, args struct{ Query string `json:"query"` }) struct{ Results []string `json:"results"` } { + func(tctx tool.Context, args struct { + Query string `json:"query"` + }) struct { + Results []string `json:"results"` + } { fmt.Printf("Tool: Searching memory for query: '%s'\n", args.Query) // The SearchMemory function is available on the context. searchResults, err := tctx.SearchMemory(context.Background(), args.Query) if err != nil { log.Printf("Error searching memory: %v", err) - return struct{ Results []string `json:"results"` }{Results: []string{"Error searching memory."}} + return struct { + Results []string `json:"results"` + }{Results: []string{"Error searching memory."}} } var results []string @@ -116,7 +126,9 @@ func main() { results = append(results, textParts(res.Content)...) } } - return struct{ Results []string `json:"results"` }{Results: results} + return struct { + Results []string `json:"results"` + }{Results: results} }, )) // --8<-- [end:tool_search] From 6f8db0c028d77c3795271aeefc229f36ad1d3850 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Wed, 5 Nov 2025 23:31:05 +0000 Subject: [PATCH 122/125] fixing go memory snippets to streamline examples in docs --- docs/sessions/memory.md | 23 ++++- .../sessions/memory_example/memory_example.go | 87 ++++++++++--------- 2 files changed, 64 insertions(+), 46 deletions(-) diff --git a/docs/sessions/memory.md b/docs/sessions/memory.md index 40184af0..22f8349b 100644 --- a/docs/sessions/memory.md +++ b/docs/sessions/memory.md @@ -36,10 +36,25 @@ The ADK offers two distinct `MemoryService` implementations, each tailored to di The `InMemoryMemoryService` stores session information in the application's memory and performs basic keyword matching for searches. It requires no setup and is best for prototyping and simple testing scenarios where persistence isn't required. -```py -from google.adk.memory import InMemoryMemoryService -memory_service = InMemoryMemoryService() -``` +=== "Python" + + ```py + from google.adk.memory import InMemoryMemoryService + memory_service = InMemoryMemoryService() + ``` + +=== "Go" + ```go + import ( + "google.golang.org/adk/memory" + "google.golang.org/adk/session" + ) + + // Services must be shared across runners to share state and memory. + sessionService := session.InMemoryService() + memoryService := memory.InMemoryService() + ``` + **Example: Adding and Searching Memory** diff --git a/examples/go/snippets/sessions/memory_example/memory_example.go b/examples/go/snippets/sessions/memory_example/memory_example.go index badf6879..2096d0e9 100644 --- a/examples/go/snippets/sessions/memory_example/memory_example.go +++ b/examples/go/snippets/sessions/memory_example/memory_example.go @@ -14,6 +14,8 @@ package main +// --8<-- [start:full_example] + import ( "context" "fmt" @@ -37,12 +39,53 @@ const ( modelID = "gemini-2.5-pro" ) +// Args defines the input structure for the memory search tool. +type Args struct { + Query string `json:"query"` +} + +// Result defines the output structure for the memory search tool. +type Result struct { + Results []string `json:"results"` +} + +// --8<-- [start:tool_search] + +// memorySearchToolFunc is the implementation of the memory search tool. +// This function demonstrates accessing memory via tool.Context. +func memorySearchToolFunc(tctx tool.Context, args Args) Result { + fmt.Printf("Tool: Searching memory for query: '%s'\n", args.Query) + // The SearchMemory function is available on the context. + searchResults, err := tctx.SearchMemory(context.Background(), args.Query) + if err != nil { + log.Printf("Error searching memory: %v", err) + return Result{Results: []string{"Error searching memory."}} + } + + var results []string + for _, res := range searchResults.Memories { + if res.Content != nil { + results = append(results, textParts(res.Content)...) + } + } + return Result{Results: results} +} + +// Define a tool that can search memory. +var memorySearchTool = must(functiontool.New[Args, Result]( + functiontool.Config{ + Name: "search_past_conversations", + Description: "Searches past conversations for relevant information.", + }, + memorySearchToolFunc, +)) + +// --8<-- [end:tool_search] + // This example demonstrates how to use the MemoryService in the Go ADK. // It covers two main scenarios: // 1. Adding a completed session to memory and recalling it in a new session. // 2. Searching memory from within a custom tool using the tool.Context. - -// --8<-- [start:full_example] func main() { ctx := context.Background() @@ -93,46 +136,6 @@ func main() { // --- Scenario 2: Recall the information in a new session using a tool --- fmt.Println("\n--- Turn 2: Recalling Information ---") - // --8<-- [start:tool_search] - // Define a tool that can search memory. - memorySearchTool := must(functiontool.New[struct { - Query string `json:"query"` - }, struct { - Results []string `json:"results"` - }]( - functiontool.Config{ - Name: "search_past_conversations", - Description: "Searches past conversations for relevant information.", - }, - // This function demonstrates accessing memory via tool.Context. - func(tctx tool.Context, args struct { - Query string `json:"query"` - }) struct { - Results []string `json:"results"` - } { - fmt.Printf("Tool: Searching memory for query: '%s'\n", args.Query) - // The SearchMemory function is available on the context. - searchResults, err := tctx.SearchMemory(context.Background(), args.Query) - if err != nil { - log.Printf("Error searching memory: %v", err) - return struct { - Results []string `json:"results"` - }{Results: []string{"Error searching memory."}} - } - - var results []string - for _, res := range searchResults.Memories { - if res.Content != nil { - results = append(results, textParts(res.Content)...) - } - } - return struct { - Results []string `json:"results"` - }{Results: results} - }, - )) - // --8<-- [end:tool_search] - memoryRecallAgent := must(llmagent.New(llmagent.Config{ Name: "MemoryRecallAgent", Model: must(gemini.NewModel(ctx, modelID, nil)), From 99c694dadf2625bda705d5df94540d8eee375cb8 Mon Sep 17 00:00:00 2001 From: Toni Klopfenstein Date: Thu, 6 Nov 2025 03:13:45 +0000 Subject: [PATCH 123/125] removing unnecessary go.mod for memory_example.go and snippet cleanup --- .../snippets/sessions/memory_example/go.mod | 41 ------------------- .../sessions/memory_example/memory_example.go | 6 +-- 2 files changed, 3 insertions(+), 44 deletions(-) delete mode 100644 examples/go/snippets/sessions/memory_example/go.mod diff --git a/examples/go/snippets/sessions/memory_example/go.mod b/examples/go/snippets/sessions/memory_example/go.mod deleted file mode 100644 index 1886c82f..00000000 --- a/examples/go/snippets/sessions/memory_example/go.mod +++ /dev/null @@ -1,41 +0,0 @@ -module memory_example - -go 1.24.4 - -toolchain go1.24.8 - -require ( - google.golang.org/adk v0.0.0-00010101000000-000000000000 - google.golang.org/genai v1.33.0 -) - -require ( - cloud.google.com/go v0.123.0 // indirect - cloud.google.com/go/auth v0.17.0 // indirect - cloud.google.com/go/compute/metadata v0.9.0 // indirect - github.com/felixge/httpsnoop v1.0.4 // indirect - github.com/go-logr/logr v1.4.3 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/google/go-cmp v0.7.0 // indirect - github.com/google/jsonschema-go v0.3.0 // indirect - github.com/google/s2a-go v0.1.9 // indirect - github.com/google/uuid v1.6.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect - github.com/googleapis/gax-go/v2 v2.15.0 // indirect - github.com/gorilla/websocket v1.5.3 // indirect - go.opentelemetry.io/auto/sdk v1.2.1 // indirect - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/sdk v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect - golang.org/x/crypto v0.43.0 // indirect - golang.org/x/net v0.46.0 // indirect - golang.org/x/sys v0.37.0 // indirect - golang.org/x/text v0.30.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f // indirect - google.golang.org/grpc v1.76.0 // indirect - google.golang.org/protobuf v1.36.10 // indirect - rsc.io/omap v1.2.0 // indirect - rsc.io/ordered v1.1.1 // indirect -) diff --git a/examples/go/snippets/sessions/memory_example/memory_example.go b/examples/go/snippets/sessions/memory_example/memory_example.go index 2096d0e9..6eda0fdf 100644 --- a/examples/go/snippets/sessions/memory_example/memory_example.go +++ b/examples/go/snippets/sessions/memory_example/memory_example.go @@ -119,7 +119,7 @@ func main() { log.Printf("Agent 1 Error: %v", err) continue } - if event.LLMResponse.Content != nil && !event.LLMResponse.Partial { + if event.Content != nil && !event.LLMResponse.Partial { finalResponseText = strings.Join(textParts(event.LLMResponse.Content), "") } } @@ -127,7 +127,7 @@ func main() { // Add the completed session to the Memory Service fmt.Println("\n--- Adding Session 1 to Memory ---") - completedSession := must(sessionService.Get(ctx, &session.GetRequest{AppName: appName, UserID: userID, SessionID: session1ID})).Session + completedSession := sessionService.Get(ctx, &session.GetRequest{AppName: appName, UserID: userID, SessionID: session1ID}).Session if err := memoryService.AddSession(ctx, completedSession); err != nil { log.Fatalf("Failed to add session to memory: %v", err) } @@ -160,7 +160,7 @@ func main() { log.Printf("Agent 2 Error: %v", err) continue } - if event.LLMResponse.Content != nil && !event.LLMResponse.Partial { + if event.Content != nil && !event.LLMResponse.Partial { finalResponseText2 = strings.Join(textParts(event.LLMResponse.Content), "") } } From 0886799c85b7865d191da7109a09ae3405e095f5 Mon Sep 17 00:00:00 2001 From: Disha Prakash Date: Tue, 4 Nov 2025 14:31:50 +0000 Subject: [PATCH 124/125] docs: Add MCP Toolbox for databases Go SDK Documentation --- .../google-cloud/mcp-toolbox-for-databases.md | 65 ++++++++++++++++++- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/docs/tools/google-cloud/mcp-toolbox-for-databases.md b/docs/tools/google-cloud/mcp-toolbox-for-databases.md index 3f096dd2..2d01e05a 100644 --- a/docs/tools/google-cloud/mcp-toolbox-for-databases.md +++ b/docs/tools/google-cloud/mcp-toolbox-for-databases.md @@ -79,7 +79,9 @@ documentation: * [Installing the Server](https://googleapis.github.io/genai-toolbox/getting-started/introduction/#installing-the-server) * [Configuring Toolbox](https://googleapis.github.io/genai-toolbox/getting-started/configure/) -## Install client SDK for ADK +## Install Client SDK for ADK + +## Python SDK ADK relies on the `toolbox-core` python package to use Toolbox. Install the package before getting started: @@ -88,7 +90,7 @@ package before getting started: pip install toolbox-core ``` -## Loading Toolbox Tools +### Loading Toolbox Tools Once you’re Toolbox server is configured and up and running, you can load tools from your server using ADK: @@ -111,6 +113,65 @@ root_agent = Agent( ) ``` +## Go SDK + +ADK relies on the `mcp-toolbox-sdk-go` go module to use Toolbox. Install the +package before getting started: + +```shell +go get github.com/googleapis/mcp-toolbox-sdk-go +``` + +### Loading Toolbox Tools + +Once you’re Toolbox server is configured and up and running, you can load tools +from your server using ADK: + +```go +import ( + "context" + "fmt" + + "github.com/googleapis/mcp-toolbox-sdk-go/tbadk" + "google.golang.org/adk/agent/llmagent" +) + +func main() { + + toolboxClient, err := tbadk.NewToolboxClient("https://127.0.0.1:5000") + if err != nil { + log.Fatalf("Failed to create MCP Toolbox client: %v", err) + } + + // Load a specific set of tools + toolboxtools, err := toolboxClient.LoadToolset("my-toolset-name", ctx) + if err != nil { + return fmt.Sprintln("Could not load Toolbox Toolset", err) + } + + toolsList := make([]tool.Tool, len(toolboxtools)) + for i := range toolboxtools { + toolsList[i] = &toolboxtools[i] + } + + llmagent, err := llmagent.New(llmagent.Config{ + ..., + Tools: toolsList, + }) + + // Load a single tool + tool, err := client.LoadTool("my-tool-name", ctx) + if err != nil { + return fmt.Sprintln("Could not load Toolbox Tool", err) + } + + llmagent, err := llmagent.New(llmagent.Config{ + ..., + Tools: []tool.Tool{&toolboxtool}, + }) +} +``` + ## Advanced Toolbox Features Toolbox has a variety of features to make developing Gen AI tools for databases. From 64b2294ee28da616432934be3338486449622a5a Mon Sep 17 00:00:00 2001 From: Disha Prakash Date: Tue, 4 Nov 2025 14:34:32 +0000 Subject: [PATCH 125/125] minor fix --- docs/tools/google-cloud/mcp-toolbox-for-databases.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/tools/google-cloud/mcp-toolbox-for-databases.md b/docs/tools/google-cloud/mcp-toolbox-for-databases.md index 2d01e05a..303bfbd3 100644 --- a/docs/tools/google-cloud/mcp-toolbox-for-databases.md +++ b/docs/tools/google-cloud/mcp-toolbox-for-databases.md @@ -116,7 +116,7 @@ root_agent = Agent( ## Go SDK ADK relies on the `mcp-toolbox-sdk-go` go module to use Toolbox. Install the -package before getting started: +module before getting started: ```shell go get github.com/googleapis/mcp-toolbox-sdk-go @@ -128,6 +128,8 @@ Once you’re Toolbox server is configured and up and running, you can load tool from your server using ADK: ```go +package main + import ( "context" "fmt"