From abc7e8fac1f5afda0e758143c00c0bdc48045988 Mon Sep 17 00:00:00 2001 From: Usha Rout Date: Sun, 16 Nov 2025 01:01:00 -0800 Subject: [PATCH 1/7] feat(samples): add hello_doctor health assessment agent --- contributing/samples/hello_doctor/README.md | 65 ++++++++ contributing/samples/hello_doctor/__init__.py | 3 + contributing/samples/hello_doctor/agent.py | 155 ++++++++++++++++++ contributing/samples/hello_doctor/main.py | 77 +++++++++ 4 files changed, 300 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..e8fd56dad5 --- /dev/null +++ b/contributing/samples/hello_doctor/README.md @@ -0,0 +1,65 @@ +# hello_doctor sample + +This sample demonstrates a simple **health-oriented educational agent** +built with ADK. It is designed to provide high-level information about +symptoms and wellness while repeatedly reminding users that it is **not +a medical professional** and cannot diagnose, treat, or prescribe. + +> This sample is for educational and testing purposes only and must not +> be used as a substitute for professional medical advice, diagnosis, or +> treatment. + +## Files + +- `agent.py`: Defines `hello_doctor_agent` and two tools: + - `log_health_answer(question, answer, tool_context)`: Logs structured + question/answer pairs into the session state so the agent can build a + longitudinal view of the conversation. + - `summarize_risk_profile(tool_context)`: Produces a brief, **non- + diagnostic** textual summary based on the logged answers, which the + agent can incorporate into its final response. +- `main.py`: Minimal CLI demo that runs a one-shot health assessment + prompt using the agent and prints the response to the console. + +## Running the sample + +From the repository root: + +```bash +source .venv/bin/activate +python contributing/samples/hello_doctor/main.py +``` + +This sends a single, long health-assessment request to the agent and +prints the model's reply. To experiment interactively, use the ADK web +UI instead. + +## Using with ADK Web + +Start the web server from the project root: + +```bash +source .venv/bin/activate +adk web contributing/samples +``` + +Then open `http://127.0.0.1:8000` in a browser and choose the +`hello_doctor` app from the dropdown. You can then chat with the agent +and, if desired, instruct it to log answers and summarize its risk +profile using the tools described above. + +## Configuration and API keys + +This sample relies on a Gemini model (`gemini-2.0-flash`) and expects +credentials to be provided via environment variables, typically loaded +from a local `.env` file (which should **not** be committed to source +control). A common configuration is: + +```env +GOOGLE_GENAI_API_KEY=your_api_key_here +``` + +Contributors and users should supply their own API keys or Vertex AI +configuration when running the sample locally. + + diff --git a/contributing/samples/hello_doctor/__init__.py b/contributing/samples/hello_doctor/__init__.py new file mode 100644 index 0000000000..88ac930105 --- /dev/null +++ b/contributing/samples/hello_doctor/__init__.py @@ -0,0 +1,3 @@ +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..b924761110 --- /dev/null +++ b/contributing/samples/hello_doctor/agent.py @@ -0,0 +1,155 @@ +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", + ) + lower_answers = " ".join(a["answer"].lower() for a in answers) + has_concerning = any(k in lower_answers for k in concerning_keywords) + + 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..59556f5d4a --- /dev/null +++ b/contributing/samples/hello_doctor/main.py @@ -0,0 +1,77 @@ +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_app" + 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 053081849168c2fbff7e4cfb614794a997930ae2 Mon Sep 17 00:00:00 2001 From: sarojrout Date: Sun, 16 Nov 2025 13:52:55 -0800 Subject: [PATCH 2/7] feat(samples): refine hello_doctor sample and address review comments (#356 --- contributing/samples/hello_doctor/agent.py | 8 ++++++-- contributing/samples/hello_doctor/main.py | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/contributing/samples/hello_doctor/agent.py b/contributing/samples/hello_doctor/agent.py index b924761110..327bfb5a2c 100644 --- a/contributing/samples/hello_doctor/agent.py +++ b/contributing/samples/hello_doctor/agent.py @@ -41,8 +41,12 @@ def summarize_risk_profile(tool_context: ToolContext) -> str: "severe bleeding", "suicidal", ) - lower_answers = " ".join(a["answer"].lower() for a in answers) - has_concerning = any(k in lower_answers for k in concerning_keywords) + 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: diff --git a/contributing/samples/hello_doctor/main.py b/contributing/samples/hello_doctor/main.py index 59556f5d4a..136243227f 100644 --- a/contributing/samples/hello_doctor/main.py +++ b/contributing/samples/hello_doctor/main.py @@ -15,7 +15,7 @@ async def main(): - app_name = "hello_doctor_app" + app_name = "hello_doctor" user_id = "user1" session_service = InMemorySessionService() From 47c73f40162bd46ecb480e5d452700e3d6c8e351 Mon Sep 17 00:00:00 2001 From: sarojrout Date: Sun, 23 Nov 2025 00:16:09 -0800 Subject: [PATCH 3/7] fix(models): Add PDF support for Claude models - Add _is_pdf_part() helper function to detect PDF parts - Add PDF handling in part_to_message_block() function - PDFs are encoded as base64 and sent as document blocks to Anthropic API - Update return type annotation to include dict for PDF document blocks - Add test for PDF support Fixes #3614 --- src/google/adk/models/anthropic_llm.py | 23 +++++++++++++++ tests/unittests/models/test_anthropic_llm.py | 31 ++++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/google/adk/models/anthropic_llm.py b/src/google/adk/models/anthropic_llm.py index 6f343367a3..93a25fddc8 100644 --- a/src/google/adk/models/anthropic_llm.py +++ b/src/google/adk/models/anthropic_llm.py @@ -76,6 +76,15 @@ def _is_image_part(part: types.Part) -> bool: ) +def _is_pdf_part(part: types.Part) -> bool: + """Check if the part contains PDF data.""" + return ( + part.inline_data + and part.inline_data.mime_type + and part.inline_data.mime_type == "application/pdf" + ) + + def part_to_message_block( part: types.Part, ) -> Union[ @@ -83,6 +92,7 @@ def part_to_message_block( anthropic_types.ImageBlockParam, anthropic_types.ToolUseBlockParam, anthropic_types.ToolResultBlockParam, + dict[str, Any], # For PDF document blocks ]: if part.text: return anthropic_types.TextBlockParam(text=part.text, type="text") @@ -134,6 +144,19 @@ def part_to_message_block( type="base64", media_type=part.inline_data.mime_type, data=data ), ) + elif _is_pdf_part(part): + # Handle PDF documents - Anthropic supports PDFs as document blocks + # PDFs are encoded as base64 and sent with document type + data = base64.b64encode(part.inline_data.data).decode() + # Anthropic API supports PDFs using document block structure + # Construct document block similar to image block but with document type + # Note: Anthropic accepts PDFs with type "document" in the block structure + return dict( + type="document", + source=dict( + type="base64", media_type=part.inline_data.mime_type, data=data + ), + ) elif part.executable_code: return anthropic_types.TextBlockParam( type="text", diff --git a/tests/unittests/models/test_anthropic_llm.py b/tests/unittests/models/test_anthropic_llm.py index e5ac8cc051..dae7bc5d5b 100644 --- a/tests/unittests/models/test_anthropic_llm.py +++ b/tests/unittests/models/test_anthropic_llm.py @@ -462,3 +462,34 @@ def test_part_to_message_block_with_multiple_content_items(): assert isinstance(result, dict) # Multiple text items should be joined with newlines assert result["content"] == "First part\nSecond part" + + +def test_part_to_message_block_with_pdf(): + """Test that part_to_message_block handles PDF documents.""" + import base64 + + from google.adk.models.anthropic_llm import part_to_message_block + + # Create a PDF part with inline data + pdf_data = ( + b"%PDF-1.4\n1 0 obj\n<<\n/Type /Catalog\n>>\nendobj\nxref\n0" + b" 1\ntrailer\n<<\n/Root 1 0 R\n>>\n%%EOF" + ) + pdf_part = types.Part( + inline_data=types.Blob( + mime_type="application/pdf", + data=pdf_data, + ) + ) + + result = part_to_message_block(pdf_part) + + # PDF should be returned as a document block dictionary + assert isinstance(result, dict) + assert result["type"] == "document" + assert "source" in result + assert result["source"]["type"] == "base64" + assert result["source"]["media_type"] == "application/pdf" + # Verify the data is base64 encoded + decoded_data = base64.b64decode(result["source"]["data"]) + assert decoded_data == pdf_data From 6f3ec1ded0ded34fe7ce97ca792e66f94b7925d2 Mon Sep 17 00:00:00 2001 From: sarojrout Date: Sun, 23 Nov 2025 00:31:40 -0800 Subject: [PATCH 4/7] refactor(models): Improve type safety for PDF document blocks - Add DocumentBlockParam TypedDict for better type safety - Check for anthropic_types.DocumentBlockParam and use it if available - Fallback to local TypedDict when DocumentBlockParam not in anthropic library - Replace dict[str, Any] with DocumentBlockParam in return type annotation - Maintains backward compatibility while improving type safety Addresses reviewer feedback on PR #3614 --- src/google/adk/models/anthropic_llm.py | 49 ++++++++++++++++++++------ 1 file changed, 39 insertions(+), 10 deletions(-) diff --git a/src/google/adk/models/anthropic_llm.py b/src/google/adk/models/anthropic_llm.py index 93a25fddc8..41604e4e28 100644 --- a/src/google/adk/models/anthropic_llm.py +++ b/src/google/adk/models/anthropic_llm.py @@ -26,6 +26,7 @@ from typing import Literal from typing import Optional from typing import TYPE_CHECKING +from typing import TypedDict from typing import Union from anthropic import AnthropicVertex @@ -85,6 +86,21 @@ def _is_pdf_part(part: types.Part) -> bool: ) +class DocumentBlockParam(TypedDict): + """Type definition for Anthropic document block parameters. + + Represents a PDF document block in Anthropic's message format. + This TypedDict provides type safety for document blocks when + DocumentBlockParam is not available in the anthropic_types module. + + The structure matches Anthropic's API specification for document blocks, + similar to ImageBlockParam but with type="document". + """ + + type: Literal["document"] + source: dict[str, str] # Contains: type, media_type, data + + def part_to_message_block( part: types.Part, ) -> Union[ @@ -92,7 +108,7 @@ def part_to_message_block( anthropic_types.ImageBlockParam, anthropic_types.ToolUseBlockParam, anthropic_types.ToolResultBlockParam, - dict[str, Any], # For PDF document blocks + DocumentBlockParam, # For PDF document blocks ]: if part.text: return anthropic_types.TextBlockParam(text=part.text, type="text") @@ -148,15 +164,28 @@ def part_to_message_block( # Handle PDF documents - Anthropic supports PDFs as document blocks # PDFs are encoded as base64 and sent with document type data = base64.b64encode(part.inline_data.data).decode() - # Anthropic API supports PDFs using document block structure - # Construct document block similar to image block but with document type - # Note: Anthropic accepts PDFs with type "document" in the block structure - return dict( - type="document", - source=dict( - type="base64", media_type=part.inline_data.mime_type, data=data - ), - ) + # Check if DocumentBlockParam is available in anthropic_types + # If available, use it for better type safety; otherwise use our TypedDict + if hasattr(anthropic_types, "DocumentBlockParam"): + return anthropic_types.DocumentBlockParam( + type="document", + source={ + "type": "base64", + "media_type": part.inline_data.mime_type, + "data": data, + }, + ) + else: + # Fallback to TypedDict for type safety when DocumentBlockParam + # is not available in the anthropic library version + return DocumentBlockParam( + type="document", + source={ + "type": "base64", + "media_type": part.inline_data.mime_type, + "data": data, + }, + ) elif part.executable_code: return anthropic_types.TextBlockParam( type="text", From 058814ef5929aae0bf8f3c8a72a04ca8519f3a20 Mon Sep 17 00:00:00 2001 From: sarojrout Date: Sun, 23 Nov 2025 18:19:13 -0800 Subject: [PATCH 5/7] refactor(models): refactored anthropic llm and using anthropic_types.DocumentBlockParam directly - Removed custom DocumentBlockParam TypedDict (not needed) - Use anthropic_types.DocumentBlockParam which exists in the library - Simplified code by removing hasattr check and fallback logic - Updated testcase to explicitly import anthropic_types --- src/google/adk/models/anthropic_llm.py | 48 ++++---------------- tests/unittests/models/test_anthropic_llm.py | 6 ++- 2 files changed, 13 insertions(+), 41 deletions(-) diff --git a/src/google/adk/models/anthropic_llm.py b/src/google/adk/models/anthropic_llm.py index 41604e4e28..e3a302a8ca 100644 --- a/src/google/adk/models/anthropic_llm.py +++ b/src/google/adk/models/anthropic_llm.py @@ -26,7 +26,6 @@ from typing import Literal from typing import Optional from typing import TYPE_CHECKING -from typing import TypedDict from typing import Union from anthropic import AnthropicVertex @@ -86,21 +85,6 @@ def _is_pdf_part(part: types.Part) -> bool: ) -class DocumentBlockParam(TypedDict): - """Type definition for Anthropic document block parameters. - - Represents a PDF document block in Anthropic's message format. - This TypedDict provides type safety for document blocks when - DocumentBlockParam is not available in the anthropic_types module. - - The structure matches Anthropic's API specification for document blocks, - similar to ImageBlockParam but with type="document". - """ - - type: Literal["document"] - source: dict[str, str] # Contains: type, media_type, data - - def part_to_message_block( part: types.Part, ) -> Union[ @@ -108,7 +92,7 @@ def part_to_message_block( anthropic_types.ImageBlockParam, anthropic_types.ToolUseBlockParam, anthropic_types.ToolResultBlockParam, - DocumentBlockParam, # For PDF document blocks + anthropic_types.DocumentBlockParam, # For PDF document blocks ]: if part.text: return anthropic_types.TextBlockParam(text=part.text, type="text") @@ -164,28 +148,14 @@ def part_to_message_block( # Handle PDF documents - Anthropic supports PDFs as document blocks # PDFs are encoded as base64 and sent with document type data = base64.b64encode(part.inline_data.data).decode() - # Check if DocumentBlockParam is available in anthropic_types - # If available, use it for better type safety; otherwise use our TypedDict - if hasattr(anthropic_types, "DocumentBlockParam"): - return anthropic_types.DocumentBlockParam( - type="document", - source={ - "type": "base64", - "media_type": part.inline_data.mime_type, - "data": data, - }, - ) - else: - # Fallback to TypedDict for type safety when DocumentBlockParam - # is not available in the anthropic library version - return DocumentBlockParam( - type="document", - source={ - "type": "base64", - "media_type": part.inline_data.mime_type, - "data": data, - }, - ) + return anthropic_types.DocumentBlockParam( + type="document", + source={ + "type": "base64", + "media_type": part.inline_data.mime_type, + "data": data, + }, + ) elif part.executable_code: return anthropic_types.TextBlockParam( type="text", diff --git a/tests/unittests/models/test_anthropic_llm.py b/tests/unittests/models/test_anthropic_llm.py index dae7bc5d5b..265d41fb0f 100644 --- a/tests/unittests/models/test_anthropic_llm.py +++ b/tests/unittests/models/test_anthropic_llm.py @@ -468,6 +468,7 @@ def test_part_to_message_block_with_pdf(): """Test that part_to_message_block handles PDF documents.""" import base64 + from anthropic import types as anthropic_types from google.adk.models.anthropic_llm import part_to_message_block # Create a PDF part with inline data @@ -484,12 +485,13 @@ def test_part_to_message_block_with_pdf(): result = part_to_message_block(pdf_part) - # PDF should be returned as a document block dictionary + # PDF should be returned as DocumentBlockParam (TypedDict, which is a dict) assert isinstance(result, dict) + # Verify it matches DocumentBlockParam structure assert result["type"] == "document" assert "source" in result assert result["source"]["type"] == "base64" assert result["source"]["media_type"] == "application/pdf" - # Verify the data is base64 encoded + # Verify the data is base64 encoded and can be decoded back decoded_data = base64.b64decode(result["source"]["data"]) assert decoded_data == pdf_data From 6db684681593cae61d4a858dfc62f23b5abccfb7 Mon Sep 17 00:00:00 2001 From: sarojrout Date: Sat, 29 Nov 2025 21:58:30 -0800 Subject: [PATCH 6/7] resolved conflicts #3671 --- tests/unittests/models/test_anthropic_llm.py | 79 ++++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/tests/unittests/models/test_anthropic_llm.py b/tests/unittests/models/test_anthropic_llm.py index 265d41fb0f..8c93b71bb7 100644 --- a/tests/unittests/models/test_anthropic_llm.py +++ b/tests/unittests/models/test_anthropic_llm.py @@ -20,6 +20,7 @@ from google.adk import version as adk_version from google.adk.models import anthropic_llm from google.adk.models.anthropic_llm import Claude +from google.adk.models.anthropic_llm import content_to_message_param from google.adk.models.anthropic_llm import function_declaration_to_tool_param from google.adk.models.llm_request import LlmRequest from google.adk.models.llm_response import LlmResponse @@ -495,3 +496,81 @@ def test_part_to_message_block_with_pdf(): # Verify the data is base64 encoded and can be decoded back decoded_data = base64.b64decode(result["source"]["data"]) assert decoded_data == pdf_data + + +content_to_message_param_test_cases = [ + ( + "user_role_with_text_and_image", + Content( + role="user", + parts=[ + Part.from_text(text="What's in this image?"), + Part( + inline_data=types.Blob( + mime_type="image/jpeg", data=b"fake_image_data" + ) + ), + ], + ), + "user", + 2, # Expected content length + False, # Should not log warning + ), + ( + "model_role_with_text_and_image", + Content( + role="model", + parts=[ + Part.from_text(text="I see a cat."), + Part( + inline_data=types.Blob( + mime_type="image/png", data=b"fake_image_data" + ) + ), + ], + ), + "assistant", + 1, # Image filtered out, only text remains + True, # Should log warning + ), + ( + "assistant_role_with_text_and_image", + Content( + role="assistant", + parts=[ + Part.from_text(text="Here's what I found."), + Part( + inline_data=types.Blob( + mime_type="image/webp", data=b"fake_image_data" + ) + ), + ], + ), + "assistant", + 1, # Image filtered out, only text remains + True, # Should log warning + ), +] + + +@pytest.mark.parametrize( + "_, content, expected_role, expected_content_length, should_log_warning", + content_to_message_param_test_cases, + ids=[case[0] for case in content_to_message_param_test_cases], +) +def test_content_to_message_param_with_images( + _, content, expected_role, expected_content_length, should_log_warning +): + """Test content_to_message_param handles images correctly based on role.""" + with mock.patch("google.adk.models.anthropic_llm.logger") as mock_logger: + result = content_to_message_param(content) + + assert result["role"] == expected_role + assert len(result["content"]) == expected_content_length + + if should_log_warning: + mock_logger.warning.assert_called_once_with( + "Image data is not supported in Claude for assistant turns." + ) + else: + mock_logger.warning.assert_not_called() From c00e26422d8255632a042fea2d2b3a971eaecdca Mon Sep 17 00:00:00 2001 From: sarojrout Date: Sat, 29 Nov 2025 22:11:01 -0800 Subject: [PATCH 7/7] chore: remove hello_doctor sample from PR (not part of PDF support feature) --- contributing/samples/hello_doctor/README.md | 65 ------- contributing/samples/hello_doctor/__init__.py | 3 - contributing/samples/hello_doctor/agent.py | 159 ------------------ contributing/samples/hello_doctor/main.py | 77 --------- 4 files changed, 304 deletions(-) delete mode 100644 contributing/samples/hello_doctor/README.md delete mode 100644 contributing/samples/hello_doctor/__init__.py delete mode 100644 contributing/samples/hello_doctor/agent.py delete mode 100644 contributing/samples/hello_doctor/main.py diff --git a/contributing/samples/hello_doctor/README.md b/contributing/samples/hello_doctor/README.md deleted file mode 100644 index e8fd56dad5..0000000000 --- a/contributing/samples/hello_doctor/README.md +++ /dev/null @@ -1,65 +0,0 @@ -# hello_doctor sample - -This sample demonstrates a simple **health-oriented educational agent** -built with ADK. It is designed to provide high-level information about -symptoms and wellness while repeatedly reminding users that it is **not -a medical professional** and cannot diagnose, treat, or prescribe. - -> This sample is for educational and testing purposes only and must not -> be used as a substitute for professional medical advice, diagnosis, or -> treatment. - -## Files - -- `agent.py`: Defines `hello_doctor_agent` and two tools: - - `log_health_answer(question, answer, tool_context)`: Logs structured - question/answer pairs into the session state so the agent can build a - longitudinal view of the conversation. - - `summarize_risk_profile(tool_context)`: Produces a brief, **non- - diagnostic** textual summary based on the logged answers, which the - agent can incorporate into its final response. -- `main.py`: Minimal CLI demo that runs a one-shot health assessment - prompt using the agent and prints the response to the console. - -## Running the sample - -From the repository root: - -```bash -source .venv/bin/activate -python contributing/samples/hello_doctor/main.py -``` - -This sends a single, long health-assessment request to the agent and -prints the model's reply. To experiment interactively, use the ADK web -UI instead. - -## Using with ADK Web - -Start the web server from the project root: - -```bash -source .venv/bin/activate -adk web contributing/samples -``` - -Then open `http://127.0.0.1:8000` in a browser and choose the -`hello_doctor` app from the dropdown. You can then chat with the agent -and, if desired, instruct it to log answers and summarize its risk -profile using the tools described above. - -## Configuration and API keys - -This sample relies on a Gemini model (`gemini-2.0-flash`) and expects -credentials to be provided via environment variables, typically loaded -from a local `.env` file (which should **not** be committed to source -control). A common configuration is: - -```env -GOOGLE_GENAI_API_KEY=your_api_key_here -``` - -Contributors and users should supply their own API keys or Vertex AI -configuration when running the sample locally. - - diff --git a/contributing/samples/hello_doctor/__init__.py b/contributing/samples/hello_doctor/__init__.py deleted file mode 100644 index 88ac930105..0000000000 --- a/contributing/samples/hello_doctor/__init__.py +++ /dev/null @@ -1,3 +0,0 @@ -from . import agent - - diff --git a/contributing/samples/hello_doctor/agent.py b/contributing/samples/hello_doctor/agent.py deleted file mode 100644 index 327bfb5a2c..0000000000 --- a/contributing/samples/hello_doctor/agent.py +++ /dev/null @@ -1,159 +0,0 @@ -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 deleted file mode 100644 index 136243227f..0000000000 --- a/contributing/samples/hello_doctor/main.py +++ /dev/null @@ -1,77 +0,0 @@ -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()) - -