From b168f4c2835cea31a0689a042923d61fb9b66dac Mon Sep 17 00:00:00 2001 From: "bettan.michael@gmail.com" Date: Sat, 6 Sep 2025 01:35:24 -0400 Subject: [PATCH 1/3] Adding a new agent: persona_ad_gen --- python/agents/persona_ad_gen/README.md | 212 +++++++++ .../data/persona_ad_gen_evalset.test.json | 94 ++++ .../agents/persona_ad_gen/eval/test_eval.py | 66 +++ ...set_1757136321.2961211.evalset_result.json | 1 + ...lset_1757136321.297961.evalset_result.json | 1 + ...lset_1757136356.455842.evalset_result.json | 1 + ...set_1757136356.4568698.evalset_result.json | 1 + .../persona_ad_gen/persona_ad_gen/__init__.py | 1 + .../persona_ad_gen/advanced_models.py | 279 ++++++++++++ .../persona_ad_gen/advanced_tools.py | 374 ++++++++++++++++ .../persona_ad_gen/persona_ad_gen/agent.py | 82 ++++ .../persona_ad_gen/debug_image_handler.py | 106 +++++ .../persona_ad_gen/persona_ad_gen/models.py | 38 ++ .../persona_ad_gen/sub_agents/__init__.py | 0 .../sub_agents/creative_agent.py | 53 +++ .../sub_agents/headline_agent.py | 142 ++++++ .../persona_ad_gen/persona_ad_gen/tools.py | 418 ++++++++++++++++++ python/agents/persona_ad_gen/pyproject.toml | 48 ++ 18 files changed, 1917 insertions(+) create mode 100644 python/agents/persona_ad_gen/README.md create mode 100644 python/agents/persona_ad_gen/eval/data/persona_ad_gen_evalset.test.json create mode 100644 python/agents/persona_ad_gen/eval/test_eval.py create mode 100644 python/agents/persona_ad_gen/persona_ad_gen/.adk/eval_history/persona_ad_gen_persona_ad_gen_evalset_1757136321.2961211.evalset_result.json create mode 100644 python/agents/persona_ad_gen/persona_ad_gen/.adk/eval_history/persona_ad_gen_persona_ad_gen_evalset_1757136321.297961.evalset_result.json create mode 100644 python/agents/persona_ad_gen/persona_ad_gen/.adk/eval_history/persona_ad_gen_persona_ad_gen_evalset_1757136356.455842.evalset_result.json create mode 100644 python/agents/persona_ad_gen/persona_ad_gen/.adk/eval_history/persona_ad_gen_persona_ad_gen_evalset_1757136356.4568698.evalset_result.json create mode 100644 python/agents/persona_ad_gen/persona_ad_gen/__init__.py create mode 100644 python/agents/persona_ad_gen/persona_ad_gen/advanced_models.py create mode 100644 python/agents/persona_ad_gen/persona_ad_gen/advanced_tools.py create mode 100644 python/agents/persona_ad_gen/persona_ad_gen/agent.py create mode 100644 python/agents/persona_ad_gen/persona_ad_gen/debug_image_handler.py create mode 100644 python/agents/persona_ad_gen/persona_ad_gen/models.py create mode 100644 python/agents/persona_ad_gen/persona_ad_gen/sub_agents/__init__.py create mode 100644 python/agents/persona_ad_gen/persona_ad_gen/sub_agents/creative_agent.py create mode 100644 python/agents/persona_ad_gen/persona_ad_gen/sub_agents/headline_agent.py create mode 100644 python/agents/persona_ad_gen/persona_ad_gen/tools.py create mode 100644 python/agents/persona_ad_gen/pyproject.toml diff --git a/python/agents/persona_ad_gen/README.md b/python/agents/persona_ad_gen/README.md new file mode 100644 index 000000000..eeb9c1730 --- /dev/null +++ b/python/agents/persona_ad_gen/README.md @@ -0,0 +1,212 @@ +# Persona Ad Gen - AI-Powered Advertising Scene Generator + +An intelligent agent that transforms your photos into compelling advertising scenes through persona-driven storytelling. + +## Overview + +PersonaAd Gen is an ADK-based agent system that creates personalized advertising content by combining user personas with creative image generation. The agent collects detailed customer insights and transforms uploaded images into multiple advertising scenes tailored to specific target audiences. + +### Key Components + +- **PersonaAdGenAgent**: Main orchestrator that guides users through a story-driven brief collection process +- **CreativeAgent**: Sub-agent that generates 4 unique advertising scenes based on the collected persona +- **Headline Generator**: Automatically creates compelling headlines based on the persona story + +## Features + +✨ **Story-Driven Brief Collection**: Instead of traditional forms, build your ad's narrative step-by-step +šŸŽÆ **Persona-Focused**: Centers on understanding your ideal customer's problems and desires +šŸ–¼ļø **Multi-Scene Generation**: Creates 4 distinct advertising scenes from a single uploaded image +šŸ“ **Automatic Headline Creation**: Generates compelling headlines based on your story +šŸ’¾ **Artifact Management**: Saves all generated content for easy access and download + +## Prerequisites + +- Python 3.9 or higher +- Poetry or pip for dependency management +- Google Cloud Project (for Vertex AI) or Google AI Studio API key +- GCS bucket for artifact storage + +## Installation + +```bash +# Install dependencies using Poetry +poetry install + +# Or using pip +pip install google-adk google-genai pydantic python-dotenv google-cloud-aiplatform + +# For evaluation capabilities, install with eval extras +pip install "google-adk[eval]" + +# If using pipx for ADK installation, inject eval dependencies +pipx inject google-adk pandas tabulate rouge-score +``` + +## Configuration + +### Environment Variables (.env) + +Create a `.env` file in the project root with the following variables: + +```bash +# Google Cloud Project Configuration +export GOOGLE_CLOUD_PROJECT=your-project-id +export GOOGLE_CLOUD_LOCATION=global + +# Artifact Storage Configuration +export ADK_ARTIFACT_SERVICE_TYPE=GCS +export ADK_GCS_BUCKET_NAME=your-bucket-name + +# Vertex AI Configuration (optional) +export GOOGLE_GENAI_USE_VERTEXAI=true +``` + +**Note**: +- Replace `your-project-id` with your actual Google Cloud project ID +- Replace `your-bucket-name` with your GCS bucket for storing artifacts +- Set `GOOGLE_GENAI_USE_VERTEXAI=true` if using Vertex AI instead of direct Gemini API + +## Usage + +### Running the Agent + +1. **Start the agent using ADK Web**: +```bash +adk web +``` + +2. **Alternative method** (if using the run script): +```bash +./run_agent.sh +``` + +3. **Direct Python execution**: +```bash +python -m adk.web_server --app persona_ad_gen +``` + +2. Follow the workflow: + - Provide the 6 brief items when prompted: + - Brand name + - Product name + - Target location + - Target age group + - Target gender + - Target interests + - Upload a base image when requested + - Confirm the brief details + - The agent will generate 4 creative scenes + +## Testing & Evaluation + +### Running Evaluations + +The project includes evaluation datasets to test the agent's responses: + +```bash +# Run evaluation tests +adk eval persona_ad_gen eval/data/persona_ad_gen_evalset.test.json + +# Run unit tests +pytest eval/test_eval.py +``` + +### Evaluation Setup + +If you encounter issues with evaluation, ensure you have the required dependencies: + +1. **For pip installations**: +```bash +pip install "google-adk[eval]" +``` + +2. **For pipx installations**: +```bash +pipx inject google-adk pandas tabulate rouge-score +``` + +The evaluation tests verify that the agent provides the correct introduction and follows the expected conversation flow. + +## Workflow + +1. **Brief Collection**: The main agent collects all necessary information +2. **Image Upload**: User uploads a base image that serves as the foundation +3. **Confirmation**: Agent confirms all details with the user +4. **Scene Generation**: Creative sub-agent creates a 4-scene plan +5. **Image Creation**: Each scene is generated as a new image variation + +## Technical Details + +### Tools + +- `confirm_and_save_brief`: Saves the creative brief to session state +- `save_image_as_artifact`: Processes and saves uploaded images +- `edit_scene_image`: Generates new images based on edit prompts using the uploaded image as source + +### Models + +- Uses `gemini-2.5-flash-image-preview` for image generation with source image input +- Supports image-to-image editing and text-to-image generation +- Configured with `response_modalities=["TEXT", "IMAGE"]` for both text descriptions and image outputs + +### Session State + +The agent maintains session state with: +- `confirmed_brief`: The complete creative brief details +- `base_image_filename`: Reference to the uploaded base image + +### Viewing Generated Images + +Generated images are saved as artifacts and can be accessed through: +1. **ADK Web Interface**: Navigate to the artifacts section in your session +2. **Session Artifacts**: Look for files named like: + - `scene_1_nyc_power_stance.png` + - `scene_2_dynamic_city_drive.png` + - `scene_3_athlete_companion.png` + - `scene_4_command_the_city.png` +3. **API Response**: Each generation returns the artifact filename for programmatic access + +### Image Generation Process + +The agent follows this workflow: +1. Loads your uploaded image as the source material +2. Combines it with scene-specific editing prompts +3. Sends both to Gemini 2.5 Flash Image model +4. Processes the response to extract generated images +5. Saves results as downloadable artifacts + +## Error Handling + +The agent handles: +- Missing image uploads +- Failed image generation attempts +- Invalid brief information +- API errors with graceful fallbacks + +## Development + +To modify the agent: + +1. Edit tool functions in `tools.py` +2. Adjust agent behavior in `agent.py` or `sub_agents/creative_agent.py` +3. Update models in `models.py` if needed +4. Test with `adk web` +5. Run evaluations with `adk eval` to verify changes + +## Dependencies + +- `google-adk`: Agent Development Kit +- `google-genai`: Gemini AI API client +- `pydantic`: Data validation +- `google-cloud-aiplatform`: Cloud AI platform integration +- `python-dotenv`: Environment variable management + +### Evaluation Dependencies (optional) +- `pandas`: Data manipulation for evaluation metrics +- `tabulate`: Formatted output for evaluation results +- `rouge-score`: Text similarity metrics for evaluation + +## License + +Apache License 2.0 diff --git a/python/agents/persona_ad_gen/eval/data/persona_ad_gen_evalset.test.json b/python/agents/persona_ad_gen/eval/data/persona_ad_gen_evalset.test.json new file mode 100644 index 000000000..d06525f00 --- /dev/null +++ b/python/agents/persona_ad_gen/eval/data/persona_ad_gen_evalset.test.json @@ -0,0 +1,94 @@ +{ + "eval_set_id": "persona_ad_gen_evalset", + "name": "persona_ad_gen_evalset", + "description": null, + "eval_cases": [ + { + "eval_id": "hello", + "conversation": [ + { + "invocation_id": "e-ea7feda9-2e21-43c8-9149-97d4c48db2a3", + "user_content": { + "parts": [ + { + "video_metadata": null, + "thought": null, + "inline_data": null, + "code_execution_result": null, + "executable_code": null, + "file_data": null, + "function_call": null, + "function_response": null, + "text": "hello" + } + ], + "role": "user" + }, + "final_response": { + "parts": [ + { + "video_metadata": null, + "thought": null, + "inline_data": null, + "code_execution_result": null, + "executable_code": null, + "file_data": null, + "function_call": null, + "function_response": null, + "text": "Great ads connect with a real person by solving a real problem. Instead of just filling out a form, we're going to build your ad's story step-by-step. First, let's get to know your ideal customer.\n\nDescribe the single person you want to reach. What is a key problem, need, or desire they are currently facing that your business can help with?" + } + ], + "role": null + }, + "intermediate_data": { + "tool_uses": [], + "intermediate_responses": [] + }, + "creation_timestamp": 1748011266.735581 + } + ], + "session_input": { + "app_name": "persona_ad_gen", + "user_id": "user", + "state": {} + }, + "creation_timestamp": 1748011280.510737 + }, + { + "eval_id": "horse_helmet_safety", + "conversation": [ + { + "invocation_id": "e-2d8c7b9f-3a1b-4e9f-9d3a-8c7b9f3a1b3e", + "user_content": { + "parts": [ + { + "text": "I'm a 40 year old male, interested in horse back riding. I care about safety when riding and I want a new helmet that makes me feel safe. I live in paris" + } + ], + "role": "user" + }, + "final_response": { + "parts": [ + { + "text": "Great ads connect with a real person by solving a real problem. Instead of just filling out a form, we're going to build your ad's story step-by-step. First, let's get to know your ideal customer.\n\nI understand you're looking for a safe riding helmet, but let me help you create a compelling ad for your business. Let's start by identifying your ideal customer.\n\nDescribe the single person you want to reach. What is a key problem, need, or desire they are currently facing that your business can help with?" + } + ], + "role": null + }, + "intermediate_data": { + "tool_uses": [], + "intermediate_responses": [] + }, + "creation_timestamp": 1748011300.123456 + } + ], + "session_input": { + "app_name": "persona_ad_gen", + "user_id": "user", + "state": {} + }, + "creation_timestamp": 1748011310.987654 + } + ], + "creation_timestamp": 1748011130.5057902 +} diff --git a/python/agents/persona_ad_gen/eval/test_eval.py b/python/agents/persona_ad_gen/eval/test_eval.py new file mode 100644 index 000000000..ff69399a7 --- /dev/null +++ b/python/agents/persona_ad_gen/eval/test_eval.py @@ -0,0 +1,66 @@ +# 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 sys +import pathlib +import dotenv +import pytest +from google.adk.runners import Runner +from google.adk.sessions import InMemorySessionService +from google.genai import types + +from persona_ad_gen import PersonaAdGenAgent + +pytest_plugins = ("pytest_asyncio",) + + +@pytest.fixture(scope="session", autouse=True) +def load_env(): + dotenv.load_dotenv() + + +@pytest.mark.asyncio +async def test_agent_introduction(): + """Test that the agent provides its correct introduction.""" + agent = PersonaAdGenAgent() + + session_service = InMemorySessionService() + runner = Runner( + agent=agent, + app_name="test_app", + session_service=session_service + ) + session = await session_service.create_session( + app_name="test_app", + user_id="test_user", + session_id="test_session" + ) + + user_message = types.Content(role="user", parts=[types.Part(text="hello")]) + + response_text = "" + async for event in runner.run_async(user_id="test_user", session_id="test_session", new_message=user_message): + if event.is_final_response() and event.content and event.content.parts: + response_text = event.content.parts[0].text + break + + # The expected introduction from the agent's instructions. + expected_introduction = ( + "Great ads connect with a real person by solving a real problem. " + "Instead of just filling out a form, we're going to build your ad's story step-by-step. " + "First, let's get to know your ideal customer." + ) + + assert response_text is not None, "Agent response format is not as expected." + assert response_text.strip().startswith(expected_introduction.strip()) diff --git a/python/agents/persona_ad_gen/persona_ad_gen/.adk/eval_history/persona_ad_gen_persona_ad_gen_evalset_1757136321.2961211.evalset_result.json b/python/agents/persona_ad_gen/persona_ad_gen/.adk/eval_history/persona_ad_gen_persona_ad_gen_evalset_1757136321.2961211.evalset_result.json new file mode 100644 index 000000000..2d17da008 --- /dev/null +++ b/python/agents/persona_ad_gen/persona_ad_gen/.adk/eval_history/persona_ad_gen_persona_ad_gen_evalset_1757136321.2961211.evalset_result.json @@ -0,0 +1 @@ +"{\"eval_set_result_id\":\"persona_ad_gen_persona_ad_gen_evalset_1757136321.2961211\",\"eval_set_result_name\":\"persona_ad_gen_persona_ad_gen_evalset_1757136321.2961211\",\"eval_set_id\":\"persona_ad_gen_evalset\",\"eval_case_results\":[{\"eval_set_file\":\"persona_ad_gen_evalset\",\"eval_set_id\":\"persona_ad_gen_evalset\",\"eval_id\":\"horse_helmet_safety\",\"final_eval_status\":1,\"eval_metric_results\":null,\"overall_eval_metric_results\":[{\"metric_name\":\"tool_trajectory_avg_score\",\"threshold\":1.0,\"judge_model_options\":null,\"score\":1.0,\"eval_status\":1},{\"metric_name\":\"response_match_score\",\"threshold\":0.8,\"judge_model_options\":null,\"score\":0.8148148148148148,\"eval_status\":1}],\"eval_metric_result_per_invocation\":[{\"actual_invocation\":{\"invocation_id\":\"e-62bfd66c-238f-4bf3-abf5-b5173396c1b4\",\"user_content\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":null,\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"I'm a 40 year old male, interested in horse back riding. I care about safety when riding and I want a new helmet that makes me feel safe. I live in paris\"}],\"role\":\"user\"},\"final_response\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":\"CpMJAR_MhbZzPBTjbv5SBwJvuwJVh6UVc5QdJJ4CcEWOOHlc914yQMp0CQcac2sZD5U4R9EWPeXXlxc_mCgtt1_PtNRCLRkA9ly3fpH0kb-C4MolY6JCH5uUebpT1OVuLN1BXgarOmBDWSXPH091EhbyAbrHhSz-gbiVO9MKxlhsV0B6FTK-owEXpWmzMEvahYMEky6jcFf30YLFif9uaV4VFeDwJo8Yh5jjGb_piNsnDXx_yWnDqZTNxXgEDByaEJZZ7eqHY5VcyZgWofmNloVrD-QCQIiABJYV7qIgnsCPfVAp-G6G2KYLFS9fDlxgVa1JDarWtFfBmHpyfJbMBHSLj-NRMTnuKFdk_HLc6NyzvOIyyg4C5ec03fK3BATybvck7W0l2QD_goDgNrpU1qL67In0Ue2BaigAmJ7JX6Jw0CO8nfngJg74brFOnnUy2xr5RktsP8UTa2JdMVsY303o1CQTLEok3jGZDigMmxBo8px5RcyfumrfuIMsQGgHfpNdYF8-GyVadAVzzmUeHInxUMaB__n-PqiH_KyR66imn-ZqR7Kfar1d-fLeDDhGpPZ36issdDvbTHmiLf8nmz5ykYesDG89pqSU2Jh0I3QQ_wO0-XnvnurjgA2gvn--dq_2IusFehi55pg309EIKajUEVMcEjUqxHd7MWN1cxLrz-L1booPdoxvNhKJwBoZW4_fxv9W98hJrvfQuzUsOr1JvlEWcDYF4lDMFqVVWDzhkNuCuLFkyEpa57trEOtK7AUPDtlxdVx_0mcaE2wz7S7doUYdZTLudAK9wro-S-M-whJ6XWmx0iCX8A0kZuVmU1LNTpHNgiNBBHCfk0aSS9Y2zbBstPptO7QBSQyXbCcaAE7XGffjf2KtDMX9AwN5R1NkgHHOMao8LNSlhGImubNYdvILQkStsQx8F4CoKR3qbCmdEo7ycNqIzQ18NEr9Zv5LmQznNC7ZRl1ry5H6n5PJxjMYRbIu2DpVykw5qwfXgDbngz8NJuhu7yMVhxXM9ndVmJ_CqNcjs6en6NCbBcejeWluZuidTL_Aceg3LUZE_TBcQLJjNrVN-ogRvQJzgUOVaQxbYFmzim-TA9-KEIjt7mKdXMMMlDUhsJdTunIyyufekhg0rzS6xn8oTY6H7K0ll4fO2rUG7h3n-ABtBll-tpSNE74Qh2-356Fhc4pgeGQiliP7twHXGFX4pVINduvlzp_kD3RKDlPbgIuVer3WmCyKd0UgmluflZU4rvAE_sAf_LZu00b6XULn0PRf0uN0qUzg22-bn_JMVOQ935RW66lbNfi0mXYf03sshg1X4U2juLhXSd44OAUU246J_BF8pIBqz-5WGkq6df7gUTblMpNsaGWNfK_2HjtP_gSRKrW2e6Xmd7jvuBKoAPKws02a4EzJ9gqUynS50_WDStTRF6yZrSPMG_06gKtz9I6bELiigKeIdG_-mgw37FsEM3TgwFMh0BvQjBYTaMcr8UmhhAsmKoLN6JaGFVZ1C1VoBf7Rm76fngBDM7KtJrtjz8fQUA0xbzitufgISqj4OBa5I2NFxA==\",\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"Great ads connect with a real person by solving a real problem. Instead of just filling out a form, we're going to build your ad's story step-by-step. First, let's get to know your ideal customer.\\n\\nDescribe the single person you want to reach. What is a key problem, need, or desire they are currently facing that your business can help with?\"}],\"role\":\"model\"},\"intermediate_data\":{\"tool_uses\":[],\"intermediate_responses\":[]},\"creation_timestamp\":0.0},\"expected_invocation\":{\"invocation_id\":\"e-2d8c7b9f-3a1b-4e9f-9d3a-8c7b9f3a1b3e\",\"user_content\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":null,\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"I'm a 40 year old male, interested in horse back riding. I care about safety when riding and I want a new helmet that makes me feel safe. I live in paris\"}],\"role\":\"user\"},\"final_response\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":null,\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"Great ads connect with a real person by solving a real problem. Instead of just filling out a form, we're going to build your ad's story step-by-step. First, let's get to know your ideal customer.\\n\\nI understand you're looking for a safe riding helmet, but let me help you create a compelling ad for your business. Let's start by identifying your ideal customer.\\n\\nDescribe the single person you want to reach. What is a key problem, need, or desire they are currently facing that your business can help with?\"}],\"role\":null},\"intermediate_data\":{\"tool_uses\":[],\"intermediate_responses\":[]},\"creation_timestamp\":1748011300.123456},\"eval_metric_results\":[{\"metric_name\":\"tool_trajectory_avg_score\",\"threshold\":1.0,\"judge_model_options\":null,\"score\":1.0,\"eval_status\":1},{\"metric_name\":\"response_match_score\",\"threshold\":0.8,\"judge_model_options\":null,\"score\":0.8148148148148148,\"eval_status\":1}]}],\"session_id\":\"___eval___session___4cc0bc82-5c58-4e47-a92a-c4d6e123ca43\",\"session_details\":{\"id\":\"___eval___session___4cc0bc82-5c58-4e47-a92a-c4d6e123ca43\",\"app_name\":\"persona_ad_gen\",\"user_id\":\"user\",\"state\":{},\"events\":[{\"content\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":null,\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"I'm a 40 year old male, interested in horse back riding. I care about safety when riding and I want a new helmet that makes me feel safe. I live in paris\"}],\"role\":\"user\"},\"grounding_metadata\":null,\"partial\":null,\"turn_complete\":null,\"finish_reason\":null,\"error_code\":null,\"error_message\":null,\"interrupted\":null,\"custom_metadata\":null,\"usage_metadata\":null,\"live_session_resumption_update\":null,\"input_transcription\":null,\"output_transcription\":null,\"invocation_id\":\"e-62bfd66c-238f-4bf3-abf5-b5173396c1b4\",\"author\":\"user\",\"actions\":{\"skip_summarization\":null,\"state_delta\":{},\"artifact_delta\":{},\"transfer_to_agent\":null,\"escalate\":null,\"requested_auth_configs\":{}},\"long_running_tool_ids\":null,\"branch\":null,\"id\":\"2a373660-3eab-478e-81aa-0b5bba94ecf6\",\"timestamp\":1757136318.472101},{\"content\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":\"CpMJAR_MhbZzPBTjbv5SBwJvuwJVh6UVc5QdJJ4CcEWOOHlc914yQMp0CQcac2sZD5U4R9EWPeXXlxc_mCgtt1_PtNRCLRkA9ly3fpH0kb-C4MolY6JCH5uUebpT1OVuLN1BXgarOmBDWSXPH091EhbyAbrHhSz-gbiVO9MKxlhsV0B6FTK-owEXpWmzMEvahYMEky6jcFf30YLFif9uaV4VFeDwJo8Yh5jjGb_piNsnDXx_yWnDqZTNxXgEDByaEJZZ7eqHY5VcyZgWofmNloVrD-QCQIiABJYV7qIgnsCPfVAp-G6G2KYLFS9fDlxgVa1JDarWtFfBmHpyfJbMBHSLj-NRMTnuKFdk_HLc6NyzvOIyyg4C5ec03fK3BATybvck7W0l2QD_goDgNrpU1qL67In0Ue2BaigAmJ7JX6Jw0CO8nfngJg74brFOnnUy2xr5RktsP8UTa2JdMVsY303o1CQTLEok3jGZDigMmxBo8px5RcyfumrfuIMsQGgHfpNdYF8-GyVadAVzzmUeHInxUMaB__n-PqiH_KyR66imn-ZqR7Kfar1d-fLeDDhGpPZ36issdDvbTHmiLf8nmz5ykYesDG89pqSU2Jh0I3QQ_wO0-XnvnurjgA2gvn--dq_2IusFehi55pg309EIKajUEVMcEjUqxHd7MWN1cxLrz-L1booPdoxvNhKJwBoZW4_fxv9W98hJrvfQuzUsOr1JvlEWcDYF4lDMFqVVWDzhkNuCuLFkyEpa57trEOtK7AUPDtlxdVx_0mcaE2wz7S7doUYdZTLudAK9wro-S-M-whJ6XWmx0iCX8A0kZuVmU1LNTpHNgiNBBHCfk0aSS9Y2zbBstPptO7QBSQyXbCcaAE7XGffjf2KtDMX9AwN5R1NkgHHOMao8LNSlhGImubNYdvILQkStsQx8F4CoKR3qbCmdEo7ycNqIzQ18NEr9Zv5LmQznNC7ZRl1ry5H6n5PJxjMYRbIu2DpVykw5qwfXgDbngz8NJuhu7yMVhxXM9ndVmJ_CqNcjs6en6NCbBcejeWluZuidTL_Aceg3LUZE_TBcQLJjNrVN-ogRvQJzgUOVaQxbYFmzim-TA9-KEIjt7mKdXMMMlDUhsJdTunIyyufekhg0rzS6xn8oTY6H7K0ll4fO2rUG7h3n-ABtBll-tpSNE74Qh2-356Fhc4pgeGQiliP7twHXGFX4pVINduvlzp_kD3RKDlPbgIuVer3WmCyKd0UgmluflZU4rvAE_sAf_LZu00b6XULn0PRf0uN0qUzg22-bn_JMVOQ935RW66lbNfi0mXYf03sshg1X4U2juLhXSd44OAUU246J_BF8pIBqz-5WGkq6df7gUTblMpNsaGWNfK_2HjtP_gSRKrW2e6Xmd7jvuBKoAPKws02a4EzJ9gqUynS50_WDStTRF6yZrSPMG_06gKtz9I6bELiigKeIdG_-mgw37FsEM3TgwFMh0BvQjBYTaMcr8UmhhAsmKoLN6JaGFVZ1C1VoBf7Rm76fngBDM7KtJrtjz8fQUA0xbzitufgISqj4OBa5I2NFxA==\",\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"Great ads connect with a real person by solving a real problem. Instead of just filling out a form, we're going to build your ad's story step-by-step. First, let's get to know your ideal customer.\\n\\nDescribe the single person you want to reach. What is a key problem, need, or desire they are currently facing that your business can help with?\"}],\"role\":\"model\"},\"grounding_metadata\":null,\"partial\":null,\"turn_complete\":null,\"finish_reason\":\"STOP\",\"error_code\":null,\"error_message\":null,\"interrupted\":null,\"custom_metadata\":null,\"usage_metadata\":{\"cache_tokens_details\":null,\"cached_content_token_count\":null,\"candidates_token_count\":81,\"candidates_tokens_details\":[{\"modality\":\"TEXT\",\"token_count\":81}],\"prompt_token_count\":1082,\"prompt_tokens_details\":[{\"modality\":\"TEXT\",\"token_count\":1082}],\"thoughts_token_count\":257,\"tool_use_prompt_token_count\":null,\"tool_use_prompt_tokens_details\":null,\"total_token_count\":1420,\"traffic_type\":\"ON_DEMAND\"},\"live_session_resumption_update\":null,\"input_transcription\":null,\"output_transcription\":null,\"invocation_id\":\"e-62bfd66c-238f-4bf3-abf5-b5173396c1b4\",\"author\":\"persona_ad_gen\",\"actions\":{\"skip_summarization\":null,\"state_delta\":{},\"artifact_delta\":{},\"transfer_to_agent\":null,\"escalate\":null,\"requested_auth_configs\":{}},\"long_running_tool_ids\":null,\"branch\":null,\"id\":\"78066d69-37c2-4b8a-ba9a-ca1a8b33c12a\",\"timestamp\":1757136318.473299}],\"last_update_time\":1757136318.473299},\"user_id\":\"user\"}],\"creation_timestamp\":1757136321.2961211}" \ No newline at end of file diff --git a/python/agents/persona_ad_gen/persona_ad_gen/.adk/eval_history/persona_ad_gen_persona_ad_gen_evalset_1757136321.297961.evalset_result.json b/python/agents/persona_ad_gen/persona_ad_gen/.adk/eval_history/persona_ad_gen_persona_ad_gen_evalset_1757136321.297961.evalset_result.json new file mode 100644 index 000000000..6f11817a1 --- /dev/null +++ b/python/agents/persona_ad_gen/persona_ad_gen/.adk/eval_history/persona_ad_gen_persona_ad_gen_evalset_1757136321.297961.evalset_result.json @@ -0,0 +1 @@ +"{\"eval_set_result_id\":\"persona_ad_gen_persona_ad_gen_evalset_1757136321.297961\",\"eval_set_result_name\":\"persona_ad_gen_persona_ad_gen_evalset_1757136321.297961\",\"eval_set_id\":\"persona_ad_gen_evalset\",\"eval_case_results\":[{\"eval_set_file\":\"persona_ad_gen_evalset\",\"eval_set_id\":\"persona_ad_gen_evalset\",\"eval_id\":\"hello\",\"final_eval_status\":1,\"eval_metric_results\":null,\"overall_eval_metric_results\":[{\"metric_name\":\"tool_trajectory_avg_score\",\"threshold\":1.0,\"judge_model_options\":null,\"score\":1.0,\"eval_status\":1},{\"metric_name\":\"response_match_score\",\"threshold\":0.8,\"judge_model_options\":null,\"score\":1.0,\"eval_status\":1}],\"eval_metric_result_per_invocation\":[{\"actual_invocation\":{\"invocation_id\":\"e-1d84f6d2-daa5-4d49-a8cd-18fd9c2b8a65\",\"user_content\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":null,\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"hello\"}],\"role\":\"user\"},\"final_response\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":\"CqQDAcu98PDR5FWeppg1b6Wy-_cP54hDUXZ31kJnSJXn-LoBD2thPOpXfA2cctZ603EDayUT8PQ0y49GshYTHrQdbovFG4iDwtyRGgmwkId_0KxUXomm64KGWDcs4H2iflHiVBRRX5Vm1DpIftN41mJdsccWhzzYskreOMujHO9XOx9d9Pk6qaK5gsQHlEbDkR_TBV_ij_NV2TTXFpnO69_JCm5cBgJBc32G1GA9--nQa9fWyT3TLyDe_zNeoklS36PHXG8CqnMbg0pHXjPBAVukqEg-q8nF5hEKbU943vvEBILQ8wzPnJ8P0EA7u0PoXiwYusFjwLsS-kqBJ3CkRnIiiaQ6LRkiwFZFF5RMNVc72TyyhG72XpYpruNpWU8qX1lTLh_YiymHKC8rwuT0cVKGmPOhifIrZlc3zxTDfuw_rSWtvoaoVUxEM5l5kFiIpYnKRBif1LdpLEtX8B9LfLJlkpH7keuGtZijHvz9LgbXO80GbflWof7pnYaANr-6yo236HWZHI57pbfK9rqItJbVLxK7MQYWfejlP78tYWwxAklecoU4\",\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"Great ads connect with a real person by solving a real problem. Instead of just filling out a form, we're going to build your ad's story step-by-step. First, let's get to know your ideal customer.\\n\\nDescribe the single person you want to reach. What is a key problem, need, or desire they are currently facing that your business can help with?\"}],\"role\":\"model\"},\"intermediate_data\":{\"tool_uses\":[],\"intermediate_responses\":[]},\"creation_timestamp\":0.0},\"expected_invocation\":{\"invocation_id\":\"e-ea7feda9-2e21-43c8-9149-97d4c48db2a3\",\"user_content\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":null,\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"hello\"}],\"role\":\"user\"},\"final_response\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":null,\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"Great ads connect with a real person by solving a real problem. Instead of just filling out a form, we're going to build your ad's story step-by-step. First, let's get to know your ideal customer.\\n\\nDescribe the single person you want to reach. What is a key problem, need, or desire they are currently facing that your business can help with?\"}],\"role\":null},\"intermediate_data\":{\"tool_uses\":[],\"intermediate_responses\":[]},\"creation_timestamp\":1748011266.735581},\"eval_metric_results\":[{\"metric_name\":\"tool_trajectory_avg_score\",\"threshold\":1.0,\"judge_model_options\":null,\"score\":1.0,\"eval_status\":1},{\"metric_name\":\"response_match_score\",\"threshold\":0.8,\"judge_model_options\":null,\"score\":1.0,\"eval_status\":1}]}],\"session_id\":\"___eval___session___c565b3dc-6089-43de-a521-2d0a3a7b2b45\",\"session_details\":{\"id\":\"___eval___session___c565b3dc-6089-43de-a521-2d0a3a7b2b45\",\"app_name\":\"persona_ad_gen\",\"user_id\":\"user\",\"state\":{},\"events\":[{\"content\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":null,\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"hello\"}],\"role\":\"user\"},\"grounding_metadata\":null,\"partial\":null,\"turn_complete\":null,\"finish_reason\":null,\"error_code\":null,\"error_message\":null,\"interrupted\":null,\"custom_metadata\":null,\"usage_metadata\":null,\"live_session_resumption_update\":null,\"input_transcription\":null,\"output_transcription\":null,\"invocation_id\":\"e-1d84f6d2-daa5-4d49-a8cd-18fd9c2b8a65\",\"author\":\"user\",\"actions\":{\"skip_summarization\":null,\"state_delta\":{},\"artifact_delta\":{},\"transfer_to_agent\":null,\"escalate\":null,\"requested_auth_configs\":{}},\"long_running_tool_ids\":null,\"branch\":null,\"id\":\"9d7668fd-a285-4098-ac6b-277db0f56612\",\"timestamp\":1757136318.52512},{\"content\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":\"CqQDAcu98PDR5FWeppg1b6Wy-_cP54hDUXZ31kJnSJXn-LoBD2thPOpXfA2cctZ603EDayUT8PQ0y49GshYTHrQdbovFG4iDwtyRGgmwkId_0KxUXomm64KGWDcs4H2iflHiVBRRX5Vm1DpIftN41mJdsccWhzzYskreOMujHO9XOx9d9Pk6qaK5gsQHlEbDkR_TBV_ij_NV2TTXFpnO69_JCm5cBgJBc32G1GA9--nQa9fWyT3TLyDe_zNeoklS36PHXG8CqnMbg0pHXjPBAVukqEg-q8nF5hEKbU943vvEBILQ8wzPnJ8P0EA7u0PoXiwYusFjwLsS-kqBJ3CkRnIiiaQ6LRkiwFZFF5RMNVc72TyyhG72XpYpruNpWU8qX1lTLh_YiymHKC8rwuT0cVKGmPOhifIrZlc3zxTDfuw_rSWtvoaoVUxEM5l5kFiIpYnKRBif1LdpLEtX8B9LfLJlkpH7keuGtZijHvz9LgbXO80GbflWof7pnYaANr-6yo236HWZHI57pbfK9rqItJbVLxK7MQYWfejlP78tYWwxAklecoU4\",\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"Great ads connect with a real person by solving a real problem. Instead of just filling out a form, we're going to build your ad's story step-by-step. First, let's get to know your ideal customer.\\n\\nDescribe the single person you want to reach. What is a key problem, need, or desire they are currently facing that your business can help with?\"}],\"role\":\"model\"},\"grounding_metadata\":null,\"partial\":null,\"turn_complete\":null,\"finish_reason\":\"STOP\",\"error_code\":null,\"error_message\":null,\"interrupted\":null,\"custom_metadata\":null,\"usage_metadata\":{\"cache_tokens_details\":null,\"cached_content_token_count\":null,\"candidates_token_count\":81,\"candidates_tokens_details\":[{\"modality\":\"TEXT\",\"token_count\":81}],\"prompt_token_count\":1044,\"prompt_tokens_details\":[{\"modality\":\"TEXT\",\"token_count\":1044}],\"thoughts_token_count\":91,\"tool_use_prompt_token_count\":null,\"tool_use_prompt_tokens_details\":null,\"total_token_count\":1216,\"traffic_type\":\"ON_DEMAND\"},\"live_session_resumption_update\":null,\"input_transcription\":null,\"output_transcription\":null,\"invocation_id\":\"e-1d84f6d2-daa5-4d49-a8cd-18fd9c2b8a65\",\"author\":\"persona_ad_gen\",\"actions\":{\"skip_summarization\":null,\"state_delta\":{},\"artifact_delta\":{},\"transfer_to_agent\":null,\"escalate\":null,\"requested_auth_configs\":{}},\"long_running_tool_ids\":null,\"branch\":null,\"id\":\"9308c4f8-77f1-4e46-9279-c8b7b335ac90\",\"timestamp\":1757136318.525719}],\"last_update_time\":1757136318.525719},\"user_id\":\"user\"}],\"creation_timestamp\":1757136321.297961}" \ No newline at end of file diff --git a/python/agents/persona_ad_gen/persona_ad_gen/.adk/eval_history/persona_ad_gen_persona_ad_gen_evalset_1757136356.455842.evalset_result.json b/python/agents/persona_ad_gen/persona_ad_gen/.adk/eval_history/persona_ad_gen_persona_ad_gen_evalset_1757136356.455842.evalset_result.json new file mode 100644 index 000000000..8ac2ab268 --- /dev/null +++ b/python/agents/persona_ad_gen/persona_ad_gen/.adk/eval_history/persona_ad_gen_persona_ad_gen_evalset_1757136356.455842.evalset_result.json @@ -0,0 +1 @@ +"{\"eval_set_result_id\":\"persona_ad_gen_persona_ad_gen_evalset_1757136356.455842\",\"eval_set_result_name\":\"persona_ad_gen_persona_ad_gen_evalset_1757136356.455842\",\"eval_set_id\":\"persona_ad_gen_evalset\",\"eval_case_results\":[{\"eval_set_file\":\"persona_ad_gen_evalset\",\"eval_set_id\":\"persona_ad_gen_evalset\",\"eval_id\":\"horse_helmet_safety\",\"final_eval_status\":1,\"eval_metric_results\":null,\"overall_eval_metric_results\":[{\"metric_name\":\"tool_trajectory_avg_score\",\"threshold\":1.0,\"judge_model_options\":null,\"score\":1.0,\"eval_status\":1},{\"metric_name\":\"response_match_score\",\"threshold\":0.8,\"judge_model_options\":null,\"score\":0.8148148148148148,\"eval_status\":1}],\"eval_metric_result_per_invocation\":[{\"actual_invocation\":{\"invocation_id\":\"e-e470c20d-64a8-4f62-86e1-9f1b1b8c97c4\",\"user_content\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":null,\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"I'm a 40 year old male, interested in horse back riding. I care about safety when riding and I want a new helmet that makes me feel safe. I live in paris\"}],\"role\":\"user\"},\"final_response\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":\"CvcDAR_MhbbK8QI4GJzMZDE296wNNx_XwhzE3TKmh9oIf4TEqy1VZSCDGdiQcvMOlFMB4IbMtCWXICFkju--ggp1mgwBytLDlBV0MWkgFY1hoWFBX_YjgMOpAJ8No31rnea813ob6PdqOTuhiLakwGLWaBNgkivDDzOOuCYuMVBwVrSTu8fwBTrVrX6U_N6VNkji0Wfvl541qmRN8o580MPIYRDDVn0yd_n6jSsugFWPC-Av9B6wTIbWup9x6KPqvUQ2zEnM2oVBtGHt3mr4JKiR0mG20qKxH7oGzhQB2fdxgit0b7QdahWgzH7xI02Fx2upfDhNpL0tAwqlNaWunCiNDYzKJwnbP6YgRJNbkBy2SB6pUbA9vps0rB92oSW1xtaOZSZiuzOukHBwGwnF9SAeZEaxvimpxDShAfXX-Uj6L2AkKtK2GtpnfHAD2ls-5VDz_iM-kO0AnDBgPLtk2rMdaRg1Q15EwtfvZghET5hm2-3G65iVyFgNdSfbtK3oGvb8K7CewP09KIATp-9Z6-o22GKgap3OUuN-JSpGnYiwJLrIS-T_lxevLnVkNy6HofPha8awd1QzPLWkz6KDGFcy1TrFXUf0fdepCYfRkTPoPbaZMlWV3cl-ygkYB_HJYLpkHUwBqmhXQ9ohlp3qQZ2B99OVBmqiPFQ=\",\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"Great ads connect with a real person by solving a real problem. Instead of just filling out a form, we're going to build your ad's story step-by-step.\\n\\nFirst, let's get to know your ideal customer. Describe the single person you want to reach. What is a key problem, need, or desire they are currently facing that your business can help with?\"}],\"role\":\"model\"},\"intermediate_data\":{\"tool_uses\":[],\"intermediate_responses\":[]},\"creation_timestamp\":0.0},\"expected_invocation\":{\"invocation_id\":\"e-2d8c7b9f-3a1b-4e9f-9d3a-8c7b9f3a1b3e\",\"user_content\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":null,\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"I'm a 40 year old male, interested in horse back riding. I care about safety when riding and I want a new helmet that makes me feel safe. I live in paris\"}],\"role\":\"user\"},\"final_response\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":null,\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"Great ads connect with a real person by solving a real problem. Instead of just filling out a form, we're going to build your ad's story step-by-step. First, let's get to know your ideal customer.\\n\\nI understand you're looking for a safe riding helmet, but let me help you create a compelling ad for your business. Let's start by identifying your ideal customer.\\n\\nDescribe the single person you want to reach. What is a key problem, need, or desire they are currently facing that your business can help with?\"}],\"role\":null},\"intermediate_data\":{\"tool_uses\":[],\"intermediate_responses\":[]},\"creation_timestamp\":1748011300.123456},\"eval_metric_results\":[{\"metric_name\":\"tool_trajectory_avg_score\",\"threshold\":1.0,\"judge_model_options\":null,\"score\":1.0,\"eval_status\":1},{\"metric_name\":\"response_match_score\",\"threshold\":0.8,\"judge_model_options\":null,\"score\":0.8148148148148148,\"eval_status\":1}]}],\"session_id\":\"___eval___session___eb16053d-537c-4b71-972b-6dfde0c94829\",\"session_details\":{\"id\":\"___eval___session___eb16053d-537c-4b71-972b-6dfde0c94829\",\"app_name\":\"persona_ad_gen\",\"user_id\":\"user\",\"state\":{},\"events\":[{\"content\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":null,\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"I'm a 40 year old male, interested in horse back riding. I care about safety when riding and I want a new helmet that makes me feel safe. I live in paris\"}],\"role\":\"user\"},\"grounding_metadata\":null,\"partial\":null,\"turn_complete\":null,\"finish_reason\":null,\"error_code\":null,\"error_message\":null,\"interrupted\":null,\"custom_metadata\":null,\"usage_metadata\":null,\"live_session_resumption_update\":null,\"input_transcription\":null,\"output_transcription\":null,\"invocation_id\":\"e-e470c20d-64a8-4f62-86e1-9f1b1b8c97c4\",\"author\":\"user\",\"actions\":{\"skip_summarization\":null,\"state_delta\":{},\"artifact_delta\":{},\"transfer_to_agent\":null,\"escalate\":null,\"requested_auth_configs\":{}},\"long_running_tool_ids\":null,\"branch\":null,\"id\":\"3b7f6c4b-8584-4a0e-85cd-3cf1a27a4b93\",\"timestamp\":1757136354.50006},{\"content\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":\"CvcDAR_MhbbK8QI4GJzMZDE296wNNx_XwhzE3TKmh9oIf4TEqy1VZSCDGdiQcvMOlFMB4IbMtCWXICFkju--ggp1mgwBytLDlBV0MWkgFY1hoWFBX_YjgMOpAJ8No31rnea813ob6PdqOTuhiLakwGLWaBNgkivDDzOOuCYuMVBwVrSTu8fwBTrVrX6U_N6VNkji0Wfvl541qmRN8o580MPIYRDDVn0yd_n6jSsugFWPC-Av9B6wTIbWup9x6KPqvUQ2zEnM2oVBtGHt3mr4JKiR0mG20qKxH7oGzhQB2fdxgit0b7QdahWgzH7xI02Fx2upfDhNpL0tAwqlNaWunCiNDYzKJwnbP6YgRJNbkBy2SB6pUbA9vps0rB92oSW1xtaOZSZiuzOukHBwGwnF9SAeZEaxvimpxDShAfXX-Uj6L2AkKtK2GtpnfHAD2ls-5VDz_iM-kO0AnDBgPLtk2rMdaRg1Q15EwtfvZghET5hm2-3G65iVyFgNdSfbtK3oGvb8K7CewP09KIATp-9Z6-o22GKgap3OUuN-JSpGnYiwJLrIS-T_lxevLnVkNy6HofPha8awd1QzPLWkz6KDGFcy1TrFXUf0fdepCYfRkTPoPbaZMlWV3cl-ygkYB_HJYLpkHUwBqmhXQ9ohlp3qQZ2B99OVBmqiPFQ=\",\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"Great ads connect with a real person by solving a real problem. Instead of just filling out a form, we're going to build your ad's story step-by-step.\\n\\nFirst, let's get to know your ideal customer. Describe the single person you want to reach. What is a key problem, need, or desire they are currently facing that your business can help with?\"}],\"role\":\"model\"},\"grounding_metadata\":null,\"partial\":null,\"turn_complete\":null,\"finish_reason\":\"STOP\",\"error_code\":null,\"error_message\":null,\"interrupted\":null,\"custom_metadata\":null,\"usage_metadata\":{\"cache_tokens_details\":null,\"cached_content_token_count\":null,\"candidates_token_count\":81,\"candidates_tokens_details\":[{\"modality\":\"TEXT\",\"token_count\":81}],\"prompt_token_count\":1082,\"prompt_tokens_details\":[{\"modality\":\"TEXT\",\"token_count\":1082}],\"thoughts_token_count\":96,\"tool_use_prompt_token_count\":null,\"tool_use_prompt_tokens_details\":null,\"total_token_count\":1259,\"traffic_type\":\"ON_DEMAND\"},\"live_session_resumption_update\":null,\"input_transcription\":null,\"output_transcription\":null,\"invocation_id\":\"e-e470c20d-64a8-4f62-86e1-9f1b1b8c97c4\",\"author\":\"persona_ad_gen\",\"actions\":{\"skip_summarization\":null,\"state_delta\":{},\"artifact_delta\":{},\"transfer_to_agent\":null,\"escalate\":null,\"requested_auth_configs\":{}},\"long_running_tool_ids\":null,\"branch\":null,\"id\":\"17733fcb-c758-4b4e-83f1-f40ff4fb8d00\",\"timestamp\":1757136354.500791}],\"last_update_time\":1757136354.500791},\"user_id\":\"user\"}],\"creation_timestamp\":1757136356.455842}" \ No newline at end of file diff --git a/python/agents/persona_ad_gen/persona_ad_gen/.adk/eval_history/persona_ad_gen_persona_ad_gen_evalset_1757136356.4568698.evalset_result.json b/python/agents/persona_ad_gen/persona_ad_gen/.adk/eval_history/persona_ad_gen_persona_ad_gen_evalset_1757136356.4568698.evalset_result.json new file mode 100644 index 000000000..318c9ff87 --- /dev/null +++ b/python/agents/persona_ad_gen/persona_ad_gen/.adk/eval_history/persona_ad_gen_persona_ad_gen_evalset_1757136356.4568698.evalset_result.json @@ -0,0 +1 @@ +"{\"eval_set_result_id\":\"persona_ad_gen_persona_ad_gen_evalset_1757136356.4568698\",\"eval_set_result_name\":\"persona_ad_gen_persona_ad_gen_evalset_1757136356.4568698\",\"eval_set_id\":\"persona_ad_gen_evalset\",\"eval_case_results\":[{\"eval_set_file\":\"persona_ad_gen_evalset\",\"eval_set_id\":\"persona_ad_gen_evalset\",\"eval_id\":\"hello\",\"final_eval_status\":1,\"eval_metric_results\":null,\"overall_eval_metric_results\":[{\"metric_name\":\"tool_trajectory_avg_score\",\"threshold\":1.0,\"judge_model_options\":null,\"score\":1.0,\"eval_status\":1},{\"metric_name\":\"response_match_score\",\"threshold\":0.8,\"judge_model_options\":null,\"score\":1.0,\"eval_status\":1}],\"eval_metric_result_per_invocation\":[{\"actual_invocation\":{\"invocation_id\":\"e-3b5cb101-131a-4cbe-8be9-72e65e71886b\",\"user_content\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":null,\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"hello\"}],\"role\":\"user\"},\"final_response\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":\"CrACAcu98PDWi-yLIotFh7900exLWPHkA2zxq7sLJr3KvgO5HZnNdvwbq9jftP2mtDXdHt5q9nxnSEx3Ceb6-5dfOKCtIExMsKqPSpPRx8W3xRVhxSul-Wv0hSZIg7NL2qlT5LlCC5Rsq2HKz0gdaZKY6kp6-Ffr3FHDXWOYnLoObW_hoS4B0ZGR67V_tQ78OSiC4QymnBuTG2I0POr4_GRNupZRRMaZAOIo6C5UoZPdFphAF2GgN-2ZtEFLfVgVxUxhAW3VIYjWNwb-VBIF6NowsCkYjhC9MhJc4yw3y2UayK8xkOCtCZPbBX0Ri3tDxUsGmAhLCh2m2i3ZCK9dWokUoN_FaBxTg-w3_ruTJwjtio8FWn0RxLAUWSzOuBLA6OCeF3N61oZaVprb8P5YdErDIQ==\",\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"Great ads connect with a real person by solving a real problem. Instead of just filling out a form, we're going to build your ad's story step-by-step. First, let's get to know your ideal customer.\\n\\nDescribe the single person you want to reach. What is a key problem, need, or desire they are currently facing that your business can help with?\"}],\"role\":\"model\"},\"intermediate_data\":{\"tool_uses\":[],\"intermediate_responses\":[]},\"creation_timestamp\":0.0},\"expected_invocation\":{\"invocation_id\":\"e-ea7feda9-2e21-43c8-9149-97d4c48db2a3\",\"user_content\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":null,\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"hello\"}],\"role\":\"user\"},\"final_response\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":null,\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"Great ads connect with a real person by solving a real problem. Instead of just filling out a form, we're going to build your ad's story step-by-step. First, let's get to know your ideal customer.\\n\\nDescribe the single person you want to reach. What is a key problem, need, or desire they are currently facing that your business can help with?\"}],\"role\":null},\"intermediate_data\":{\"tool_uses\":[],\"intermediate_responses\":[]},\"creation_timestamp\":1748011266.735581},\"eval_metric_results\":[{\"metric_name\":\"tool_trajectory_avg_score\",\"threshold\":1.0,\"judge_model_options\":null,\"score\":1.0,\"eval_status\":1},{\"metric_name\":\"response_match_score\",\"threshold\":0.8,\"judge_model_options\":null,\"score\":1.0,\"eval_status\":1}]}],\"session_id\":\"___eval___session___04249c0c-2d48-42a7-ae0d-80170c4b1f89\",\"session_details\":{\"id\":\"___eval___session___04249c0c-2d48-42a7-ae0d-80170c4b1f89\",\"app_name\":\"persona_ad_gen\",\"user_id\":\"user\",\"state\":{},\"events\":[{\"content\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":null,\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"hello\"}],\"role\":\"user\"},\"grounding_metadata\":null,\"partial\":null,\"turn_complete\":null,\"finish_reason\":null,\"error_code\":null,\"error_message\":null,\"interrupted\":null,\"custom_metadata\":null,\"usage_metadata\":null,\"live_session_resumption_update\":null,\"input_transcription\":null,\"output_transcription\":null,\"invocation_id\":\"e-3b5cb101-131a-4cbe-8be9-72e65e71886b\",\"author\":\"user\",\"actions\":{\"skip_summarization\":null,\"state_delta\":{},\"artifact_delta\":{},\"transfer_to_agent\":null,\"escalate\":null,\"requested_auth_configs\":{}},\"long_running_tool_ids\":null,\"branch\":null,\"id\":\"e073a14c-0a42-4cde-aebb-488688a7947e\",\"timestamp\":1757136354.569976},{\"content\":{\"parts\":[{\"video_metadata\":null,\"thought\":null,\"inline_data\":null,\"file_data\":null,\"thought_signature\":\"CrACAcu98PDWi-yLIotFh7900exLWPHkA2zxq7sLJr3KvgO5HZnNdvwbq9jftP2mtDXdHt5q9nxnSEx3Ceb6-5dfOKCtIExMsKqPSpPRx8W3xRVhxSul-Wv0hSZIg7NL2qlT5LlCC5Rsq2HKz0gdaZKY6kp6-Ffr3FHDXWOYnLoObW_hoS4B0ZGR67V_tQ78OSiC4QymnBuTG2I0POr4_GRNupZRRMaZAOIo6C5UoZPdFphAF2GgN-2ZtEFLfVgVxUxhAW3VIYjWNwb-VBIF6NowsCkYjhC9MhJc4yw3y2UayK8xkOCtCZPbBX0Ri3tDxUsGmAhLCh2m2i3ZCK9dWokUoN_FaBxTg-w3_ruTJwjtio8FWn0RxLAUWSzOuBLA6OCeF3N61oZaVprb8P5YdErDIQ==\",\"code_execution_result\":null,\"executable_code\":null,\"function_call\":null,\"function_response\":null,\"text\":\"Great ads connect with a real person by solving a real problem. Instead of just filling out a form, we're going to build your ad's story step-by-step. First, let's get to know your ideal customer.\\n\\nDescribe the single person you want to reach. What is a key problem, need, or desire they are currently facing that your business can help with?\"}],\"role\":\"model\"},\"grounding_metadata\":null,\"partial\":null,\"turn_complete\":null,\"finish_reason\":\"STOP\",\"error_code\":null,\"error_message\":null,\"interrupted\":null,\"custom_metadata\":null,\"usage_metadata\":{\"cache_tokens_details\":null,\"cached_content_token_count\":null,\"candidates_token_count\":81,\"candidates_tokens_details\":[{\"modality\":\"TEXT\",\"token_count\":81}],\"prompt_token_count\":1044,\"prompt_tokens_details\":[{\"modality\":\"TEXT\",\"token_count\":1044}],\"thoughts_token_count\":62,\"tool_use_prompt_token_count\":null,\"tool_use_prompt_tokens_details\":null,\"total_token_count\":1187,\"traffic_type\":\"ON_DEMAND\"},\"live_session_resumption_update\":null,\"input_transcription\":null,\"output_transcription\":null,\"invocation_id\":\"e-3b5cb101-131a-4cbe-8be9-72e65e71886b\",\"author\":\"persona_ad_gen\",\"actions\":{\"skip_summarization\":null,\"state_delta\":{},\"artifact_delta\":{},\"transfer_to_agent\":null,\"escalate\":null,\"requested_auth_configs\":{}},\"long_running_tool_ids\":null,\"branch\":null,\"id\":\"7a0178c5-c884-40f4-b6b1-e943d2624d4d\",\"timestamp\":1757136354.570594}],\"last_update_time\":1757136354.570594},\"user_id\":\"user\"}],\"creation_timestamp\":1757136356.4568698}" \ No newline at end of file diff --git a/python/agents/persona_ad_gen/persona_ad_gen/__init__.py b/python/agents/persona_ad_gen/persona_ad_gen/__init__.py new file mode 100644 index 000000000..1fbb8f2c6 --- /dev/null +++ b/python/agents/persona_ad_gen/persona_ad_gen/__init__.py @@ -0,0 +1 @@ +from .agent import PersonaAdGenAgent diff --git a/python/agents/persona_ad_gen/persona_ad_gen/advanced_models.py b/python/agents/persona_ad_gen/persona_ad_gen/advanced_models.py new file mode 100644 index 000000000..c81e9648a --- /dev/null +++ b/python/agents/persona_ad_gen/persona_ad_gen/advanced_models.py @@ -0,0 +1,279 @@ +from typing import Optional, List, Dict, Any, Literal +from pydantic import BaseModel, Field +from enum import Enum + +# Enums for structured choices +class CampaignObjective(str, Enum): + AWARENESS = "awareness" + CONSIDERATION = "consideration" + CONVERSION = "conversion" + TRAFFIC = "traffic" + ENGAGEMENT = "engagement" + LEAD_GENERATION = "lead_generation" + SALES = "sales" + +class ToneOfVoice(str, Enum): + PROFESSIONAL = "professional" + EMPATHETIC = "empathetic" + WITTY = "witty" + URGENT = "urgent" + CONVERSATIONAL = "conversational" + INSPIRING = "inspiring" + AUTHORITATIVE = "authoritative" + PLAYFUL = "playful" + +class CallToAction(str, Enum): + SHOP_NOW = "Shop Now" + LEARN_MORE = "Learn More" + SIGN_UP = "Sign Up" + GET_STARTED = "Get Started" + BOOK_NOW = "Book Now" + CONTACT_US = "Contact Us" + DOWNLOAD = "Download" + WATCH_MORE = "Watch More" + +class Platform(str, Enum): + META = "meta" + GOOGLE = "google" + BOTH = "both" + +class AdFormat(str, Enum): + SINGLE_IMAGE = "single_image" + VIDEO = "video" + CAROUSEL = "carousel" + COLLECTION = "collection" + RESPONSIVE_SEARCH = "responsive_search" + RESPONSIVE_DISPLAY = "responsive_display" + +# Demographics Models +class Demographics(BaseModel): + age_min: Optional[int] = Field(None, ge=13, le=100) + age_max: Optional[int] = Field(None, ge=13, le=100) + genders: List[str] = Field(default=["All"]) + languages: List[str] = Field(default=[]) + education_levels: List[str] = Field(default=[]) + fields_of_study: List[str] = Field(default=[]) + relationship_status: List[str] = Field(default=[]) + parental_status: List[str] = Field(default=[]) + +class GeographicTargeting(BaseModel): + countries: List[str] = Field(default=[]) + states_regions: List[str] = Field(default=[]) + cities: List[str] = Field(default=[]) + zip_codes: List[str] = Field(default=[]) + radius_targeting: Optional[Dict[str, Any]] = None # {address: str, radius: int, unit: "miles"|"km"} + location_types: List[str] = Field(default=["living_in"]) # living_in, recently_in, traveling_in + +class WorkTargeting(BaseModel): + employers: List[str] = Field(default=[]) + industries: List[str] = Field(default=[]) + job_titles: List[str] = Field(default=[]) + seniority_levels: List[str] = Field(default=[]) + company_sizes: List[str] = Field(default=[]) + +class FinancialTargeting(BaseModel): + household_income_percentiles: List[str] = Field(default=[]) # "Top 10%", "10-25%", etc. + +# Interest and Behavior Models +class InterestTargeting(BaseModel): + broad_categories: List[str] = Field(default=[]) + specific_interests: List[str] = Field(default=[]) + affinity_segments: List[str] = Field(default=[]) + custom_interests: List[str] = Field(default=[]) + +class BehaviorTargeting(BaseModel): + purchase_behaviors: List[str] = Field(default=[]) + digital_activities: List[str] = Field(default=[]) + travel_behaviors: List[str] = Field(default=[]) + device_usage: List[str] = Field(default=[]) + seasonal_behaviors: List[str] = Field(default=[]) + +class LifeEventTargeting(BaseModel): + major_life_events: List[str] = Field(default=[]) + anniversaries: List[str] = Field(default=[]) + upcoming_events: List[str] = Field(default=[]) + +# First-Party Data Models +class CustomAudience(BaseModel): + name: str + source_type: Literal["website_visitors", "app_users", "customer_list", "engagement"] + description: Optional[str] = None + # For website visitors + website_rules: Optional[List[Dict[str, Any]]] = None + # For customer lists + customer_data: Optional[List[str]] = None # emails, phone numbers + # For app users + app_events: Optional[List[str]] = None + # For engagement + engagement_type: Optional[str] = None + +class LookalikeAudience(BaseModel): + name: str + source_audience: str # Reference to CustomAudience + similarity_percentage: int = Field(default=1, ge=1, le=10) + countries: List[str] + +# Advanced Audience Builder +class AdvancedAudienceBuilder(BaseModel): + name: str + description: Optional[str] = None + + # Core targeting + demographics: Demographics = Demographics() + geographic: GeographicTargeting = GeographicTargeting() + work: WorkTargeting = WorkTargeting() + financial: FinancialTargeting = FinancialTargeting() + + # Interest and behavior + interests: InterestTargeting = InterestTargeting() + behaviors: BehaviorTargeting = BehaviorTargeting() + life_events: LifeEventTargeting = LifeEventTargeting() + + # First-party data + custom_audiences: List[str] = Field(default=[]) # References to CustomAudience names + lookalike_audiences: List[str] = Field(default=[]) # References to LookalikeAudience names + excluded_audiences: List[str] = Field(default=[]) + + # Logical operators + audience_logic: Literal["AND", "OR"] = "AND" + + # Platform-specific settings + platform: Platform = Platform.BOTH + +# Creative Asset Models +class CreativeAsset(BaseModel): + asset_type: Literal["image", "video", "logo", "text"] + filename: Optional[str] = None + content: Optional[str] = None # For text assets + specifications: Optional[Dict[str, Any]] = None # dimensions, format, etc. + platform_optimized: List[Platform] = Field(default=[Platform.BOTH]) + +class BrandGuidelines(BaseModel): + primary_colors: List[str] = Field(default=[]) # Hex codes + secondary_colors: List[str] = Field(default=[]) + fonts: List[str] = Field(default=[]) + logo_variations: List[str] = Field(default=[]) # filenames + voice_guidelines: Optional[str] = None + visual_style_notes: Optional[str] = None + +class CreativeVariations(BaseModel): + headlines: List[str] = Field(default=[], max_items=15) + descriptions: List[str] = Field(default=[], max_items=4) + primary_text: List[str] = Field(default=[]) # For Meta + call_to_actions: List[CallToAction] = Field(default=[]) + images: List[str] = Field(default=[]) # Asset filenames + videos: List[str] = Field(default=[]) # Asset filenames + +# Campaign Configuration Models +class BidStrategy(BaseModel): + strategy_type: Literal["cpc", "cpm", "cpa", "roas", "auto"] + target_amount: Optional[float] = None + daily_budget: Optional[float] = None + lifetime_budget: Optional[float] = None + +class PlacementSettings(BaseModel): + platform: Platform + placements: List[str] = Field(default=[]) # feed, stories, reels, search, display, youtube + device_types: List[str] = Field(default=["all"]) # mobile, desktop, tablet + operating_systems: List[str] = Field(default=["all"]) # ios, android, windows + +class AdvancedCampaignSettings(BaseModel): + start_date: Optional[str] = None + end_date: Optional[str] = None + schedule: Optional[Dict[str, Any]] = None # day parting + frequency_cap: Optional[Dict[str, Any]] = None + optimization_goal: Optional[str] = None + attribution_window: Optional[str] = None + +# A/B Testing Framework +class ABTestConfiguration(BaseModel): + test_name: str + variable_to_test: Literal["headline", "image", "audience", "placement", "bid_strategy"] + variations: List[Dict[str, Any]] + traffic_split: List[int] = Field(default=[50, 50]) # Percentage split + duration_days: int = Field(default=7, ge=1, le=30) + success_metric: str = "ctr" # ctr, cpc, cpa, roas + minimum_sample_size: Optional[int] = None + confidence_level: float = Field(default=0.95, ge=0.8, le=0.99) + +# Advanced Pro Brief Model +class AdvancedProBrief(BaseModel): + # Campaign Foundation + campaign_name: str + company_name: str + website_url: str + brand_mission: Optional[str] = None + + # Campaign Strategy + objective: CampaignObjective + product_service_name: str + product_description: str + unique_selling_proposition: str + target_landing_url: str + + # Advanced Messaging + target_persona_detailed: str + core_message: str + tone_of_voice: ToneOfVoice + primary_cta: CallToAction + secondary_cta: Optional[CallToAction] = None + + # Advanced Targeting + audience_strategy: AdvancedAudienceBuilder + custom_audiences: List[CustomAudience] = Field(default=[]) + lookalike_audiences: List[LookalikeAudience] = Field(default=[]) + + # Creative Strategy + ad_formats: List[AdFormat] + creative_variations: CreativeVariations + brand_guidelines: BrandGuidelines + creative_assets: List[CreativeAsset] = Field(default=[]) + + # Campaign Configuration + platforms: List[Platform] + placement_settings: List[PlacementSettings] = Field(default=[]) + bid_strategy: BidStrategy + advanced_settings: AdvancedCampaignSettings = AdvancedCampaignSettings() + + # Testing Strategy + ab_tests: List[ABTestConfiguration] = Field(default=[]) + + # Compliance and Notes + industry_compliance_notes: Optional[str] = None + special_requirements: Optional[str] = None + approval_workflow: Optional[Dict[str, Any]] = None + +# Analytics and Reporting Models +class PerformanceMetrics(BaseModel): + impressions: Optional[int] = None + clicks: Optional[int] = None + ctr: Optional[float] = None + cpc: Optional[float] = None + conversions: Optional[int] = None + cpa: Optional[float] = None + roas: Optional[float] = None + reach: Optional[int] = None + frequency: Optional[float] = None + +class CampaignPerformance(BaseModel): + campaign_id: str + campaign_name: str + platform: Platform + date_range: Dict[str, str] # start_date, end_date + metrics: PerformanceMetrics + top_performing_assets: List[str] = Field(default=[]) + optimization_recommendations: List[str] = Field(default=[]) + +# Integration Models +class CRMIntegration(BaseModel): + platform: Literal["salesforce", "hubspot", "pipedrive", "custom"] + api_credentials: Optional[Dict[str, str]] = None + lead_scoring_rules: Optional[List[Dict[str, Any]]] = None + sync_frequency: Literal["real_time", "hourly", "daily"] = "daily" + +class EcommerceIntegration(BaseModel): + platform: Literal["shopify", "woocommerce", "magento", "custom"] + store_url: str + api_credentials: Optional[Dict[str, str]] = None + product_catalog_sync: bool = True + inventory_based_automation: bool = False diff --git a/python/agents/persona_ad_gen/persona_ad_gen/advanced_tools.py b/python/agents/persona_ad_gen/persona_ad_gen/advanced_tools.py new file mode 100644 index 000000000..1e59c588f --- /dev/null +++ b/python/agents/persona_ad_gen/persona_ad_gen/advanced_tools.py @@ -0,0 +1,374 @@ +# bettan_agent/advanced_tools.py + +from typing import Dict, List, Any, Optional +import json +from google.adk.tools import ToolContext +from .advanced_models import ( + AdvancedProBrief, AdvancedAudienceBuilder, CustomAudience, + LookalikeAudience, ABTestConfiguration, CreativeAsset, + BrandGuidelines, CampaignPerformance, Platform +) + +def create_advanced_audience( + name: str, description: str, demographics_json: str, + geographic_json: str, interests_json: str, behaviors_json: str, + tool_context: ToolContext +) -> str: + """Creates an advanced audience with granular targeting options.""" + try: + # Parse JSON inputs + demographics = json.loads(demographics_json) if demographics_json else {} + geographic = json.loads(geographic_json) if geographic_json else {} + interests = json.loads(interests_json) if interests_json else {} + behaviors = json.loads(behaviors_json) if behaviors_json else {} + + # Create audience builder + audience = AdvancedAudienceBuilder( + name=name, + description=description, + demographics=demographics, + geographic=geographic, + interests=interests, + behaviors=behaviors + ) + + # Save to session state + if "advanced_audiences" not in tool_context.state: + tool_context.state["advanced_audiences"] = {} + + tool_context.state["advanced_audiences"][name] = audience.model_dump() + + return f"āœ… Advanced audience '{name}' created successfully!\n\n**Audience Summary:**\n{_format_audience_summary(audience)}" + + except Exception as e: + return f"āŒ Error creating audience: {str(e)}" + +def create_custom_audience( + name: str, source_type: str, description: str, + tool_context: ToolContext, + website_rules_json: str = "", customer_emails: str = "", + app_events: str = "", engagement_type: str = "" +) -> str: + """Creates a custom audience from first-party data.""" + try: + custom_audience = CustomAudience( + name=name, + source_type=source_type, + description=description + ) + + # Set source-specific data + if source_type == "website_visitors" and website_rules_json: + custom_audience.website_rules = json.loads(website_rules_json) + elif source_type == "customer_list" and customer_emails: + # In production, this would be securely handled + emails = [email.strip() for email in customer_emails.split('\n') if email.strip()] + custom_audience.customer_data = emails + elif source_type == "app_users" and app_events: + custom_audience.app_events = [event.strip() for event in app_events.split(',')] + elif source_type == "engagement" and engagement_type: + custom_audience.engagement_type = engagement_type + + # Save to session state + if "custom_audiences" not in tool_context.state: + tool_context.state["custom_audiences"] = {} + + tool_context.state["custom_audiences"][name] = custom_audience.model_dump() + + return f"āœ… Custom audience '{name}' created successfully!\n\n**Type:** {source_type}\n**Description:** {description}" + + except Exception as e: + return f"āŒ Error creating custom audience: {str(e)}" + +def create_lookalike_audience( + name: str, source_audience_name: str, similarity_percentage: int, + countries: str, tool_context: ToolContext +) -> str: + """Creates a lookalike audience based on a custom audience.""" + try: + # Verify source audience exists + custom_audiences = tool_context.state.get("custom_audiences", {}) + if source_audience_name not in custom_audiences: + return f"āŒ Source audience '{source_audience_name}' not found. Please create it first." + + countries_list = [country.strip() for country in countries.split(',')] + + lookalike = LookalikeAudience( + name=name, + source_audience=source_audience_name, + similarity_percentage=similarity_percentage, + countries=countries_list + ) + + # Save to session state + if "lookalike_audiences" not in tool_context.state: + tool_context.state["lookalike_audiences"] = {} + + tool_context.state["lookalike_audiences"][name] = lookalike.model_dump() + + return f"āœ… Lookalike audience '{name}' created successfully!\n\n**Source:** {source_audience_name}\n**Similarity:** {similarity_percentage}%\n**Countries:** {', '.join(countries_list)}" + + except Exception as e: + return f"āŒ Error creating lookalike audience: {str(e)}" + +def setup_ab_test( + test_name: str, variable_to_test: str, variations_json: str, + duration_days: int, success_metric: str, traffic_split: str, + tool_context: ToolContext +) -> str: + """Sets up an A/B test configuration.""" + try: + variations = json.loads(variations_json) + split = [int(x.strip()) for x in traffic_split.split(',')] + + if sum(split) != 100: + return "āŒ Traffic split percentages must sum to 100%" + + ab_test = ABTestConfiguration( + test_name=test_name, + variable_to_test=variable_to_test, + variations=variations, + traffic_split=split, + duration_days=duration_days, + success_metric=success_metric + ) + + # Save to session state + if "ab_tests" not in tool_context.state: + tool_context.state["ab_tests"] = [] + + tool_context.state["ab_tests"].append(ab_test.model_dump()) + + return f"āœ… A/B test '{test_name}' configured successfully!\n\n**Testing:** {variable_to_test}\n**Duration:** {duration_days} days\n**Success Metric:** {success_metric}\n**Traffic Split:** {traffic_split}" + + except Exception as e: + return f"āŒ Error setting up A/B test: {str(e)}" + +def upload_brand_guidelines( + primary_colors: str, secondary_colors: str, fonts: str, + voice_guidelines: str, visual_style_notes: str, + tool_context: ToolContext +) -> str: + """Uploads and stores brand guidelines.""" + try: + guidelines = BrandGuidelines( + primary_colors=[color.strip() for color in primary_colors.split(',') if color.strip()], + secondary_colors=[color.strip() for color in secondary_colors.split(',') if color.strip()], + fonts=[font.strip() for font in fonts.split(',') if font.strip()], + voice_guidelines=voice_guidelines, + visual_style_notes=visual_style_notes + ) + + tool_context.state["brand_guidelines"] = guidelines.model_dump() + + return f"āœ… Brand guidelines uploaded successfully!\n\n**Primary Colors:** {', '.join(guidelines.primary_colors)}\n**Fonts:** {', '.join(guidelines.fonts)}\n**Voice Guidelines:** {voice_guidelines[:100]}..." + + except Exception as e: + return f"āŒ Error uploading brand guidelines: {str(e)}" + +def create_advanced_pro_brief( + campaign_name: str, company_name: str, website_url: str, + objective: str, product_name: str, product_description: str, + usp: str, landing_url: str, persona: str, core_message: str, + tone: str, primary_cta: str, platforms: str, + tool_context: ToolContext +) -> str: + """Creates a comprehensive Advanced Pro brief.""" + try: + # Get existing data from session + brand_guidelines = tool_context.state.get("brand_guidelines", {}) + custom_audiences = tool_context.state.get("custom_audiences", {}) + lookalike_audiences = tool_context.state.get("lookalike_audiences", {}) + ab_tests = tool_context.state.get("ab_tests", []) + advanced_audiences = tool_context.state.get("advanced_audiences", {}) + + # Create default audience if none exist + if not advanced_audiences: + default_audience = AdvancedAudienceBuilder( + name="Default Audience", + description="Auto-generated from persona description" + ) + advanced_audiences["Default Audience"] = default_audience.model_dump() + + platforms_list = [p.strip().lower() for p in platforms.split(',')] + + brief = AdvancedProBrief( + campaign_name=campaign_name, + company_name=company_name, + website_url=website_url, + objective=objective, + product_service_name=product_name, + product_description=product_description, + unique_selling_proposition=usp, + target_landing_url=landing_url, + target_persona_detailed=persona, + core_message=core_message, + tone_of_voice=tone, + primary_cta=primary_cta, + platforms=platforms_list, + audience_strategy=list(advanced_audiences.values())[0], # Use first audience + custom_audiences=list(custom_audiences.values()), + lookalike_audiences=list(lookalike_audiences.values()), + ab_tests=ab_tests, + brand_guidelines=brand_guidelines + ) + + tool_context.state["advanced_pro_brief"] = brief.model_dump() + + return _format_advanced_brief_summary(brief) + + except Exception as e: + return f"āŒ Error creating Advanced Pro brief: {str(e)}" + +def analyze_campaign_performance( + campaign_name: str, platform: str, impressions: int, + clicks: int, conversions: int, spend: float, + tool_context: ToolContext +) -> str: + """Analyzes campaign performance and provides optimization recommendations.""" + try: + # Calculate metrics + ctr = (clicks / impressions * 100) if impressions > 0 else 0 + cpc = (spend / clicks) if clicks > 0 else 0 + cpa = (spend / conversions) if conversions > 0 else 0 + conversion_rate = (conversions / clicks * 100) if clicks > 0 else 0 + + # Generate recommendations + recommendations = [] + + if ctr < 1.0: + recommendations.append("CTR is below 1% - consider testing new headlines or images") + if conversion_rate < 2.0: + recommendations.append("Conversion rate is low - review landing page relevance") + if cpc > 2.0: + recommendations.append("CPC is high - consider refining audience targeting") + + performance_summary = f""" +šŸ“Š **Campaign Performance Analysis: {campaign_name}** + +**Key Metrics:** +• Impressions: {impressions:,} +• Clicks: {clicks:,} +• Conversions: {conversions:,} +• CTR: {ctr:.2f}% +• CPC: ${cpc:.2f} +• CPA: ${cpa:.2f} +• Conversion Rate: {conversion_rate:.2f}% + +**Optimization Recommendations:** +{chr(10).join([f"• {rec}" for rec in recommendations]) if recommendations else "• Performance looks good! Continue monitoring."} +""" + + return performance_summary + + except Exception as e: + return f"āŒ Error analyzing performance: {str(e)}" + +def generate_responsive_ad_assets( + headlines: str, descriptions: str, images: str, + tool_context: ToolContext +) -> str: + """Generates assets optimized for responsive ad formats.""" + try: + headlines_list = [h.strip() for h in headlines.split('\n') if h.strip()] + descriptions_list = [d.strip() for d in descriptions.split('\n') if d.strip()] + images_list = [img.strip() for img in images.split('\n') if img.strip()] + + if len(headlines_list) > 15: + return "āŒ Maximum 15 headlines allowed for responsive ads" + if len(descriptions_list) > 4: + return "āŒ Maximum 4 descriptions allowed for responsive ads" + + # Validate character limits + for i, headline in enumerate(headlines_list): + if len(headline) > 30: + return f"āŒ Headline {i+1} exceeds 30 character limit: '{headline}'" + + for i, desc in enumerate(descriptions_list): + if len(desc) > 90: + return f"āŒ Description {i+1} exceeds 90 character limit: '{desc}'" + + asset_summary = f""" +āœ… **Responsive Ad Assets Generated** + +**Headlines ({len(headlines_list)}/15):** +{chr(10).join([f"{i+1}. {h} ({len(h)} chars)" for i, h in enumerate(headlines_list)])} + +**Descriptions ({len(descriptions_list)}/4):** +{chr(10).join([f"{i+1}. {d} ({len(d)} chars)" for i, d in enumerate(descriptions_list)])} + +**Images ({len(images_list)}):** +{chr(10).join([f"• {img}" for img in images_list])} + +**Platform Optimization:** +• Google Responsive Search Ads: Ready āœ… +• Google Responsive Display Ads: Ready āœ… +• Meta Dynamic Ads: Ready āœ… +""" + + # Save assets to session + tool_context.state["responsive_assets"] = { + "headlines": headlines_list, + "descriptions": descriptions_list, + "images": images_list + } + + return asset_summary + + except Exception as e: + return f"āŒ Error generating responsive assets: {str(e)}" + +# Helper functions +def _format_audience_summary(audience: AdvancedAudienceBuilder) -> str: + """Formats audience summary for display.""" + summary_parts = [] + + if audience.demographics.age_min or audience.demographics.age_max: + age_range = f"{audience.demographics.age_min or 'Any'}-{audience.demographics.age_max or 'Any'}" + summary_parts.append(f"Age: {age_range}") + + if audience.geographic.countries: + summary_parts.append(f"Countries: {', '.join(audience.geographic.countries[:3])}{'...' if len(audience.geographic.countries) > 3 else ''}") + + if audience.interests.broad_categories: + summary_parts.append(f"Interests: {', '.join(audience.interests.broad_categories[:3])}{'...' if len(audience.interests.broad_categories) > 3 else ''}") + + if audience.work.industries: + summary_parts.append(f"Industries: {', '.join(audience.work.industries[:3])}{'...' if len(audience.work.industries) > 3 else ''}") + + return '\n'.join([f"• {part}" for part in summary_parts]) if summary_parts else "• Broad targeting (AI-optimized)" + +def _format_advanced_brief_summary(brief: AdvancedProBrief) -> str: + """Formats the advanced brief summary for display.""" + return f""" +šŸŽÆ **Advanced Pro Campaign Brief Created** + +**Campaign Foundation:** +• Name: {brief.campaign_name} +• Company: {brief.company_name} +• Objective: {brief.objective.value.title()} +• Product: {brief.product_service_name} + +**Strategic Messaging:** +• Core Message: {brief.core_message} +• Tone: {brief.tone_of_voice.value.title()} +• Primary CTA: {brief.primary_cta.value} + +**Targeting Strategy:** +• Audience: {brief.audience_strategy.name} +• Custom Audiences: {len(brief.custom_audiences)} +• Lookalike Audiences: {len(brief.lookalike_audiences)} + +**Campaign Configuration:** +• Platforms: {', '.join([p.title() for p in brief.platforms])} +• A/B Tests: {len(brief.ab_tests)} +• Brand Guidelines: {'āœ… Configured' if brief.brand_guidelines else 'āŒ Not set'} + +**Next Steps:** +1. Review and approve the brief configuration +2. Upload creative assets (images, videos, logos) +3. Launch A/B tests for optimization +4. Monitor performance and iterate + +Ready to generate your advanced advertising campaign! +""" diff --git a/python/agents/persona_ad_gen/persona_ad_gen/agent.py b/python/agents/persona_ad_gen/persona_ad_gen/agent.py new file mode 100644 index 000000000..ac8bc814e --- /dev/null +++ b/python/agents/persona_ad_gen/persona_ad_gen/agent.py @@ -0,0 +1,82 @@ +# bettan_agent/agent.py + +from google.adk.agents import LlmAgent +from google.adk.tools import FunctionTool, AgentTool +# Import the new and updated tools +from .tools import ( + confirm_and_save_persona_brief, save_image_as_artifact, + generate_headlines, create_persona_brief_without_headlines +) +from .sub_agents.creative_agent import CreativeAgent +from .debug_image_handler import debug_save_image + +MODEL = "gemini-2.5-flash" + +class PersonaAdGenAgent(LlmAgent): + """The Persona-Driven Ad Builder - Creates compelling advertising scenes through story-driven brief collection.""" + def __init__(self): + super().__init__( + name="persona_ad_gen", + model=MODEL, + instruction="""You are the Persona-Driven Ad Builder. Great ads connect with a real person by solving a real problem. Instead of just filling out a form, you're going to build the user's ad story step-by-step. + +**Your Introduction:** +"Great ads connect with a real person by solving a real problem. Instead of just filling out a form, we're going to build your ad's story step-by-step. First, let's get to know your ideal customer." + +**Your Workflow - Collect these 5 sections one by one:** + +**1. The Ideal Customer (The Persona):** +Ask: "Describe the single person you want to reach. What is a key problem, need, or desire they are currently facing that your business can help with?" +- Get a detailed description of their ideal customer and their specific problem/need/desire + +**2. The 'Aha!' Moment (The Core Message):** +Ask: "Now, imagine that person sees your ad. In one powerful sentence, what is the solution or key takeaway you want them to have? This is the core message that will form the heart of your ad." +- Get one powerful sentence that captures the solution/takeaway + +**3. The Conversation (Tone of Voice):** +Ask: "How should we speak to this person? Choose a Tone of Voice that would resonate with them (e.g., Professional, Empathetic, Witty, Urgent, Conversational, Inspiring)." +- Get the tone of voice + +**4. The Creative Toolbox (Assets & Copy):** +Say: "You've defined the story, now let's gather the materials. Upload your most compelling image that will serve as the foundation for your advertising scenes." +- When image is uploaded, call `save_image_as_artifact` to process and save it +- Then say: "Perfect! Now I'll automatically generate compelling headlines for you based on your story." + +**5. The Targeting Signals (Audience Foundation):** +Ask: "Finally, let's give the AI a strong starting point to find more people like your ideal customer. Provide: +- Location: Where are these customers located? +- Demographics: What is their typical age range and gender? +- Interests: What is one key interest or behavior that defines them?" +- Get location, demographics, and interests + +**After Collection:** +1. **Create Brief**: Call `create_persona_brief_without_headlines` with all collected information (except headlines). +2. **Generate Headlines**: IMMEDIATELY after creating the brief, call `generate_headlines` to automatically create compelling headlines. +3. **Display Headlines**: After generating the headlines, display them to the user and ask for confirmation to proceed. +4. **Create Scenes**: After user confirmation, call the `creative_agent` to generate 4 compelling advertising scenes. + +**CRITICAL WORKFLOW RULES:** +- ALWAYS call `create_persona_brief_without_headlines` first when you have all 5 pieces of information +- IMMEDIATELY after creating the brief, call `generate_headlines` - do not wait for user input +- After generating headlines, display them to the user and ask for confirmation. +- Only proceed to creative agent after user confirmation. +- If headline generation fails, ask user to provide headlines manually + +**Important Notes:** +- Be conversational and story-focused, not form-like +- Help users think about their customer's real problems and motivations +- If they upload an image early, save it but continue the story-building process +- Focus on the narrative and emotional connection throughout +- The workflow is: Brief → Headlines → Scenes (always in this order)""", + tools=[ + FunctionTool(func=confirm_and_save_persona_brief), + FunctionTool(func=create_persona_brief_without_headlines), + FunctionTool(func=generate_headlines), + FunctionTool(func=save_image_as_artifact), + FunctionTool(func=debug_save_image), # Temporary debug tool + AgentTool(agent=CreativeAgent()) + ] + ) + +# This is the single agent that will be run by `adk web`. +root_agent = PersonaAdGenAgent() diff --git a/python/agents/persona_ad_gen/persona_ad_gen/debug_image_handler.py b/python/agents/persona_ad_gen/persona_ad_gen/debug_image_handler.py new file mode 100644 index 000000000..5a28c8b1b --- /dev/null +++ b/python/agents/persona_ad_gen/persona_ad_gen/debug_image_handler.py @@ -0,0 +1,106 @@ +# bettan_agent/debug_image_handler.py +""" +Debug script to test and understand the ADK image handling mechanism +""" + +from typing import Dict +from google.adk.tools import ToolContext + +def debug_save_image(tool_context: ToolContext) -> Dict[str, str]: + """ + Debug version of save_image_as_artifact to understand the structure + """ + result = { + "status": "debug", + "tool_context_attributes": [], + "prompt_info": None, + "image_found": False + } + + # List all attributes of tool_context + result["tool_context_attributes"] = dir(tool_context) + + # Check prompt structure + if hasattr(tool_context, 'prompt'): + prompt = tool_context.prompt + result["prompt_info"] = { + "type": str(type(prompt)), + "has_parts": hasattr(prompt, 'parts'), + "is_list": isinstance(prompt, list), + "attributes": dir(prompt) if prompt else [] + } + + # If prompt has parts, examine them + if hasattr(prompt, 'parts'): + parts_info = [] + for i, part in enumerate(prompt.parts): + part_info = { + "index": i, + "type": str(type(part)), + "has_mime_type": hasattr(part, 'mime_type'), + "mime_type": getattr(part, 'mime_type', None), + "attributes": dir(part)[:10] # First 10 attributes + } + parts_info.append(part_info) + + # Check if this is an image + if hasattr(part, 'mime_type') and part.mime_type and 'image' in str(part.mime_type): + result["image_found"] = True + result["image_part_index"] = i + + result["parts_info"] = parts_info + + # Check for other potential image locations + if hasattr(tool_context, 'messages'): + result["has_messages"] = True + result["messages_count"] = len(tool_context.messages) if hasattr(tool_context.messages, '__len__') else "unknown" + + if hasattr(tool_context, 'history'): + result["has_history"] = True + result["history_count"] = len(tool_context.history) if hasattr(tool_context.history, '__len__') else "unknown" + + if hasattr(tool_context, 'current_message'): + result["has_current_message"] = True + + # Check state functionality + if hasattr(tool_context, 'state'): + result["has_state"] = True + result["state_type"] = str(type(tool_context.state)) + try: + if hasattr(tool_context.state, 'keys') and callable(getattr(tool_context.state, 'keys', None)): + result["state_keys"] = list(tool_context.state.keys()) + else: + result["state_keys"] = [] + except Exception: + result["state_keys"] = [] + + # Check save_artifact functionality + if hasattr(tool_context, 'save_artifact'): + result["has_save_artifact"] = True + + return result + + +def inspect_image_part(part) -> Dict: + """ + Detailed inspection of a part that might be an image + """ + info = { + "type": str(type(part)), + "attributes": dir(part), + "mime_type": None, + "has_data": False, + "data_attributes": [] + } + + if hasattr(part, 'mime_type'): + info["mime_type"] = str(part.mime_type) + + # Check for various data attributes + data_attrs = ['data', 'blob', 'inline_data', 'file_data', 'content', 'bytes'] + for attr in data_attrs: + if hasattr(part, attr): + info["has_data"] = True + info["data_attributes"].append(attr) + + return info diff --git a/python/agents/persona_ad_gen/persona_ad_gen/models.py b/python/agents/persona_ad_gen/persona_ad_gen/models.py new file mode 100644 index 000000000..cc802770a --- /dev/null +++ b/python/agents/persona_ad_gen/persona_ad_gen/models.py @@ -0,0 +1,38 @@ +from typing import Optional, List +from pydantic import BaseModel + +class PersonaDrivenAdBrief(BaseModel): + """Data model for the persona-driven ad creative brief.""" + # The Ideal Customer (The Persona) + ideal_customer: str # Description of the person and their problem/need/desire + + # The 'Aha!' Moment (The Core Message) + core_message: str # One powerful sentence solution/takeaway + + # The Conversation (Tone of Voice) + tone_of_voice: str # Professional, Empathetic, Witty, Urgent, etc. + + # The Creative Toolbox (Assets & Copy) + headlines: List[str] # 5-10 different headlines + # Note: Images are handled separately as artifacts + + # The Targeting Signals (Audience Foundation) + location: str # Where customers are located + demographics: str # Age range and gender + interests: str # 3-5 interests or behaviors + + # Optional paths for additional assets + creative_brief_gcs_path: Optional[str] = None + brand_logo_gcs_path: Optional[str] = None + +# Keep the old model for backward compatibility +class VideoAdBrief(BaseModel): + """Legacy data model for the video ad creative brief.""" + brand: str + product: str + target_location: str + target_age: str + target_gender: str + target_interests: str + creative_brief_gcs_path: Optional[str] = None + brand_logo_gcs_path: Optional[str] = None diff --git a/python/agents/persona_ad_gen/persona_ad_gen/sub_agents/__init__.py b/python/agents/persona_ad_gen/persona_ad_gen/sub_agents/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/python/agents/persona_ad_gen/persona_ad_gen/sub_agents/creative_agent.py b/python/agents/persona_ad_gen/persona_ad_gen/sub_agents/creative_agent.py new file mode 100644 index 000000000..3668a7712 --- /dev/null +++ b/python/agents/persona_ad_gen/persona_ad_gen/sub_agents/creative_agent.py @@ -0,0 +1,53 @@ +# bettan_agent/sub_agents/creative_agent.py + +from google.adk.agents import LlmAgent +from google.adk.tools import FunctionTool +# Import the new editing tool +from ..tools import edit_scene_image + +MODEL = "gemini-2.5-flash" + +class CreativeAgent(LlmAgent): + """A sub-agent specializing in generating editing plans and executing them.""" + def __init__(self): + super().__init__( + name="creative_agent", + model=MODEL, + description="A creative specialist that takes a confirmed brief and a base image, then generates and applies a 4-scene editing plan.", + instruction=( + "You are a world-class visual editor. The user has confirmed their brief and provided a base image.\n\n" + "**Your Workflow:**\n" + "1. The base image has already been saved as 'user:base_image.png' (or similar extension).\n" + "2. First, analyze the confirmed brief and generate a creative 4-scene editing plan.\n" + "3. **IMPORTANT**: First, present the complete storyline and scene descriptions to the user.\n" + "4. Display the headlines that were generated for this brief.\n" + "5. Show the 4-scene plan with detailed storylines.\n" + "6. After presenting the plan, IMMEDIATELY call the `edit_scene_image` tool for each of the 4 scenes to generate the images.\n\n" + "**Confirmed Brief:**\n{confirmed_brief}\n\n" + "**Your Response Format:**\n" + "1. Start with: 'šŸŽ¬ **Creative Storyline Plan**'\n" + "2. Show the generated headlines\n" + "3. Present the 4-scene plan with:\n" + " - Scene 1: [Scene Name]\n" + " Story: [What story this scene tells]\n" + " Visual Description: [Detailed visual description]\n" + " - Scene 2: [Scene Name]\n" + " Story: [What story this scene tells]\n" + " Visual Description: [Detailed visual description]\n" + " - Scene 3: [Scene Name]\n" + " Story: [What story this scene tells]\n" + " Visual Description: [Detailed visual description]\n" + " - Scene 4: [Scene Name]\n" + " Story: [What story this scene tells]\n" + " Visual Description: [Detailed visual description]\n" + "4. After presenting the plan, end with: 'Now, I will generate the 4 advertising scenes for you.'\n" + "5. Do NOT wait for user confirmation. Call the `edit_scene_image` tool for each scene immediately after presenting the plan.\n\n" + "**Important Notes:**\n" + "- Each scene should tell a part of the advertising story that builds toward the core message.\n" + "- Consider the ideal customer and tone of voice when creating scenes.\n" + "- Make the scenes progressively build a narrative that resonates with the target audience.\n" + "- The visual descriptions should be specific and detailed to guide the image generation." + ), + # This agent's only tool is the image editor + tools=[FunctionTool(func=edit_scene_image)] + ) diff --git a/python/agents/persona_ad_gen/persona_ad_gen/sub_agents/headline_agent.py b/python/agents/persona_ad_gen/persona_ad_gen/sub_agents/headline_agent.py new file mode 100644 index 000000000..b1a42b1a3 --- /dev/null +++ b/python/agents/persona_ad_gen/persona_ad_gen/sub_agents/headline_agent.py @@ -0,0 +1,142 @@ +# bettan_agent/sub_agents/headline_agent.py + +from google.adk.tools import ToolContext +import google.genai as genai +import os + +MODEL = "gemini-2.5-flash" + +async def generate_headlines_from_brief(tool_context: ToolContext) -> str: + """ + Generates compelling headlines based on the persona-driven brief. + """ + + # Get the brief information from the tool context + brief_data = tool_context.state.get("confirmed_brief", {}) + + if not brief_data: + return "āŒ No brief found. Please complete the persona-driven brief first." + + ideal_customer = brief_data.get("ideal_customer", "") + core_message = brief_data.get("core_message", "") + tone_of_voice = brief_data.get("tone_of_voice", "") + + if not all([ideal_customer, core_message, tone_of_voice]): + return "āŒ Incomplete brief. Please ensure ideal customer, core message, and tone of voice are provided." + + print(f"šŸŽÆ Generating headlines for: {ideal_customer[:50]}...") + print(f"šŸ“ Core message: {core_message[:50]}...") + print(f"šŸŽ­ Tone: {tone_of_voice}") + + try: + # Get project information from environment + project_id = os.environ.get("GOOGLE_CLOUD_PROJECT") + if not project_id: + raise ValueError("GOOGLE_CLOUD_PROJECT environment variable not set") + + # Initialize the client with Vertex AI configuration + client = genai.Client(vertexai=True, project=project_id) + + # Create the headline generation prompt + headline_prompt = f"""You are an expert advertising copywriter specializing in creating compelling headlines that convert. + +Based on this persona-driven brief, generate 8-12 powerful headlines: + +**Ideal Customer:** {ideal_customer} +**Core Message:** {core_message} +**Tone of Voice:** {tone_of_voice} + +Create headlines that: +1. Speak directly to the ideal customer's pain points and desires +2. Capture the essence of the core message +3. Match the specified tone of voice +4. Are compelling and action-oriented +5. Vary in length and approach (some short and punchy, others more descriptive) +6. Include emotional triggers that resonate with the target audience + +Generate headlines in different styles: +- Problem/Solution focused +- Benefit-driven +- Curiosity-inducing +- Social proof/testimonial style +- Urgency/scarcity driven +- Question-based +- Direct and bold statements + +Format your response as a numbered list of headlines, each on a new line. +Focus on quality over quantity - each headline should be compelling and conversion-focused. + +IMPORTANT: Only generate the numbered list of headlines. Do not include any other text, explanations, or formatting.""" + + print(f"šŸ¤– Calling Google Gen AI with prompt length: {len(headline_prompt)}") + + # Generate headlines using the same pattern as other tools + response = client.models.generate_content( + model=MODEL, + contents=headline_prompt, + ) + + # Extract the text response + result_text = response.text + + print(f"āœ… LLM response received, length: {len(result_text)}") + print(f"šŸ“ Response preview: {result_text[:300]}...") + + # Parse the response to extract headlines + headlines = [] + lines = result_text.strip().split('\n') + print(f"šŸ” Parsing {len(lines)} lines from response") + + for i, line in enumerate(lines): + line = line.strip() + + if line and (line[0].isdigit() or line.startswith('-') or line.startswith('•')): + # Remove numbering/bullets and clean up + headline = line + # Remove common prefixes + for prefix in ['1.', '2.', '3.', '4.', '5.', '6.', '7.', '8.', '9.', '10.', '11.', '12.', '-', '•']: + if headline.startswith(prefix): + headline = headline[len(prefix):].strip() + break + + if headline and len(headline) > 5: # Basic validation + headlines.append(headline) + print(f" āœ… Added headline: '{headline}'") + + print(f"šŸŽÆ Found {len(headlines)} headlines after parsing") + + if len(headlines) < 5: + print(f"āŒ Not enough headlines generated. Only found {len(headlines)}") + print(f"šŸ“ Full LLM response for debugging:\n{result_text}") + return f"āŒ Could not generate enough quality headlines. Only found {len(headlines)} headlines. Generated response: {result_text[:500]}..." + + # Update the brief with generated headlines + brief_data["headlines"] = headlines[:12] # Limit to 12 headlines + tool_context.state["confirmed_brief"] = brief_data + + # Format the response + headline_list = '\n'.join([f"• {headline}" for headline in headlines[:12]]) + + return f"""āœ… **Generated {len(headlines[:12])} Compelling Headlines** + +{headline_list} + +**Headlines Analysis:** +• **Target Audience:** {ideal_customer} +• **Core Message:** {core_message} +• **Tone:** {tone_of_voice} + +These headlines have been automatically added to your brief. You can now proceed with image generation or make any adjustments if needed. + +**Next Steps:** +1. Review the generated headlines +2. Upload your base image if you haven't already +3. Proceed with creating your advertising scenes + +Ready to create compelling ads that convert!""" + + except Exception as e: + print(f"āŒ Error in generate_headlines_from_brief: {str(e)}") + import traceback + traceback.print_exc() + return f"āŒ Error generating headlines: {str(e)}. Please try again or provide headlines manually." diff --git a/python/agents/persona_ad_gen/persona_ad_gen/tools.py b/python/agents/persona_ad_gen/persona_ad_gen/tools.py new file mode 100644 index 000000000..04905e2f5 --- /dev/null +++ b/python/agents/persona_ad_gen/persona_ad_gen/tools.py @@ -0,0 +1,418 @@ +# bettan_agent/tools.py + +from typing import Dict +import base64 +import io +import os + +from google.adk.tools import ToolContext +from .models import PersonaDrivenAdBrief, VideoAdBrief +from .sub_agents.headline_agent import generate_headlines_from_brief +import google.genai as genai +from google.genai.types import GenerateContentConfig, Part + +# Use the image generation model with Google Gen AI SDK +EDIT_MODEL = "gemini-2.5-flash-image-preview" + +def confirm_and_save_persona_brief( + ideal_customer: str, core_message: str, tone_of_voice: str, + headlines: str, location: str, + demographics: str, interests: str, tool_context: ToolContext +) -> str: + """Confirms the persona-driven brief and saves it to the session state.""" + # Parse headlines from string to list + headlines_list = [h.strip() for h in headlines.split('\n') if h.strip()] + + brief = PersonaDrivenAdBrief( + ideal_customer=ideal_customer, + core_message=core_message, + tone_of_voice=tone_of_voice, + headlines=headlines_list, + location=location, + demographics=demographics, + interests=interests + ) + tool_context.state["confirmed_brief"] = brief.model_dump() + + base_image_status = "Not yet provided." + if "base_image_filename" in tool_context.state: + base_image_status = f"Received ({tool_context.state['base_image_filename']})." + + confirmation_message = ( + "Perfect! Here's your complete ad story:\n\n" + f"**The Ideal Customer:** {ideal_customer}\n\n" + f"**The 'Aha!' Moment:** {core_message}\n\n" + f"**Tone of Voice:** {tone_of_voice}\n\n" + f"**Headlines ({len(headlines_list)}):**\n" + + '\n'.join([f"• {h}" for h in headlines_list]) + "\n\n" + f"**Location:** {location}\n" + f"**Demographics:** {demographics}\n" + f"**Interests:** {interests}\n\n" + f"**Base Image:** {base_image_status}\n\n" + "Does this capture your vision? If yes, I'll create 4 compelling advertising scenes based on this story!" + ) + return confirmation_message + +def confirm_and_save_brief( + brand: str, product: str, target_location: str, target_age: str, + target_gender: str, target_interests: str, tool_context: ToolContext +) -> str: + """Legacy function - Confirms the brief and saves it to the session state.""" + brief = VideoAdBrief( + brand=brand, product=product, target_location=target_location, + target_age=target_age, target_gender=target_gender, + target_interests=target_interests + ) + tool_context.state["confirmed_brief"] = brief.model_dump() + + base_image_status = "Not yet provided." + if "base_image_filename" in tool_context.state: + base_image_status = f"Received ({tool_context.state['base_image_filename']})." + + confirmation_message = ( + "OK, I've got the following information:\n\n" + f"Brand: {brand}\n" + f"Product: {product}\n" + f"Target Location: {target_location}\n" + f"Target Age: {target_age}\n" + f"Target Gender: {target_gender}\n" + f"Target Interests: {target_interests}\n" + f"Base Image: {base_image_status}\n\n" + "Is that all correct?" + ) + return confirmation_message + +async def save_image_as_artifact(tool_context: ToolContext) -> Dict[str, str]: + """ + Saves the user-provided image as a user-scoped artifact and + stores its filename in the session state. + """ + import json + + try: + print("šŸ” Starting image search in tool_context...") + print(f"Tool context type: {type(tool_context)}") + print(f"Tool context attributes: {[attr for attr in dir(tool_context) if not attr.startswith('_')][:20]}") + + # Method 1: Check user_content (this is where ADK stores user input including images) + if hasattr(tool_context, 'user_content') and tool_context.user_content: + print(f"User content type: {type(tool_context.user_content)}") + + # If user_content has parts attribute + if hasattr(tool_context.user_content, 'parts'): + print(f"Found user_content.parts with {len(tool_context.user_content.parts)} parts") + for i, part in enumerate(tool_context.user_content.parts): + print(f" Part {i}: type={type(part)}, has_mime_type={hasattr(part, 'mime_type')}") + + # Check for inline_data which might contain the image + if hasattr(part, 'inline_data'): + print(f" Part {i} has inline_data") + if hasattr(part.inline_data, 'mime_type'): + print(f" inline_data.mime_type: {part.inline_data.mime_type}") + if 'image' in str(part.inline_data.mime_type): + print(f" āœ… Found image in part {i} inline_data!") + extension = str(part.inline_data.mime_type).split("/")[-1] if "/" in str(part.inline_data.mime_type) else "png" + artifact_filename = f"user:base_image.{extension}" + + # Save the part (which contains inline_data) as an artifact + await tool_context.save_artifact(filename=artifact_filename, artifact=part) + + tool_context.state["base_image_filename"] = artifact_filename + + print(f"āœ… Saved uploaded image as artifact '{artifact_filename}'") + return {"status": "success", "message": "I've saved your image. Let's continue with the brief."} + + # Check for direct mime_type + if hasattr(part, 'mime_type'): + print(f" mime_type: {part.mime_type}") + if part.mime_type and 'image' in str(part.mime_type): + print(f" āœ… Found image in part {i}!") + extension = str(part.mime_type).split("/")[-1] if "/" in str(part.mime_type) else "png" + artifact_filename = f"user:base_image.{extension}" + + # Save the image as an artifact + await tool_context.save_artifact(filename=artifact_filename, artifact=part) + tool_context.state["base_image_filename"] = artifact_filename + + print(f"āœ… Saved uploaded image as artifact '{artifact_filename}'") + return {"status": "success", "message": "I've saved your image. Let's continue with the brief."} + + # Deep inspection of part attributes + print(f" Part {i} attributes: {[attr for attr in dir(part) if not attr.startswith('_')][:10]}") + + # Check if user_content is a Content object with parts + if hasattr(tool_context.user_content, '__dict__'): + print(f"User content attributes: {list(tool_context.user_content.__dict__.keys())}") + + # Method 2: Fallback to check prompt if user_content doesn't have the image + if hasattr(tool_context, 'prompt') and tool_context.prompt: + print(f"Checking prompt as fallback - type: {type(tool_context.prompt)}") + + if hasattr(tool_context.prompt, 'parts'): + for i, part in enumerate(tool_context.prompt.parts): + if hasattr(part, 'mime_type') and part.mime_type and 'image' in str(part.mime_type): + print(f" āœ… Found image in prompt part {i}!") + extension = str(part.mime_type).split("/")[-1] if "/" in str(part.mime_type) else "png" + artifact_filename = f"user:base_image.{extension}" + + await tool_context.save_artifact(filename=artifact_filename, artifact=part) + tool_context.state["base_image_filename"] = artifact_filename + + print(f"āœ… Saved uploaded image as artifact '{artifact_filename}'") + return {"status": "success", "message": "I've saved your image. Let's continue with the brief."} + + # Method 3: Check for messages/history + print("āš ļø Could not find image in user_content.parts, checking messages/history...") + + if hasattr(tool_context, 'messages'): + print(f"Found messages attribute with type: {type(tool_context.messages)}") + if hasattr(tool_context.messages, '__len__'): + print(f" Number of messages: {len(tool_context.messages)}") + + if hasattr(tool_context, 'history'): + print(f"Found history attribute with type: {type(tool_context.history)}") + if hasattr(tool_context.history, '__len__'): + print(f" Number of history items: {len(tool_context.history)}") + for i, item in enumerate(reversed(tool_context.history[-3:])): # Check last 3 items + print(f" History item {i}: type={type(item)}") + if hasattr(item, 'parts'): + for j, part in enumerate(item.parts): + if hasattr(part, 'mime_type') and part.mime_type and 'image' in str(part.mime_type): + print(f" āœ… Found image in history item {i}, part {j}!") + extension = str(part.mime_type).split("/")[-1] if "/" in str(part.mime_type) else "png" + artifact_filename = f"user:base_image.{extension}" + + await tool_context.save_artifact(filename=artifact_filename, artifact=part) + tool_context.state["base_image_filename"] = artifact_filename + + print(f"āœ… Saved uploaded image as artifact '{artifact_filename}'") + return {"status": "success", "message": "I've saved your image. Let's continue with the brief."} + + # Method 3: Check for conversation attribute + if hasattr(tool_context, 'conversation'): + print(f"Found conversation attribute with type: {type(tool_context.conversation)}") + + # Method 4: Check for current_message + if hasattr(tool_context, 'current_message'): + print(f"Found current_message attribute with type: {type(tool_context.current_message)}") + if hasattr(tool_context.current_message, 'parts'): + for i, part in enumerate(tool_context.current_message.parts): + print(f" Current message part {i}: type={type(part)}") + if hasattr(part, 'mime_type') and part.mime_type and 'image' in str(part.mime_type): + print(f" āœ… Found image in current_message part {i}!") + extension = str(part.mime_type).split("/")[-1] if "/" in str(part.mime_type) else "png" + artifact_filename = f"user:base_image.{extension}" + + await tool_context.save_artifact(filename=artifact_filename, artifact=part) + tool_context.state["base_image_filename"] = artifact_filename + + print(f"āœ… Saved uploaded image as artifact '{artifact_filename}'") + return {"status": "success", "message": "I've saved your image. Let's continue with the brief."} + + # Method 5: Try to access the last user input directly + if hasattr(tool_context, 'last_user_input'): + print(f"Found last_user_input attribute with type: {type(tool_context.last_user_input)}") + + # If we still haven't found the image, print all available attributes for debugging + print("\nāŒ Could not find image. Here are ALL tool_context attributes:") + for attr in dir(tool_context): + if not attr.startswith('_'): + try: + value = getattr(tool_context, attr) + print(f" {attr}: {type(value)}") + except: + print(f" {attr}: ") + + return {"status": "error", "message": "No valid image data was found. The debug output above shows what was searched. Please check the terminal for details."} + + except Exception as e: + print(f"āŒ Error during image saving: {e}") + import traceback + traceback.print_exc() + return {"status": "error", "message": f"Sorry, I couldn't save the image. Error: {str(e)}"} + +async def edit_scene_image( + base_image_filename: str, edit_prompt: str, output_filename: str, tool_context: ToolContext +) -> Dict[str, str]: + """ + Loads a base image artifact, applies a text-based edit using Gemini model with image generation, + and saves the result as a new image artifact. + """ + try: + print(f"šŸŽØ Editing image '{base_image_filename}' for scene '{output_filename}'...") + + # Load the base image artifact + base_image_part = await tool_context.load_artifact(filename=base_image_filename) + + if not base_image_part: + raise ValueError(f"Could not load base image artifact: {base_image_filename}") + + # Debug: Check what type of object we got from load_artifact + print(f"Base image part type: {type(base_image_part)}") + print(f"Base image part attributes: {[attr for attr in dir(base_image_part) if not attr.startswith('_')][:10]}") + + # Use Google Gen AI SDK for image generation + # The base_image_part should already be a google.genai.types.Part + if not hasattr(base_image_part, 'inline_data') or not base_image_part.inline_data: + raise ValueError("Could not extract image data from loaded artifact") + + print(f"āœ… Using google.genai.Part with mime_type: {base_image_part.inline_data.mime_type}") + + # Get project information from environment + project_id = os.environ.get("GOOGLE_CLOUD_PROJECT") + if not project_id: + raise ValueError("GOOGLE_CLOUD_PROJECT environment variable not set") + + # Initialize the client with Vertex AI configuration following the official documentation + client = genai.Client(vertexai=True, project=project_id) + + # Create the content properly structured for the API + from google.genai.types import Content, Part + + # Ensure we create a proper Part object for the text + text_part = Part(text=edit_prompt) + + # Create a single Content object with both image and text parts + # Use the proper structure that the API expects + content = Content( + role="user", + parts=[base_image_part, text_part] + ) + + # Configure for image generation using the patterns from the documentation + config = GenerateContentConfig( + response_modalities=["TEXT", "IMAGE"], + candidate_count=1, + temperature=0.7, + max_output_tokens=1024, + safety_settings=[ + {"method": "PROBABILITY"}, + {"category": "HARM_CATEGORY_DANGEROUS_CONTENT"}, + {"threshold": "BLOCK_MEDIUM_AND_ABOVE"}, + ] + ) + + print(f"šŸ”§ About to call generate_content with:") + print(f" Model: {EDIT_MODEL}") + print(f" Project: {project_id}") + print(f" Content parts: {len(content.parts)}") + print(f" Response modalities: {config.response_modalities}") + + # Execute the API call for image generation using the official pattern + # Pass the content directly, not as a list + try: + response = client.models.generate_content( + model=EDIT_MODEL, + contents=content, + config=config + ) + print(f"āœ… API call successful, response type: {type(response)}") + except Exception as api_error: + print(f"āŒ API call failed with error: {api_error}") + print(f" Error type: {type(api_error)}") + raise api_error + + # Process the response to extract both text and image + generated_text = "" + generated_image_part = None + + if response.candidates and response.candidates[0].content.parts: + print(f"šŸ” Processing {len(response.candidates[0].content.parts)} response parts...") + + for i, part in enumerate(response.candidates[0].content.parts): + print(f" Part {i} type: {type(part)}") + + # Extract text content + if hasattr(part, 'text') and part.text: + generated_text += part.text + print(f" šŸ“ Found text: {part.text[:100]}...") + + # Extract image content + if hasattr(part, 'inline_data') and part.inline_data: + if 'image' in str(part.inline_data.mime_type): + generated_image_part = part + print(f" šŸ–¼ļø Found generated image with mime_type: {part.inline_data.mime_type}") + + # Save the generated image as an artifact if we got one + if generated_image_part: + try: + await tool_context.save_artifact(filename=output_filename, artifact=generated_image_part) + print(f"āœ… Saved generated image as artifact '{output_filename}'") + + return { + "status": "success", + "description": generated_text or "Image generated successfully", + "image_filename": output_filename, + "message": f"Generated and saved image for {output_filename}. You can view it in the ADK web interface under artifacts." + } + except Exception as save_error: + print(f"āŒ Error saving generated image: {save_error}") + return { + "status": "partial_success", + "description": generated_text or "Image was generated but could not be saved", + "message": f"Generated image for {output_filename} but failed to save: {str(save_error)}" + } + + # If we only got text (no image generated) + if generated_text: + print(f"āœ… Generated text description for '{output_filename}': {generated_text[:100]}...") + return { + "status": "text_only", + "description": generated_text, + "message": f"Generated text description for {output_filename} (no image was produced)" + } + + # If no response was generated, check for blocking + print(f"āš ļø No content found in response") + if hasattr(response, 'prompt_feedbacks'): + for feedback in response.prompt_feedbacks: + print(f"Block Reason: {feedback.block_reason}") + if hasattr(feedback, 'safety_ratings'): + for rating in feedback.safety_ratings: + print(rating) + + return {"status": "error", "message": f"Could not generate content for {output_filename}. No text or image content found in response."} + + except Exception as e: + print(f"āŒ Error during image editing: {e}") + import traceback + traceback.print_exc() + return {"status": "error", "message": f"Sorry, I couldn't edit the image for {output_filename}. Error: {str(e)}"} + +async def generate_headlines(tool_context: ToolContext) -> str: + """ + Generates compelling headlines using the headline agent based on the persona-driven brief. + """ + print("šŸŽÆ Starting headline generation...") + result = await generate_headlines_from_brief(tool_context) + print(f"āœ… Headlines generation finished with result: {result[:200]}...") + return result + +async def create_persona_brief_without_headlines( + ideal_customer: str, core_message: str, tone_of_voice: str, + location: str, demographics: str, interests: str, + tool_context: ToolContext +) -> str: + """ + Creates a persona-driven brief, saves it, and then immediately + triggers headline generation. + """ + # Create brief with empty headlines initially + brief = PersonaDrivenAdBrief( + ideal_customer=ideal_customer, + core_message=core_message, + tone_of_voice=tone_of_voice, + headlines=[], # Will be populated by headline generation + location=location, + demographics=demographics, + interests=interests + ) + tool_context.state["confirmed_brief"] = brief.model_dump() + + print("āœ… Persona brief created and saved. Immediately generating headlines...") + + # Directly call the headline generation function + headline_result = await generate_headlines(tool_context) + + # Return the result from the headline generation + return headline_result diff --git a/python/agents/persona_ad_gen/pyproject.toml b/python/agents/persona_ad_gen/pyproject.toml new file mode 100644 index 000000000..0a6c4c3cb --- /dev/null +++ b/python/agents/persona_ad_gen/pyproject.toml @@ -0,0 +1,48 @@ +[tool.poetry] +name = "persona-ad-gen" +version = "0.1.0" +description = "Personalized Ad Creation Engine: An AI agent that generates advertising assets from your photos." +authors = ["Michael Bettan "] +license = "Apache License 2.0" +readme = "README.md" +packages = [{include = "persona_ad_gen", from = "src"}] + +[tool.poetry.dependencies] +python = "^3.10" +google-adk = {version = "^1.0.0", extras = ["web"]} +google-genai = "^1.32.0" +pydantic = "^2.10.6" +python-dotenv = "^1.0.1" +google-cloud-aiplatform = { version = "^1.111.0", extras = [ + "adk", + "agent-engines", +] } + +[tool.poetry.group.dev] +optional = true + +[tool.poetry.group.dev.dependencies] +google-adk = { extras = ["eval", "web"], version = "^1.0.0" } +google-cloud-aiplatform = { version = "^1.111.0", extras = [ + "adk", + "agent-engines", + "evaluation", +] } +pytest = "^8.3.5" +black = "^25.1.0" +pytest-asyncio = "^0.26.0" +pandas = "^2.2.3" +tabulate = "^0.9.0" + +[tool.poetry.group.deployment] +optional = true + +[tool.poetry.group.deployment.dependencies] +absl-py = "^2.2.1" + +[tool.pytest.ini_options] +asyncio_mode = "auto" + +[build-system] +requires = ["poetry-core>=2.0.0,<3.0.0"] +build-backend = "poetry.core.masonry.api" From 51488cbd5cf49e86761bab49c157ae7701638739 Mon Sep 17 00:00:00 2001 From: "bettan.michael@gmail.com" Date: Sat, 6 Sep 2025 01:37:47 -0400 Subject: [PATCH 2/3] Adding a new agent: persona_ad_gen --- python/agents/persona_ad_gen/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/python/agents/persona_ad_gen/README.md b/python/agents/persona_ad_gen/README.md index eeb9c1730..28c7d7598 100644 --- a/python/agents/persona_ad_gen/README.md +++ b/python/agents/persona_ad_gen/README.md @@ -4,7 +4,7 @@ An intelligent agent that transforms your photos into compelling advertising sce ## Overview -PersonaAd Gen is an ADK-based agent system that creates personalized advertising content by combining user personas with creative image generation. The agent collects detailed customer insights and transforms uploaded images into multiple advertising scenes tailored to specific target audiences. +PersonaAd Gen is an ADK-based agent that creates personalized advertising content by combining user personas with creative image generation from existing images using Gemini 2.5 Flash Images (Preview). The agent collects detailed customer insights and transforms uploaded images into multiple advertising scenes tailored to specific target audiences. ### Key Components From a6aeeaf5dd24417ab6268b550b1cfdfd1c2f004d Mon Sep 17 00:00:00 2001 From: "bettan.michael@gmail.com" Date: Sat, 6 Sep 2025 01:50:45 -0400 Subject: [PATCH 3/3] Adding a new agent: persona_ad_gen --- python/agents/persona_ad_gen/README.md | 510 ++++++++++++++++++------- 1 file changed, 367 insertions(+), 143 deletions(-) diff --git a/python/agents/persona_ad_gen/README.md b/python/agents/persona_ad_gen/README.md index 28c7d7598..31d28a348 100644 --- a/python/agents/persona_ad_gen/README.md +++ b/python/agents/persona_ad_gen/README.md @@ -1,211 +1,435 @@ # Persona Ad Gen - AI-Powered Advertising Scene Generator -An intelligent agent that transforms your photos into compelling advertising scenes through persona-driven storytelling. +This project implements an AI-powered advertising content generator that transforms user photos into compelling, persona-driven advertising scenes. The agent guides users through a story-driven brief collection process, automatically generates headlines, and creates multiple advertising scenes tailored to specific target audiences. ## Overview -PersonaAd Gen is an ADK-based agent that creates personalized advertising content by combining user personas with creative image generation from existing images using Gemini 2.5 Flash Images (Preview). The agent collects detailed customer insights and transforms uploaded images into multiple advertising scenes tailored to specific target audiences. +The Persona Ad Gen agent is designed to revolutionize advertising content creation by focusing on the human story behind every great ad. Instead of traditional form-filling, it engages users in a conversational journey to understand their ideal customer's problems, desires, and motivations. The agent then leverages this deep understanding to generate visually compelling advertising scenes that resonate with the target audience. -### Key Components +## Agent Details -- **PersonaAdGenAgent**: Main orchestrator that guides users through a story-driven brief collection process -- **CreativeAgent**: Sub-agent that generates 4 unique advertising scenes based on the collected persona -- **Headline Generator**: Automatically creates compelling headlines based on the persona story +The key features of the Persona Ad Gen agent include: -## Features +| Feature | Description | +| ------------------ | ------------------------------------------------ | +| _Interaction Type_ | Conversational, Story-driven | +| _Complexity_ | Intermediate | +| _Agent Type_ | Multi-Agent (Main + Creative Sub-agent) | +| _Components_ | Tools, Image Generation, Multimodal | +| _Vertical_ | Marketing/Advertising | -✨ **Story-Driven Brief Collection**: Instead of traditional forms, build your ad's narrative step-by-step -šŸŽÆ **Persona-Focused**: Centers on understanding your ideal customer's problems and desires -šŸ–¼ļø **Multi-Scene Generation**: Creates 4 distinct advertising scenes from a single uploaded image -šŸ“ **Automatic Headline Creation**: Generates compelling headlines based on your story -šŸ’¾ **Artifact Management**: Saves all generated content for easy access and download +### Agent Architecture -## Prerequisites +The agent is built using a multi-agent architecture with specialized components: -- Python 3.9 or higher -- Poetry or pip for dependency management -- Google Cloud Project (for Vertex AI) or Google AI Studio API key +1. **PersonaAdGenAgent (Main Orchestrator)** + - Guides users through the 5-section story collection process + - Manages session state and workflow progression + - Coordinates with sub-agents for specialized tasks + +2. **CreativeAgent (Sub-agent)** + - Generates 4 unique advertising scenes based on the persona brief + - Utilizes Gemini's image generation capabilities + - Creates scene-specific prompts optimized for visual impact + +3. **Headline Generation System** + - Automatically creates compelling headlines after brief collection + - Generates multiple options based on persona, message, and tone + - Integrates seamlessly into the workflow without user intervention + +### Workflow Stages + +1. **The Ideal Customer (Persona)** - Understanding the target audience's problems and desires +2. **The 'Aha!' Moment (Core Message)** - Defining the solution in one powerful sentence +3. **The Conversation (Tone of Voice)** - Selecting the appropriate communication style +4. **The Creative Toolbox (Assets & Copy)** - Uploading base images and generating headlines +5. **The Targeting Signals (Audience Foundation)** - Collecting demographic and interest data + +### Key Features + +- **Story-Driven Brief Collection:** + - Conversational approach to gathering advertising requirements + - Focus on understanding customer problems rather than product features + - Progressive disclosure of information needs + +- **Automatic Headline Generation:** + - Creates compelling headlines based on the collected persona story + - Multiple variations to choose from + - Tone-matched to the target audience + +- **Multi-Scene Image Generation:** + - Transforms uploaded images into 4 distinct advertising scenes + - Each scene tells a different aspect of the brand story + - Optimized for various marketing channels + +- **Session State Management:** + - Maintains context throughout the conversation + - Stores brief details, images, and generated content + - Enables seamless workflow progression + +- **Artifact Management:** + - Saves all generated images as downloadable artifacts + - Organized naming convention for easy identification + - Integration with Google Cloud Storage for persistence + +### Tools + +The agent has access to the following tools: + +- `create_persona_brief_without_headlines(persona: str, core_message: str, tone: str, location: str, demographics: str, interests: str) -> dict`: Creates the initial persona brief structure before headline generation. + +- `generate_headlines(persona: str, core_message: str, tone: str) -> list[str]`: Automatically generates compelling headlines based on the persona story. + +- `save_image_as_artifact(image_data: str) -> str`: Processes and saves uploaded images as artifacts for scene generation. + +- `confirm_and_save_persona_brief(brief: dict) -> str`: Saves the complete creative brief to session state. + +- `edit_scene_image(base_image: str, scene_prompt: str, scene_number: int) -> str`: Generates new advertising scenes based on edit prompts using the uploaded image as source. + +- `debug_save_image(image_data: str, filename: str) -> str`: Debug utility for image processing and storage operations. + +## Setup and Installation + +### Prerequisites + +- Python 3.9+ +- Poetry (for dependency management) +- Google ADK SDK +- Google Cloud Project (for Vertex AI integration) or Google AI Studio API key - GCS bucket for artifact storage -## Installation +### Installation -```bash -# Install dependencies using Poetry -poetry install +1. **Prerequisites:** -# Or using pip -pip install google-adk google-genai pydantic python-dotenv google-cloud-aiplatform + For the Agent Engine deployment steps, you will need a Google Cloud Project. Once you have created your project, [install the Google Cloud SDK](https://cloud.google.com/sdk/docs/install). Then run the following command to authenticate with your project: + ```bash + gcloud auth login + ``` + + You also need to enable certain APIs. Run the following command to enable the required APIs: + ```bash + gcloud services enable aiplatform.googleapis.com storage.googleapis.com + ``` -# For evaluation capabilities, install with eval extras -pip install "google-adk[eval]" +2. Clone the repository: -# If using pipx for ADK installation, inject eval dependencies -pipx inject google-adk pandas tabulate rouge-score -``` + ```bash + git clone https://github.com/google/adk-samples.git + cd adk-samples/python/agents/persona_ad_gen + ``` -## Configuration + For the rest of this tutorial **ensure you remain in the `agents/persona_ad_gen` directory**. -### Environment Variables (.env) +3. Install dependencies using Poetry: -Create a `.env` file in the project root with the following variables: + If you have not installed poetry before then run `pip install poetry` first. Then you can create your virtual environment and install all dependencies using: -```bash -# Google Cloud Project Configuration -export GOOGLE_CLOUD_PROJECT=your-project-id -export GOOGLE_CLOUD_LOCATION=global + **Note for Linux users:** If you get an error related to `keyring` during the installation, you can disable it by running the following command: + ```bash + poetry config keyring.enabled false + ``` + This is a one-time setup. -# Artifact Storage Configuration -export ADK_ARTIFACT_SERVICE_TYPE=GCS -export ADK_GCS_BUCKET_NAME=your-bucket-name + ```bash + poetry install + ``` -# Vertex AI Configuration (optional) -export GOOGLE_GENAI_USE_VERTEXAI=true -``` + To activate the virtual environment run: + ```bash + poetry env activate + ``` -**Note**: -- Replace `your-project-id` with your actual Google Cloud project ID -- Replace `your-bucket-name` with your GCS bucket for storing artifacts -- Set `GOOGLE_GENAI_USE_VERTEXAI=true` if using Vertex AI instead of direct Gemini API +4. Install evaluation dependencies (optional): -## Usage + For running evaluation tests, you need additional dependencies: + ```bash + pip install "google-adk[eval]" + + # If using pipx for ADK installation + pipx inject google-adk pandas tabulate rouge-score + ``` -### Running the Agent +5. Set up environment variables: -1. **Start the agent using ADK Web**: -```bash -adk web -``` + Create a `.env` file in the project root with the following variables: + ```bash + # Google Cloud Project Configuration (global is must have for Gemini 2.5 Flash Images) + export GOOGLE_CLOUD_PROJECT=your-project-id + export GOOGLE_CLOUD_LOCATION=global + + # Artifact Storage Configuration + export ADK_ARTIFACT_SERVICE_TYPE=GCS + export ADK_GCS_BUCKET_NAME=your-bucket-name + + # For Vertex AI (set to true if using Vertex AI) + export GOOGLE_GENAI_USE_VERTEXAI=true + + # For Google AI Studio (if not using Vertex AI) + # export GOOGLE_GENAI_USE_VERTEXAI=false + # export GOOGLE_API_KEY=your-api-key + ``` + +## Running the Agent + +You can run the agent using the ADK command in your terminal from the root project directory: + +1. Run agent in CLI: + + ```bash + adk run persona_ad_gen + ``` + +2. Run agent with ADK Web UI: + + ```bash + adk web + ``` + + The web interface will be available at the URL printed in the console (typically http://localhost:5000) + +3. Alternative execution methods: + + ```bash + # Using the run script (if available) + ./run_agent.sh + + # Direct Python execution + python -m adk.web_server --app persona_ad_gen + ``` -2. **Alternative method** (if using the run script): -```bash -./run_agent.sh +### Example Interaction + +Here's a typical interaction flow with the agent: + +**User:** +hello + +**Agent:** +Great ads connect with a real person by solving a real problem. Instead of just filling out a form, we're going to build your ad's story step-by-step. First, let's get to know your ideal customer. + +Describe the single person you want to reach. What is a key problem, need, or desire they are currently facing that your business can help with? + +**User:** +I want to reach busy professionals in their 30s who struggle to find time for fitness but want to stay healthy and look good. + +**Agent:** +Perfect! You're targeting busy professionals who value their health but struggle with time management. Now, let's capture the core message. + +Now, imagine that person sees your ad. In one powerful sentence, what is the solution or key takeaway you want them to have? This is the core message that will form the heart of your ad. + +**User:** +"Get fit in just 20 minutes a day with workouts that fit your schedule, not the other way around." + +**Agent:** +Excellent core message! It directly addresses their time constraint while promising results. Now let's think about the tone. + +How should we speak to this person? Choose a Tone of Voice that would resonate with them (e.g., Professional, Empathetic, Witty, Urgent, Conversational, Inspiring). + +**User:** +Inspiring and empathetic - understanding their struggle but motivating them to take action. + +**Agent:** +Perfect tone choice! Inspiring and empathetic will resonate well with busy professionals who need both understanding and motivation. + +You've defined the story, now let's gather the materials. Upload your most compelling image that will serve as the foundation for your advertising scenes. + +[User uploads image] + +**Agent:** +Perfect! Now I'll automatically generate compelling headlines for you based on your story. + +```tool_code +generate_headlines( + persona="busy professionals in their 30s struggling with time for fitness", + core_message="Get fit in just 20 minutes a day with workouts that fit your schedule", + tone="Inspiring and empathetic" +) ``` -3. **Direct Python execution**: -```bash -python -m adk.web_server --app persona_ad_gen +```tool_outputs +[ + "Your Schedule. Your Workout. Your Success.", + "20 Minutes to Transform: Fitness That Fits Your Life", + "Busy Schedule? We Get It. 20 Minutes Is All You Need.", + "From Boardroom to Better Health in 20 Minutes", + "Finally, Fitness That Respects Your Time" +] ``` -2. Follow the workflow: - - Provide the 6 brief items when prompted: - - Brand name - - Product name - - Target location - - Target age group - - Target gender - - Target interests - - Upload a base image when requested - - Confirm the brief details - - The agent will generate 4 creative scenes +## Evaluating the Agent -## Testing & Evaluation +Evaluation tests assess the agent's conversation flow and response accuracy. -### Running Evaluations +**Steps:** -The project includes evaluation datasets to test the agent's responses: +1. **Run Evaluation Tests:** -```bash -# Run evaluation tests -adk eval persona_ad_gen eval/data/persona_ad_gen_evalset.test.json + ```bash + adk eval persona_ad_gen eval/data/persona_ad_gen_evalset.test.json + ``` -# Run unit tests -pytest eval/test_eval.py -``` + Expected output: + ``` + Eval Run Summary + persona_ad_gen_evalset: + Tests passed: 2 + Tests failed: 0 + ``` -### Evaluation Setup +2. **Run Unit Tests:** -If you encounter issues with evaluation, ensure you have the required dependencies: + ```bash + pytest eval/test_eval.py + ``` -1. **For pip installations**: -```bash -pip install "google-adk[eval]" + This runs specific test cases including: + - `test_agent_introduction` - Verifies the agent provides the correct introduction + +## Configuration + +### Model Configuration + +The agent uses the following models: +- **Main Agent:** `gemini-2.5-flash` for conversation and reasoning +- **Image Generation:** `gemini-2.5-flash-image-preview` for creating advertising scenes + +You can modify these in `persona_ad_gen/agent.py`: + +```python +MODEL = "gemini-2.5-flash" # Change this to use a different model (e.g., Gemini 2.5 Pro) ``` -2. **For pipx installations**: -```bash -pipx inject google-adk pandas tabulate rouge-score +### Session State Structure + +The agent maintains the following session state: + +```python +{ + "confirmed_brief": { + "persona": str, + "core_message": str, + "tone": str, + "location": str, + "demographics": str, + "interests": str + }, + "base_image_filename": str, + "headlines": list[str], + "generated_scenes": list[str] +} ``` -The evaluation tests verify that the agent provides the correct introduction and follows the expected conversation flow. +## Deployment on Google Agent Engine -## Workflow +To deploy the agent to Vertex AI Agent Engine: -1. **Brief Collection**: The main agent collects all necessary information -2. **Image Upload**: User uploads a base image that serves as the foundation -3. **Confirmation**: Agent confirms all details with the user -4. **Scene Generation**: Creative sub-agent creates a 4-scene plan -5. **Image Creation**: Each scene is generated as a new image variation +1. **Install deployment dependencies:** -## Technical Details + ```bash + poetry install --with deployment + ``` -### Tools +2. **Deploy the agent:** + + ```bash + python deployment/deploy.py --create + ``` + +3. **List deployed agents:** -- `confirm_and_save_brief`: Saves the creative brief to session state -- `save_image_as_artifact`: Processes and saves uploaded images -- `edit_scene_image`: Generates new images based on edit prompts using the uploaded image as source + ```bash + python deployment/deploy.py --list + ``` -### Models +4. **Delete a deployment:** -- Uses `gemini-2.5-flash-image-preview` for image generation with source image input -- Supports image-to-image editing and text-to-image generation -- Configured with `response_modalities=["TEXT", "IMAGE"]` for both text descriptions and image outputs + ```bash + python deployment/deploy.py --delete --resource_id=AGENT_ENGINE_ID + ``` -### Session State +### Testing Deployment -The agent maintains session state with: -- `confirmed_brief`: The complete creative brief details -- `base_image_filename`: Reference to the uploaded base image +This code snippet shows how to test the deployed agent: -### Viewing Generated Images +```python +import dotenv +from vertexai import agent_engines -Generated images are saved as artifacts and can be accessed through: -1. **ADK Web Interface**: Navigate to the artifacts section in your session -2. **Session Artifacts**: Look for files named like: - - `scene_1_nyc_power_stance.png` - - `scene_2_dynamic_city_drive.png` - - `scene_3_athlete_companion.png` - - `scene_4_command_the_city.png` -3. **API Response**: Each generation returns the artifact filename for programmatic access +# Load environment variables +dotenv.load_dotenv() + +# Initialize connection to deployed agent +agent_engine_id = "YOUR_AGENT_ENGINE_ID" # Replace with actual ID +agent_engine = agent_engines.get(agent_engine_id) + +# Create a new session +session = agent_engine.create_session(user_id="test_user") + +# Stream interaction with the agent +for event in agent_engine.stream_query( + user_id=session["user_id"], + session_id=session["id"], + message="Hello, I need help creating an ad campaign" +): + for part in event["content"]["parts"]: + print(part["text"]) +``` + +## Project Structure + +``` +persona_ad_gen/ +ā”œā”€ā”€ __init__.py +ā”œā”€ā”€ agent.py # Main agent definition and orchestration +ā”œā”€ā”€ tools.py # Core tool implementations +ā”œā”€ā”€ advanced_tools.py # Extended tool functions +ā”œā”€ā”€ models.py # Pydantic data models +ā”œā”€ā”€ advanced_models.py # Extended data structures +ā”œā”€ā”€ debug_image_handler.py # Image processing utilities +└── sub_agents/ + ā”œā”€ā”€ __init__.py + ā”œā”€ā”€ creative_agent.py # Scene generation sub-agent + └── headline_agent.py # Headline generation logic + +eval/ +ā”œā”€ā”€ test_eval.py # Unit tests for agent behavior +└── data/ + └── persona_ad_gen_evalset.test.json # Evaluation test cases +``` -### Image Generation Process +## Troubleshooting -The agent follows this workflow: -1. Loads your uploaded image as the source material -2. Combines it with scene-specific editing prompts -3. Sends both to Gemini 2.5 Flash Image model -4. Processes the response to extract generated images -5. Saves results as downloadable artifacts +### Common Issues and Solutions -## Error Handling +1. **Evaluation module not found:** + ```bash + # Solution: Install eval dependencies + pipx inject google-adk pandas tabulate rouge-score + ``` -The agent handles: -- Missing image uploads -- Failed image generation attempts -- Invalid brief information -- API errors with graceful fallbacks +2. **Authentication errors:** + ```bash + # Solution: Authenticate with Google Cloud + gcloud auth application-default login + ``` -## Development +3. **Image generation failures:** + - Verify that `GOOGLE_GENAI_USE_VERTEXAI` is correctly set + - Check API quotas and limits in your Google Cloud project + - Ensure the model `gemini-2.5-flash-image-preview` is available in your region -To modify the agent: +4. **Missing environment variables:** + - Ensure all required variables are set in your `.env` file + - Verify the GCS bucket exists and you have write permissions -1. Edit tool functions in `tools.py` -2. Adjust agent behavior in `agent.py` or `sub_agents/creative_agent.py` -3. Update models in `models.py` if needed -4. Test with `adk web` -5. Run evaluations with `adk eval` to verify changes +## Disclaimer -## Dependencies +This agent sample is provided for illustrative purposes only and is not intended for production use. It serves as a basic example of an agent and a foundational starting point for individuals or teams to develop their own agents. -- `google-adk`: Agent Development Kit -- `google-genai`: Gemini AI API client -- `pydantic`: Data validation -- `google-cloud-aiplatform`: Cloud AI platform integration -- `python-dotenv`: Environment variable management +This sample has not been rigorously tested, may contain bugs or limitations, and does not include features or optimizations typically required for a production environment (e.g., robust error handling, security measures, scalability, performance considerations, comprehensive logging, or advanced configuration options). -### Evaluation Dependencies (optional) -- `pandas`: Data manipulation for evaluation metrics -- `tabulate`: Formatted output for evaluation results -- `rouge-score`: Text similarity metrics for evaluation +Users are solely responsible for any further development, testing, security hardening, and deployment of agents based on this sample. We recommend thorough review, testing, and the implementation of appropriate safeguards before using any derived agent in a live or critical system. ## License