Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion contributing/samples/gepa/experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
1 change: 0 additions & 1 deletion contributing/samples/gepa/run_experiment.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
from absl import flags
import experiment
from google.genai import types

import utils

_OUTPUT_DIR = flags.DEFINE_string(
Expand Down
52 changes: 52 additions & 0 deletions contributing/samples/hello_doctor/README.md
Original file line number Diff line number Diff line change
@@ -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.

15 changes: 15 additions & 0 deletions contributing/samples/hello_doctor/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# 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
173 changes: 173 additions & 0 deletions contributing/samples/hello_doctor/agent.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
# 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.5-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,
],
)
89 changes: 89 additions & 0 deletions contributing/samples/hello_doctor/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# 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())
15 changes: 14 additions & 1 deletion src/google/adk/evaluation/eval_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,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."
Expand Down
10 changes: 10 additions & 0 deletions tests/unittests/evaluation/test_eval_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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",
Expand Down