|
| 1 | +# Copyright 2025 DeepMind Technologies Limited. All Rights Reserved. |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | +# ============================================================================== |
| 15 | + |
| 16 | +r"""ADK agent version of the trip request planner. |
| 17 | +
|
| 18 | +We use Gemini flash-lite to formalize freeform trip request into the dates and |
| 19 | +destination. Then we use a second model to compose the trip itinerary. |
| 20 | +
|
| 21 | +This simple example shows how we can reduce perceived latency by running a fast |
| 22 | +model to validate and acknowledge user request while the good but slow model is |
| 23 | +handling it. |
| 24 | +
|
| 25 | +The approach from this example also can be used as a defense mechanism against |
| 26 | +prompt injections. The first model without tool access formalizes the request |
| 27 | +into the TripRequest dataclass. The attack surface is significantly reduced by |
| 28 | +the narrowness of the output format and lack of tools. Then a second model is |
| 29 | +run on this cleanup up input. |
| 30 | +
|
| 31 | +## Setup |
| 32 | +
|
| 33 | +To install the dependencies for this script, run: |
| 34 | +
|
| 35 | +``` |
| 36 | +pip install genai-processors google-adk |
| 37 | +``` |
| 38 | +
|
| 39 | +Before running this script, ensure the `GOOGLE_API_KEY` environment |
| 40 | +variable is set to the api-key you obtained from Google AI Studio. |
| 41 | +
|
| 42 | +## Run |
| 43 | +
|
| 44 | +Change directory to `genai-processors/examples` and run `adk web`. |
| 45 | +Then navigate to http://localhost:8000/ select "trip_request_adk" |
| 46 | +agent and enter your trip request. |
| 47 | +""" |
| 48 | + |
| 49 | +from collections.abc import AsyncIterable |
| 50 | +import datetime |
| 51 | +import os |
| 52 | + |
| 53 | +import dataclasses_json |
| 54 | +from genai_processors import content_api |
| 55 | +from genai_processors import processor |
| 56 | +from genai_processors import switch |
| 57 | +from genai_processors.core import adk |
| 58 | +from genai_processors.core import genai_model |
| 59 | +from genai_processors.core import preamble |
| 60 | +from google.genai import types as genai_types |
| 61 | +from pydantic import dataclasses |
| 62 | + |
| 63 | +# You need to define the API key in the environment variables. |
| 64 | +API_KEY = os.environ['GOOGLE_API_KEY'] |
| 65 | + |
| 66 | + |
| 67 | +@dataclasses_json.dataclass_json |
| 68 | +@dataclasses.dataclass(frozen=True) |
| 69 | +class TripRequest: |
| 70 | + """A trip request required for GenAI models to generate structured output.""" |
| 71 | + |
| 72 | + start_date: str |
| 73 | + end_date: str |
| 74 | + destination: str |
| 75 | + error: str |
| 76 | + |
| 77 | + def info(self) -> str: |
| 78 | + """Returns a string representation to be used in prompts.""" |
| 79 | + return ( |
| 80 | + '\nTrip information:\n' |
| 81 | + f'Start date: {self.start_date}\n' |
| 82 | + f'End date: {self.end_date}\n' |
| 83 | + f'Destination: {self.destination}\n' |
| 84 | + ) |
| 85 | + |
| 86 | + |
| 87 | +# A processor can be easily defined as a function with a dedicated decorator. |
| 88 | +# This is the recommended way to define stateless processors. |
| 89 | +@processor.part_processor_function |
| 90 | +async def process_json_output( |
| 91 | + part: content_api.ProcessorPart, |
| 92 | +) -> AsyncIterable[content_api.ProcessorPart]: |
| 93 | + """Process the json output of a GenAI model.""" |
| 94 | + trip_request = part.get_dataclass(TripRequest) |
| 95 | + if trip_request.error: |
| 96 | + yield content_api.ProcessorPart( |
| 97 | + trip_request.error, |
| 98 | + substream_name='error', |
| 99 | + ) |
| 100 | + else: |
| 101 | + yield content_api.ProcessorPart(trip_request.info()) |
| 102 | + |
| 103 | + |
| 104 | +def create_trip_request_processor() -> processor.Processor: |
| 105 | + """Creates a trip request processor.""" |
| 106 | + # First processor extracts a json trip request from the user input. |
| 107 | + # We need a json dataclass (we use the wrapper from pydantic) to parse the |
| 108 | + # json output of the model. We add the current date to the prompt to make |
| 109 | + # sure the model uses the current date. |
| 110 | + extract_trip_request = preamble.Suffix( |
| 111 | + content_factory=lambda: f'Today is: {datetime.date.today()}' |
| 112 | + ) + genai_model.GenaiModel( |
| 113 | + api_key=API_KEY, |
| 114 | + model_name='gemini-2.0-flash-lite', |
| 115 | + generate_content_config=genai_types.GenerateContentConfig( |
| 116 | + system_instruction=( |
| 117 | + 'You are a travel agent. You are given a trip request from a' |
| 118 | + ' user. You need to check if the user provided all necessary' |
| 119 | + ' information. If the user request is missing any' |
| 120 | + ' information, you need to return an error message. If the' |
| 121 | + ' user request is complete, you need to return the user' |
| 122 | + ' request with the start date, end date and the destination.' |
| 123 | + ), |
| 124 | + response_schema=TripRequest, |
| 125 | + response_mime_type='application/json', |
| 126 | + ), |
| 127 | + ) |
| 128 | + # Second processor generates a trip itinerary based on a valid trip request. |
| 129 | + generate_trip = genai_model.GenaiModel( |
| 130 | + api_key=API_KEY, |
| 131 | + # NOTE: To reduce cost of running the demo we use the flash model. |
| 132 | + # The real application would use a better but slower thinking model. |
| 133 | + # The perceived latency of that model would be hidden by the fast answer |
| 134 | + # from extract_trip_request and acknowledging to the user that we've |
| 135 | + # started planning the trip. |
| 136 | + model_name='gemini-2.0-flash-lite', |
| 137 | + generate_content_config=genai_types.GenerateContentConfig( |
| 138 | + system_instruction=( |
| 139 | + 'You are a travel agent. You are given a trip request from a user' |
| 140 | + ' with dates and destination. Plan a trip with hotels and' |
| 141 | + ' activities. Split the plan into daily section. Plan one' |
| 142 | + ' activity per 1/2 day max.' |
| 143 | + ), |
| 144 | + # Ground with Google Search |
| 145 | + tools=[genai_types.Tool(google_search=genai_types.GoogleSearch())], |
| 146 | + ), |
| 147 | + ) |
| 148 | + |
| 149 | + # Returns a preamble part with a message to the user. |
| 150 | + msg_to_user = preamble.Preamble( |
| 151 | + content='OK, preparing a trip for the following request:\n', |
| 152 | + ) |
| 153 | + |
| 154 | + # Plumb everything together with a logical switch that lets us handle errors. |
| 155 | + return ( |
| 156 | + extract_trip_request |
| 157 | + + process_json_output |
| 158 | + + switch.Switch(content_api.get_substream_name).case( |
| 159 | + # default substream name, no error. |
| 160 | + '', |
| 161 | + # For processors, the `parallel_concat` is a way to run them |
| 162 | + # concurrently while specify how their results should be merged, here |
| 163 | + # they should be concatenated. |
| 164 | + processor.parallel_concat([msg_to_user, generate_trip]), |
| 165 | + ) |
| 166 | + # Any error substream name is handled by the default processor. Here we |
| 167 | + # return the input part unchanged. |
| 168 | + .default(processor.passthrough()) |
| 169 | + ) |
| 170 | + |
| 171 | + |
| 172 | +root_agent = adk.ProcessorAgent( |
| 173 | + create_trip_request_processor, |
| 174 | + name='trip_request_adk', |
| 175 | +) |
0 commit comments