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..46e74e91 100644 --- a/docs/agents/custom-agents.md +++ b/docs/agents/custom-agents.md @@ -53,6 +53,15 @@ The core of any custom agent is the method where you define its unique asynchron * **Reactive Stream (`Flowable`):** It must return an `io.reactivex.rxjava3.core.Flowable`. This `Flowable` represents a stream of events that will be produced by the custom agent's logic, often by combining or transforming multiple `Flowable` from sub-agents. * **`ctx` (InvocationContext):** Provides access to crucial runtime information, most importantly `ctx.session().state()`, which is a `java.util.concurrent.ConcurrentMap`. This is the primary way to share data between steps orchestrated by your custom agent. +=== "Go" + + In Go, you implement the `Run` method 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/llm-agents.md b/docs/agents/llm-agents.md index a03437a5..239fa795 100644 --- a/docs/agents/llm-agents.md +++ b/docs/agents/llm-agents.md @@ -66,6 +66,13 @@ First, you need to establish what the agent *is* and what it's *for*. .build(); ``` +=== "Golang" + + ```go + // Example: Defining the basic identity + --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:identity" + ``` + ## Guiding the Agent: Instructions (`instruction`) @@ -136,6 +143,13 @@ tells the agent: .build(); ``` +=== "Golang" + + ```go + // Example: Adding instructions + --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:instruction" + ``` + *(Note: For instructions that apply to *all* agents in a system, consider using `global_instruction` on the root agent, detailed further in the [Multi-Agents](multi-agents.md) section.)* @@ -208,6 +222,12 @@ on the conversation and its instructions. .build(); ``` +=== "Golang" + + ```go + --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:tool_example" + ``` + Learn more about Tools in the [Tools](../tools/index.md) section. ## Advanced Configuration & Control @@ -255,6 +275,14 @@ You can adjust how the underlying LLM generates responses using `generate_conten .build(); ``` +=== "Golang" + + ```go + import "google.golang.org/genai" + + --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:gen_config" + ``` + ### Structuring Data (`input_schema`, `output_schema`, `output_key`) For scenarios requiring structured data exchange with an `LLM Agent`, the ADK provides mechanisms to define expected input and desired output formats using schema definitions. @@ -266,6 +294,7 @@ For scenarios requiring structured data exchange with an `LLM Agent`, the ADK pr * **`output_key` (Optional):** Provide a string key. If set, the text content of the agent's *final* response will be automatically saved to the session's state dictionary under this key. This is useful for passing results between agents or steps in a workflow. * In Python, this might look like: `session.state[output_key] = agent_response_text` * In Java: `session.state().put(outputKey, agentResponseText)` + * In Golang, within a callback handler: `ctx.State().Set(output_key, agentResponseText)` === "Python" @@ -315,6 +344,14 @@ For scenarios requiring structured data exchange with an `LLM Agent`, the ADK pr .build(); ``` +=== "Golang" + + The input and output schema is a `google.genai.types.Schema` object. + + ```go + --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:schema_example" + ``` + ### Managing Context (`include_contents`) Control whether the agent receives the prior conversation history. @@ -344,6 +381,14 @@ Control whether the agent receives the prior conversation history. .build(); ``` +=== "Golang" + + ```go + import "google.golang.org/adk/agent/llmagent" + + --8<-- "examples/go/snippets/agents/llm-agents/snippets/main.go:include_contents" + ``` + ### Planner
@@ -551,6 +596,12 @@ call_agent("If it's raining in New York right now, what is the current temperatu --8<-- "examples/java/snippets/src/main/java/agents/LlmAgentExample.java:full_code" ``` + === "Golang" + + ```go + --8<-- "examples/go/snippets/agents/llm-agents/main.go:full_code" + ``` + _(This example demonstrates the core concepts. More complex agents might incorporate schemas, context control, planning, etc.)_ ## Related Concepts (Deferred Topics) diff --git a/docs/agents/models.md b/docs/agents/models.md index 34f43177..dc02e1ef 100644 --- a/docs/agents/models.md +++ b/docs/agents/models.md @@ -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..fa67eaa1 100644 --- a/docs/agents/multi-agents.md +++ b/docs/agents/multi-agents.md @@ -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/loop-agents.md b/docs/agents/workflow-agents/loop-agents.md index 5c943e14..db97d9cb 100644 --- a/docs/agents/workflow-agents/loop-agents.md +++ b/docs/agents/workflow-agents/loop-agents.md @@ -52,3 +52,8 @@ In this setup, the `LoopAgent` would manage the iterative process. The `CriticA --8<-- "examples/java/snippets/src/main/java/agents/workflow/LoopAgentExample.java:init" ``` + === "Golang" + ```go + --8<-- "examples/go/snippets/agents/workflow-agents/loop/main.go:init" + ``` + diff --git a/docs/agents/workflow-agents/parallel-agents.md b/docs/agents/workflow-agents/parallel-agents.md index 5da9eff1..5ad8a8c2 100644 --- a/docs/agents/workflow-agents/parallel-agents.md +++ b/docs/agents/workflow-agents/parallel-agents.md @@ -56,3 +56,8 @@ These research tasks are independent. Using a `ParallelAgent` allows them to ru ```java --8<-- "examples/java/snippets/src/main/java/agents/workflow/ParallelResearchPipeline.java:full_code" ``` + + === "Golang" + ```go + --8<-- "examples/go/snippets/agents/workflow-agents/parallel/main.go:init" + ``` diff --git a/docs/agents/workflow-agents/sequential-agents.md b/docs/agents/workflow-agents/sequential-agents.md index 459b5a57..ce41243e 100644 --- a/docs/agents/workflow-agents/sequential-agents.md +++ b/docs/agents/workflow-agents/sequential-agents.md @@ -53,4 +53,9 @@ This ensures the code is written, *then* reviewed, and *finally* refactored, in --8<-- "examples/java/snippets/src/main/java/agents/workflow/SequentialAgentExample.java:init" ``` + === "Golang" + ```go + --8<-- "examples/go/snippets/agents/workflow-agents/sequential/main.go:init" + ``` + diff --git a/docs/artifacts/index.md b/docs/artifacts/index.md index 57538abe..559f7149 100644 --- a/docs/artifacts/index.md +++ b/docs/artifacts/index.md @@ -64,6 +64,18 @@ In ADK, **Artifacts** represent a crucial mechanism for managing named, versione } ``` +=== "Go" + + ```go + import ( + "log" + + "google.golang.org/genai" + ) + + --8<-- "examples/go/snippets/artifacts/main.go:representation" + ``` + * **Persistence & Management:** Artifacts are not stored directly within the agent or session state. Their storage and retrieval are managed by a dedicated **Artifact Service** (an implementation of `BaseArtifactService`, defined in `google.adk.artifacts`. ADK provides various implementations, such as: * An in-memory service for testing or temporary storage (e.g., `InMemoryArtifactService` in Python, defined in `google.adk.artifacts.in_memory_artifact_service.py`). * A service for persistent storage using Google Cloud Storage (GCS) (e.g., `GcsArtifactService` in Python, defined in `google.adk.artifacts.gcs_artifact_service.py`). @@ -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..cbb3502b 100644 --- a/docs/callbacks/index.md +++ b/docs/callbacks/index.md @@ -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..cf631c57 100644 --- a/docs/context/index.md +++ b/docs/context/index.md @@ -50,6 +50,13 @@ The central piece holding all this information together for a single, complete u # As a developer, you work with the context objects provided in method arguments. ``` +=== "Go" + + ```go + /* Conceptual Pseudocode: How the framework provides context (Internal Logic) */ + --8<-- "examples/go/snippets/context/main.go:conceptual_runner_example" + ``` + === "Java" ```java @@ -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/sessions/express-mode.md b/docs/sessions/express-mode.md index ccbac7f1..5d7e5bcb 100644 --- a/docs/sessions/express-mode.md +++ b/docs/sessions/express-mode.md @@ -15,7 +15,7 @@ Once you sign up, get an [API key](https://cloud.google.com/vertex-ai/generative ## Create an Agent Engine `Session` objects are children of an `AgentEngine`. When using Vertex AI Express Mode, we can create an empty `AgentEngine` parent to manage all of our `Session` and `Memory` objects. -First, ensure that your environment variables are set correctly. For example, in Python: +First, ensure that your environment variables are set correctly. For example: ```env title="weather_agent/.env" GOOGLE_GENAI_USE_VERTEXAI=TRUE @@ -28,27 +28,31 @@ Next, we can create our Agent Engine instance. You can use the Gen AI SDK. 1. Import Gen AI SDK. + === "Python" ```py from google import genai ``` 2. Set Vertex AI to be True, then use a `POST` request to create the Agent Engine - - ```py - # Create Agent Engine with Gen AI SDK - client = genai.Client(vertexai=True)._api_client - - response = client.request( - http_method='POST', - path=f'reasoningEngines', - request_dict={"displayName": "YOUR_AGENT_ENGINE_DISPLAY_NAME", "description": "YOUR_AGENT_ENGINE_DESCRIPTION"}, - ) - response - ``` + + === "Python" + ```py + # Create Agent Engine with Gen AI SDK + client = genai.Client(vertexai=True)._api_client + + response = client.request( + http_method='POST', + path=f'reasoningEngines', + request_dict={"displayName": "YOUR_AGENT_ENGINE_DISPLAY_NAME", "description": "YOUR_AGENT_ENGINE_DESCRIPTION"}, + ) + response + ``` + 3. Replace `YOUR_AGENT_ENGINE_DISPLAY_NAME` and `YOUR_AGENT_ENGINE_DESCRIPTION` with your use case. 4. Get the Agent Engine name and ID from the response + === "Python" ```py APP_NAME = "/".join(response['name'].split("/")[:6]) APP_ID = APP_NAME.split('/')[-1] @@ -56,24 +60,32 @@ Next, we can create our Agent Engine instance. You can use the Gen AI SDK. ## Managing Sessions with a `VertexAiSessionService` -[`VertexAiSessionService`](session.md###sessionservice-implementations) is compatible with Vertex AI Express mode API Keys. We can +[`VertexAiSessionService`](session.md###sessionservice-implementations) is compatible with Vertex AI Express mode API Keys. We can instead initialize the session object without any project or location. -```py -# Requires: pip install google-adk[vertexai] -# Plus environment variable setup: -# GOOGLE_GENAI_USE_VERTEXAI=TRUE -# GOOGLE_API_KEY=PASTE_YOUR_ACTUAL_EXPRESS_MODE_API_KEY_HERE -from google.adk.sessions import VertexAiSessionService +=== "Python" -# The app_name used with this service should be the Reasoning Engine ID or name -APP_ID = "your-reasoning-engine-id" + ```py + # Requires: pip install google-adk[vertexai] + # Plus environment variable setup: + # GOOGLE_GENAI_USE_VERTEXAI=TRUE + # GOOGLE_API_KEY=PASTE_YOUR_ACTUAL_EXPRESS_MODE_API_KEY_HERE + from google.adk.sessions import VertexAiSessionService -# Project and location are not required when initializing with Vertex Express Mode -session_service = VertexAiSessionService(agent_engine_id=APP_ID) -# Use REASONING_ENGINE_APP_ID when calling service methods, e.g.: -# session = await session_service.create_session(app_name=REASONING_ENGINE_APP_ID, user_id= ...) -``` + # The app_name used with this service should be the Reasoning Engine ID or name + APP_ID = "your-reasoning-engine-id" + + # Project and location are not required when initializing with Vertex Express Mode + session_service = VertexAiSessionService(agent_engine_id=APP_ID) + # Use REASONING_ENGINE_APP_ID when calling service methods, e.g.: + # session = await session_service.create_session(app_name=REASONING_ENGINE_APP_ID, user_id= ...) + ``` + +=== "Go" + + ```go + --8<-- "examples/go/snippets/sessions/express_mode_example/express_mode_example.go:session_service" + ``` !!! info Session Service Quotas @@ -84,24 +96,26 @@ session_service = VertexAiSessionService(agent_engine_id=APP_ID) ## Managing Memories with a `VertexAiMemoryBankService` -[`VertexAiMemoryBankService`](memory.md###memoryservice-implementations) is compatible with Vertex AI Express mode API Keys. We can +[`VertexAiMemoryBankService`](memory.md###memoryservice-implementations) is compatible with Vertex AI Express mode API Keys. We can instead initialize the memory object without any project or location. -```py -# Requires: pip install google-adk[vertexai] -# Plus environment variable setup: -# GOOGLE_GENAI_USE_VERTEXAI=TRUE -# GOOGLE_API_KEY=PASTE_YOUR_ACTUAL_EXPRESS_MODE_API_KEY_HERE -from google.adk.sessions import VertexAiMemoryBankService +=== "Python" -# The app_name used with this service should be the Reasoning Engine ID or name -APP_ID = "your-reasoning-engine-id" + ```py + # Requires: pip install google-adk[vertexai] + # Plus environment variable setup: + # GOOGLE_GENAI_USE_VERTEXAI=TRUE + # GOOGLE_API_KEY=PASTE_YOUR_ACTUAL_EXPRESS_MODE_API_KEY_HERE + from google.adk.sessions import VertexAiMemoryBankService -# Project and location are not required when initializing with Vertex Express Mode -memory_service = VertexAiMemoryBankService(agent_engine_id=APP_ID) -# Generate a memory from that session so the Agent can remember relevant details about the user -# memory = await memory_service.add_session_to_memory(session) -``` + # The app_name used with this service should be the Reasoning Engine ID or name + APP_ID = "your-reasoning-engine-id" + + # Project and location are not required when initializing with Vertex Express Mode + memory_service = VertexAiMemoryBankService(agent_engine_id=APP_ID) + # Generate a memory from that session so the Agent can remember relevant details about the user + # memory = await memory_service.add_session_to_memory(session) + ``` !!! info Memory Service Quotas 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..28fe0894 100644 --- a/docs/tools/function-tools.md +++ b/docs/tools/function-tools.md @@ -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. @@ -95,6 +135,7 @@ You can also mark a parameter as optional using `typing.Optional[SomeType]` or t ##### Variadic Parameters (`*args` and `**kwargs`) While you can include `*args` (variable positional arguments) and `**kwargs` (variable keyword arguments) in your function signature for other purposes, they are **ignored by the ADK framework** when generating the tool schema for the LLM. The LLM will not be aware of them and cannot pass arguments to them. It's best to rely on explicitly defined parameters for all data you expect from the LLM. + #### Return Type The preferred return type for a Function Tool is a **dictionary** in Python or **Map** in Java. This allows you to structure the response with key-value pairs, providing context and clarity to the LLM. If your function returns a type other than a dictionary, the framework automatically wraps it into a dictionary with a single key named **"result"**. @@ -148,6 +189,20 @@ A tool can write data to a `temp:` variable, and a subsequent tool can read it. For input `GOOG`: {"symbol": "GOOG", "price": "1.0"} ``` + === "Go" + + This tool retrieves the mocked value of a stock price. + + ```go + --8<-- "examples/go/snippets/tools/function-tools/func_tool.go" + ``` + + The return value from this tool will be a `map[string]any` marshalled into a JSON object. + + ```json + For input `{"symbol": "GOOG"}`: {"price":1,"symbol":"GOOG"} + ``` + ### Best Practices While you have considerable flexibility in defining your function, remember that simplicity enhances usability for the LLM. Consider these guidelines: @@ -241,6 +296,12 @@ Define your tool function and wrap it using the `LongRunningFunctionTool` class: } ``` +=== "Go" + + ```go + --8<-- "examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go:create_long_running_tool" + ``` + ### Intermediate / Final result Updates Agent client received an event with long running function calls and check the status of the ticket. Then Agent client can send the intermediate or final response back to update the progress. The framework packages this value (even if it's None) into the content of the `FunctionResponse` sent back to the LLM. @@ -302,6 +363,14 @@ Agent client received an event with long running function calls and check the st --8<-- "examples/java/snippets/src/main/java/tools/LongRunningFunctionExample.java:full_code" ``` +=== "Go" + + The following example demonstrates a multi-turn workflow. First, the user asks the agent to create a ticket. The agent calls the long-running tool and the client captures the `FunctionCall` ID. The client then simulates the asynchronous work completing by sending subsequent `FunctionResponse` messages back to the agent to provide the ticket ID and final status. + + ```go + --8<-- "examples/go/snippets/tools/function-tools/long-running-tool/long_running_tool.go:run_long_running_tool" + ``` + ??? "Python complete example: File Processing Simulation" diff --git a/examples/go/snippets/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/express_mode_example/express_mode_example.go b/examples/go/snippets/sessions/express_mode_example/express_mode_example.go new file mode 100644 index 00000000..ba64fc24 --- /dev/null +++ b/examples/go/snippets/sessions/express_mode_example/express_mode_example.go @@ -0,0 +1,130 @@ +// 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" + + "google.golang.org/adk/agent" + "google.golang.org/adk/agent/llmagent" + "google.golang.org/adk/cmd/launcher/adk" + "google.golang.org/adk/cmd/launcher/full" + "google.golang.org/adk/cmd/restapi/services" + "google.golang.org/adk/model/gemini" + "google.golang.org/adk/session" + "google.golang.org/adk/tool" + "google.golang.org/adk/tool/geminitool" + "google.golang.org/genai" +) + +const ( + modelName = "gemini-2.5-flash" +) + +// This example demonstrates how to initialize and use the VertexAiSessionService +// with Vertex AI Express Mode, and how to create and run an Agent Engine. +// +// Before running, ensure you have set up your environment: +// 1. Sign up for Vertex AI Express Mode. +// 2. Create an Agent Engine to get an AGENT_ENGINE_ID. +// 3. Set the following environment variables: +// export GOOGLE_API_KEY="your-express-mode-api-key" +// export GOOGLE_CLOUD_PROJECT="your-gcp-project-id" +// export GOOGLE_CLOUD_LOCATION="your-gcp-location" +func main() { + ctx := context.Background() + + // --8<-- [start:session_service] + // The appName should be the Reasoning Engine ID. + // In a real application, get this from your Agent Engine in the Google Cloud Console. + agentEngineID := "your-reasoning-engine-id" // <-- Replace with your actual Agent Engine ID + + // When using Vertex AI Express Mode with an API key, project and location + // are typically picked up from environment variables, and you can initialize + // the service like this. + sessionService, err := session.VertexAIService(ctx, agentEngineID) + if err != nil { + // This will fail if GOOGLE_API_KEY is not set. + log.Fatalf("Failed to create Vertex AI session service: %v", err) + } + + fmt.Println("Successfully initialized VertexAiSessionService.") + + // You can now use the sessionService to manage sessions. + userID := "express-mode-user-go" + sessionID := "express-mode-session-go" + + fmt.Printf("Creating session %s...\n", sessionID) + createResp, err := sessionService.Create(ctx, &session.CreateRequest{ + AppName: agentEngineID, + UserID: userID, + SessionID: sessionID, + }) + if err != nil { + log.Fatalf("Failed to create session: %v", err) + } + + fmt.Printf("Successfully created session with ID: %s\n", createResp.Session.ID()) + + // Session cleanup is not explicitly supported by VertexAIService in Express Mode. + // Sessions are typically ephemeral and managed automatically. + // --8<-- [end:session_service] + + // --8<-- [start:agent_engine] + // Create the root agent for the Agent Engine. + rootAgent, err := createAgent() + if err != nil { + log.Fatalf("Failed to create agent: %v", err) + } + + // Configure the ADK with the session service and the agent. + config := &adk.Config{ + SessionService: sessionService, + AgentLoader: services.NewSingleAgentLoader(rootAgent), + } + + // Use the full launcher to run the agent engine. + // This will start a server that can handle requests. + fmt.Println("Starting Agent Engine...") + l := full.NewLauncher() + if err := l.Execute(ctx, config, os.Args[1:]); err != nil { + log.Fatalf("run failed: %v\n\n%s", err, l.CommandLineSyntax()) + } + // --8<-- [end:agent_engine] +} + +func createAgent() (agent.Agent, error) { + ctx := context.Background() + + model, err := gemini.NewModel(ctx, modelName, &genai.ClientConfig{}) + if err != nil { + return nil, fmt.Errorf("failed to create gemini model: %w", err) + } + + a, err := llmagent.New(llmagent.Config{ + Name: "example_agent", + Model: model, + Description: "An example agent that can use Google Search.", + Instruction: "You are a helpful assistant. Use your tools to answer the user's questions.", + Tools: []tool.Tool{geminitool.GoogleSearch{}}, + }) + if err != nil { + return nil, fmt.Errorf("failed to create llmagent: %w", err) + } + return a, nil +} diff --git a/examples/go/snippets/sessions/express_mode_example/go.mod b/examples/go/snippets/sessions/express_mode_example/go.mod new file mode 100644 index 00000000..449a4d84 --- /dev/null +++ b/examples/go/snippets/sessions/express_mode_example/go.mod @@ -0,0 +1,44 @@ +module express_mode_example + +go 1.24.7 + +require ( + google.golang.org/adk v0.0.0-20251103183939-c390af83ff8d + google.golang.org/genai v1.20.0 +) + +require ( + cloud.google.com/go v0.123.0 // indirect + cloud.google.com/go/auth v0.17.0 // indirect + cloud.google.com/go/compute/metadata v0.9.0 // indirect + github.com/a2aproject/a2a-go v0.0.0-20251031084236-fbf3bcff0c4a // indirect + github.com/awalterschulze/gographviz v2.0.3+incompatible // indirect + github.com/felixge/httpsnoop v1.0.4 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/google/go-cmp v0.7.0 // indirect + github.com/google/s2a-go v0.1.9 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect + github.com/googleapis/gax-go/v2 v2.15.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect + go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.38.0 // indirect + go.opentelemetry.io/otel/sdk v1.38.0 // indirect + go.opentelemetry.io/otel/trace v1.38.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20251014184007-4626949a642f // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251014184007-4626949a642f // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect + rsc.io/omap v1.2.0 // indirect + rsc.io/ordered v1.1.1 // indirect +) diff --git a/examples/go/snippets/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..8b74921d --- /dev/null +++ b/examples/go/snippets/tools/function-tools/func_tool.go @@ -0,0 +1,177 @@ +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" +) + +// 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.StreamingModeSSE, + }) { + if err != nil { + fmt.Printf("\nAGENT_ERROR: %v\n", err) + } else if event.Partial { + for _, p := range event.Content.Parts { + fmt.Print(p.Text) + } + } + } +} + +// 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---") + } +} + +func main() { + fmt.Println("Attempting to run the agent simulation...") + RunAgentSimulation() +} 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]