diff --git a/.github/workflows/go-fmt.yaml b/.github/workflows/go-fmt.yaml new file mode 100644 index 00000000..59f0d72d --- /dev/null +++ b/.github/workflows/go-fmt.yaml @@ -0,0 +1,41 @@ +name: Go Fmt + +on: + pull_request: + paths: + - '**.go' + +jobs: + gofmt: + 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 + 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 diff --git a/docs/agents/custom-agents.md b/docs/agents/custom-agents.md index 33e1cf87..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. @@ -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 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 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 `ctx.Session().State()`. + **Key Capabilities within the Core Asynchronous Method:** === "Python" @@ -127,6 +136,46 @@ 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 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. + ```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 { + // 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 +211,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 +252,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 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 `storyGenerator` runs. Its output is expected to be in the session state under the key `"current_story"`. + 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. + --- ### Part 3: Defining the LLM Sub-Agents { #part-3-defining-the-llm-sub-agents } @@ -216,6 +287,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 +311,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 +338,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/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 a03437a5..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, @@ -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) diff --git a/docs/agents/models.md b/docs/agents/models.md index 577c3cb8..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 @@ -184,6 +184,18 @@ For deployed applications, a service account is the standard method. // different availability or quota limitations. ``` +=== "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" + ``` + !!!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/docs/agents/multi-agents.md b/docs/agents/multi-agents.md index 48d3b032..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)**. @@ -81,6 +81,17 @@ The foundation for structuring multi-agent systems is the parent-child relations // assert taskDoer.parentAgent().equals(coordinator); ``` +=== "Go" + + ```go + 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 } ADK includes specialized agents derived from `BaseAgent` that don't perform tasks themselves but orchestrate the execution flow of their `sub_agents`. @@ -115,6 +126,18 @@ 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 + 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. * **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 +182,18 @@ ADK includes specialized agents derived from `BaseAgent` that don't perform task // A subsequent agent could read state['weather'] and state['news']. ``` +=== "Go" + + ```go + 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. * **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 +262,20 @@ 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 + 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 } Agents within a system often need to exchange data or trigger actions in one another. ADK facilitates this through: @@ -281,6 +330,18 @@ 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 + 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) Leverages an [`LlmAgent`](llm-agents.md)'s understanding to dynamically route tasks to other suitable agents within the hierarchy. @@ -344,6 +405,16 @@ Leverages an [`LlmAgent`](llm-agents.md)'s understanding to dynamically route ta // ADK framework then routes execution to bookingAgent. ``` +=== "Go" + + ```go + import ( + "google.golang.org/adk/agent/llmagent" + ) + + --8<-- "examples/go/snippets/agents/multi-agent/main.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 +520,24 @@ 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 + 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. ## 2. Common Multi-Agent Patterns using ADK Primitives { #common-multi-agent-patterns-using-adk-primitives } @@ -516,6 +605,17 @@ By combining ADK's composition primitives, you can implement various established // transferToAgent(agentName='Support') ``` +=== "Go" + + ```go + 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 * **Structure:** A [`SequentialAgent`](workflow-agents/sequential-agents.md) contains `sub_agents` executed in a fixed order. @@ -576,6 +676,18 @@ By combining ADK's composition primitives, you can implement various established // reporter runs -> reads state['result'] ``` +=== "Go" + + ```go + 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 * **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 +761,19 @@ By combining ADK's composition primitives, you can implement various established // synthesizer runs afterwards, reading state['api1_data'] and state['api2_data']. ``` +=== "Go" + + ```go + 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" + ``` + ### Hierarchical Task Decomposition @@ -732,6 +857,18 @@ By combining ADK's composition primitives, you can implement various established // Results flow back up. ``` +=== "Go" + + ```go + 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) * **Structure:** Typically involves two agents within a [`SequentialAgent`](workflow-agents/sequential-agents.md): a Generator and a Critic/Reviewer. @@ -798,6 +935,18 @@ 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 + 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 * **Structure:** Uses a [`LoopAgent`](workflow-agents/loop-agents.md) containing one or more agents that work on a task over multiple iterations. @@ -903,6 +1052,20 @@ By combining ADK's composition primitives, you can implement various established // iterations. ``` +=== "Go" + + ```go + 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 * **Structure:** Integrates human intervention points within an agent workflow. @@ -1000,4 +1163,17 @@ By combining ADK's composition primitives, you can implement various established .build(); ``` +=== "Go" + + ```go + 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/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 5c943e14..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. @@ -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/main.go:init" + ``` + diff --git a/docs/agents/workflow-agents/parallel-agents.md b/docs/agents/workflow-agents/parallel-agents.md index 5da9eff1..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. @@ -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 459b5a57..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. @@ -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/main.go:init" + ``` + diff --git a/docs/artifacts/index.md b/docs/artifacts/index.md index 57538abe..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. @@ -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`). @@ -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:** @@ -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:** @@ -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,6 +821,16 @@ ADK provides concrete implementations of the `BaseArtifactService` interface, of } ``` + === "Go" + + ```go + import ( + "google.golang.org/adk/artifactservice" + ) + + --8<-- "examples/go/snippets/artifacts/main.go:in-memory-service" + ``` + ### GcsArtifactService diff --git a/docs/callbacks/index.md b/docs/callbacks/index.md index 152e4ca8..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. @@ -46,6 +46,15 @@ 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:imports" + + + --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 +98,13 @@ 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:imports" + + + --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/docs/callbacks/types-of-callbacks.md b/docs/callbacks/types-of-callbacks.md index f6ae1688..c9b976f1 100644 --- a/docs/callbacks/types-of-callbacks.md +++ b/docs/callbacks/types-of-callbacks.md @@ -29,6 +29,15 @@ 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 +70,15 @@ 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 +117,15 @@ 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,6 +151,15 @@ 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" ``` + === "Golang" + + ```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. @@ -153,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 @@ -178,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/docs/context/index.md b/docs/context/index.md index fdf46777..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. @@ -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 @@ -104,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 @@ -180,7 +198,15 @@ 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." ``` - + + === "Go" + + ```go + import "google.golang.org/adk/agent" + + --8<-- "examples/go/snippets/context/main.go:readonly_context_instruction" + ``` + === "Java" ```java @@ -223,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 @@ -282,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 @@ -350,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 @@ -396,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 @@ -433,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 @@ -481,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 @@ -523,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 @@ -573,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 @@ -659,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 @@ -710,7 +826,7 @@ Use artifacts to handle files or large data blobs associated with the session. C } } ``` - + * **Listing Artifacts:** Discover what files are available. === "Python" @@ -728,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 diff --git a/docs/deploy/cloud-run.md b/docs/deploy/cloud-run.md index e92b1df2..5948359a 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,18 @@ 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. @@ -30,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. @@ -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" @@ -279,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 @@ -386,7 +507,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/docs/events/index.md b/docs/events/index.md index 3254384e..5d119402 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. @@ -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 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 EventActions // Important for side-effects & control + Branch string // Hierarchy path + // ... other fields + } + + // model.LLMResponse contains the Content field + type LLMResponse 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 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 || event.LLMResponse.Content == nil { + return + } + calls := event.Content.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 || event.LLMResponse.Content == nil { + return + } + responses := event.Content.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 @@ -252,8 +395,24 @@ The `event.actions` object signals changes that occurred or should occur. Always } ``` + === "Go" + `delta := event.Actions.StateDelta` (a `map[string]any`) + ```go + import ( + "fmt" + "google.golang.org/adk/session" + ) + + func handleStateChanges(event *session.Event) { + if 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,29 @@ The `event.actions` object signals changes that occurred or should occur. Always } ``` + === "Go" + `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: - + === "Python" * `event.actions.transfer_to_agent` (string): Control should pass to the named agent. * `event.actions.escalate` (bool): A loop should terminate. @@ -320,6 +500,28 @@ 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.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 +530,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 +584,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 ---"); @@ -411,6 +613,76 @@ 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" + ) + + // 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() { + var fullResponseText strings.Builder + // for event := range runner.Run(...) { // Example loop + // // Accumulate streaming text if needed... + // 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 isFinalResponse(event) { + // fmt.Println("\n--- Final Output Detected ---") + // if event.LLMResponse != nil && event.LLMResponse.Content != nil { + // 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.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.SkipSummarization && event.LLMResponse.Content != nil && len(event.LLMResponse.Content.FunctionResponses()) > 0 { + // // Handle displaying the raw tool result if needed + // 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...") + // } 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 +838,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. 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..404f998a 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. @@ -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) @@ -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,34 +72,84 @@ 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; }); } ``` +=== "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) 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 { + yield(nil, err) + return + } + + // 2. Kick off event stream by calling the agent + // Assuming agent.Run also returns iter.Seq2[*Event, error] + agentEventsAndErrs := r.agent.Run(ctx, &agent.RunRequest{Session: session, Input: newQuery}) + + for event, err := range agentEventsAndErrs { + if err != nil { + if !yield(event, err) { // Yield event even if there's an error, then stop + return + } + 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 { + 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 + if !yield(event, nil) { + return // Upstream consumer stopped + } + } + // Agent finished successfully + } + } + + ``` ### Execution Logic's Role (Agent, Tool, Callback) @@ -117,9 +167,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'} @@ -129,20 +179,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... ``` @@ -152,37 +202,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) @@ -191,14 +241,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 >>>>>>>>>>>> @@ -206,19 +256,58 @@ 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.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) + + // ... 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. @@ -311,15 +400,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' @@ -331,19 +420,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( @@ -353,16 +442,62 @@ 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) + + 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) + } + } + ``` ### "Dirty Reads" of Session State @@ -375,14 +510,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. ``` @@ -403,6 +538,25 @@ 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. + 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 := 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'} + // 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. diff --git a/docs/runtime/runconfig.md b/docs/runtime/runconfig.md index 93c6f3c1..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 @@ -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,41 +46,64 @@ 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 + type StreamingMode string + + const ( + StreamingModeNone StreamingMode = "none" + StreamingModeSSE StreamingMode = "sse" + ) + + // RunConfig controls runtime behavior. + type RunConfig struct { + // Streaming mode, None or StreamingMode.SSE. + StreamingMode StreamingMode + // Whether or not to save the input blobs as artifacts + SaveInputBlobsAsArtifacts bool + } + ``` + ## 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`) | 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) (**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.** | ### `speech_config` +
+ Supported in ADKPython v0.1.0Java v0.1.0 +
+ !!! Note The interface or definition of `SpeechConfig` is the same, irrespective of the language. @@ -134,29 +159,45 @@ how your agent sounds when speaking. ### `response_modalities` +
+ 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 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.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. -!!! example "Experimental release" +!!! example "Experimental release" The `support_cfc` feature is experimental and its API or behavior might change in future releases. ### `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 @@ -167,12 +208,20 @@ Streaming modes affect both performance and user experience. SSE streaming lets ### `output_audio_transcription` +
+ 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 accessibility, record-keeping, and multi-modal applications. ### `max_llm_calls` +
+ Supported in ADKPython v0.1.0Java v0.1.0 +
+ 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 @@ -199,7 +248,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 +260,23 @@ 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, + } + ``` + 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 +287,7 @@ preferable. ```python from google.genai.adk import RunConfig, StreamingMode - + config = RunConfig( streaming_mode=StreamingMode.SSE, max_llm_calls=200 @@ -240,13 +299,23 @@ 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, + } + ``` + 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,12 +375,13 @@ providing a more responsive feel for chatbots and assistants. .build(); ``` + + 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 @@ -321,15 +391,15 @@ This comprehensive example configures an agent with: Supported in ADKPython 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 + ) + ``` Enabling Compositional Function Calling creates an agent that can dynamically execute functions based on model outputs, powerful for applications requiring 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..22f8349b 100644 --- a/docs/sessions/memory.md +++ b/docs/sessions/memory.md @@ -1,21 +1,21 @@ # 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. 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 @@ -36,16 +36,31 @@ 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** This example demonstrates the basic flow using the `InMemoryMemoryService` for simplicity. -??? "Full Code" +=== "Python" ```py import asyncio @@ -140,6 +155,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. @@ -177,22 +209,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 @@ -203,6 +236,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 @@ -217,6 +251,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 @@ -239,12 +274,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? @@ -259,6 +294,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/docs/sessions/session.md b/docs/sessions/session.md index b038358a..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 @@ -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,30 +70,36 @@ 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); ``` +=== "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.)* @@ -133,18 +139,26 @@ 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(); ``` + === "Go" + + ```go + import "google.golang.org/adk/session" + + inMemoryService := session.InMemoryService() + ``` + 2. **`VertexAiSessionService`** * **How it works:** Uses Google Cloud Vertex AI infrastructure via API @@ -162,7 +176,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 @@ -177,9 +191,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 @@ -202,10 +216,30 @@ the storage backend that best suits your needs: .blockingGet(); ``` + === "Go" + + ```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`**
- 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, 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..3c289431 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 @@ -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/built-in-tools.md b/docs/tools/built-in-tools.md index 8c6ed257..0374a364 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 supports the Google Search tool and other built-in tools via the `geminitool` package. ### Google Search @@ -43,6 +44,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/docs/tools/function-tools.md b/docs/tools/function-tools.md index 5243a20b..e2184813 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. @@ -27,13 +27,12 @@ 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" + 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): """ @@ -48,11 +47,33 @@ A parameter is considered **required** if it has a type hint but **no default va ``` 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. +=== "Go" + In Go, you use struct tags to control the JSON schema. The two primary tags are `json` and `jsonschema`. -???+ "Example: Optional Parameter with Default Value" - === "Python" + 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. + + ???+ "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. + +##### 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 def search_flights(destination: str, departure_date: str, flexible_days: int = 0): """ @@ -70,6 +91,25 @@ A parameter is considered **optional** if you provide a **default value**. This ``` 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. + + ???+ "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"` + + // 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. + ##### 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. @@ -148,6 +188,31 @@ 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 + 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" + ``` + + The return value from this tool will be a `getStockPriceResults` instance. + + ```json + For input `{"symbol": "GOOG"}`: {"price":300.6,"symbol":"GOOG"} + ``` + ### Best Practices While you have considerable flexibility in defining your function, remember that simplicity enhances usability for the LLM. Consider these guidelines: @@ -241,6 +306,21 @@ 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" + ``` + ### 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. @@ -302,6 +382,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" @@ -345,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: @@ -365,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/docs/tools/google-cloud/mcp-toolbox-for-databases.md b/docs/tools/google-cloud/mcp-toolbox-for-databases.md index 3f096dd2..303bfbd3 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,67 @@ root_agent = Agent( ) ``` +## Go SDK + +ADK relies on the `mcp-toolbox-sdk-go` go module to use Toolbox. Install the +module 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 +package main + +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. 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. 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()) + } +} 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..4e6af708 --- /dev/null +++ b/examples/go/snippets/agents/custom-agent/storyflow_agent.go @@ -0,0 +1,292 @@ +// --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/model/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/session" + "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 + revisionLoopAgent agent.Agent + postProcessorAgent 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, + revisionLoopAgent: loopAgent, + postProcessorAgent: 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.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) { + 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.revisionLoopAgent.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.postProcessorAgent.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 := session.InMemoryService() + initialState := map[string]any{ + "topic": "a brave kitten exploring a haunted house", + } + sessionInstance, err := sessionService.Create(ctx, &session.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) + events := r.Run(ctx, userID, sessionInstance.Session.ID(), input, agent.RunConfig{ + StreamingMode: agent.StreamingModeSSE, + }) + + var finalResponse string + for event, err := range events { + if err != nil { + log.Fatalf("An error occurred during agent execution: %v", err) + } + + for _, part := range event.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, 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()) +} + +// --8<-- [end:story_flow_agent] +// --8<-- [end:full_code] 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..5c393a35 --- /dev/null +++ b/examples/go/snippets/agents/llm-agents/main.go @@ -0,0 +1,223 @@ +// --8<-- [start:full_code] +package main + +import ( + "context" + "encoding/json" + "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" +) + +// --- 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{ + "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) + 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} +} + +// 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() + + model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + 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) + } + + 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, + 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, + 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) + structuredInfoAgentSchema, err := llmagent.New(llmagent.Config{ + Name: "structured_info_agent_schema", + Model: model, + 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, + 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, "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, "structured_info_result", `{"country": "France"}`) + callAgent(ctx, structuredInfoAgentSchema, "structured_info_result", `{"country": "Japan"}`) +} + +// --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 new file mode 100644 index 00000000..3f5ae958 --- /dev/null +++ b/examples/go/snippets/agents/llm-agents/snippets/main.go @@ -0,0 +1,187 @@ +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/adk/tool/functiontool" + + "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 := functiontool.New( + functiontool.Config{ + 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()) +} + +func _snippet_schema_example(model model.LLM) { + // --8<-- [start:schema_example] + 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 the capitalTool tool effectively here + }) + // --8<-- [end:schema_example] + if err != nil { + log.Fatal(err) + } + fmt.Println("Agent with output schema created:", agent.Name()) +} + +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: llmagent.IncludeContentsNone, + }) + // --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. +} diff --git a/examples/go/snippets/agents/models/models.go b/examples/go/snippets/agents/models/models.go new file mode 100644 index 00000000..be65fe42 --- /dev/null +++ b/examples/go/snippets/agents/models/models.go @@ -0,0 +1,53 @@ +package main + +import ( + "context" + "log" + + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/model/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 +} 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..8db3ca12 --- /dev/null +++ b/examples/go/snippets/agents/multi-agent/main.go @@ -0,0 +1,371 @@ +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/adk/tool/functiontool" + "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, _ := functiontool.New( + functiontool.Config{ + 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() +} diff --git a/examples/go/snippets/agents/workflow-agents/loop/main.go b/examples/go/snippets/agents/workflow-agents/loop/main.go new file mode 100644 index 00000000..f9d99099 --- /dev/null +++ b/examples/go/snippets/agents/workflow-agents/loop/main.go @@ -0,0 +1,214 @@ +package main + +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" + "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." +) + +// --8<-- [start:init] +// ExitLoopArgs defines the (empty) arguments for the ExitLoop tool. +type ExitLoopArgs struct{} + +// ExitLoopResults defines the output of the ExitLoop tool. +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{} +} + +func main() { + ctx := context.Background() + + if err := runAgent(ctx, "Write a document about a cat"); err != nil { + log.Fatalf("Agent execution failed: %v", err) + } +} + +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) + } + + // STEP 1: Initial Writer Agent (Runs ONCE at the beginning) + initialWriterAgent, err := llmagent.New(llmagent.Config{ + Name: "InitialWriterAgent", + Model: model, + 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) + } + + // 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 critic agent: %v", err) + } + + exitLoopTool, err := functiontool.New( + functiontool.Config{ + Name: "exitLoop", + Description: "Call this function ONLY when the critique indicates no further changes are needed.", + }, + ExitLoop, + ) + if err != nil { + return fmt.Errorf("failed to create exit loop tool: %v", err) + } + + // 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 refiner agent: %v", err) + } + + // STEP 2: Refinement Loop Agent + refinementLoop, err := loopagent.New(loopagent.Config{ + AgentConfig: agent.Config{ + 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: iterativeWriterAgent, + SessionService: sessionService, + }) + if err != nil { + return fmt.Errorf("failed to create runner: %v", err) + } + + session, err := sessionService.Create(ctx, &session.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("---"+" 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.StreamingModeNone, + }) { + if err != nil { + return fmt.Errorf("error during agent execution: %v", err) + } + + outputText := "" + for _, p := range event.Content.Parts { + outputText += 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.Printf("\n--- Pipeline Finished ---\n") + return nil +} 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..60fe71e1 --- /dev/null +++ b/examples/go/snippets/agents/workflow-agents/parallel/main.go @@ -0,0 +1,206 @@ +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/model/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/session" + "google.golang.org/genai" +) + +const ( + appName = "parallel_research_app" + userID = "research_user_01" + modelName = "gemini-2.0-flash" +) + +func main() { + ctx := context.Background() + + if err := runAgent(ctx, "Summarize recent sustainable tech advancements."); err != nil { + log.Fatalf("Agent execution failed: %v", err) + } +} + +func runAgent(ctx context.Context, prompt string) error { + // --8<-- [start:init] + model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + return fmt.Errorf("failed to create model: %v", err) + } + + // --- 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 := 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 := 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: "ParallelWebResearchAgent", + Description: "Runs multiple research agents in parallel to gather information.", + SubAgents: []agent.Agent{researcher1, researcher2, researcher3}, + }, + }) + if err != nil { + return fmt.Errorf("failed to create parallel agent: %v", err) + } + + // --- 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 synthesis agent: %v", err) + } + + // --- 4. Create the SequentialAgent (Orchestrates the overall flow) --- + pipeline, err := sequentialagent.New(sequentialagent.Config{ + AgentConfig: agent.Config{ + Name: "ResearchAndSynthesisPipeline", + Description: "Coordinates parallel research and synthesizes the results.", + SubAgents: []agent.Agent{parallelResearchAgent, synthesisAgent}, + }, + }) + 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: pipeline, + SessionService: sessionService, + }) + if err != nil { + return fmt.Errorf("failed to create runner: %v", err) + } + + session, err := sessionService.Create(ctx, &session.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 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.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) + 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.") + return nil +} diff --git a/examples/go/snippets/agents/workflow-agents/sequential/main.go b/examples/go/snippets/agents/workflow-agents/sequential/main.go new file mode 100644 index 00000000..78709c05 --- /dev/null +++ b/examples/go/snippets/agents/workflow-agents/sequential/main.go @@ -0,0 +1,163 @@ +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/model/gemini" + "google.golang.org/adk/runner" + "google.golang.org/adk/session" + "google.golang.org/genai" +) + +const ( + appName = "CodePipelineAgent" + userID = "test_user_456" + modelName = "gemini-2.5-flash" +) + +func main() { + 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(ctx context.Context, prompt string) error { + // --8<-- [start:init] + 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.`, + OutputKey: "generated_code", + }) + 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.`, + OutputKey: "review_comments", + }) + 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.`, + OutputKey: "refactored_code", + }) + 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) + } + // --8<-- [end:init] + + sessionService := session.InMemoryService() + 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, &session.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(), userMsg, agent.RunConfig{ + StreamingMode: agent.StreamingModeNone, + }) { + if err != nil { + return fmt.Errorf("error during agent execution: %v", err) + } + + for _, p := range event.Content.Parts { + fmt.Print(p.Text) + } + } + fmt.Println("\n---\nPipeline finished.") + return nil +} diff --git a/examples/go/snippets/artifacts/image.png b/examples/go/snippets/artifacts/image.png new file mode 100644 index 00000000..b8457eae Binary files /dev/null and b/examples/go/snippets/artifacts/image.png differ diff --git a/examples/go/snippets/artifacts/main.go b/examples/go/snippets/artifacts/main.go new file mode 100644 index 00000000..d4380047 --- /dev/null +++ b/examples/go/snippets/artifacts/main.go @@ -0,0 +1,388 @@ +package main + +import ( + "context" + "fmt" + "log" + "os" + "strings" + + "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" + "google.golang.org/adk/session" + "google.golang.org/genai" +) + +// This file contains snippets for the artifacts documentation. + +// 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() + // 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(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) + } + } + } + } + } + // Return nil to continue to the next callback or the model. + 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.", + BeforeModelCallbacks: []llmagent.BeforeModelCallback{ + BeforeModelCallback, + }, + }) + if err != nil { + log.Fatalf("Failed to create agent: %v", err) + } + + // Create a new in-memory artifact service. + artifactService := artifact.InMemoryService() + // Create a new in-memory session service. + sessionService := session.InMemoryService() + + // 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 service + artifactService := artifact.InMemoryService() + log.Printf("InMemoryArtifactService (Go) instantiated: %T", artifactService) + + // 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.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. + const filenameToLoad = "generated_report.pdf" + + // Load the artifact from the artifact service. + 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. + 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 +} + +// --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, 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{ + 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] + // 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); + + // 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.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) { + // Get the report data from the session state. + 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. + } + + // 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(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. + 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 +} + +// --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.CallbackContext, req *model.LLMRequest) (*model.LLMResponse, error) { + log.Println("[Callback] listUserFilesCallback triggered.") + // List the available artifacts from the artifact service. + 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. + 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 model. + if len(req.Contents) > 0 { + lastContent := req.Contents[len(req.Contents)-1] + if len(lastContent.Parts) > 0 { + 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()) + } 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] + +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 := artifact.InMemoryService() + sessionService := session.InMemoryService() + + // 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.", + 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 + }, + }) + + // 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": reportBytes, + } + userID := "test-user" + session, _ := sessionService.Create(ctx, &session.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.UserID(), session.Session.ID(), userInput, agent.RunConfig{ + StreamingMode: agent.StreamingModeSSE, + }) { + if err != nil { + log.Printf("AGENT ERROR: %v\n", err) + } else if event != nil && event.Content != nil { + for _, p := range event.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 := &artifact.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) +} diff --git a/examples/go/snippets/artifacts/story.pdf b/examples/go/snippets/artifacts/story.pdf new file mode 100644 index 00000000..6810fe4d Binary files /dev/null and b/examples/go/snippets/artifacts/story.pdf differ diff --git a/examples/go/snippets/callbacks/main.go b/examples/go/snippets/callbacks/main.go new file mode 100644 index 00000000..52a05e97 --- /dev/null +++ b/examples/go/snippets/callbacks/main.go @@ -0,0 +1,191 @@ +// --8<-- [start:imports] +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" +) + +// --8<-- [end:imports] + +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, + BeforeModelCallbacks: []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.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, + BeforeModelCallbacks: []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() +} 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..b487425e --- /dev/null +++ b/examples/go/snippets/callbacks/types_of_callbacks/main.go @@ -0,0 +1,467 @@ +// --8<-- [start:imports] +package main + +import ( + "context" + "fmt" + "log" + "regexp" + "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/adk/tool" + "google.golang.org/adk/tool/functiontool" + "google.golang.org/genai" +) + +// --8<-- [end:imports] + +const ( + modelName = "gemini-2.5-flash" + userID = "user_1" + appName = "CallbackExamplesApp" +) + +// --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", + BeforeAgentCallbacks: []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) + } + + 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) (*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 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", + AfterAgentCallbacks: []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) + } + + 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()) + + // 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(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 + } + } + } + + log.Println("[Callback] Proceeding with LLM call.") + 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, + BeforeModelCallbacks: []llmagent.BeforeModelCallback{onBeforeModel}, + } + 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: Should proceed to LLM ---") + 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, "write a joke on BLOCK") +} + +// --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 + } + // 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 +} + +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, + AfterModelCallbacks: []llmagent.AfterModelCallback{onAfterModel}, + } + 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: 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] + +// --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() + + log.Println("\n\n--- Running AfterAgent Example ---") + runAfterAgentExample() + + log.Println("\n\n--- Running BeforeModel Example ---") + runBeforeModelExample() + + 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. +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 != 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) + } + } +} diff --git a/examples/go/snippets/context/main.go b/examples/go/snippets/context/main.go new file mode 100644 index 00000000..86e662c7 --- /dev/null +++ b/examples/go/snippets/context/main.go @@ -0,0 +1,837 @@ +package main + +import ( + "bufio" + "context" + "fmt" + "iter" + "log" + "os" + "strings" + + "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" + "google.golang.org/adk/session" + "google.golang.org/adk/tool" + "google.golang.org/adk/tool/functiontool" + "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, 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) + } + + 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 != 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) + } + } +} + +func conceptualRunnerExample(ctx context.Context, myAgent agent.Agent) { + // --8<-- [start:conceptual_runner_example] + sessionService := session.InMemoryService() + + r, err := runner.New(runner.Config{ + AppName: appName, + Agent: myAgent, + SessionService: sessionService, + }) + if err != nil { + log.Fatalf("Failed to create runner: %v", err) + } + + s, err := sessionService.Create(ctx, &session.CreateRequest{ + AppName: appName, + UserID: userID, + }) + if err != nil { + log.Fatalf("FATAL: Failed to create session: %v", err) + } + + scanner := bufio.NewScanner(os.Stdin) + for { + fmt.Print("\nYou > ") + if !scanner.Scan() { + break + } + 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<-- [start:invocation_context_agent] +// Pseudocode: Agent implementation receiving InvocationContext +type MyAgent struct { +} + +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{} + // Use agent.New to construct the base agent functionality. + return agent.New(agent.Config{ + Name: "MyAgent", + Description: "An example agent.", + Run: a.Run, // Pass the Run method of our struct. + }) +} + +func runMyAgent() { + ctx := context.Background() + + testAgent, err := NewMyAgent() + if err != nil { + log.Fatalf("FATAL: Failed to create agent: %v", err) + } + + runScenario(ctx, testAgent, "session", nil, "Hello, world!") +} + +// --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.ReadonlyState() has no Set method since 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] + +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", + BeforeModelCallbacks: []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) + } + + runScenario(ctx, testAgent, "session", nil, "Hello, world!") +} + +// --8<-- [start:tool_context_tool] +// Pseudocode: Tool function receiving ToolContext +type searchExternalAPIArgs struct { + Query string +} + +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 searchExternalAPIResults{Status: "Auth Required"} + } + + // Use the API key... + 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.SearchMemory(tc, "info related to %s", input.Query)) + // availableFiles, _ := tc.Artifacts().List() + + return searchExternalAPIResults{Result: fmt.Sprintf("Data for %s fetched.", input.Query)} +} + +// --8<-- [end:tool_context_tool] + +func runSearchExternalAPIExample() { + myTool, err := functiontool.New( + functiontool.Config{ + 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 +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" + } + 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 toolResults{} +} + +// --8<-- [end:accessing_state_tool] + +func runMyToolExample() { + myToolTool, err := functiontool.New( + functiontool.Config{ + 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) (*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, 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", + AfterAgentCallbacks: []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) + } + + // Scenario 1: Run without the state key. + log.Println("Scenario 1: State key is NOT present.") + 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, testAgent, "callback_session_2", initialState, "Trigger callback again") +} + +// --8<-- [start:accessing_ids] +// Pseudocode: In any context (ToolContext shown) +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() + + 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 := functiontool.New( + functiontool.Config{ + 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. 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] +// Pseudocode: In a Callback +func checkInitialIntent(ctx agent.CallbackContext) (*genai.Content, error) { + 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) + return nil, nil // No modification to the content +} + +// --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", + BeforeAgentCallbacks: []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) + } + + runScenario(ctx, testAgent, "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", + BeforeAgentCallbacks: []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) + } + + 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 { +} + +type getUserProfileResult struct { + ProfileStatus 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"} + } + 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 + Error string +} + +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"} + } + + fmt.Printf("Fetching orders for user ID: %v\n", userID) + // ... logic to fetch orders using user_id ... + 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 := functiontool.New( + functiontool.Config{ + 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 := functiontool.New( + functiontool.Config{ + 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. + 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.") +} + +// --8<-- [start:updating_preferences] +// Pseudocode: Tool or Callback identifies a preference +type setUserPreferenceArgs struct { + Preference string + Value string +} + +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 setUserPreferenceResult{Status: "Failed to set preference"} + } + fmt.Printf("Set user preference '%s' to '%s'\n", args.Preference, args.Value) + return setUserPreferenceResult{Status: "Preference updated"} +} + +// --8<-- [end:updating_preferences] + +func runUpdatingPreferencesExample() { + log.Println("\n--- Running Updating Preferences Example ---") + ctx := context.Background() + + // 1. Create the tool. + setPrefTool, err := functiontool.New( + functiontool.Config{ + Name: "set_user_preference", + Description: "Sets a user's preference in the system.", + }, + setUserPreference, + ) + if err != nil { + log.Fatalf("FATAL: Failed to create setPrefTool: %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: "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) + } + + // 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 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 summarizeDocumentResult{Error: "No document artifact name found in state"} + } + + // 1. Load the artifact part containing the path/URI + artifactPart, err := tc.Artifacts().Load(tc, artifactName.(string)) + if err != nil { + return summarizeDocumentResult{Error: err.Error()} + } + + if artifactPart.Part.Text == "" { + return summarizeDocumentResult{Error: "Could not load artifact or artifact has no text path."} + } + filePath := artifactPart.Part.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 summarizeDocumentResult{Summary: summary} +} + +// --8<-- [end:artifacts_summarize] + +// --8<-- [start:artifacts_list] +// Pseudocode: In a tool function +type checkAvailableDocsArgs struct{} + +type checkAvailableDocsResult struct { + AvailableDocs []string + Error string +} + +func checkAvailableDocs(tc tool.Context, args checkAvailableDocsArgs) checkAvailableDocsResult { + 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.FileNames} +} + +// --8<-- [end:artifacts_list] + +// --8<-- [start:artifacts_save_ref] +// Adapt the saveDocumentReference callback into a tool for this example. +type saveDocRefArgs struct { + FilePath string +} + +type saveDocRefResult struct { + Status string + Error string +} + +func saveDocRef(tc tool.Context, args saveDocRefArgs) saveDocRefResult { + artifactPart := genai.NewPartFromText(args.FilePath) + _, err := tc.Artifacts().Save(tc, "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{"", "Failed to set artifact name in state"} + } + return saveDocRefResult{"Reference saved", ""} +} + +// --8<-- [end:artifacts_save_ref] + +func runArtifactsExample() { + log.Println("\n--- Running Artifacts Example ---") + ctx := context.Background() + + // 1. Create the tools. + saveRefTool, err := functiontool.New( + functiontool.Config{ + Name: "save_document_reference", + Description: "Saves a reference to a document path as an artifact.", + }, + saveDocRef, + ) + if err != nil { + log.Fatalf("FATAL: Failed to create saveRefTool: %v", err) + } + summarizeTool, err := functiontool.New( + functiontool.Config{ + 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. 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 := functiontool.New( + functiontool.Config{ + Name: "check_available_docs", + Description: "Checks for available documents in artifacts.", + }, + checkAvailableDocs, + ) + if err != nil { + log.Fatalf("FATAL: Failed to create checkDocsTool: %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: "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. +func main() { + runInitialIntentCheck() + runMyAgent() + runBeforeAgentCallbackCheck() + runSearchExternalAPIExample() + runMyToolExample() + runMyCallbackExample() + runAccessIdsExample() + runAccessingInitialUserInputExample() + runPassingDataExample() + runArtifactsExample() + runUpdatingPreferencesExample() + runCheckAvailableDocsExample() +} 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..6eda0fdf --- /dev/null +++ b/examples/go/snippets/sessions/memory_example/memory_example.go @@ -0,0 +1,191 @@ +// 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 + +// --8<-- [start:full_example] + +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/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.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. +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) + continue + } + if event.Content != nil && !event.LLMResponse.Partial { + finalResponseText = strings.Join(textParts(event.LLMResponse.Content), "") + } + } + 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 := 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 ---") + + 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) + continue + } + if event.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 --- + +func must[T any](v T, err error) T { + if err != nil { + log.Fatalf("Setup failed: %v", err) + } + return v +} + +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 +} 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..e9292dd2 --- /dev/null +++ b/examples/go/snippets/sessions/session_management_example/session_management_example.go @@ -0,0 +1,97 @@ +// 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 --- + + // 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<-- [start:vertexai_service] + // 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.") + } + // --8<-- [end:vertexai_service] + _ = vertexService // Avoid unused variable error if initialization fails. + + // --- Examining Session Properties --- + // We'll use the InMemoryService 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()) + // 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. + 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] +} 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) + } + } +} 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..000c75a9 --- /dev/null +++ b/examples/go/snippets/tools/built-in-tools/google_search.go @@ -0,0 +1,92 @@ +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/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) error { + sessionService := session.InMemoryService() + session, err := sessionService.Create(ctx, &session.CreateRequest{ + AppName: appName, + UserID: userID, + }) + if err != nil { + return fmt.Errorf("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 { + return fmt.Errorf("failed to create the runner: %v", err) + } + + sessionID := session.Session.ID() + 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, + }) { + if err != nil { + fmt.Printf("\nAGENT_ERROR: %v\n", err) + } else if event.Partial { + 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) + } + 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.Println("\n---") +} 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..4238b710 --- /dev/null +++ b/examples/go/snippets/tools/function-tools/func_tool.go @@ -0,0 +1,249 @@ +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/agenttool" + "google.golang.org/adk/tool/functiontool" + + "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": 300.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 +} + +// 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 { + 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 getStockPriceResults{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 := functiontool.New( + functiontool.Config{ + 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, a agent.Agent, prompt string) { + sessionService := session.InMemoryService() + // Create a new session for the agent interactions. + 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{ + AppName: appName, + Agent: a, + 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() + + 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.StreamingModeNone, + }) { + if err != nil { + fmt.Printf("\nAGENT_ERROR: %v\n", err) + } else { + for _, p := range event.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---") + } +} + +// --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() +} 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..b484f9bf --- /dev/null +++ b/examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go @@ -0,0 +1,174 @@ +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/adk/tool/functiontool" + + "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. +type CreateTicketResults struct { + 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()) + + // "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) { + ticketTool, err := functiontool.New( + functiontool.Config{ + 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. + for _, part := range event.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 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 := 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 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) { + 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) + } + // 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]