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( 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..c48963cdc7 --- /dev/null +++ b/contributing/samples/hello_doctor/__init__.py @@ -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 diff --git a/contributing/samples/hello_doctor/agent.py b/contributing/samples/hello_doctor/agent.py new file mode 100644 index 0000000000..99484e6ff1 --- /dev/null +++ b/contributing/samples/hello_doctor/agent.py @@ -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, + ], +) diff --git a/contributing/samples/hello_doctor/main.py b/contributing/samples/hello_doctor/main.py new file mode 100644 index 0000000000..fd5fcb964d --- /dev/null +++ b/contributing/samples/hello_doctor/main.py @@ -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()) diff --git a/src/google/adk/evaluation/eval_config.py b/src/google/adk/evaluation/eval_config.py index 3cc5672ca9..331164923f 100644 --- a/src/google/adk/evaluation/eval_config.py +++ b/src/google/adk/evaluation/eval_config.py @@ -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." diff --git a/tests/unittests/evaluation/test_eval_config.py b/tests/unittests/evaluation/test_eval_config.py index 54f22b5066..f0c2658dec 100644 --- a/tests/unittests/evaluation/test_eval_config.py +++ b/tests/unittests/evaluation/test_eval_config.py @@ -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",