From 854f2491ce62ee05d852d9db8b95bf2ddfb6df77 Mon Sep 17 00:00:00 2001 From: sarojrout Date: Sun, 16 Nov 2025 15:22:16 -0800 Subject: [PATCH 1/5] feat(samples): add hello_doctor health assessment agent --- contributing/samples/hello_doctor/README.md | 52 ++++++ contributing/samples/hello_doctor/__init__.py | 16 ++ contributing/samples/hello_doctor/agent.py | 171 ++++++++++++++++++ contributing/samples/hello_doctor/main.py | 90 +++++++++ 4 files changed, 329 insertions(+) create mode 100644 contributing/samples/hello_doctor/README.md create mode 100644 contributing/samples/hello_doctor/__init__.py create mode 100644 contributing/samples/hello_doctor/agent.py create mode 100644 contributing/samples/hello_doctor/main.py diff --git a/contributing/samples/hello_doctor/README.md b/contributing/samples/hello_doctor/README.md new file mode 100644 index 0000000000..6ea609895b --- /dev/null +++ b/contributing/samples/hello_doctor/README.md @@ -0,0 +1,52 @@ +# hello_doctor + +A health assessment agent sample that demonstrates safe, educational health conversations with structured intake questions and risk assessment tools. + +## Features + +- **Structured health intake**: Asks six key questions (age, smoking, alcohol, medical conditions/medications, allergies, lifestyle) before providing any advice +- **Session state tracking**: Uses `log_health_answer` tool to build a longitudinal picture of user responses +- **Risk assessment**: Uses `summarize_risk_profile` tool to provide non-diagnostic risk summaries +- **Strong safety disclaimers**: Always emphasizes that it is not a medical professional and directs users to licensed healthcare providers +- **Few-shot examples**: Includes examples for handling mild symptoms, concerning symptoms, and supplement questions + +## Safety + +**Important**: This agent is for **educational purposes only**. It: +- Does NOT diagnose, treat, or prescribe +- Does NOT replace professional medical advice +- Always directs users to licensed healthcare professionals for any real health concerns +- Emphasizes emergency care for serious symptoms + +## Files + +- `agent.py`: Defines the `root_agent` with safety instructions, tools, and few-shot examples +- `main.py`: CLI demo script showing how to run the agent programmatically + +## Running + +### Via CLI script + +```bash +# Make sure you have GOOGLE_GENAI_API_KEY set in .env +python contributing/samples/hello_doctor/main.py +``` + +### Via ADK Web UI + +```bash +adk web contributing/samples +``` + +Then select `hello_doctor` from the app dropdown in the web UI at `http://127.0.0.1:8000`. + +## Configuration + +Create a `.env` file in the project root with: + +```env +GOOGLE_GENAI_API_KEY=your_api_key_here +``` + +Or configure Vertex AI credentials if using the Vertex AI backend. + diff --git a/contributing/samples/hello_doctor/__init__.py b/contributing/samples/hello_doctor/__init__.py new file mode 100644 index 0000000000..8ce90a27ba --- /dev/null +++ b/contributing/samples/hello_doctor/__init__.py @@ -0,0 +1,16 @@ +# 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. + +from . import agent + diff --git a/contributing/samples/hello_doctor/agent.py b/contributing/samples/hello_doctor/agent.py new file mode 100644 index 0000000000..9f0783d215 --- /dev/null +++ b/contributing/samples/hello_doctor/agent.py @@ -0,0 +1,171 @@ +# 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. + +from google.adk import Agent +from google.adk.tools.tool_context import ToolContext + + +def log_health_answer(question: str, answer: str, tool_context: ToolContext) -> str: + """Log a structured health answer into session state. + + The model can call this tool after it asks a question such as + "What is your age?" or "How often do you exercise?" to build up a + longitudinal picture of the user over the conversation. + """ + state = tool_context.state + answers = state.get("health_answers", []) + answers.append({"question": question, "answer": answer}) + state["health_answers"] = answers + return "Logged." + + +def summarize_risk_profile(tool_context: ToolContext) -> str: + """Return a simple textual summary of the collected answers. + + This is intentionally simplistic and non-diagnostic, but gives the + model a place to anchor a longitudinal summary. The LLM can call + this near the end of an assessment and include the returned text in + its final response. + """ + answers = tool_context.state.get("health_answers", []) + if not answers: + return ( + "No structured health answers have been logged yet. Ask more " + "questions first, then call this tool again." + ) + + # Very lightweight heuristic: count how many answers mention words + # like 'chest pain', 'shortness of breath', or 'bleeding'. + concerning_keywords = ( + "chest pain", + "shortness of breath", + "fainting", + "vision loss", + "severe bleeding", + "suicidal", + ) + has_concerning = False + for answer in answers: + text = str(answer.get("answer", "")).lower() + if any(keyword in text for keyword in concerning_keywords): + has_concerning = True + break + + risk_level = "low-to-moderate" + if has_concerning: + risk_level = "potentially serious – urgent evaluation recommended" + + return ( + "Based on the logged answers, this appears to be a " + f"{risk_level} situation. This is only a rough heuristic, not a " + "diagnosis. A licensed healthcare professional must make any " + "real assessment." + ) + + +root_agent = Agent( + model="gemini-2.0-flash", + name="ai_doctor_agent", + description=( + "A simple AI doctor-style assistant for educational purposes. " + "It can explain basic medical concepts and always reminds users " + "to consult a licensed healthcare professional." + ), + instruction=""" +You are AI Doctor, a friendly educational assistant that answers +high-level health and wellness questions. + +Important safety rules: +- You are NOT a medical professional and cannot diagnose, treat, + or prescribe. +- You MUST clearly remind the user to talk to a licensed healthcare + professional for any diagnosis, treatment, or emergency. +- If the user describes any urgent or severe symptoms (for example + chest pain, trouble breathing, signs of stroke, suicidal thoughts), + you must tell them to seek emergency medical care immediately. +- Keep your explanations simple, balanced, and non-alarming. + +You have access to two tools to help you reason over the conversation: +- log_health_answer(question: str, answer: str): Call this after each + important question you ask the user so that their answer is stored + in the session state as structured data. +- summarize_risk_profile(): Call this near the end of the assessment + to get a brief, non-diagnostic summary string based on everything + that has been logged so far. You should quote or paraphrase that + string in your final answer, along with your own explanation. + +For every new symptom message from the user: +- You MUST ask at least six focused follow-up questions (one at a + time) before giving any advice or summary. In most conversations, + the questions should cover: + 1) age, + 2) smoking or tobacco use, + 3) alcohol use, + 4) major medical conditions and current medications, + 5) allergies to medications or other substances, + 6) basic lifestyle factors (diet, exercise, sleep). +- After the user answers a question, you MUST call log_health_answer + with the question you asked and the user's answer. +- Only after you have asked and logged at least six follow-up + questions should you call summarize_risk_profile and then provide + your final summary and suggestions. + +Even when these tools suggest that the situation looks low risk, you +must still make it clear that only a licensed healthcare professional +can diagnose or treat medical conditions. + +Example 1: Mild symptom, low risk +User: "I am having a mild headache today." +Assistant: +- Acknowledge the symptom with empathy. +- Ask a few brief follow-up questions (for example about sleep, hydration, + screen time, or stress) and log the answers using log_health_answer. +- Offer simple, common self-care ideas such as rest, hydration, or a cool + compress, without naming specific prescription medications. +- Clearly state that you are an AI system, not a medical professional, and + that if the headache is severe, persistent, or accompanied by red-flag + symptoms like fever, neck stiffness, vision changes, or confusion, the + user should seek care from a licensed healthcare professional. + +Example 2: Concerning symptom, high risk +User: "I'm 55, I smoke, and I get chest pain when I walk up stairs." +Assistant: +- Log important details (age, smoking status, chest pain triggers) with + log_health_answer. +- Call summarize_risk_profile before giving your final answer and use its + output as part of your explanation. +- Explain that chest pain with exertion can sometimes be a sign of a + serious heart problem, without offering a diagnosis. +- Strongly recommend urgent in-person evaluation by a licensed clinician + or emergency services, depending on how severe or new the symptoms are. +- Emphasize again that you are an AI assistant, not a doctor. + +Example 3: Asking about supplements +User: "What supplements should I take to boost my immunity?" +Assistant: +- Ask a couple of follow-up questions about general health, medications, + allergies, and any chronic conditions, and log the answers. +- Provide high-level information about commonly discussed supplements + (such as vitamin D or vitamin C) but avoid specific doses or brands. +- Remind the user to review any supplement plans with their doctor or + pharmacist, especially if they take prescription medications or have + chronic health conditions. +- Clearly state that your suggestions are general wellness information + and not personalized medical advice. +""", + tools=[ + log_health_answer, + summarize_risk_profile, + ], +) diff --git a/contributing/samples/hello_doctor/main.py b/contributing/samples/hello_doctor/main.py new file mode 100644 index 0000000000..1410df6f0f --- /dev/null +++ b/contributing/samples/hello_doctor/main.py @@ -0,0 +1,90 @@ +# 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. + +import asyncio +import time + +import agent +from dotenv import load_dotenv +from google.adk import Runner +from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService +from google.adk.cli.utils import logs +from google.adk.sessions.in_memory_session_service import InMemorySessionService +from google.adk.sessions.session import Session +from google.genai import types + +load_dotenv(override=True) +logs.log_to_tmp_folder() + + +async def main(): + app_name = "hello_doctor" + user_id = "user1" + + session_service = InMemorySessionService() + artifact_service = InMemoryArtifactService() + + runner = Runner( + app_name=app_name, + agent=agent.root_agent, + artifact_service=artifact_service, + session_service=session_service, + ) + + session = await session_service.create_session( + app_name=app_name, user_id=user_id + ) + + async def run_prompt(session: Session, new_message: str): + content = types.Content( + role="user", parts=[types.Part.from_text(text=new_message)] + ) + print("** User says:", content.model_dump(exclude_none=True)) + async for event in runner.run_async( + user_id=user_id, + session_id=session.id, + new_message=content, + ): + if event.content.parts and event.content.parts[0].text: + print(f"** {event.author}: {event.content.parts[0].text}") + + start_time = time.time() + print("Start time:", start_time) + print("------------------------------------") + + await run_prompt( + session, + ( + "I'd like you to perform a high-level health assessment. Ask me " + "structured questions about my age, lifestyle, symptoms, and " + "medical history one by one. At the end, provide: " + "1) a concise longitudinal summary of my situation, " + "2) general wellness suggestions including over-the-counter " + "supplements that are commonly considered safe for most adults, " + "3) clear guidance on which licensed medical professionals I " + "should talk to and which medical tests I could ask them about. " + "You must clearly state that you are not a doctor and that your " + "advice is not a diagnosis or a substitute for professional care." + ), + ) + + end_time = time.time() + print("------------------------------------") + print("End time:", end_time) + print("Total time:", end_time - start_time) + + +if __name__ == "__main__": + asyncio.run(main()) + From b95d91e61fb6a81f3a861aa5b35b83bacbcffc7e Mon Sep 17 00:00:00 2001 From: sarojrout Date: Sun, 16 Nov 2025 21:16:30 -0800 Subject: [PATCH 2/5] fix(samples): use publicly available gemini-2.5-flash model --- contributing/samples/hello_doctor/agent.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contributing/samples/hello_doctor/agent.py b/contributing/samples/hello_doctor/agent.py index 9f0783d215..7b7b184c94 100644 --- a/contributing/samples/hello_doctor/agent.py +++ b/contributing/samples/hello_doctor/agent.py @@ -75,7 +75,7 @@ def summarize_risk_profile(tool_context: ToolContext) -> str: root_agent = Agent( - model="gemini-2.0-flash", + model="gemini-2.5-flash", name="ai_doctor_agent", description=( "A simple AI doctor-style assistant for educational purposes. " From e1c5fa0b10b134ceb18148d0d4182c80e3603178 Mon Sep 17 00:00:00 2001 From: sarojrout Date: Thu, 20 Nov 2025 16:19:56 -0800 Subject: [PATCH 3/5] fix(evaluation): Handle empty and invalid JSON in eval config files - Add error handling for empty config files in get_evaluation_criteria_or_default - Catch ValidationError and ValueError when parsing JSON config - Return default config with warning log when file is empty or invalid - Add test case for empty file handling - Fix pyink formatting issues in the sample files --- contributing/samples/hello_doctor/__init__.py | 1 - contributing/samples/hello_doctor/agent.py | 4 +++- contributing/samples/hello_doctor/main.py | 1 - src/google/adk/evaluation/eval_config.py | 16 +++++++++++++++- tests/unittests/evaluation/test_eval_config.py | 10 ++++++++++ 5 files changed, 28 insertions(+), 4 deletions(-) diff --git a/contributing/samples/hello_doctor/__init__.py b/contributing/samples/hello_doctor/__init__.py index 8ce90a27ba..c48963cdc7 100644 --- a/contributing/samples/hello_doctor/__init__.py +++ b/contributing/samples/hello_doctor/__init__.py @@ -13,4 +13,3 @@ # limitations under the License. from . import agent - diff --git a/contributing/samples/hello_doctor/agent.py b/contributing/samples/hello_doctor/agent.py index 7b7b184c94..99484e6ff1 100644 --- a/contributing/samples/hello_doctor/agent.py +++ b/contributing/samples/hello_doctor/agent.py @@ -16,7 +16,9 @@ from google.adk.tools.tool_context import ToolContext -def log_health_answer(question: str, answer: str, tool_context: ToolContext) -> str: +def log_health_answer( + question: str, answer: str, tool_context: ToolContext +) -> str: """Log a structured health answer into session state. The model can call this tool after it asks a question such as diff --git a/contributing/samples/hello_doctor/main.py b/contributing/samples/hello_doctor/main.py index 1410df6f0f..fd5fcb964d 100644 --- a/contributing/samples/hello_doctor/main.py +++ b/contributing/samples/hello_doctor/main.py @@ -87,4 +87,3 @@ async def run_prompt(session: Session, new_message: str): if __name__ == "__main__": asyncio.run(main()) - diff --git a/src/google/adk/evaluation/eval_config.py b/src/google/adk/evaluation/eval_config.py index d5b94af5e1..414ba08426 100644 --- a/src/google/adk/evaluation/eval_config.py +++ b/src/google/adk/evaluation/eval_config.py @@ -19,6 +19,7 @@ from typing import Optional from typing import Union +from pydantic import ValidationError from pydantic import alias_generators from pydantic import BaseModel from pydantic import ConfigDict @@ -93,7 +94,20 @@ def get_evaluation_criteria_or_default( if eval_config_file_path and os.path.exists(eval_config_file_path): with open(eval_config_file_path, "r", encoding="utf-8") as f: content = f.read() - return EvalConfig.model_validate_json(content) + if not content or not content.strip(): + logger.warning( + f"Config file {eval_config_file_path} exists but is empty. " + "Using default criteria." + ) + return _DEFAULT_EVAL_CONFIG + try: + return EvalConfig.model_validate_json(content) + except (ValidationError, ValueError) as e: + logger.warning( + f"Failed to parse config file {eval_config_file_path}: {e}. " + "Using default criteria." + ) + return _DEFAULT_EVAL_CONFIG logger.info( "No config file supplied or file not found. Using default criteria." diff --git a/tests/unittests/evaluation/test_eval_config.py b/tests/unittests/evaluation/test_eval_config.py index a1f9c8af0a..50f0e56b7a 100644 --- a/tests/unittests/evaluation/test_eval_config.py +++ b/tests/unittests/evaluation/test_eval_config.py @@ -46,6 +46,16 @@ def test_get_evaluation_criteria_or_default_returns_default_if_file_not_found( ) +def test_get_evaluation_criteria_or_default_returns_default_if_file_is_empty( + mocker, +): + mocker.patch("os.path.exists", return_value=True) + mocker.patch("builtins.open", mocker.mock_open(read_data="")) + assert ( + get_evaluation_criteria_or_default("dummy_path") == _DEFAULT_EVAL_CONFIG + ) + + def test_get_eval_metrics_from_config(): rubric_1 = Rubric( rubric_id="test-rubric", From 29212b6158b847750cc1ae0683eca5397559e085 Mon Sep 17 00:00:00 2001 From: sarojrout Date: Thu, 20 Nov 2025 16:40:07 -0800 Subject: [PATCH 4/5] fix(evaluation): Fix isort import order for pydantic imports --- src/google/adk/evaluation/eval_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/google/adk/evaluation/eval_config.py b/src/google/adk/evaluation/eval_config.py index 414ba08426..b659e4ae31 100644 --- a/src/google/adk/evaluation/eval_config.py +++ b/src/google/adk/evaluation/eval_config.py @@ -19,11 +19,11 @@ from typing import Optional from typing import Union -from pydantic import ValidationError from pydantic import alias_generators from pydantic import BaseModel from pydantic import ConfigDict from pydantic import Field +from pydantic import ValidationError from ..evaluation.eval_metrics import EvalMetric from .eval_metrics import BaseCriterion From 4a9f03509a429e76dfd3630b7edcb39d67ac6a57 Mon Sep 17 00:00:00 2001 From: sarojrout Date: Thu, 20 Nov 2025 16:44:18 -0800 Subject: [PATCH 5/5] style(samples): Fix import order in gepa sample files --- contributing/samples/gepa/experiment.py | 1 - contributing/samples/gepa/run_experiment.py | 1 - 2 files changed, 2 deletions(-) diff --git a/contributing/samples/gepa/experiment.py b/contributing/samples/gepa/experiment.py index 2f5d03a772..f68b349d9c 100644 --- a/contributing/samples/gepa/experiment.py +++ b/contributing/samples/gepa/experiment.py @@ -43,7 +43,6 @@ from tau_bench.types import EnvRunResult from tau_bench.types import RunConfig import tau_bench_agent as tau_bench_agent_lib - import utils diff --git a/contributing/samples/gepa/run_experiment.py b/contributing/samples/gepa/run_experiment.py index cfd850b3a3..1bc4ee58c8 100644 --- a/contributing/samples/gepa/run_experiment.py +++ b/contributing/samples/gepa/run_experiment.py @@ -25,7 +25,6 @@ from absl import flags import experiment from google.genai import types - import utils _OUTPUT_DIR = flags.DEFINE_string(