From 21a43dc6e906b190939e1923f0ce1b1c45942b23 Mon Sep 17 00:00:00 2001 From: Nandinski Date: Wed, 28 Jan 2026 11:17:16 +0300 Subject: [PATCH] Add CI and pre-commit configuration - Add .pre-commit-config.yaml for code quality checks (whitespace, file endings, large file prevention) - Add .github/workflows/ci.yaml for continuous integration - Add .python-version, pyproject.toml, and uv.lock for Python environment and dependency management - Apply whitespace and file ending fixes across the codebase --- .github/workflows/ci.yaml | 23 ++ .pre-commit-config.yaml | 14 + .python-version | 1 + README.md | 14 +- examples/.gitignore | 2 +- .../adk/brand-search-optimization/README.md | 2 +- .../sub_agents/keyword_finding/prompt.py | 2 +- .../sub_agents/search_results/prompt.py | 4 +- .../telemetry_setup.py | 195 +++++---- .../tools/bq_connector.py | 2 +- .../customization.md | 2 +- .../deployment/bq_data_setup.sql | 2 +- .../deployment/test.sh | 2 +- .../eval/data/eval_data1.evalset.json | 2 +- .../eval/data/test_config.json | 2 +- .../example_input.md | 4 +- .../tests/example_interaction.md | 2 +- examples/adk/content-creation/.gitignore | 2 +- .../local_deployment/start_local_agents.sh | 22 +- .../local_deployment/stop_local_agents.sh | 14 +- .../local_deployment/verify_setup.sh | 4 +- .../adk/content-creation/run_experiment.sh | 34 +- examples/adk/content-creation/send_request.py | 56 +-- .../src/agents/content_editor/Dockerfile | 2 +- .../src/agents/content_editor/build_docker.sh | 2 +- .../content_editor/content_editor/README.md | 1 - .../content_editor/agent-card.json | 2 +- .../content_editor/content_editor/agent.json | 2 +- .../content_editor/agent_executor.py | 2 +- .../content_editor/requirements.txt | 2 +- .../content_planner/content_planner/README.md | 2 +- .../content_planner/agent_executor.py | 2 +- .../content_planner/requirements.txt | 2 +- .../src/agents/content_planner/pyproject.toml | 2 +- .../content_writer/content_writer/README.md | 2 +- .../content_writer/agent_executor.py | 2 +- .../content_writer/requirements.txt | 2 +- .../src/agents/content_writer/pyproject.toml | 2 +- .../src/agents/shared/.env.example | 2 +- .../src/agents/shared/__init__.py | 1 - .../src/agents/shared/telemetry_setup.py | 199 +++++---- .../src/hosts/a2a_samples_ui/Containerfile | 2 +- .../src/hosts/a2a_samples_ui/README.md | 2 +- .../src/hosts/coordinator/Dockerfile | 2 +- .../coordinator/coordinator/__main_api__.py | 36 +- .../hosts/coordinator/coordinator/agent.py | 10 +- .../src/hosts/coordinator/pyproject.toml | 1 - .../SOURCES.txt | 2 +- .../dependency_links.txt | 1 - examples/adk/image-scoring/.env.example | 6 +- examples/adk/image-scoring/README.md | 12 +- .../image-scoring/eval/data/test_config.json | 2 +- .../adk/image-scoring/image_scoring/agent.py | 6 +- .../image-scoring/image_scoring/policy.json | 2 - .../adk/image-scoring/image_scoring/prompt.py | 2 +- .../image_scoring/sub_agents/image/prompt.py | 2 +- .../image/tools/image_generation_tool.py | 2 +- .../image_scoring/sub_agents/prompt/prompt.py | 22 +- .../sub_agents/scoring/prompt.py | 4 +- .../scoring/tools/get_images_tool.py | 4 +- .../image_scoring/telemetry_setup.py | 213 +++++----- .../tools/loop_condition_tool.py | 2 +- .../adk/marketing-agency/demo_html/about.html | 2 +- .../marketing-agency/demo_html/contact.html | 2 +- .../marketing-agency/demo_html/gallery.html | 2 +- .../adk/marketing-agency/demo_html/index.html | 2 +- .../adk/marketing-agency/demo_html/script.js | 2 +- .../adk/marketing-agency/demo_html/style.css | 2 +- .../adk/marketing-agency/example_input1.md | 52 +-- .../adk/marketing-agency/example_input2.md | 72 ++-- .../marketing_agency/agent.py | 2 +- .../marketing_agency/prompt.py | 9 +- .../sub_agents/domain_create/agent.py | 2 +- .../sub_agents/logo_create/agent.py | 4 +- .../sub_agents/marketing_create/agent.py | 2 +- .../sub_agents/website_create/agent.py | 2 +- .../marketing_agency/telemetry_setup.py | 187 +++++---- examples/autogen/Magentic-One/code.txt | 2 +- examples/autogen/data_convert.py | 26 +- examples/autogen/extractor.py | 18 +- .../data/hotpot_dev_questions.csv | 8 +- examples/langgraph/crag/.gitignore | 2 +- .../faq_redis_semantic_cache_naive/.gitignore | 2 +- .../cache/__init__.py | 1 - .../cache/config.py | 1 - .../cache/evals.py | 1 - .../cache/faq_data_container.py | 1 - .../data/hotpot_dev_questions.csv | 10 +- .../data/game_of_24_google_full.csv | 2 +- .../.gitignore | 2 +- .../3m_company_report_20251202_122931.md | 2 +- ...ott_laboratories_report_20251128_165925.md | 2 +- .../altria_group_report_20251202_102643.md | 2 +- .../amazon_report_20251127_121725.md | 2 +- .../amd_report_20251127_145830.md | 2 +- ...lobal_management_report_20251128_180420.md | 2 +- .../apple_report_20251125_110130.md | 14 +- .../apple_report_20251127_113046.md | 2 +- .../apple_report_20251127_115926.md | 4 +- .../at&t_report_20251128_154634.md | 2 +- ...rica_corporation_report_20251128_162900.md | 2 +- ...rkshire_hathaway_report_20251127_124034.md | 4 +- ...s_squibb_company_report_20251202_101925.md | 2 +- .../broadcom_report_20251127_122348.md | 2 +- ...ntas_corporation_report_20251128_192235.md | 2 +- .../cisco_report_20251127_150406.md | 4 +- .../coca-cola_report_20251128_160426.md | 2 +- .../coinbase_global_report_20251128_192716.md | 2 +- .../conocophillips_report_20251204_172333.md | 2 +- .../costco_report_20251127_145149.md | 2 +- ...dstrike_holdings_report_20251204_171703.md | 2 +- .../csx_corporation_report_20251205_142851.md | 2 +- .../csx_corporation_report_20251205_143049.md | 2 +- .../cummins_inc._report_20251204_181419.md | 2 +- .../cummins_inc._report_20251204_181638.md | 2 +- .../deere_&_company_report_20251204_170518.md | 2 +- ...echnologies_inc._report_20251202_125745.md | 2 +- ...ergy_corporation_report_20251202_104712.md | 2 +- .../elevance_health_report_20251128_183844.md | 2 +- .../eli_lilly_report_20251127_124705.md | 2 +- ...emerson_electric_report_20251128_191815.md | 2 +- .../equinix_report_20251128_193409.md | 2 +- ...edex_corporation_report_20251205_144829.md | 2 +- ...edex_corporation_report_20251205_145017.md | 2 +- ...mics_corporation_report_20251202_122121.md | 2 +- ...general_electric_report_20251127_153033.md | 2 +- .../home_depot_report_20251128_161003.md | 2 +- .../home_depot_report_20251205_145933.md | 2 +- .../home_depot_report_20251205_150349.md | 2 +- ...nternational_inc_report_20251201_111436.md | 2 +- ...nternational_inc_report_20251201_133039.md | 2 +- ...nternational_inc_report_20251201_135155.md | 2 +- .../ibm_report_20251127_151858.md | 2 +- ..._tool_works_inc._report_20251204_180010.md | 2 +- ..._tool_works_inc._report_20251204_180244.md | 2 +- ...l_exchange,_inc._report_20251202_124820.md | 2 +- ...tuitive_surgical_report_20251128_171245.md | 2 +- .../jpmorgan_report_20251127_131155.md | 2 +- ...tt_international_report_20251128_172318.md | 2 +- .../mastercard_report_20251127_143851.md | 6 +- .../mcdonald's_report_20251128_170730.md | 2 +- .../medtronic_plc_report_20251204_165106.md | 2 +- .../micron_report_20251127_152526.md | 4 +- .../microsoft_report_20251127_120532.md | 2 +- ...rage_corporation_report_20251128_193726.md | 2 +- ...dy's_corporation_report_20251202_135840.md | 2 +- .../netflix_report_20251127_144458.md | 2 +- ...mont_corporation_report_20251202_104236.md | 2 +- .../nextera_energy_report_20251127_170453.md | 2 +- .../nike_report_20251202_094201.md | 2 +- ...hern_corporation_report_20251205_144013.md | 2 +- ...hern_corporation_report_20251205_144220.md | 2 +- .../p&g_report_20251127_153958.md | 2 +- .../pepsico_report_20251127_162309.md | 2 +- ...procter_&_gamble_report_20251127_153423.md | 2 +- .../prologis_report_20251201_110047.md | 2 +- .../quanta_services_report_20251204_182336.md | 2 +- .../quanta_services_report_20251204_182555.md | 10 +- ...n_property_group_report_20251204_183308.md | 2 +- ...n_property_group_report_20251204_183738.md | 2 +- ...ucks_corporation_report_20251202_103407.md | 2 +- .../synopsys_report_20251128_171821.md | 2 +- .../t-mobile_report_20251128_164145.md | 2 +- ...southern_company_report_20251202_095851.md | 2 +- ...isher_scientific_report_20251127_155304.md | 2 +- ...technologies_plc_report_20251202_105201.md | 2 +- ...oup_incorporated_report_20251128_183255.md | 2 +- .../u.s._bancorp_report_20251128_182226.md | 2 +- ...d_parcel_service_report_20251128_174219.md | 2 +- .../unitedhealth_report_20251128_155920.md | 2 +- .../visa_report_20251127_132025.md | 2 +- .../walmart_report_20251127_125312.md | 2 +- ...management,_inc._report_20251202_131842.md | 2 +- .../mcp_financial_analyzer_benchmark/main.py | 276 ++++++------- .../mcp_agent.config.yaml | 4 +- .../sample_report.md | 14 +- examples/mcp-agent/temporal/.gitignore | 2 +- plot/.gitignore | 2 +- plot/README.md | 2 +- plot/lib/analyzer.py | 134 +++--- plot/lib/comparison.py | 130 +++--- plot/lib/extractors.py | 348 ++++++++-------- plot/lib/per_run_analyzer.py | 354 ++++++++-------- plot/lib/visualizers.py | 386 +++++++++--------- plot/plot_cpu_mem_by_example_summary.py | 2 +- plot/plot_example_metrics.py | 80 ++-- pyproject.toml | 13 + src/ADK_otel/README.md | 2 +- src/ADK_otel/telemetry_setup.py | 195 +++++---- ...el_metrics_template_for_human_reading.json | 2 +- tools/.gitignore | 2 +- .../traces_2025-12-16T22-01-24.370201.json | 2 +- tools/fake_collector/template.json | 2 +- tools/pricing.json | 2 +- uv.lock | 176 ++++++++ 195 files changed, 1971 insertions(+), 1750 deletions(-) create mode 100644 .github/workflows/ci.yaml create mode 100644 .pre-commit-config.yaml create mode 100644 .python-version create mode 100644 pyproject.toml create mode 100644 uv.lock diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..46ede96 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,23 @@ +name: CI + +on: + push: + branches: [main, add_ci] + + pull_request: + branches: [main, add_ci] + + workflow_dispatch: + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + + - name: "Set up Python" + uses: actions/setup-python@v6 + with: + python-version-file: ".python-version" + + - uses: pre-commit/action@v3.0.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..e54beed --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,14 @@ +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: end-of-file-fixer + - id: trailing-whitespace + - id: check-added-large-files # Default is 500kB + + # Ensure uv.lock file is up to date with pyproject.toml + - repo: https://github.com/astral-sh/uv-pre-commit + # uv version. + rev: 0.9.27 + hooks: + - id: uv-lock diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..c8cfe39 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.10 diff --git a/README.md b/README.md index ab677a5..ce3d711 100644 --- a/README.md +++ b/README.md @@ -128,13 +128,13 @@ If you find MAESTRO or its dataset useful in your research, please consider citi ``` @misc{maestro, - title={MAESTRO: Multi-Agent Evaluation Suite for Testing, Reliability, and Observability}, + title={MAESTRO: Multi-Agent Evaluation Suite for Testing, Reliability, and Observability}, author={Tie Ma and Yixi Chen and Vaastav Anand and Alessandro Cornacchia and Amândio R. Faustino and Guanheng Liu and Shan Zhang and Hongbin Luo and Suhaib A. Fahmy and Zafar A. Qazi and Marco Canini}, year={2026}, eprint={2601.00481}, archivePrefix={arXiv}, primaryClass={cs.NI}, - url={https://arxiv.org/abs/2601.00481}, + url={https://arxiv.org/abs/2601.00481}, } ``` @@ -158,3 +158,13 @@ If you find MAESTRO or its dataset useful in your research, please consider citi 10. [Testing and Enhancing Multi-Agent Systems for Robust Code Generation](https://arxiv.org/html/2510.10460) 11. [Evaluating Variance in Visual Question Answering Benchmarks](https://arxiv.org/html/2508.02645) --> + + +## Developing MAESTRO +```bash +git clone git@github.com:sands-lab/maestro.git +cd maestro +uv sync +# Install pre-commit hooks +uv run -- pre-commit install +``` diff --git a/examples/.gitignore b/examples/.gitignore index feb9221..a765d5e 100644 --- a/examples/.gitignore +++ b/examples/.gitignore @@ -1,4 +1,4 @@ deprecated/ agntcy/ -langgraph_flat/ \ No newline at end of file +langgraph_flat/ diff --git a/examples/adk/brand-search-optimization/README.md b/examples/adk/brand-search-optimization/README.md index ce948bc..99a5270 100644 --- a/examples/adk/brand-search-optimization/README.md +++ b/examples/adk/brand-search-optimization/README.md @@ -219,4 +219,4 @@ This agent sample is provided for illustrative purposes only and is not intended 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). -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. \ No newline at end of file +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. diff --git a/examples/adk/brand-search-optimization/brand_search_optimization/sub_agents/keyword_finding/prompt.py b/examples/adk/brand-search-optimization/brand_search_optimization/sub_agents/keyword_finding/prompt.py index d75d7ee..d52f1dd 100644 --- a/examples/adk/brand-search-optimization/brand_search_optimization/sub_agents/keyword_finding/prompt.py +++ b/examples/adk/brand-search-optimization/brand_search_optimization/sub_agents/keyword_finding/prompt.py @@ -22,7 +22,7 @@ 6. Transfer to root_agent You are helpful keyword finding agent for a brand name. -Your primary function is to find keywords shoppers would type in when trying to find for the products from the brand user provided. +Your primary function is to find keywords shoppers would type in when trying to find for the products from the brand user provided. - call `get_product_details_for_brand` tool to find product from a brand diff --git a/examples/adk/brand-search-optimization/brand_search_optimization/sub_agents/search_results/prompt.py b/examples/adk/brand-search-optimization/brand_search_optimization/sub_agents/search_results/prompt.py index d4d3241..2ce5f06 100644 --- a/examples/adk/brand-search-optimization/brand_search_optimization/sub_agents/search_results/prompt.py +++ b/examples/adk/brand-search-optimization/brand_search_optimization/sub_agents/search_results/prompt.py @@ -31,7 +31,7 @@ - if the user says google shopping, visit this website link is https://www.google.com/search?hl=en&q= and click on "shopping" tab - + - getting titles of the top 3 products by analyzing the webpage - Do not make up 3 products - Show title of the products in a markdown format @@ -40,7 +40,7 @@ - Continue until you believe the title, description and attribute information is gathered - Do not make up title, description and attribute information - - If you can not find the information, convery this information to the user + - If you can not find the information, convery this information to the user Please follow these steps to accomplish the task at hand: diff --git a/examples/adk/brand-search-optimization/brand_search_optimization/telemetry_setup.py b/examples/adk/brand-search-optimization/brand_search_optimization/telemetry_setup.py index 51daa0d..f991b59 100644 --- a/examples/adk/brand-search-optimization/brand_search_optimization/telemetry_setup.py +++ b/examples/adk/brand-search-optimization/brand_search_optimization/telemetry_setup.py @@ -46,10 +46,10 @@ class JsonFileMetricExporter(MetricExporter): """Exporter that writes metrics to local JSON files.""" - + def __init__(self, file_path: str): """Initialize JSON file exporter. - + Args: file_path: Path to JSON file """ @@ -59,54 +59,54 @@ def __init__(self, file_path: str): # Use None to accept defaults (same as ConsoleMetricExporter) self._preferred_temporality = None self._preferred_aggregation = None - + def export(self, metrics_data, timeout_millis: float = 10000, **kwargs): """Export metrics to JSON file. - + Args: metrics_data: Metrics data to export timeout_millis: Maximum time to wait for export (not used for file export) **kwargs: Additional arguments - + Returns: MetricExportResult.SUCCESS """ try: # Convert metrics to serializable format metric_records = [] - + # Extract metric data from the metrics_data object if hasattr(metrics_data, 'resource_metrics'): for resource_metric in metrics_data.resource_metrics: resource_attrs = dict(resource_metric.resource.attributes) if resource_metric.resource and resource_metric.resource.attributes else {} - + for scope_metric in resource_metric.scope_metrics: scope_name = scope_metric.scope.name if scope_metric.scope else "unknown" - + for metric in scope_metric.metrics: metric_name = metric.name metric_description = metric.description if hasattr(metric, 'description') else None metric_unit = metric.unit if hasattr(metric, 'unit') else None - + # Extract data points data_points = [] if hasattr(metric, 'data') and hasattr(metric.data, 'data_points'): for data_point in metric.data.data_points: point_attrs = dict(data_point.attributes) if hasattr(data_point, 'attributes') and data_point.attributes else {} - + # Add service name as agent identifier if not present if "agent.name" not in point_attrs: # Try to infer from resource attributes or use service name agent_name = resource_attrs.get("service.name", "brand-search-optimization") point_attrs["agent.name"] = agent_name - + point_dict = { "value": data_point.value if hasattr(data_point, 'value') else None, "timestamp": data_point.time_unix_nano if hasattr(data_point, 'time_unix_nano') else None, "attributes": point_attrs, } data_points.append(point_dict) - + metric_record = { "timestamp": datetime.now().isoformat(), "metric_name": metric_name, @@ -119,31 +119,31 @@ def export(self, metrics_data, timeout_millis: float = 10000, **kwargs): "scope": scope_name, } metric_records.append(metric_record) - + # Append to file (supports incremental writes) if metric_records: with open(self.file_path, "a", encoding="utf-8") as f: for record in metric_records: f.write(json.dumps(record, default=str) + "\n") - + return MetricExportResult.SUCCESS except Exception as e: logger.error(f"Failed to export metrics to {self.file_path}: {e}", exc_info=True) return MetricExportResult.FAILURE - + def force_flush(self, timeout_millis: int = 30000, **kwargs): """Force flush any pending metrics. - + Args: timeout_millis: Maximum time to wait for flush. **kwargs: Additional arguments. """ # For file exporter, data is written immediately, so no explicit flush needed pass - + def shutdown(self, timeout_millis: int = 30000, **kwargs): """Shutdown the exporter. - + Args: timeout_millis: Maximum time to wait for shutdown. **kwargs: Additional arguments. @@ -153,23 +153,23 @@ def shutdown(self, timeout_millis: int = 30000, **kwargs): class JsonFileSpanExporter(SpanExporter): """Exporter that writes spans to local JSON files.""" - + def __init__(self, file_path: str): """Initialize JSON file exporter. - + Args: file_path: Path to JSON file """ self.file_path = Path(file_path) self.file_path.parent.mkdir(parents=True, exist_ok=True) self.spans = [] - + def export(self, spans): """Export spans to JSON file. - + Args: spans: List of spans to export - + Returns: SpanExportResult.SUCCESS """ @@ -187,7 +187,7 @@ def export(self, spans): parent_span_id = format(span.parent.span_id, "016x") elif hasattr(span, "parent_context") and span.parent_context: parent_span_id = format(span.parent_context.span_id, "016x") - + # Extract agent name from span attributes or name for better identification attributes = dict(span.attributes) if span.attributes else {} agent_name = ( @@ -196,11 +196,11 @@ def export(self, spans): (span.name.split()[-1] if "invoke_agent" in span.name and len(span.name.split()) > 1 else None) or "unknown" ) - + # Add agent_name to attributes if not already present (for backward compatibility) if "agent.name" not in attributes and agent_name != "unknown": attributes["agent.name"] = agent_name - + # Ensure gen_ai.agent.name is present (required by template) # For call_llm spans, try to extract from gcp.vertex.agent.llm_request labels if "gen_ai.agent.name" not in attributes: @@ -219,7 +219,7 @@ def export(self, spans): agent_name = adk_agent_name except (json.JSONDecodeError, KeyError, TypeError): pass - + # If still not found, use extracted agent_name or agent.name if "gen_ai.agent.name" not in attributes: if agent_name != "unknown": @@ -229,7 +229,7 @@ def export(self, spans): else: # Field must exist per template, set to null if not available attributes["gen_ai.agent.name"] = None - + # Ensure LLM usage fields are present (default to 0 if missing) if "gen_ai.usage.input_tokens" not in attributes: attributes["gen_ai.usage.input_tokens"] = 0 @@ -240,14 +240,14 @@ def export(self, spans): input_tokens = attributes.get("gen_ai.usage.input_tokens", 0) or 0 output_tokens = attributes.get("gen_ai.usage.output_tokens", 0) or 0 attributes["gen_ai.usage.total_tokens"] = input_tokens + output_tokens - + # Add LLM/MCP call counts (default to 0 if missing) if "gen_ai.llm.call.count" not in attributes: # Set to 1 if this is a call_llm span, otherwise 0 attributes["gen_ai.llm.call.count"] = 1 if "call_llm" in span.name else 0 if "gen_ai.mcp.call.count" not in attributes: attributes["gen_ai.mcp.call.count"] = 0 - + # Ensure gen_ai.operation.name is present (required by template) if "gen_ai.operation.name" not in attributes: if "call_llm" in span.name: @@ -259,7 +259,7 @@ def export(self, spans): elif span.name.lower() == "invocation": # invocation spans are top-level entry points attributes["gen_ai.operation.name"] = "invocation" - + # Ensure gen_ai.conversation.id is present (required by template) # Copy from session_id if available, otherwise set to null if "gen_ai.conversation.id" not in attributes: @@ -269,14 +269,14 @@ def export(self, spans): else: # Field must exist per template, set to null if not available attributes["gen_ai.conversation.id"] = None - + # Add missing optional fields with default values (per template) # gen_ai.agent.description - optional, only in invoke_agent spans if "gen_ai.agent.description" not in attributes: # Try to extract from system_instruction or other sources # For now, set to empty string if not available attributes["gen_ai.agent.description"] = "" - + # gen_ai.tool.* fields - only for execute_tool spans if "gen_ai.tool.name" not in attributes: attributes["gen_ai.tool.name"] = "" @@ -286,31 +286,31 @@ def export(self, spans): attributes["gen_ai.tool.call.id"] = "" if "gen_ai.tool.description" not in attributes: attributes["gen_ai.tool.description"] = "" - + # gcp.vertex.agent.tool_* fields - optional if "gcp.vertex.agent.tool_call_args" not in attributes: attributes["gcp.vertex.agent.tool_call_args"] = "" if "gcp.vertex.agent.tool_response" not in attributes: attributes["gcp.vertex.agent.tool_response"] = "" - + # mcp.* fields - optional, not applicable for Vertex AI if "mcp.server" not in attributes: attributes["mcp.server"] = "" if "mcp.tool" not in attributes: attributes["mcp.tool"] = "" - + # agent.log - optional if "agent.log" not in attributes: attributes["agent.log"] = "" - + # Note: CPU/memory metrics are collected separately in metrics JSONL file, # so we don't add them to span attributes (per template guidelines) - + # Calculate communication metrics for agent-to-agent calls communication_metrics = {} input_size = 0 output_size = 0 - + # Check if this is agent-to-agent communication # Only AgentTool calls are agent communication, not FunctionTool calls is_agent_comm = False @@ -327,53 +327,53 @@ def export(self, spans): elif "invoke_agent" in span.name: # invoke_agent spans are entry points, not agent-to-agent communication is_agent_comm = False - + if is_agent_comm: communication_metrics["is_agent_communication"] = True - + # For execute_tool spans (AgentTool): measure actual tool call arguments and responses if "execute_tool" in span.name: # Input: tool call arguments (actual data passed to the agent) tool_call_args = attributes.get("gcp.vertex.agent.tool_call_args", "") if tool_call_args and isinstance(tool_call_args, str) and tool_call_args != "{}": input_size = len(tool_call_args.encode('utf-8')) - + # Output: tool response (actual data returned from the agent) tool_response = attributes.get("gcp.vertex.agent.tool_response", "") if tool_response and isinstance(tool_response, str) and tool_response != "{}": output_size = len(tool_response.encode('utf-8')) - + # For call_llm spans: measure actual LLM request and response elif "call_llm" in span.name: # Input: LLM request (actual data sent to LLM) llm_request = attributes.get("gcp.vertex.agent.llm_request", "") if llm_request and isinstance(llm_request, str) and llm_request != "{}": input_size = len(llm_request.encode('utf-8')) - + # Output: LLM response (actual data received from LLM) llm_response = attributes.get("gcp.vertex.agent.llm_response", "") if llm_response and isinstance(llm_response, str) and llm_response != "{}": output_size = len(llm_response.encode('utf-8')) - + # Store communication metrics and add to attributes for template compliance # Template shows these fields, but per README: "If a field doesn't apply, omit it" # So we only add them when there's actual communication data # Ensure is_in_process_call is always present (default false) if "is_in_process_call" not in communication_metrics: communication_metrics["is_in_process_call"] = False - + if input_size > 0: communication_metrics["input_message_size_bytes"] = input_size attributes["communication.input_message_size_bytes"] = input_size - + if output_size > 0: communication_metrics["output_message_size_bytes"] = output_size attributes["communication.output_message_size_bytes"] = output_size - + if input_size > 0 or output_size > 0: communication_metrics["total_message_size_bytes"] = input_size + output_size attributes["communication.total_message_size_bytes"] = input_size + output_size - + # Get span kind span_kind = "INTERNAL" # Default if hasattr(span, "kind"): @@ -382,7 +382,7 @@ def export(self, spans): span_kind = SpanKind(kind_value).name if kind_value is not None else "INTERNAL" except (ValueError, AttributeError): span_kind = "INTERNAL" - + # Get resource attributes and add host.name if available resource_attrs = dict(span.resource.attributes) if span.resource and span.resource.attributes else {} if "host.name" not in resource_attrs: @@ -392,7 +392,7 @@ def export(self, spans): resource_attrs["host.name"] = hostname except Exception: pass # host.name is optional - + span_dict = { "trace_id": format(span.context.trace_id, "032x"), "span_id": format(span.context.span_id, "016x"), @@ -422,7 +422,7 @@ def export(self, spans): }, } span_data.append(span_dict) - + # Append to file (supports incremental writes) if span_data: with open(self.file_path, "a", encoding="utf-8") as f: @@ -431,12 +431,12 @@ def export(self, spans): logger.info(f"Exported {len(span_data)} spans to {self.file_path}") else: logger.warning(f"No spans to export (received {len(spans)} spans, but span_data is empty)") - + return SpanExportResult.SUCCESS except Exception as e: logger.error(f"Failed to export spans to {self.file_path}: {e}", exc_info=True) return SpanExportResult.FAILURE - + def shutdown(self): """Shutdown the exporter.""" pass @@ -444,17 +444,17 @@ def shutdown(self): def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] = None): """Setup OpenTelemetry tracing with local JSON file export. - + This function configures OpenTelemetry to automatically collect telemetry data from ADK agents. The instrumentation is automatic - ADK and OpenTelemetry instrumentation libraries will automatically create spans for: - + - Agent invocations (invoke_agent) - LLM calls (call_llm) with model, tokens, and request/response data - Tool/function executions (execute_tool) - A2A server operations (request handling, event queue operations) - HTTP requests between agents - + The collected data includes: - Trace IDs and Span IDs for distributed tracing - Timing information (start time, end time, duration) @@ -462,7 +462,7 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] - Attributes (gen_ai.system, gen_ai.request.model, gen_ai.usage.*, etc.) - Events (exceptions, custom events) - Resource attributes (service.name, service.version, etc.) - + Args: service_name: Service name, defaults to OTEL_SERVICE_NAME env var or "adk-agent" trace_file: Trace file path, defaults to OTEL_TRACE_FILE env var or auto-generated @@ -471,28 +471,28 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] # Get configuration from environment variables service_name = service_name or os.getenv("OTEL_SERVICE_NAME", "adk-agent") trace_file = trace_file or os.getenv("OTEL_TRACE_FILE") - + if not trace_file: # Default file path: traces directory in current working directory trace_dir = Path.cwd() / "traces" trace_dir.mkdir(exist_ok=True) timestamp = os.getenv("OTEL_RUN_TIMESTAMP") or datetime.now().strftime("%Y%m%d_%H%M%S") trace_file = str(trace_dir / f"{service_name}_{timestamp}.jsonl") - + logger.info(f"Setting up OpenTelemetry tracing for {service_name}") logger.info(f"Trace file: {trace_file}") - + # Create resource resource = Resource.create({ "service.name": service_name, "service.version": os.getenv("OTEL_SERVICE_VERSION", "1.0.0"), "deployment.environment": os.getenv("OTEL_DEPLOYMENT_ENVIRONMENT", "local"), }) - + # Check if tracer provider already exists current_provider = trace.get_tracer_provider() provider_already_set = isinstance(current_provider, TracerProvider) - + # If current provider is not a TracerProvider instance, create a new one if not provider_already_set: tracer_provider = TracerProvider(resource=resource) @@ -504,7 +504,7 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] # For now, we'll just use the existing provider as-is tracer_provider = current_provider logger.debug(f"Using existing TracerProvider (resource attributes cannot be updated)") - + # Create and add JSON file exporter json_exporter = JsonFileSpanExporter(trace_file) # Use SimpleSpanProcessor to export spans immediately when they end @@ -512,10 +512,10 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] span_processor = SimpleSpanProcessor(json_exporter) tracer_provider.add_span_processor(span_processor) logger.info(f"Using SimpleSpanProcessor for trace export to {trace_file}") - + # Ensure tracer provider is set trace.set_tracer_provider(tracer_provider) - + # Set environment variables to enable instrumentation libraries os.environ.setdefault("OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED", "true") # Enable full message content capture for GenAI instrumentation @@ -526,7 +526,7 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] os.environ.setdefault("OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE", ".*") # Increase max attribute length to capture full messages (default is 250) os.environ.setdefault("OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT", "10000") - + # Try to enable ADK's telemetry (only succeeds if OTLP endpoint is set) # Even if it fails, instrumentation libraries will use our tracer provider otel_endpoint = os.getenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") or os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT") @@ -537,11 +537,11 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] logger.info(f"ADK OTLP exporter enabled for {otel_endpoint}") except Exception as e: logger.warning(f"Failed to enable ADK OTLP exporter: {e}") - + logger.info("Tracer provider configured with JSON file exporter") - + logger.info("OpenTelemetry tracing setup completed") - + except Exception as e: logger.error(f"Failed to setup tracing: {e}", exc_info=True) # Don't raise exception, allow application to continue (tracing is optional) @@ -549,67 +549,67 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] def setup_metrics(service_name: Optional[str] = None, metrics_file: Optional[str] = None, enable_system_metrics: bool = True): """Setup OpenTelemetry metrics with system resource monitoring and local JSON file export. - + This function configures OpenTelemetry Metrics to collect: - CPU usage (percentage) - Memory usage (percentage and absolute values) - Process-specific metrics - + Metrics are exported to local JSON files, similar to trace export. - + Note: In containerized environments, psutil will report container-level metrics, not host-level metrics. For host-level metrics, use cadvisor. - + Args: service_name: Service name for resource attributes metrics_file: Metrics file path, defaults to OTEL_METRICS_FILE env var or auto-generated enable_system_metrics: Whether to enable CPU/memory monitoring (default: True) """ global _metrics_initialized, _metrics_thread - + if _metrics_initialized: logger.warning("Metrics already initialized, skipping") return - + try: service_name = service_name or os.getenv("OTEL_SERVICE_NAME", "adk-agent") metrics_file = metrics_file or os.getenv("OTEL_METRICS_FILE") - + if not metrics_file: # Default file path: metrics directory in current working directory metrics_dir = Path.cwd() / "metrics" metrics_dir.mkdir(exist_ok=True) timestamp = os.getenv("OTEL_RUN_TIMESTAMP") or datetime.now().strftime("%Y%m%d_%H%M%S") metrics_file = str(metrics_dir / f"{service_name}_{timestamp}.jsonl") - + logger.info(f"Setting up OpenTelemetry metrics for {service_name}") logger.info(f"Metrics file: {metrics_file}") - + # Create resource resource = Resource.create({ "service.name": service_name, "service.version": os.getenv("OTEL_SERVICE_VERSION", "1.0.0"), "deployment.environment": os.getenv("OTEL_DEPLOYMENT_ENVIRONMENT", "local"), }) - + # Create JSON file exporter json_exporter = JsonFileMetricExporter(metrics_file) - + # Create metric reader with periodic export (every 1 seconds) reader = PeriodicExportingMetricReader( exporter=json_exporter, export_interval_millis=1000, # Export every 1 seconds ) - + # Create meter provider meter_provider = MeterProvider( resource=resource, metric_readers=[reader], ) metrics.set_meter_provider(meter_provider) - + meter = metrics.get_meter(__name__) - + # Create custom metrics for agent communication # These will be updated via span processor hooks communication_counter = meter.create_counter( @@ -617,25 +617,25 @@ def setup_metrics(service_name: Optional[str] = None, metrics_file: Optional[str description="Number of agent-to-agent communications", unit="1", ) - + communication_duration = meter.create_histogram( name="agent.communication.duration", description="Duration of agent-to-agent communications", unit="ms", ) - + communication_message_size = meter.create_histogram( name="agent.communication.message_size", description="Size of messages passed between agents", unit="bytes", ) - + logger.info("Custom communication metrics created") - + if enable_system_metrics: try: import psutil - + # Process-specific metrics only (system metrics removed per user request) process_cpu_gauge = meter.create_observable_gauge( name="process.cpu.usage", @@ -643,26 +643,26 @@ def setup_metrics(service_name: Optional[str] = None, metrics_file: Optional[str unit="%", callbacks=[_get_process_cpu_usage], ) - + process_memory_gauge = meter.create_observable_gauge( name="process.memory.usage_bytes", description="Process memory usage in bytes", unit="bytes", callbacks=[_get_process_memory_usage], ) - + logger.info("Process metrics (CPU/memory) enabled using psutil") logger.info("Note: System-level metrics removed per user request") - + except ImportError: logger.warning("psutil not installed, skipping system metrics. Install with: pip install psutil") except Exception as e: logger.warning(f"Failed to setup system metrics: {e}") - + _metrics_initialized = True logger.info("Meter provider configured with JSON file exporter") logger.info("OpenTelemetry metrics setup completed") - + except Exception as e: logger.error(f"Failed to setup metrics: {e}", exc_info=True) # Don't raise exception, allow application to continue (metrics are optional) @@ -691,7 +691,7 @@ def _get_memory_usage_bytes(callback_options): def _get_process_cpu_usage(callback_options): """Callback to get process CPU usage percentage. - + Note: psutil.Process().cpu_percent(interval=None) requires two calls to calculate. First call returns 0.0, subsequent calls return the percentage since last call. We use interval=0.1 to get immediate accurate reading. @@ -700,13 +700,13 @@ def _get_process_cpu_usage(callback_options): try: import psutil from opentelemetry.metrics import Observation - + if _process_obj is None: _process_obj = psutil.Process() # First call to initialize, returns 0.0 _process_obj.cpu_percent(interval=0.5) return [Observation(0.0)] - + # Subsequent calls return actual CPU usage # Use interval=0.1 for more accurate reading cpu_percent = _process_obj.cpu_percent(interval=0.5) @@ -726,4 +726,3 @@ def _get_process_memory_usage(callback_options): return [Observation(memory_info.rss)] # Resident Set Size except Exception: return [] - diff --git a/examples/adk/brand-search-optimization/brand_search_optimization/tools/bq_connector.py b/examples/adk/brand-search-optimization/brand_search_optimization/tools/bq_connector.py index 0e0129c..471d769 100644 --- a/examples/adk/brand-search-optimization/brand_search_optimization/tools/bq_connector.py +++ b/examples/adk/brand-search-optimization/brand_search_optimization/tools/bq_connector.py @@ -44,7 +44,7 @@ def get_product_details_for_brand(tool_context: ToolContext): '| Title | Description | Attributes | Brand |\\n|---|---|---|---|\\n| Nike Air Max | Comfortable running shoes | Size: 10, Color: Blue | Nike\\n| Nike Sportswear T-Shirt | Cotton blend, short sleeve | Size: L, Color: Black | Nike\\n| Nike Pro Training Shorts | Moisture-wicking fabric | Size: M, Color: Gray | Nike\\n' """ brand = tool_context.user_content.parts[0].text - + # --- MOCK IMPLEMENTATION START --- print(f"DEBUG: Mocking BigQuery result for brand: {brand}") return f'''| Title | Description | Attributes | Brand | diff --git a/examples/adk/brand-search-optimization/customization.md b/examples/adk/brand-search-optimization/customization.md index 96424f2..6a68bef 100644 --- a/examples/adk/brand-search-optimization/customization.md +++ b/examples/adk/brand-search-optimization/customization.md @@ -63,4 +63,4 @@ table.iam_policy = policy table = client.update_table(table) # Updates the table's IAM policy print(f"Granted role '{ROLE}' to user '{USER_EMAIL}' on table '{table.full_table_id}'.") -``` \ No newline at end of file +``` diff --git a/examples/adk/brand-search-optimization/deployment/bq_data_setup.sql b/examples/adk/brand-search-optimization/deployment/bq_data_setup.sql index 02ebafe..7786e46 100644 --- a/examples/adk/brand-search-optimization/deployment/bq_data_setup.sql +++ b/examples/adk/brand-search-optimization/deployment/bq_data_setup.sql @@ -31,4 +31,4 @@ VALUES ('Kids\' Joggers', 'Size: 12 Preschool, Color: Black', 'BSOAgentTestBrand'); -- Notes: --- - Ensure the project and dataset IDs match the ones used in the CREATE TABLE statement. \ No newline at end of file +-- - Ensure the project and dataset IDs match the ones used in the CREATE TABLE statement. diff --git a/examples/adk/brand-search-optimization/deployment/test.sh b/examples/adk/brand-search-optimization/deployment/test.sh index cac786c..9060c98 100644 --- a/examples/adk/brand-search-optimization/deployment/test.sh +++ b/examples/adk/brand-search-optimization/deployment/test.sh @@ -24,4 +24,4 @@ run_unit_tests(){ run_unit_tests -exit 0 \ No newline at end of file +exit 0 diff --git a/examples/adk/brand-search-optimization/eval/data/eval_data1.evalset.json b/examples/adk/brand-search-optimization/eval/data/eval_data1.evalset.json index df0a1ea..ff482ed 100644 --- a/examples/adk/brand-search-optimization/eval/data/eval_data1.evalset.json +++ b/examples/adk/brand-search-optimization/eval/data/eval_data1.evalset.json @@ -118,4 +118,4 @@ "session": {} } } -] \ No newline at end of file +] diff --git a/examples/adk/brand-search-optimization/eval/data/test_config.json b/examples/adk/brand-search-optimization/eval/data/test_config.json index 8fb2ff8..9f93dd6 100644 --- a/examples/adk/brand-search-optimization/eval/data/test_config.json +++ b/examples/adk/brand-search-optimization/eval/data/test_config.json @@ -3,4 +3,4 @@ "tool_trajectory_avg_score": 0.2, "response_match_score": 0.2 } -} \ No newline at end of file +} diff --git a/examples/adk/brand-search-optimization/example_input.md b/examples/adk/brand-search-optimization/example_input.md index c82a364..d5dfe12 100644 --- a/examples/adk/brand-search-optimization/example_input.md +++ b/examples/adk/brand-search-optimization/example_input.md @@ -63,7 +63,7 @@ This report compares the titles of products from the brand "Penguin" with the ti | It's Only the Himalayas | "Penguin", "Guide" | Add brand name and general category, e.g., "Penguin's It's Only the Himalayas Travel Guide " | | Full Moon over Noah’s Ark: An Odyssey to Mount Ararat and Beyond | "Penguin", "Guide" | Add brand name and general category, e.g., "Penguin's Full Moon over Noah’s Ark Travel G uide" | | See America: A Celebration of Our National Parks & Treasured Sites | "Penguin", "Guide" | Add brand name and general category, e.g., "Penguin's See America Travel Guide" | -| Vagabonding: An Uncommon Guide to the Art of Long-Term World Travel | "Penguin" | Add brand name, e.g., "Penguin's Vagabonding: An Uncommon Guide to the Art of Long-Term World +| Vagabonding: An Uncommon Guide to the Art of Long-Term World Travel | "Penguin" | Add brand name, e.g., "Penguin's Vagabonding: An Uncommon Guide to the Art of Long-Term World Travel" | | Under the Tuscan Sun | "Penguin", "Travel Guide" | Add brand name and general category, e.g., "Penguin's Under the Tuscan Sun Travel Guide" | | A Summer In Europe | "Penguin", "Travel Guide" | Add brand name and general category, e.g., "Penguin's A Summer In Europe Travel Guide" | | The Great Railway Bazaar | "Penguin", "Travel Guide" | Add brand name and general category, e.g., "Penguin's The Great Railway Bazaar Travel Guide" | @@ -81,5 +81,5 @@ ypes of travel to attract more targeted searches. For example, if it's about the a "travel" search. No improvements are suggested for these titles in the context of travel searches. * **Search Result Titles:** Many of the search results for "travel" are detailed and descriptive. However, if these were Penguin products, adding "Penguin" to the title would h elp with brand recognition and searchability when users are specifically looking for Penguin publications. Also, adding a more general category like "Travel Guide" to relevant titles would help if the title itself is very specific (e.g., "It's Only the Himalayas"). [user]: It is good / Please have the critic review this [comparison_critic_agent]: I am satisfied with the comparison. -[user]: +[user]: Aborted! diff --git a/examples/adk/brand-search-optimization/tests/example_interaction.md b/examples/adk/brand-search-optimization/tests/example_interaction.md index cce12ad..ce3110a 100644 --- a/examples/adk/brand-search-optimization/tests/example_interaction.md +++ b/examples/adk/brand-search-optimization/tests/example_interaction.md @@ -105,4 +105,4 @@ Could you please tell me if you want more information on any of these products o - **Add Keywords:** Consider adding keywords like "sneakers," "running shoes," "light up," or "school" to titles where relevant to improve general search visibility. ---- \ No newline at end of file +--- diff --git a/examples/adk/content-creation/.gitignore b/examples/adk/content-creation/.gitignore index e26cc81..9e34d3c 100644 --- a/examples/adk/content-creation/.gitignore +++ b/examples/adk/content-creation/.gitignore @@ -37,4 +37,4 @@ Thumbs.db *.jsonl -trash \ No newline at end of file +trash diff --git a/examples/adk/content-creation/local_deployment/start_local_agents.sh b/examples/adk/content-creation/local_deployment/start_local_agents.sh index e8f2848..5e51d17 100755 --- a/examples/adk/content-creation/local_deployment/start_local_agents.sh +++ b/examples/adk/content-creation/local_deployment/start_local_agents.sh @@ -48,11 +48,11 @@ start_agent() { local host=$4 local port=$5 local extra_args=$6 - + echo "Starting $name on $host:$port..." - + cd "$workdir" - + # Start the agent in background # Pass OTEL_RUN_TIMESTAMP if set (for unified timestamp across all agents) if [ -n "$extra_args" ]; then @@ -62,14 +62,14 @@ start_agent() { nohup env OTEL_RUN_TIMESTAMP="${OTEL_RUN_TIMESTAMP:-}" python -m "$module" --host "$host" --port "$port" \ > "$LOG_DIR/${name}_${TS}.log" 2>&1 & fi - + local pid=$! echo $pid > "$PID_DIR/${name}.pid" echo " ✓ Started with PID: $pid" - + # Wait a moment for startup sleep 1 - + # Check if process is still running if ! kill -0 $pid 2>/dev/null; then echo " ✗ ERROR: Process died immediately! Check logs:" @@ -81,9 +81,9 @@ start_agent() { # Function to start coordinator (API mode, no UI) start_coordinator() { echo "Starting Coordinator API on $COORDINATOR_HOST:$COORDINATOR_PORT..." - + cd "$PROJECT_ROOT/src/hosts/coordinator" - + # Ensure environment variables are passed export PROVIDER=${PROVIDER:-ollama} export MODEL=${MODEL:-gpt-3.5-turbo} @@ -92,17 +92,17 @@ start_coordinator() { export CONTENT_PLANNER_AGENT_URL=${CONTENT_PLANNER_AGENT_URL:-http://127.0.0.1:10001} export CONTENT_WRITER_AGENT_URL=${CONTENT_WRITER_AGENT_URL:-http://127.0.0.1:10002} export CONTENT_EDITOR_AGENT_URL=${CONTENT_EDITOR_AGENT_URL:-http://127.0.0.1:10003} - + # Use the API-only version without Gradio UI # Pass OTEL_RUN_TIMESTAMP if set (for unified timestamp across all agents) nohup env OTEL_RUN_TIMESTAMP="${OTEL_RUN_TIMESTAMP:-}" python -m coordinator.__main_api__ --host "$COORDINATOR_HOST" --port "$COORDINATOR_PORT" \ > "$LOG_DIR/coordinator_${TS}.log" 2>&1 & - + local pid=$! echo $pid > "$PID_DIR/coordinator.pid" echo " ✓ Started with PID: $pid" sleep 2 - + if ! kill -0 $pid 2>/dev/null; then echo " ✗ ERROR: Coordinator died immediately! Check logs:" echo " tail -50 $LOG_DIR/coordinator_${TS}.log" diff --git a/examples/adk/content-creation/local_deployment/stop_local_agents.sh b/examples/adk/content-creation/local_deployment/stop_local_agents.sh index 062e653..07ab123 100755 --- a/examples/adk/content-creation/local_deployment/stop_local_agents.sh +++ b/examples/adk/content-creation/local_deployment/stop_local_agents.sh @@ -17,37 +17,37 @@ fi stop_process() { local name=$1 local pid_file="$PID_DIR/${name}.pid" - + if [ ! -f "$pid_file" ]; then echo " $name: No PID file found (already stopped?)" return fi - + local pid=$(cat "$pid_file") - + if ! kill -0 $pid 2>/dev/null; then echo " $name: Process $pid not running (stale PID file)" rm -f "$pid_file" return fi - + echo " $name: Stopping PID $pid..." kill $pid 2>/dev/null || true - + # Wait up to 5 seconds for graceful shutdown local count=0 while kill -0 $pid 2>/dev/null && [ $count -lt 50 ]; do sleep 0.1 count=$((count + 1)) done - + # Force kill if still running if kill -0 $pid 2>/dev/null; then echo " $name: Force killing..." kill -9 $pid 2>/dev/null || true sleep 0.5 fi - + rm -f "$pid_file" echo " $name: ✓ Stopped" } diff --git a/examples/adk/content-creation/local_deployment/verify_setup.sh b/examples/adk/content-creation/local_deployment/verify_setup.sh index 67201a2..f223917 100755 --- a/examples/adk/content-creation/local_deployment/verify_setup.sh +++ b/examples/adk/content-creation/local_deployment/verify_setup.sh @@ -14,7 +14,7 @@ echo "1. Checking Python modules..." check_module() { local dir=$1 local module=$2 - + cd "$dir" if python -c "import $module" 2>/dev/null; then echo " ✓ $module importable" @@ -39,7 +39,7 @@ echo "2. Checking port availability..." check_port() { local port=$1 local name=$2 - + if lsof -i :$port > /dev/null 2>&1; then echo " ✗ Port $port ($name) already in use" echo " Kill with: lsof -ti :$port | xargs kill" diff --git a/examples/adk/content-creation/run_experiment.sh b/examples/adk/content-creation/run_experiment.sh index f5a5b7b..b4ac473 100755 --- a/examples/adk/content-creation/run_experiment.sh +++ b/examples/adk/content-creation/run_experiment.sh @@ -235,20 +235,20 @@ run_local() { echo ">>> Generating $INPUT_COUNT random inputs at $INPUT_FILE" generate_inputs "$INPUT_FILE" "$INPUT_COUNT" fi - + for i in $(seq 1 $ITERATIONS); do echo "" echo ">>> [local] Run $i/$ITERATIONS" - + # Cleanup previous run cleanup_local - + # Generate unified timestamp for this run (all agents will use the same timestamp) # This ensures files from the same run can be grouped accurately without time windows UNIFIED_TIMESTAMP=$(date +%Y%m%d_%H%M%S) export OTEL_RUN_TIMESTAMP="$UNIFIED_TIMESTAMP" echo ">>> Using unified timestamp for this run: $UNIFIED_TIMESTAMP" - + # Clear old traces/metrics from agent directories only (not from root) # This ensures each run starts fresh in agent directories, but root directories accumulate # The root traces/ and metrics/ directories accumulate all runs for analysis @@ -265,13 +265,13 @@ run_local() { rm -f "$agent_dir/metrics"/*.jsonl 2>/dev/null || true fi done - + # Start agents (OTEL_RUN_TIMESTAMP will be passed via environment) (cd "$SCRIPT_DIR/local_deployment" && ./start_local_agents.sh) - + local coord_port coord_port=$(get_env_var "COORDINATOR_PORT" "8093") - + # Health check loop echo ">>> Waiting for coordinator to be ready on port $coord_port..." for retry in {1..30}; do @@ -282,7 +282,7 @@ run_local() { echo ">>> Waiting for coordinator... ($retry/30)" sleep 2 done - + # Send request echo ">>> Sending request..." local run_message @@ -297,18 +297,18 @@ run_local() { else echo ">>> ✗ Run $i failed (check $result_file)" fi - + # Collect traces and metrics echo ">>> Collecting traces and metrics..." local_run_dir="$EXP_DIR/run_${i}_data" mkdir -p "$local_run_dir" - + # Copy traces and metrics to project root (like marketing-agency) # This allows direct analysis from project root without going into subdirectories unified_traces_dir="$SCRIPT_DIR/traces" unified_metrics_dir="$SCRIPT_DIR/metrics" mkdir -p "$unified_traces_dir" "$unified_metrics_dir" - + # Copy traces and metrics from all agents (both to run-specific dir and unified dir) for agent_dir in \ "$SCRIPT_DIR/src/agents/content_planner" \ @@ -316,7 +316,7 @@ run_local() { "$SCRIPT_DIR/src/agents/content_editor" \ "$SCRIPT_DIR/src/hosts/coordinator"; do agent_name=$(basename "$agent_dir") - + # Copy to run-specific directory if [[ -d "$agent_dir/traces" ]]; then cp -r "$agent_dir/traces" "$local_run_dir/${agent_name}_traces" 2>/dev/null || true @@ -324,7 +324,7 @@ run_local() { if [[ -d "$agent_dir/metrics" ]]; then cp -r "$agent_dir/metrics" "$local_run_dir/${agent_name}_metrics" 2>/dev/null || true fi - + # Copy to unified directories for visualization if [[ -d "$agent_dir/traces" ]]; then cp "$agent_dir/traces"/*.jsonl "$unified_traces_dir/" 2>/dev/null || true @@ -333,18 +333,18 @@ run_local() { cp "$agent_dir/metrics"/*.jsonl "$unified_metrics_dir/" 2>/dev/null || true fi done - + # Note: Root traces and metrics are already copied from agent directories above # No need to copy again from root directories to avoid duplication - + # Move logs if [ -d "$SCRIPT_DIR/local_deployment/logs" ]; then mkdir -p "$local_run_dir/logs" mv "$SCRIPT_DIR/local_deployment/logs/"* "$local_run_dir/logs/" 2>/dev/null || true fi - + cleanup_local - + if [[ $i -lt $ITERATIONS ]]; then echo ">>> Waiting 5 seconds before next run..." sleep 5 diff --git a/examples/adk/content-creation/send_request.py b/examples/adk/content-creation/send_request.py index 2fe6242..48b0c69 100755 --- a/examples/adk/content-creation/send_request.py +++ b/examples/adk/content-creation/send_request.py @@ -22,12 +22,12 @@ def send_request( timeout: float = 300.0, ) -> dict: """Send a request to the coordinator. - + Args: message: Message content to send coordinator_url: Coordinator URL timeout: Request timeout in seconds - + Returns: Response data dictionary """ @@ -50,11 +50,11 @@ def send_request( } } } - + print(f"Sending request to: {coordinator_url}") print(f"Message: {message}") print("-" * 60) - + try: with httpx.Client(timeout=timeout) as client: response = client.post( @@ -62,26 +62,26 @@ def send_request( json=payload, headers={"Content-Type": "application/json"} ) - + print(f"HTTP status code: {response.status_code}") - + if response.status_code != 200: print(f"Error response: {response.text}") return {"error": f"HTTP {response.status_code}", "detail": response.text} - + data = response.json() - + # Check JSON-RPC response if "error" in data: print(f"JSON-RPC error: {data['error']}") return data - + if "result" in data: print("✓ Request successful") - + # Try to extract response content result = data.get("result", {}) - + # Check for message message_data = result.get("message", {}) if message_data: @@ -90,7 +90,7 @@ def send_request( if part.get("kind") == "text": # Show full text without truncation print(f"\nResponse message: {part.get('text', '')}") - + # Check for artifacts artifacts = result.get("artifacts", []) if artifacts: @@ -107,7 +107,7 @@ def send_request( print(f" {text}") else: print(f" (empty)") - + # Also check history for any agent messages with content history = result.get("history", []) if history: @@ -121,12 +121,12 @@ def send_request( if text and len(text) > 20: # Only show substantial messages # Show full text without truncation print(f" [{i}] {role}: {text}") - + return data else: print(f"Unknown response format: {data}") return data - + except httpx.TimeoutException: print(f"✗ Request timeout (exceeded {timeout} seconds)") return {"error": "timeout"} @@ -148,69 +148,69 @@ def main(): Examples: # Use default message python send_request.py - + # Custom message python send_request.py "Create a blog post about Python" - + # Specify coordinator URL python send_request.py --coordinator http://localhost:8093 --message "Your message" - + # Display full JSON response python send_request.py --message "Your message" --json """ ) - + parser.add_argument( "message", nargs="?", default="Create a LinkedIn post about getting started with the Agent2Agent protocol", help="Message to send (default: LinkedIn post request)." ) - + parser.add_argument( "--coordinator", default="http://localhost:8093", help="Coordinator URL (default: http://localhost:8093)" ) - + parser.add_argument( "--message", dest="message_opt", help="Specify message via --message argument (overrides positional argument)" ) - + parser.add_argument( "--timeout", type=float, default=300.0, help="Request timeout in seconds (default: 300)" ) - + parser.add_argument( "--json", action="store_true", help="Output full response in JSON format" ) - + args = parser.parse_args() - + # Prefer --message argument message = args.message_opt if args.message_opt else args.message - + # Send request result = send_request( message=message, coordinator_url=args.coordinator, timeout=args.timeout ) - + # If JSON output is needed if args.json: print("\n" + "=" * 60) print("Full JSON Response:") print("=" * 60) print(json.dumps(result, indent=2, ensure_ascii=False)) - + # Return appropriate exit code if "error" in result: sys.exit(1) diff --git a/examples/adk/content-creation/src/agents/content_editor/Dockerfile b/examples/adk/content-creation/src/agents/content_editor/Dockerfile index d8cfb4d..13f6e95 100644 --- a/examples/adk/content-creation/src/agents/content_editor/Dockerfile +++ b/examples/adk/content-creation/src/agents/content_editor/Dockerfile @@ -23,4 +23,4 @@ RUN uv sync --no-cache-dir && \ rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/* COPY content_editor/ content_editor/ -CMD ["content_editor"] \ No newline at end of file +CMD ["content_editor"] diff --git a/examples/adk/content-creation/src/agents/content_editor/build_docker.sh b/examples/adk/content-creation/src/agents/content_editor/build_docker.sh index ec4df1a..fe9f42a 100755 --- a/examples/adk/content-creation/src/agents/content_editor/build_docker.sh +++ b/examples/adk/content-creation/src/agents/content_editor/build_docker.sh @@ -2,4 +2,4 @@ sudo docker build . -t 10.140.0.2:5001/content_editor_agent:latest \ --build-arg DOCKER_REGISTRY=localhost:5001 \ --build-arg VERSION=latest \ --push \ - --no-cache \ No newline at end of file + --no-cache diff --git a/examples/adk/content-creation/src/agents/content_editor/content_editor/README.md b/examples/adk/content-creation/src/agents/content_editor/content_editor/README.md index d70e040..5c3a26c 100644 --- a/examples/adk/content-creation/src/agents/content_editor/content_editor/README.md +++ b/examples/adk/content-creation/src/agents/content_editor/content_editor/README.md @@ -14,4 +14,3 @@ Given a high-level description of the content that's needed, this sample agent c ```bash docker build -t a2a/content_editor . ``` - diff --git a/examples/adk/content-creation/src/agents/content_editor/content_editor/agent-card.json b/examples/adk/content-creation/src/agents/content_editor/content_editor/agent-card.json index d856fe0..6e4ca13 100644 --- a/examples/adk/content-creation/src/agents/content_editor/content_editor/agent-card.json +++ b/examples/adk/content-creation/src/agents/content_editor/content_editor/agent-card.json @@ -19,4 +19,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/examples/adk/content-creation/src/agents/content_editor/content_editor/agent.json b/examples/adk/content-creation/src/agents/content_editor/content_editor/agent.json index d856fe0..6e4ca13 100644 --- a/examples/adk/content-creation/src/agents/content_editor/content_editor/agent.json +++ b/examples/adk/content-creation/src/agents/content_editor/content_editor/agent.json @@ -19,4 +19,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/examples/adk/content-creation/src/agents/content_editor/content_editor/agent_executor.py b/examples/adk/content-creation/src/agents/content_editor/content_editor/agent_executor.py index 7179d74..1f8cc36 100644 --- a/examples/adk/content-creation/src/agents/content_editor/content_editor/agent_executor.py +++ b/examples/adk/content-creation/src/agents/content_editor/content_editor/agent_executor.py @@ -107,4 +107,4 @@ async def execute( TaskState.failed, new_agent_text_message(f"Error: {e!s}", task.context_id, task.id), final=True, - ) \ No newline at end of file + ) diff --git a/examples/adk/content-creation/src/agents/content_editor/content_editor/requirements.txt b/examples/adk/content-creation/src/agents/content_editor/content_editor/requirements.txt index 23b4a59..dfe8ac8 100644 --- a/examples/adk/content-creation/src/agents/content_editor/content_editor/requirements.txt +++ b/examples/adk/content-creation/src/agents/content_editor/content_editor/requirements.txt @@ -6,4 +6,4 @@ litellm opentelemetry-api>=1.20.0 opentelemetry-sdk>=1.20.0 opentelemetry-instrumentation-google-genai>=0.4b0 -opentelemetry-instrumentation-vertexai>=2.0b0 \ No newline at end of file +opentelemetry-instrumentation-vertexai>=2.0b0 diff --git a/examples/adk/content-creation/src/agents/content_planner/content_planner/README.md b/examples/adk/content-creation/src/agents/content_planner/content_planner/README.md index 6e37835..52542c4 100644 --- a/examples/adk/content-creation/src/agents/content_planner/content_planner/README.md +++ b/examples/adk/content-creation/src/agents/content_planner/content_planner/README.md @@ -13,4 +13,4 @@ Given a high-level description of the content that's needed, this sample agent c ```bash docker build -t a2a/content_planner . -``` \ No newline at end of file +``` diff --git a/examples/adk/content-creation/src/agents/content_planner/content_planner/agent_executor.py b/examples/adk/content-creation/src/agents/content_planner/content_planner/agent_executor.py index 7179d74..1f8cc36 100644 --- a/examples/adk/content-creation/src/agents/content_planner/content_planner/agent_executor.py +++ b/examples/adk/content-creation/src/agents/content_planner/content_planner/agent_executor.py @@ -107,4 +107,4 @@ async def execute( TaskState.failed, new_agent_text_message(f"Error: {e!s}", task.context_id, task.id), final=True, - ) \ No newline at end of file + ) diff --git a/examples/adk/content-creation/src/agents/content_planner/content_planner/requirements.txt b/examples/adk/content-creation/src/agents/content_planner/content_planner/requirements.txt index 23b4a59..dfe8ac8 100644 --- a/examples/adk/content-creation/src/agents/content_planner/content_planner/requirements.txt +++ b/examples/adk/content-creation/src/agents/content_planner/content_planner/requirements.txt @@ -6,4 +6,4 @@ litellm opentelemetry-api>=1.20.0 opentelemetry-sdk>=1.20.0 opentelemetry-instrumentation-google-genai>=0.4b0 -opentelemetry-instrumentation-vertexai>=2.0b0 \ No newline at end of file +opentelemetry-instrumentation-vertexai>=2.0b0 diff --git a/examples/adk/content-creation/src/agents/content_planner/pyproject.toml b/examples/adk/content-creation/src/agents/content_planner/pyproject.toml index efbc745..0e985c9 100644 --- a/examples/adk/content-creation/src/agents/content_planner/pyproject.toml +++ b/examples/adk/content-creation/src/agents/content_planner/pyproject.toml @@ -11,4 +11,4 @@ dependencies = [ "gradio>=5.30.0", "a2a-sdk>=0.2.6", "litellm>=1.0.0" -] \ No newline at end of file +] diff --git a/examples/adk/content-creation/src/agents/content_writer/content_writer/README.md b/examples/adk/content-creation/src/agents/content_writer/content_writer/README.md index a84defe..336f1a8 100644 --- a/examples/adk/content-creation/src/agents/content_writer/content_writer/README.md +++ b/examples/adk/content-creation/src/agents/content_writer/content_writer/README.md @@ -13,4 +13,4 @@ Given a high-level description of the content that's needed, this sample agent c ```bash docker build -t a2a/content_writer . -``` \ No newline at end of file +``` diff --git a/examples/adk/content-creation/src/agents/content_writer/content_writer/agent_executor.py b/examples/adk/content-creation/src/agents/content_writer/content_writer/agent_executor.py index 7179d74..1f8cc36 100644 --- a/examples/adk/content-creation/src/agents/content_writer/content_writer/agent_executor.py +++ b/examples/adk/content-creation/src/agents/content_writer/content_writer/agent_executor.py @@ -107,4 +107,4 @@ async def execute( TaskState.failed, new_agent_text_message(f"Error: {e!s}", task.context_id, task.id), final=True, - ) \ No newline at end of file + ) diff --git a/examples/adk/content-creation/src/agents/content_writer/content_writer/requirements.txt b/examples/adk/content-creation/src/agents/content_writer/content_writer/requirements.txt index 23b4a59..dfe8ac8 100644 --- a/examples/adk/content-creation/src/agents/content_writer/content_writer/requirements.txt +++ b/examples/adk/content-creation/src/agents/content_writer/content_writer/requirements.txt @@ -6,4 +6,4 @@ litellm opentelemetry-api>=1.20.0 opentelemetry-sdk>=1.20.0 opentelemetry-instrumentation-google-genai>=0.4b0 -opentelemetry-instrumentation-vertexai>=2.0b0 \ No newline at end of file +opentelemetry-instrumentation-vertexai>=2.0b0 diff --git a/examples/adk/content-creation/src/agents/content_writer/pyproject.toml b/examples/adk/content-creation/src/agents/content_writer/pyproject.toml index efbc745..0e985c9 100644 --- a/examples/adk/content-creation/src/agents/content_writer/pyproject.toml +++ b/examples/adk/content-creation/src/agents/content_writer/pyproject.toml @@ -11,4 +11,4 @@ dependencies = [ "gradio>=5.30.0", "a2a-sdk>=0.2.6", "litellm>=1.0.0" -] \ No newline at end of file +] diff --git a/examples/adk/content-creation/src/agents/shared/.env.example b/examples/adk/content-creation/src/agents/shared/.env.example index b8426bf..2254851 100644 --- a/examples/adk/content-creation/src/agents/shared/.env.example +++ b/examples/adk/content-creation/src/agents/shared/.env.example @@ -1 +1 @@ -GOOGLE_API_KEY="your key" \ No newline at end of file +GOOGLE_API_KEY="your key" diff --git a/examples/adk/content-creation/src/agents/shared/__init__.py b/examples/adk/content-creation/src/agents/shared/__init__.py index 1701ffd..2b8199f 100644 --- a/examples/adk/content-creation/src/agents/shared/__init__.py +++ b/examples/adk/content-creation/src/agents/shared/__init__.py @@ -1,2 +1 @@ # Shared utilities for agents - diff --git a/examples/adk/content-creation/src/agents/shared/telemetry_setup.py b/examples/adk/content-creation/src/agents/shared/telemetry_setup.py index bdd26f0..eaad977 100644 --- a/examples/adk/content-creation/src/agents/shared/telemetry_setup.py +++ b/examples/adk/content-creation/src/agents/shared/telemetry_setup.py @@ -46,10 +46,10 @@ class JsonFileMetricExporter(MetricExporter): """Exporter that writes metrics to local JSON files.""" - + def __init__(self, file_path: str): """Initialize JSON file exporter. - + Args: file_path: Path to JSON file """ @@ -59,35 +59,35 @@ def __init__(self, file_path: str): # Use None to accept defaults (same as ConsoleMetricExporter) self._preferred_temporality = None self._preferred_aggregation = None - + def export(self, metrics_data, timeout_millis: float = 10000, **kwargs): """Export metrics to JSON file. - + Args: metrics_data: Metrics data to export timeout_millis: Maximum time to wait for export (not used for file export) **kwargs: Additional arguments - + Returns: MetricExportResult.SUCCESS """ try: # Convert metrics to serializable format metric_records = [] - + # Extract metric data from the metrics_data object if hasattr(metrics_data, 'resource_metrics'): for resource_metric in metrics_data.resource_metrics: resource_attrs = dict(resource_metric.resource.attributes) if resource_metric.resource and resource_metric.resource.attributes else {} - + for scope_metric in resource_metric.scope_metrics: scope_name = scope_metric.scope.name if scope_metric.scope else "unknown" - + for metric in scope_metric.metrics: metric_name = metric.name metric_description = metric.description if hasattr(metric, 'description') else None metric_unit = metric.unit if hasattr(metric, 'unit') else None - + # Extract data points data_points = [] if hasattr(metric, 'data') and hasattr(metric.data, 'data_points'): @@ -98,7 +98,7 @@ def export(self, metrics_data, timeout_millis: float = 10000, **kwargs): "attributes": dict(data_point.attributes) if hasattr(data_point, 'attributes') and data_point.attributes else {}, } data_points.append(point_dict) - + metric_record = { "timestamp": datetime.now().isoformat(), "metric_name": metric_name, @@ -111,32 +111,32 @@ def export(self, metrics_data, timeout_millis: float = 10000, **kwargs): "scope": scope_name, } metric_records.append(metric_record) - + # Append to file (supports incremental writes) if metric_records: with open(self.file_path, "a", encoding="utf-8") as f: for record in metric_records: f.write(json.dumps(record, default=str) + "\n") - + return MetricExportResult.SUCCESS except Exception as e: logger.error(f"Failed to export metrics to {self.file_path}: {e}", exc_info=True) return MetricExportResult.FAILURE - + def force_flush(self, timeout_millis: int = 30000): """Force flush any pending metrics. - + Args: timeout_millis: Maximum time to wait for flush - + Returns: True if flush succeeded, False otherwise """ return True - + def shutdown(self, timeout_millis: float = 30000, **kwargs): """Shutdown the exporter. - + Args: timeout_millis: Maximum time to wait for shutdown **kwargs: Additional arguments @@ -146,23 +146,23 @@ def shutdown(self, timeout_millis: float = 30000, **kwargs): class JsonFileSpanExporter(SpanExporter): """Exporter that writes spans to local JSON files.""" - + def __init__(self, file_path: str): """Initialize JSON file exporter. - + Args: file_path: Path to JSON file """ self.file_path = Path(file_path) self.file_path.parent.mkdir(parents=True, exist_ok=True) self.spans = [] - + def export(self, spans): """Export spans to JSON file. - + Args: spans: List of spans to export - + Returns: SpanExportResult.SUCCESS """ @@ -176,7 +176,7 @@ def export(self, spans): parent_span_id = format(span.parent.span_id, "016x") elif hasattr(span, "parent_context") and span.parent_context: parent_span_id = format(span.parent_context.span_id, "016x") - + # Extract agent name from span attributes or name for better identification attributes = dict(span.attributes) if span.attributes else {} agent_name = ( @@ -185,11 +185,11 @@ def export(self, spans): (span.name.split()[-1] if "invoke_agent" in span.name and len(span.name.split()) > 1 else None) or "unknown" ) - + # Add agent_name to attributes if not already present (for backward compatibility) if "agent.name" not in attributes and agent_name != "unknown": attributes["agent.name"] = agent_name - + # Ensure gen_ai.agent.name is present (required by template) # For call_llm spans, try to extract from gcp.vertex.agent.llm_request labels if "gen_ai.agent.name" not in attributes: @@ -208,7 +208,7 @@ def export(self, spans): agent_name = adk_agent_name except (json.JSONDecodeError, KeyError, TypeError): pass - + # If still not found, use extracted agent_name or agent.name if "gen_ai.agent.name" not in attributes: if agent_name != "unknown": @@ -218,7 +218,7 @@ def export(self, spans): else: # Field must exist per template, set to null if not available attributes["gen_ai.agent.name"] = None - + # Ensure LLM usage fields are present (default to 0 if missing) if "gen_ai.usage.input_tokens" not in attributes: attributes["gen_ai.usage.input_tokens"] = 0 @@ -229,14 +229,14 @@ def export(self, spans): input_tokens = attributes.get("gen_ai.usage.input_tokens", 0) or 0 output_tokens = attributes.get("gen_ai.usage.output_tokens", 0) or 0 attributes["gen_ai.usage.total_tokens"] = input_tokens + output_tokens - + # Add LLM/MCP call counts (default to 0 if missing) if "gen_ai.llm.call.count" not in attributes: # Set to 1 if this is a call_llm span, otherwise 0 attributes["gen_ai.llm.call.count"] = 1 if "call_llm" in span.name.lower() else 0 if "gen_ai.mcp.call.count" not in attributes: attributes["gen_ai.mcp.call.count"] = 0 - + # Ensure gen_ai.operation.name is present (required by template) if "gen_ai.operation.name" not in attributes: if "call_llm" in span.name.lower(): @@ -248,7 +248,7 @@ def export(self, spans): elif span.name.lower() == "invocation": # invocation spans are top-level entry points attributes["gen_ai.operation.name"] = "invocation" - + # Ensure gen_ai.conversation.id is present (required by template) # Copy from session_id if available, otherwise set to null if "gen_ai.conversation.id" not in attributes: @@ -258,14 +258,14 @@ def export(self, spans): else: # Field must exist per template, set to null if not available attributes["gen_ai.conversation.id"] = None - + # Add missing optional fields with default values (per template) # gen_ai.agent.description - optional, only in invoke_agent spans if "gen_ai.agent.description" not in attributes: # Try to extract from system_instruction or other sources # For now, set to empty string if not available attributes["gen_ai.agent.description"] = "" - + # gen_ai.tool.* fields - only for execute_tool spans if "gen_ai.tool.name" not in attributes: attributes["gen_ai.tool.name"] = "" @@ -275,48 +275,48 @@ def export(self, spans): attributes["gen_ai.tool.call.id"] = "" if "gen_ai.tool.description" not in attributes: attributes["gen_ai.tool.description"] = "" - + # gcp.vertex.agent.tool_* fields - optional if "gcp.vertex.agent.tool_call_args" not in attributes: attributes["gcp.vertex.agent.tool_call_args"] = "" if "gcp.vertex.agent.tool_response" not in attributes: attributes["gcp.vertex.agent.tool_response"] = "" - + # mcp.* fields - optional, not applicable for Vertex AI if "mcp.server" not in attributes: attributes["mcp.server"] = "" if "mcp.tool" not in attributes: attributes["mcp.tool"] = "" - + # agent.log - optional if "agent.log" not in attributes: attributes["agent.log"] = "" - + # Note: CPU/memory metrics are collected separately in metrics JSONL file, # so we don't add them to span attributes (per template guidelines) - + # Calculate communication metrics for agent-to-agent calls communication_metrics = {} input_size = 0 output_size = 0 - + # Check if this is an agent-to-agent communication # In distributed systems, this could be AgentTool or HTTP calls between agents tool_type = attributes.get("gen_ai.tool.type", "") is_agent_tool = tool_type == "AgentTool" - + # For execute_tool spans: check if it's agent communication if "execute_tool" in span.name.lower(): # Check if it's AgentTool (in-process) or HTTP call to another agent (distributed) if is_agent_tool: # In-process agent-to-agent call communication_metrics["is_agent_communication"] = True - + # Input: tool call arguments tool_call_args = attributes.get("gcp.vertex.agent.tool_call_args", "") if tool_call_args and isinstance(tool_call_args, str) and tool_call_args != "{}": input_size = len(tool_call_args.encode('utf-8')) - + # Output: tool response tool_response = attributes.get("gcp.vertex.agent.tool_response", "") if tool_response and isinstance(tool_response, str) and tool_response != "{}": @@ -327,46 +327,46 @@ def export(self, spans): tool_name = attributes.get("gen_ai.tool.name", "").lower() if "message" in tool_name or "agent" in tool_name: communication_metrics["is_agent_communication"] = True - + tool_call_args = attributes.get("gcp.vertex.agent.tool_call_args", "") if tool_call_args and isinstance(tool_call_args, str) and tool_call_args != "{}": input_size = len(tool_call_args.encode('utf-8')) - + tool_response = attributes.get("gcp.vertex.agent.tool_response", "") if tool_response and isinstance(tool_response, str) and tool_response != "{}": output_size = len(tool_response.encode('utf-8')) - + # For call_llm spans: measure actual LLM request and response elif "call_llm" in span.name.lower(): # Input: LLM request llm_request = attributes.get("gcp.vertex.agent.llm_request", "") if llm_request and isinstance(llm_request, str) and llm_request != "{}": input_size = len(llm_request.encode('utf-8')) - + # Output: LLM response llm_response = attributes.get("gcp.vertex.agent.llm_response", "") if llm_response and isinstance(llm_response, str) and llm_response != "{}": output_size = len(llm_response.encode('utf-8')) - + # Store communication metrics and add to attributes for template compliance # Template shows these fields, but per README: "If a field doesn't apply, omit it" # So we only add them when there's actual communication data # Ensure is_in_process_call is always present (default false) if "is_in_process_call" not in communication_metrics: communication_metrics["is_in_process_call"] = False - + if input_size > 0: communication_metrics["input_message_size_bytes"] = input_size attributes["communication.input_message_size_bytes"] = input_size - + if output_size > 0: communication_metrics["output_message_size_bytes"] = output_size attributes["communication.output_message_size_bytes"] = output_size - + if input_size > 0 or output_size > 0: communication_metrics["total_message_size_bytes"] = input_size + output_size attributes["communication.total_message_size_bytes"] = input_size + output_size - + # Get span kind span_kind = "INTERNAL" # Default if hasattr(span, "kind"): @@ -375,7 +375,7 @@ def export(self, spans): span_kind = SpanKind(kind_value).name if kind_value is not None else "INTERNAL" except (ValueError, AttributeError): span_kind = "INTERNAL" - + # Get resource attributes and add host.name if available resource_attrs = dict(span.resource.attributes) if span.resource and span.resource.attributes else {} if "host.name" not in resource_attrs: @@ -385,7 +385,7 @@ def export(self, spans): resource_attrs["host.name"] = hostname except Exception: pass # host.name is optional - + span_dict = { "trace_id": format(span.context.trace_id, "032x"), "span_id": format(span.context.span_id, "016x"), @@ -415,17 +415,17 @@ def export(self, spans): }, } span_data.append(span_dict) - + # Append to file (supports incremental writes) with open(self.file_path, "a", encoding="utf-8") as f: for span_dict in span_data: f.write(json.dumps(span_dict, default=str) + "\n") - + return SpanExportResult.SUCCESS except Exception as e: logger.error(f"Failed to export spans to {self.file_path}: {e}", exc_info=True) return SpanExportResult.FAILURE - + def shutdown(self): """Shutdown the exporter.""" pass @@ -433,17 +433,17 @@ def shutdown(self): def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] = None): """Setup OpenTelemetry tracing with local JSON file export. - + This function configures OpenTelemetry to automatically collect telemetry data from ADK agents. The instrumentation is automatic - ADK and OpenTelemetry instrumentation libraries will automatically create spans for: - + - Agent invocations (invoke_agent) - LLM calls (call_llm) with model, tokens, and request/response data - Tool/function executions (execute_tool) - A2A server operations (request handling, event queue operations) - HTTP requests between agents - + The collected data includes: - Trace IDs and Span IDs for distributed tracing - Timing information (start time, end time, duration) @@ -451,7 +451,7 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] - Attributes (gen_ai.system, gen_ai.request.model, gen_ai.usage.*, etc.) - Events (exceptions, custom events) - Resource attributes (service.name, service.version, etc.) - + Args: service_name: Service name, defaults to OTEL_SERVICE_NAME env var or "adk-agent" trace_file: Trace file path, defaults to OTEL_TRACE_FILE env var or auto-generated @@ -460,7 +460,7 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] # Get configuration from environment variables service_name = service_name or os.getenv("OTEL_SERVICE_NAME", "adk-agent") trace_file = trace_file or os.getenv("OTEL_TRACE_FILE") - + if not trace_file: # Default file path: traces directory in current working directory trace_dir = Path.cwd() / "traces" @@ -471,21 +471,21 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] if not timestamp: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") trace_file = str(trace_dir / f"{service_name}_{timestamp}.jsonl") - + logger.info(f"Setting up OpenTelemetry tracing for {service_name}") logger.info(f"Trace file: {trace_file}") - + # Create resource resource = Resource.create({ "service.name": service_name, "service.version": os.getenv("OTEL_SERVICE_VERSION", "1.0.0"), "deployment.environment": os.getenv("OTEL_DEPLOYMENT_ENVIRONMENT", "local"), }) - + # Check if tracer provider already exists current_provider = trace.get_tracer_provider() provider_already_set = isinstance(current_provider, TracerProvider) - + # If current provider is not a TracerProvider instance, create a new one if not provider_already_set: tracer_provider = TracerProvider(resource=resource) @@ -501,15 +501,15 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] tracer_provider.resource = Resource.create(merged_attrs) else: tracer_provider.resource = resource - + # Create and add JSON file exporter json_exporter = JsonFileSpanExporter(trace_file) span_processor = SimpleSpanProcessor(json_exporter) tracer_provider.add_span_processor(span_processor) - + # Ensure tracer provider is set trace.set_tracer_provider(tracer_provider) - + # Set environment variables to enable instrumentation libraries os.environ.setdefault("OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED", "true") # Enable full message content capture for GenAI instrumentation @@ -520,7 +520,7 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] os.environ.setdefault("OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE", ".*") # Increase max attribute length to capture full messages (default is 250) os.environ.setdefault("OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT", "10000") - + # Try to enable ADK's telemetry (only succeeds if OTLP endpoint is set) # Even if it fails, instrumentation libraries will use our tracer provider otel_endpoint = os.getenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") or os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT") @@ -531,11 +531,11 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] logger.info(f"ADK OTLP exporter enabled for {otel_endpoint}") except Exception as e: logger.warning(f"Failed to enable ADK OTLP exporter: {e}") - + logger.info("Tracer provider configured with JSON file exporter") - + logger.info("OpenTelemetry tracing setup completed") - + except Exception as e: logger.error(f"Failed to setup tracing: {e}", exc_info=True) # Don't raise exception, allow application to continue (tracing is optional) @@ -543,32 +543,32 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] def setup_metrics(service_name: Optional[str] = None, metrics_file: Optional[str] = None, enable_system_metrics: bool = True): """Setup OpenTelemetry metrics with system resource monitoring and local JSON file export. - + This function configures OpenTelemetry Metrics to collect: - CPU usage (percentage) - Memory usage (percentage and absolute values) - Process-specific metrics - + Metrics are exported to local JSON files, similar to trace export. - + Note: In containerized environments, psutil will report container-level metrics, not host-level metrics. For host-level metrics, use cadvisor. - + Args: service_name: Service name for resource attributes metrics_file: Metrics file path, defaults to OTEL_METRICS_FILE env var or auto-generated enable_system_metrics: Whether to enable CPU/memory monitoring (default: True) """ global _metrics_initialized, _metrics_thread - + if _metrics_initialized: logger.warning("Metrics already initialized, skipping") return - + try: service_name = service_name or os.getenv("OTEL_SERVICE_NAME", "adk-agent") metrics_file = metrics_file or os.getenv("OTEL_METRICS_FILE") - + if not metrics_file: # Default file path: metrics directory in current working directory metrics_dir = Path.cwd() / "metrics" @@ -579,39 +579,39 @@ def setup_metrics(service_name: Optional[str] = None, metrics_file: Optional[str if not timestamp: timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") metrics_file = str(metrics_dir / f"{service_name}_{timestamp}.jsonl") - + logger.info(f"Setting up OpenTelemetry metrics for {service_name}") logger.info(f"Metrics file: {metrics_file}") - + # Create resource resource = Resource.create({ "service.name": service_name, "service.version": os.getenv("OTEL_SERVICE_VERSION", "1.0.0"), "deployment.environment": os.getenv("OTEL_DEPLOYMENT_ENVIRONMENT", "local"), }) - + # Create JSON file exporter json_exporter = JsonFileMetricExporter(metrics_file) - + # Create metric reader with periodic export (every 0.1 seconds for fine granularity) reader = PeriodicExportingMetricReader( exporter=json_exporter, export_interval_millis=100, # Export every 0.1 seconds for fine granularity ) - + # Create meter provider meter_provider = MeterProvider( resource=resource, metric_readers=[reader], ) metrics.set_meter_provider(meter_provider) - + meter = metrics.get_meter(__name__) - + if enable_system_metrics: try: import psutil - + # Create observable gauges for CPU and memory cpu_gauge = meter.create_observable_gauge( name="system.cpu.usage", @@ -619,21 +619,21 @@ def setup_metrics(service_name: Optional[str] = None, metrics_file: Optional[str unit="%", callbacks=[_get_cpu_usage], ) - + memory_gauge = meter.create_observable_gauge( name="system.memory.usage", description="Memory usage percentage", unit="%", callbacks=[_get_memory_usage], ) - + memory_bytes_gauge = meter.create_observable_gauge( name="system.memory.usage_bytes", description="Memory usage in bytes", unit="bytes", callbacks=[_get_memory_usage_bytes], ) - + # Process-specific metrics process_cpu_gauge = meter.create_observable_gauge( name="process.cpu.usage", @@ -641,26 +641,26 @@ def setup_metrics(service_name: Optional[str] = None, metrics_file: Optional[str unit="%", callbacks=[_get_process_cpu_usage], ) - + process_memory_gauge = meter.create_observable_gauge( name="process.memory.usage_bytes", description="Process memory usage in bytes", unit="bytes", callbacks=[_get_process_memory_usage], ) - + logger.info("System metrics (CPU/memory) enabled using psutil") logger.info("Note: In containers, psutil reports container-level metrics") - + except ImportError: logger.warning("psutil not installed, skipping system metrics. Install with: pip install psutil") except Exception as e: logger.warning(f"Failed to setup system metrics: {e}") - + _metrics_initialized = True logger.info("Meter provider configured with JSON file exporter") logger.info("OpenTelemetry metrics setup completed") - + except Exception as e: logger.error(f"Failed to setup metrics: {e}", exc_info=True) # Don't raise exception, allow application to continue (metrics are optional) @@ -704,7 +704,7 @@ def _get_memory_usage_bytes(callback_options): def _get_process_cpu_usage(callback_options): """Callback to get process CPU usage percentage. - + Note: psutil.Process().cpu_percent(interval=None) requires two calls to calculate. First call returns 0.0, subsequent calls return the percentage since last call. We use interval=0.1 to get immediate accurate reading. @@ -713,13 +713,13 @@ def _get_process_cpu_usage(callback_options): try: import psutil from opentelemetry.metrics import Observation - + if _process_obj is None: _process_obj = psutil.Process() # First call to initialize, returns 0.0 _process_obj.cpu_percent(interval=0.5) return [Observation(0.0)] - + # Subsequent calls return actual CPU usage # Use interval=0.1 for more accurate reading cpu_percent = _process_obj.cpu_percent(interval=0.5) @@ -735,12 +735,11 @@ def _get_process_memory_usage(callback_options): try: import psutil from opentelemetry.metrics import Observation - + if _process_obj is None: _process_obj = psutil.Process() - + memory_info = _process_obj.memory_info() return [Observation(memory_info.rss)] # Resident Set Size except Exception: return [] - diff --git a/examples/adk/content-creation/src/hosts/a2a_samples_ui/Containerfile b/examples/adk/content-creation/src/hosts/a2a_samples_ui/Containerfile index 8e9ba57..c833e6f 100644 --- a/examples/adk/content-creation/src/hosts/a2a_samples_ui/Containerfile +++ b/examples/adk/content-creation/src/hosts/a2a_samples_ui/Containerfile @@ -39,4 +39,4 @@ EXPOSE 12000 USER 1001 # Run the agent -CMD ["uv", "run", "main.py", "--host", "0.0.0.0"] \ No newline at end of file +CMD ["uv", "run", "main.py", "--host", "0.0.0.0"] diff --git a/examples/adk/content-creation/src/hosts/a2a_samples_ui/README.md b/examples/adk/content-creation/src/hosts/a2a_samples_ui/README.md index 3d5680e..df3bd16 100644 --- a/examples/adk/content-creation/src/hosts/a2a_samples_ui/README.md +++ b/examples/adk/content-creation/src/hosts/a2a_samples_ui/README.md @@ -1,3 +1,3 @@ # Host agent with UI -This host agent is from a2a_samples, please refer to their [documents](https://github.com/a2aproject/a2a-samples/tree/883c8906a60cb48e43599db0480d2217bd02c320/demo). \ No newline at end of file +This host agent is from a2a_samples, please refer to their [documents](https://github.com/a2aproject/a2a-samples/tree/883c8906a60cb48e43599db0480d2217bd02c320/demo). diff --git a/examples/adk/content-creation/src/hosts/coordinator/Dockerfile b/examples/adk/content-creation/src/hosts/coordinator/Dockerfile index ba2fe83..ca30585 100644 --- a/examples/adk/content-creation/src/hosts/coordinator/Dockerfile +++ b/examples/adk/content-creation/src/hosts/coordinator/Dockerfile @@ -18,4 +18,4 @@ COPY . . RUN uv sync --no-cache-dir && \ rm -rf /tmp/* /var/lib/apt/lists/* /var/tmp/* -CMD ["coordinator"] \ No newline at end of file +CMD ["coordinator"] diff --git a/examples/adk/content-creation/src/hosts/coordinator/coordinator/__main_api__.py b/examples/adk/content-creation/src/hosts/coordinator/coordinator/__main_api__.py index ac1b78c..8e4b0d2 100644 --- a/examples/adk/content-creation/src/hosts/coordinator/coordinator/__main_api__.py +++ b/examples/adk/content-creation/src/hosts/coordinator/coordinator/__main_api__.py @@ -71,7 +71,7 @@ def setup_metrics(*args, **kwargs): class ADKAgentExecutor(AgentExecutor): """Executor that wraps Google ADK Agent for A2A protocol""" - + def __init__( self, agent, @@ -113,7 +113,7 @@ async def execute( ) -> None: from a2a.utils import new_task from google.genai import types - + query = context.get_user_input() task = context.current_task or new_task(context.message) await event_queue.enqueue_event(task) @@ -166,7 +166,7 @@ async def execute( resp_data = response_content else: resp_data = func_resp - + # Handle case where resp_data might be a string representation if isinstance(resp_data, str): # Try to extract #IamEditor from string representation @@ -188,7 +188,7 @@ async def execute( if matches: text_matches.extend(matches) break - + if not text_matches: # Fallback: find #IamEditor and extract surrounding text iam_pos = resp_data.find('#IamEditor') @@ -201,7 +201,7 @@ async def execute( if matches: start_pos = matches[-1].end() break - + if start_pos > 0: # Look forward for closing quote or end end_pos = resp_data.find("'", iam_pos) @@ -211,10 +211,10 @@ async def execute( end_pos = resp_data.find('\\n', iam_pos) if end_pos == -1: end_pos = len(resp_data) - + text_match = resp_data[start_pos:end_pos] text_matches.append(text_match) - + if text_matches: # Use the last match (should be the final editor response) for match in reversed(text_matches): @@ -224,7 +224,7 @@ async def execute( response_text = text logger.info(f"Extracted text with #IamEditor from string representation (length: {len(text)})") break - + # Extract text from Task.artifacts (object format) if hasattr(resp_data, "artifacts"): artifacts = resp_data.artifacts @@ -239,7 +239,7 @@ async def execute( text = art_part.root.text elif hasattr(art_part, "text"): text = art_part.text - + if text and isinstance(text, str): # If contains #IamEditor, use it as final response if "#IamEditor" in text: @@ -256,7 +256,7 @@ async def execute( logger.warning(f"Error extracting function response: {e}", exc_info=True) elif hasattr(part, "function_call"): pass # Function calls are handled internally by ADK - + # Also check final response if event.is_final_response() and event.content and event.content.parts: for part in event.content.parts: @@ -286,9 +286,9 @@ async def execute( @click.option('--port', default=8093, help='Port to bind to') def main(host: str, port: int): """Start the Coordinator API server""" - + logger.info(f"Starting Coordinator API server on {host}:{port}") - + # Create agent card agent_card = AgentCard( name='Coordinator Agent', @@ -309,30 +309,30 @@ def main(host: str, port: int): ), ], ) - + # Create task store and executor task_store = InMemoryTaskStore() agent_executor = ADKAgentExecutor( agent=coordinator, ) - + # Create request handler request_handler = DefaultRequestHandler( agent_executor=agent_executor, task_store=task_store, ) - + # Create A2A application app = A2AStarletteApplication( agent_card=agent_card, http_handler=request_handler, ) - + # Build the ASGI app asgi_app = app.build() - + logger.info(f"Coordinator API server ready at http://{host}:{port}") - + # Run server with the ASGI app uvicorn.run( asgi_app, diff --git a/examples/adk/content-creation/src/hosts/coordinator/coordinator/agent.py b/examples/adk/content-creation/src/hosts/coordinator/coordinator/agent.py index 29e59c2..8321ec4 100644 --- a/examples/adk/content-creation/src/hosts/coordinator/coordinator/agent.py +++ b/examples/adk/content-creation/src/hosts/coordinator/coordinator/agent.py @@ -91,7 +91,7 @@ async def _async_init_components( ) -> None: """Asynchronous part of initialization.""" import asyncio - + # Use a single httpx.AsyncClient for all card resolutions for efficiency # Increased timeout from 30s to 60s for better reliability async with httpx.AsyncClient(timeout=60) as client: @@ -99,12 +99,12 @@ async def _async_init_components( card_resolver = A2ACardResolver( client, address ) # Constructor is sync - + # Retry mechanism: up to 5 retries with 2 second intervals max_retries = 5 retry_delay = 2.0 card = None - + for attempt in range(max_retries): try: card = await card_resolver.get_agent_card() @@ -129,7 +129,7 @@ async def _async_init_components( print( f'ERROR: Failed to initialize connection for {address} after {max_retries} attempts: {e}' ) - + # If card is successfully obtained, create connection if card: try: @@ -189,7 +189,7 @@ def create_agent(self) -> Agent: raise ValueError("Unsupported PROVIDER. Please set PROVIDER to 'google', 'aliyun', or 'ollama'.") model_id = llm - + print(f'[Coordinator] Using model: {model_id}') if os.getenv("PROVIDER") == "ollama": print(f'[Coordinator] Provider: OpenAI-compatible (Mock LLM)') diff --git a/examples/adk/content-creation/src/hosts/coordinator/pyproject.toml b/examples/adk/content-creation/src/hosts/coordinator/pyproject.toml index d692063..6abf726 100644 --- a/examples/adk/content-creation/src/hosts/coordinator/pyproject.toml +++ b/examples/adk/content-creation/src/hosts/coordinator/pyproject.toml @@ -17,4 +17,3 @@ dependencies = [ "opentelemetry-instrumentation-vertexai>=2.0b0", "python-dotenv>=1.0.0" ] - diff --git a/examples/adk/content-creation/src/hosts/coordinator/python_and_java_multiagent.egg-info/SOURCES.txt b/examples/adk/content-creation/src/hosts/coordinator/python_and_java_multiagent.egg-info/SOURCES.txt index 97248ee..9afd633 100644 --- a/examples/adk/content-creation/src/hosts/coordinator/python_and_java_multiagent.egg-info/SOURCES.txt +++ b/examples/adk/content-creation/src/hosts/coordinator/python_and_java_multiagent.egg-info/SOURCES.txt @@ -8,4 +8,4 @@ python_and_java_multiagent.egg-info/PKG-INFO python_and_java_multiagent.egg-info/SOURCES.txt python_and_java_multiagent.egg-info/dependency_links.txt python_and_java_multiagent.egg-info/requires.txt -python_and_java_multiagent.egg-info/top_level.txt \ No newline at end of file +python_and_java_multiagent.egg-info/top_level.txt diff --git a/examples/adk/content-creation/src/hosts/coordinator/python_and_java_multiagent.egg-info/dependency_links.txt b/examples/adk/content-creation/src/hosts/coordinator/python_and_java_multiagent.egg-info/dependency_links.txt index 8b13789..e69de29 100644 --- a/examples/adk/content-creation/src/hosts/coordinator/python_and_java_multiagent.egg-info/dependency_links.txt +++ b/examples/adk/content-creation/src/hosts/coordinator/python_and_java_multiagent.egg-info/dependency_links.txt @@ -1 +0,0 @@ - diff --git a/examples/adk/image-scoring/.env.example b/examples/adk/image-scoring/.env.example index 73daacb..88a3ec6 100644 --- a/examples/adk/image-scoring/.env.example +++ b/examples/adk/image-scoring/.env.example @@ -5,9 +5,9 @@ GOOGLE_CLOUD_PROJECT= GOOGLE_CLOUD_LOCATION= GOOGLE_CLOUD_STORAGE_BUCKET= # Only required for deployment on Agent Engine GCS_BUCKET_NAME= #Bucket for storing generated images. -SCORE_THRESHOLD= # Min threshold for image_score. Max Score is 50 , hence should be less than 50. +SCORE_THRESHOLD= # Min threshold for image_score. Max Score is 50 , hence should be less than 50. #If the computed score is higher then loop will terminate MAX_ITERATIONS= #Max iterations for evaluating the image_score before terminating the loop. -IMAGEN_MODEL="imagen-3.0-generate-002" +IMAGEN_MODEL="imagen-3.0-generate-002" GENAI_MODEL="gemini-2.0-flash" -AGENT_ENGINE_ID= #The Agent Engine ID obtained after deploying to agent engine. \ No newline at end of file +AGENT_ENGINE_ID= #The Agent Engine ID obtained after deploying to agent engine. diff --git a/examples/adk/image-scoring/README.md b/examples/adk/image-scoring/README.md index 65c255c..5ec0132 100644 --- a/examples/adk/image-scoring/README.md +++ b/examples/adk/image-scoring/README.md @@ -24,11 +24,11 @@ The key features of the Image Scoring Agent include: | **Components** | Tools: Imagen, Image Evaluation Tools | | **Vertical** | Horizontal | -### Agent architecture: +### Agent architecture: -This diagram shows the detailed architecture of the agents and tools used to implement this workflow. +This diagram shows the detailed architecture of the agents and tools used to implement this workflow. -Image Scoring Architecture +Image Scoring Architecture ## Setup and Installation @@ -68,7 +68,7 @@ This diagram shows the detailed architecture of the agents and tools used to imp with the values appropriate to your project, and save it as `.env`. The values in this file will be read into the environment of your application. - + * Authenticate your GCloud account. ```bash @@ -101,7 +101,7 @@ adk web The command `adk web` will start a web server on your machine and print the URL. You may open the URL, select "image_scoring" in the top-left drop-down menu, and a chatbot interface will appear on the right. The conversation is initially -blank. +blank. ### Using Agent Starter Pack @@ -154,7 +154,7 @@ The Image Scoring Agent can be customized to better suit your requirements. For 1. **Policy Customization:** Modify the policy evaluation criteria to match your specific requirements and standards. 2. **Image Generation Parameters:** Adjust the Imagen parameters to control image generation quality and characteristics. 3. **Evaluation Metrics:** Add or modify evaluation metrics to assess different aspects of the generated images. -4. **Iteration Strategy:** Customize the iteration process to optimize for specific aspects of image quality or policy compliance. +4. **Iteration Strategy:** Customize the iteration process to optimize for specific aspects of image quality or policy compliance. ## Sub-Agents and Workflow diff --git a/examples/adk/image-scoring/eval/data/test_config.json b/examples/adk/image-scoring/eval/data/test_config.json index 89cf58a..991a93d 100644 --- a/examples/adk/image-scoring/eval/data/test_config.json +++ b/examples/adk/image-scoring/eval/data/test_config.json @@ -3,4 +3,4 @@ "tool_trajectory_avg_score": 1.0, "response_match_score": 0.2 } -} \ No newline at end of file +} diff --git a/examples/adk/image-scoring/image_scoring/agent.py b/examples/adk/image-scoring/image_scoring/agent.py index 55fe767..7b46a26 100644 --- a/examples/adk/image-scoring/image_scoring/agent.py +++ b/examples/adk/image-scoring/image_scoring/agent.py @@ -2,9 +2,9 @@ from zoneinfo import ZoneInfo import os import google.auth -from .sub_agents.prompt import image_generation_prompt_agent -from .sub_agents.image import image_generation_agent -from .sub_agents.scoring import scoring_images_prompt +from .sub_agents.prompt import image_generation_prompt_agent +from .sub_agents.image import image_generation_agent +from .sub_agents.scoring import scoring_images_prompt from .checker_agent import checker_agent_instance from google.adk.agents import SequentialAgent, LoopAgent from google.adk.agents.callback_context import CallbackContext diff --git a/examples/adk/image-scoring/image_scoring/policy.json b/examples/adk/image-scoring/image_scoring/policy.json index 63cf284..5866a46 100644 --- a/examples/adk/image-scoring/image_scoring/policy.json +++ b/examples/adk/image-scoring/image_scoring/policy.json @@ -10,5 +10,3 @@ "Composition Styles": "Provides different composition styles to make lockscreen content visually appealing. It defines multiple different styles with its description, effect and other parameter.", "Color Scheme Definitions": "Standards for color usage, combinations, and schemes in lockscreen content. It ensures sufficient contrast for visibility and to create harmonious color relationships. Color schemes include monochrome, analogous, complementary, black and white, and natural." } - - diff --git a/examples/adk/image-scoring/image_scoring/prompt.py b/examples/adk/image-scoring/image_scoring/prompt.py index f8c83a9..23d7f34 100644 --- a/examples/adk/image-scoring/image_scoring/prompt.py +++ b/examples/adk/image-scoring/image_scoring/prompt.py @@ -1,5 +1,5 @@ CHECKER_PROMPT = """ -You are an agent to evaluate the quality of image based on the total_score of the image +You are an agent to evaluate the quality of image based on the total_score of the image generation. 1. Invoke the `image_generation_scoring_agent` first to generate images and score the images. diff --git a/examples/adk/image-scoring/image_scoring/sub_agents/image/prompt.py b/examples/adk/image-scoring/image_scoring/sub_agents/image/prompt.py index 0ed840a..7c19ee3 100644 --- a/examples/adk/image-scoring/image_scoring/sub_agents/image/prompt.py +++ b/examples/adk/image-scoring/image_scoring/sub_agents/image/prompt.py @@ -1,4 +1,4 @@ IMAGEGEN_PROMPT = """ -Your job is to invoke the 'generate_images' tool by passing the `image generation prompt` provided +Your job is to invoke the 'generate_images' tool by passing the `image generation prompt` provided to you as a parameter . """ diff --git a/examples/adk/image-scoring/image_scoring/sub_agents/image/tools/image_generation_tool.py b/examples/adk/image-scoring/image_scoring/sub_agents/image/tools/image_generation_tool.py index a4652c0..2094976 100644 --- a/examples/adk/image-scoring/image_scoring/sub_agents/image/tools/image_generation_tool.py +++ b/examples/adk/image-scoring/image_scoring/sub_agents/image/tools/image_generation_tool.py @@ -46,7 +46,7 @@ async def generate_images(imagen_prompt: str, tool_context: ToolContext): ) await tool_context.save_artifact(artifact_name, report_artifact) - + # Save locally for user visibility with open(artifact_name, "wb") as f: f.write(image_bytes) diff --git a/examples/adk/image-scoring/image_scoring/sub_agents/prompt/prompt.py b/examples/adk/image-scoring/image_scoring/sub_agents/prompt/prompt.py index da337e7..dcad977 100644 --- a/examples/adk/image-scoring/image_scoring/sub_agents/prompt/prompt.py +++ b/examples/adk/image-scoring/image_scoring/sub_agents/prompt/prompt.py @@ -1,23 +1,23 @@ PROMPT = """ - Your primary objective: Transform the input text into a pair of highly optimized prompts—one positive and + Your primary objective: Transform the input text into a pair of highly optimized prompts—one positive and one negative—specifically designed for generating a visually compelling, rule-compliant lockscreen image using the Imagen3 text-to-image model (provided by Google/GCP). - Critical First Step: Before constructing any prompts, you must first analyze the + Critical First Step: Before constructing any prompts, you must first analyze the input text to identify or conceptualize a primary subject. This subject MUST: - 1. Be very much related to the input text presented. The viewer should - feel that the generated image of that subject is conveying + 1. Be very much related to the input text presented. The viewer should + feel that the generated image of that subject is conveying what he/she is reading from that new article. - 2. It should not violate any content restrictions (especially regarding humans, + 2. It should not violate any content restrictions (especially regarding humans, politics, religion, etc.). 3. Describe in detail on what we would like to represent around the primary subject, - as-in, paint a complete picture. - This chosen subject will be the cornerstone of your "Image Generation Prompt". - - Invoke the 'get_policy_text' tool to obtain the 'policy_text'. The 'policy_text' + as-in, paint a complete picture. + This chosen subject will be the cornerstone of your "Image Generation Prompt". + + Invoke the 'get_policy_text' tool to obtain the 'policy_text'. The 'policy_text' defines the rules for the image generation. The image also should comply with rules defined in the 'policy_text'. - - Negative Prompt: Generate a negative prompt to ensure the image does not + + Negative Prompt: Generate a negative prompt to ensure the image does not violate the rules defined in the 'policy_text'. IMPORTANT: You do not have access to image generation tools. You must ONLY output the text prompts. Do not attempt to generate images yourself. diff --git a/examples/adk/image-scoring/image_scoring/sub_agents/scoring/prompt.py b/examples/adk/image-scoring/image_scoring/sub_agents/scoring/prompt.py index 76bc83f..e231586 100644 --- a/examples/adk/image-scoring/image_scoring/sub_agents/scoring/prompt.py +++ b/examples/adk/image-scoring/image_scoring/sub_agents/scoring/prompt.py @@ -60,10 +60,10 @@ "5. Compute the total_score by adding each individual score point for each rule in the JSON " "6. Invoke the set_score tool and pass the total_score. " - + "OUTPUT JSON FORMAT SPECIFICATION:\n" "The JSON object MUST have exactly two top-level keys:" " - 'total_score': Iterate through each individual score element in the json and add those to arrive at total_score. " " - 'scores': The existing rules json with a score attribute assigned to each rule and a reason attribute" - + """ diff --git a/examples/adk/image-scoring/image_scoring/sub_agents/scoring/tools/get_images_tool.py b/examples/adk/image-scoring/image_scoring/sub_agents/scoring/tools/get_images_tool.py index d616a31..5dec5cd 100644 --- a/examples/adk/image-scoring/image_scoring/sub_agents/scoring/tools/get_images_tool.py +++ b/examples/adk/image-scoring/image_scoring/sub_agents/scoring/tools/get_images_tool.py @@ -3,12 +3,12 @@ async def get_image(tool_context: ToolContext): try: - + artifact_name = ( f"generated_image_" + str(tool_context.state.get("loop_iteration", 0)) + ".png" ) artifact = await tool_context.load_artifact(artifact_name) - + return { diff --git a/examples/adk/image-scoring/image_scoring/telemetry_setup.py b/examples/adk/image-scoring/image_scoring/telemetry_setup.py index 401dca1..d06d634 100644 --- a/examples/adk/image-scoring/image_scoring/telemetry_setup.py +++ b/examples/adk/image-scoring/image_scoring/telemetry_setup.py @@ -46,10 +46,10 @@ class JsonFileMetricExporter(MetricExporter): """Exporter that writes metrics to local JSON files.""" - + def __init__(self, file_path: str): """Initialize JSON file exporter. - + Args: file_path: Path to JSON file """ @@ -59,54 +59,54 @@ def __init__(self, file_path: str): # Use None to accept defaults (same as ConsoleMetricExporter) self._preferred_temporality = None self._preferred_aggregation = None - + def export(self, metrics_data, timeout_millis: float = 10000, **kwargs): """Export metrics to JSON file. - + Args: metrics_data: Metrics data to export timeout_millis: Maximum time to wait for export (not used for file export) **kwargs: Additional arguments - + Returns: MetricExportResult.SUCCESS """ try: # Convert metrics to serializable format metric_records = [] - + # Extract metric data from the metrics_data object if hasattr(metrics_data, 'resource_metrics'): for resource_metric in metrics_data.resource_metrics: resource_attrs = dict(resource_metric.resource.attributes) if resource_metric.resource and resource_metric.resource.attributes else {} - + for scope_metric in resource_metric.scope_metrics: scope_name = scope_metric.scope.name if scope_metric.scope else "unknown" - + for metric in scope_metric.metrics: metric_name = metric.name metric_description = metric.description if hasattr(metric, 'description') else None metric_unit = metric.unit if hasattr(metric, 'unit') else None - + # Extract data points data_points = [] if hasattr(metric, 'data') and hasattr(metric.data, 'data_points'): for data_point in metric.data.data_points: point_attrs = dict(data_point.attributes) if hasattr(data_point, 'attributes') and data_point.attributes else {} - + # Add service name as agent identifier if not present if "agent.name" not in point_attrs: # Try to infer from resource attributes or use service name agent_name = resource_attrs.get("service.name", "image-scoring") point_attrs["agent.name"] = agent_name - + point_dict = { "value": data_point.value if hasattr(data_point, 'value') else None, "timestamp": data_point.time_unix_nano if hasattr(data_point, 'time_unix_nano') else None, "attributes": point_attrs, } data_points.append(point_dict) - + metric_record = { "timestamp": datetime.now().isoformat(), "metric_name": metric_name, @@ -119,31 +119,31 @@ def export(self, metrics_data, timeout_millis: float = 10000, **kwargs): "scope": scope_name, } metric_records.append(metric_record) - + # Append to file (supports incremental writes) if metric_records: with open(self.file_path, "a", encoding="utf-8") as f: for record in metric_records: f.write(json.dumps(record, default=str) + "\n") - + return MetricExportResult.SUCCESS except Exception as e: logger.error(f"Failed to export metrics to {self.file_path}: {e}", exc_info=True) return MetricExportResult.FAILURE - + def force_flush(self, timeout_millis: int = 30000, **kwargs): """Force flush any pending metrics. - + Args: timeout_millis: Maximum time to wait for flush. **kwargs: Additional arguments. """ # For file exporter, data is written immediately, so no explicit flush needed pass - + def shutdown(self, timeout_millis: int = 30000, **kwargs): """Shutdown the exporter. - + Args: timeout_millis: Maximum time to wait for shutdown. **kwargs: Additional arguments. @@ -153,23 +153,23 @@ def shutdown(self, timeout_millis: int = 30000, **kwargs): class JsonFileSpanExporter(SpanExporter): """Exporter that writes spans to local JSON files.""" - + def __init__(self, file_path: str): """Initialize JSON file exporter. - + Args: file_path: Path to JSON file """ self.file_path = Path(file_path) self.file_path.parent.mkdir(parents=True, exist_ok=True) self.spans = [] - + def export(self, spans): """Export spans to JSON file. - + Args: spans: List of spans to export - + Returns: SpanExportResult.SUCCESS """ @@ -179,12 +179,12 @@ def export(self, spans): if not spans: logger.warning(f"export() called with empty spans list for {self.file_path}") return SpanExportResult.SUCCESS - + # Debug logging: log all spans received logger.info(f"[DEBUG] export() called with {len(spans)} spans for {self.file_path}") for i, span in enumerate(spans): logger.info(f"[DEBUG] Span {i}: name={span.name}, kind={getattr(span, 'kind', 'N/A')}, type={type(span).__name__}") - + logger.info(f"export() called with {len(spans)} spans for {self.file_path}") for span in spans: # Get parent span ID if exists @@ -193,7 +193,7 @@ def export(self, spans): parent_span_id = format(span.parent.span_id, "016x") elif hasattr(span, "parent_context") and span.parent_context: parent_span_id = format(span.parent_context.span_id, "016x") - + # Extract agent name from span attributes or name for better identification attributes = dict(span.attributes) if span.attributes else {} agent_name = ( @@ -202,11 +202,11 @@ def export(self, spans): (span.name.split()[-1] if "invoke_agent" in span.name and len(span.name.split()) > 1 else None) or "unknown" ) - + # Add agent_name to attributes if not already present (for backward compatibility) if "agent.name" not in attributes and agent_name != "unknown": attributes["agent.name"] = agent_name - + # Ensure gen_ai.agent.name is present (required by template) # For call_llm spans, try to extract from gcp.vertex.agent.llm_request labels if "gen_ai.agent.name" not in attributes: @@ -225,7 +225,7 @@ def export(self, spans): agent_name = adk_agent_name except (json.JSONDecodeError, KeyError, TypeError): pass - + # If still not found, use extracted agent_name or agent.name if "gen_ai.agent.name" not in attributes: if agent_name != "unknown": @@ -235,7 +235,7 @@ def export(self, spans): else: # Field must exist per template, set to null if not available attributes["gen_ai.agent.name"] = None - + # Ensure LLM usage fields are present (default to 0 if missing) if "gen_ai.usage.input_tokens" not in attributes: attributes["gen_ai.usage.input_tokens"] = 0 @@ -246,14 +246,14 @@ def export(self, spans): input_tokens = attributes.get("gen_ai.usage.input_tokens", 0) or 0 output_tokens = attributes.get("gen_ai.usage.output_tokens", 0) or 0 attributes["gen_ai.usage.total_tokens"] = input_tokens + output_tokens - + # Add LLM/MCP call counts (default to 0 if missing) if "gen_ai.llm.call.count" not in attributes: # Set to 1 if this is a call_llm span, otherwise 0 attributes["gen_ai.llm.call.count"] = 1 if "call_llm" in span.name else 0 if "gen_ai.mcp.call.count" not in attributes: attributes["gen_ai.mcp.call.count"] = 0 - + # Ensure gen_ai.operation.name is present (required by template) if "gen_ai.operation.name" not in attributes: if "call_llm" in span.name: @@ -268,7 +268,7 @@ def export(self, spans): elif "invocation" in span.name.lower(): # invocation spans are top-level entry points attributes["gen_ai.operation.name"] = "invocation" - + # Ensure gen_ai.conversation.id is present (required by template) # Copy from session_id if available, otherwise set to null if "gen_ai.conversation.id" not in attributes: @@ -278,14 +278,14 @@ def export(self, spans): else: # Field must exist per template, set to null if not available attributes["gen_ai.conversation.id"] = None - + # Add missing optional fields with default values (per template) # gen_ai.agent.description - optional, only in invoke_agent spans if "gen_ai.agent.description" not in attributes: # Try to extract from system_instruction or other sources # For now, set to empty string if not available attributes["gen_ai.agent.description"] = "" - + # gen_ai.tool.* fields - only for execute_tool spans if "gen_ai.tool.name" not in attributes: attributes["gen_ai.tool.name"] = "" @@ -295,23 +295,23 @@ def export(self, spans): attributes["gen_ai.tool.call.id"] = "" if "gen_ai.tool.description" not in attributes: attributes["gen_ai.tool.description"] = "" - + # gcp.vertex.agent.tool_* fields - optional if "gcp.vertex.agent.tool_call_args" not in attributes: attributes["gcp.vertex.agent.tool_call_args"] = "" if "gcp.vertex.agent.tool_response" not in attributes: attributes["gcp.vertex.agent.tool_response"] = "" - + # mcp.* fields - optional, not applicable for Vertex AI if "mcp.server" not in attributes: attributes["mcp.server"] = "" if "mcp.tool" not in attributes: attributes["mcp.tool"] = "" - + # agent.log - optional if "agent.log" not in attributes: attributes["agent.log"] = "" - + # Ensure gen_ai.agent.name is present for all spans (required by template) # For call_llm and execute_tool spans, try to get from parent context or use extracted agent_name if "gen_ai.agent.name" not in attributes: @@ -332,7 +332,7 @@ def export(self, spans): agent_name = adk_agent_name except (json.JSONDecodeError, KeyError, TypeError): pass - + # If still not found and agent_name is not "unknown", use it if "gen_ai.agent.name" not in attributes and agent_name != "unknown": attributes["gen_ai.agent.name"] = agent_name @@ -345,15 +345,15 @@ def export(self, spans): attributes["gen_ai.agent.name"] = agent_name elif "agent.name" in attributes: attributes["gen_ai.agent.name"] = attributes["agent.name"] - + # Note: CPU/memory metrics are collected separately in metrics JSONL file, # so we don't add them to span attributes (per template guidelines) - + # Calculate communication metrics for agent-to-agent calls communication_metrics = {} input_size = 0 output_size = 0 - + # Check if this is agent-to-agent communication # Only AgentTool calls are agent communication, not FunctionTool calls is_agent_comm = False @@ -375,17 +375,17 @@ def export(self, spans): # and extractors.py will filter based on parent span information. # This is similar to AgentTool in marketing-agency, but uses invoke_agent spans instead of execute_tool spans is_agent_comm = False # Will be determined in extractors.py based on parent span - + # For invoke_agent spans (SequentialAgent/LoopAgent): measure actual input/output data # Try to get tool_call_args and tool_response (same attributes as execute_tool) tool_call_args = attributes.get("gcp.vertex.agent.tool_call_args", "") if tool_call_args and isinstance(tool_call_args, str) and tool_call_args != "{}": input_size = len(tool_call_args.encode('utf-8')) - + tool_response = attributes.get("gcp.vertex.agent.tool_response", "") if tool_response and isinstance(tool_response, str) and tool_response != "{}": output_size = len(tool_response.encode('utf-8')) - + # If not found in gcp.vertex.agent.*, try to serialize Python objects from events # Check span events for input/output data if input_size == 0 and output_size == 0 and hasattr(span, 'events') and span.events: @@ -409,41 +409,41 @@ def export(self, spans): output_size = len(json.dumps(event_data).encode('utf-8')) except: pass - + if is_agent_comm: communication_metrics["is_agent_communication"] = True - + # For execute_tool spans (AgentTool): measure actual tool call arguments and responses if "execute_tool" in span.name: # Input: tool call arguments (actual data passed to the agent) tool_call_args = attributes.get("gcp.vertex.agent.tool_call_args", "") if tool_call_args and isinstance(tool_call_args, str) and tool_call_args != "{}": input_size = len(tool_call_args.encode('utf-8')) - + # Output: tool response (actual data returned from the agent) tool_response = attributes.get("gcp.vertex.agent.tool_response", "") if tool_response and isinstance(tool_response, str) and tool_response != "{}": output_size = len(tool_response.encode('utf-8')) - + # For call_llm spans: measure actual LLM request and response elif "call_llm" in span.name: # Input: LLM request (actual data sent to LLM) llm_request = attributes.get("gcp.vertex.agent.llm_request", "") if llm_request and isinstance(llm_request, str) and llm_request != "{}": input_size = len(llm_request.encode('utf-8')) - + # Output: LLM response (actual data received from LLM) llm_response = attributes.get("gcp.vertex.agent.llm_response", "") if llm_response and isinstance(llm_response, str) and llm_response != "{}": output_size = len(llm_response.encode('utf-8')) - + # Store communication metrics and add to attributes for template compliance # Template shows these fields, but per README: "If a field doesn't apply, omit it" # So we only add them when there's actual communication data # Ensure is_in_process_call is always present (default false) if "is_in_process_call" not in communication_metrics: communication_metrics["is_in_process_call"] = False - + if input_size > 0 or output_size > 0: if input_size > 0: communication_metrics["input_message_size_bytes"] = input_size @@ -462,7 +462,7 @@ def export(self, spans): communication_metrics["input_message_size_bytes"] = 0 communication_metrics["output_message_size_bytes"] = 0 communication_metrics["total_message_size_bytes"] = 0 - + # Get span kind span_kind = "INTERNAL" # Default if hasattr(span, "kind"): @@ -471,7 +471,7 @@ def export(self, spans): span_kind = SpanKind(kind_value).name if kind_value is not None else "INTERNAL" except (ValueError, AttributeError): span_kind = "INTERNAL" - + # Get resource attributes and add host.name if available resource_attrs = dict(span.resource.attributes) if span.resource and span.resource.attributes else {} if "host.name" not in resource_attrs: @@ -481,7 +481,7 @@ def export(self, spans): resource_attrs["host.name"] = hostname except Exception: pass # host.name is optional - + span_dict = { "trace_id": format(span.context.trace_id, "032x"), "span_id": format(span.context.span_id, "016x"), @@ -511,14 +511,14 @@ def export(self, spans): }, } span_data.append(span_dict) - + # Append to file (supports incremental writes) if span_data: with open(self.file_path, "a", encoding="utf-8") as f: for span_dict in span_data: f.write(json.dumps(span_dict, default=str) + "\n") logger.info(f"Exported {len(span_data)} spans to {self.file_path}") - + # Debug: Log span types summary span_types = {} for span_dict in span_data: @@ -527,12 +527,12 @@ def export(self, spans): logger.info(f"[DEBUG] Span types summary: {span_types}") else: logger.warning(f"No spans to export (received {len(spans)} spans, but span_data is empty)") - + return SpanExportResult.SUCCESS except Exception as e: logger.error(f"Failed to export spans to {self.file_path}: {e}", exc_info=True) return SpanExportResult.FAILURE - + def shutdown(self): """Shutdown the exporter.""" pass @@ -540,17 +540,17 @@ def shutdown(self): def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] = None): """Setup OpenTelemetry tracing with local JSON file export. - + This function configures OpenTelemetry to automatically collect telemetry data from ADK agents. The instrumentation is automatic - ADK and OpenTelemetry instrumentation libraries will automatically create spans for: - + - Agent invocations (invoke_agent) - LLM calls (call_llm) with model, tokens, and request/response data - Tool/function executions (execute_tool) - A2A server operations (request handling, event queue operations) - HTTP requests between agents - + The collected data includes: - Trace IDs and Span IDs for distributed tracing - Timing information (start time, end time, duration) @@ -558,7 +558,7 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] - Attributes (gen_ai.system, gen_ai.request.model, gen_ai.usage.*, etc.) - Events (exceptions, custom events) - Resource attributes (service.name, service.version, etc.) - + Args: service_name: Service name, defaults to OTEL_SERVICE_NAME env var or "adk-agent" trace_file: Trace file path, defaults to OTEL_TRACE_FILE env var or auto-generated @@ -567,7 +567,7 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] # Get configuration from environment variables service_name = service_name or os.getenv("OTEL_SERVICE_NAME", "adk-agent") trace_file = trace_file or os.getenv("OTEL_TRACE_FILE") - + if not trace_file: # Default file path: traces directory in current working directory trace_dir = Path.cwd() / "traces" @@ -575,25 +575,25 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] # Use unified timestamp if available, otherwise generate new one timestamp = os.getenv("OTEL_RUN_TIMESTAMP") or datetime.now().strftime("%Y%m%d_%H%M%S") trace_file = str(trace_dir / f"{service_name}_{timestamp}.jsonl") - + logger.info(f"Setting up OpenTelemetry tracing for {service_name}") logger.info(f"Trace file: {trace_file}") - + # Debug: Log current tracer provider state current_provider = trace.get_tracer_provider() logger.info(f"[DEBUG] Current tracer provider: {type(current_provider).__name__}") - + # Create resource resource = Resource.create({ "service.name": service_name, "service.version": os.getenv("OTEL_SERVICE_VERSION", "1.0.0"), "deployment.environment": os.getenv("OTEL_DEPLOYMENT_ENVIRONMENT", "local"), }) - + # Check if tracer provider already exists current_provider = trace.get_tracer_provider() provider_already_set = isinstance(current_provider, TracerProvider) - + # If current provider is not a TracerProvider instance, create a new one if not provider_already_set: tracer_provider = TracerProvider(resource=resource) @@ -605,7 +605,7 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] # For now, we'll just use the existing provider as-is tracer_provider = current_provider logger.debug(f"Using existing TracerProvider (resource attributes cannot be updated)") - + # Create and add JSON file exporter json_exporter = JsonFileSpanExporter(trace_file) # Use SimpleSpanProcessor to export spans immediately when they end @@ -613,10 +613,10 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] span_processor = SimpleSpanProcessor(json_exporter) tracer_provider.add_span_processor(span_processor) logger.info(f"Using SimpleSpanProcessor for trace export to {trace_file}") - + # Ensure tracer provider is set trace.set_tracer_provider(tracer_provider) - + # Set environment variables to enable instrumentation libraries os.environ.setdefault("OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED", "true") # Enable full message content capture for GenAI instrumentation @@ -627,12 +627,12 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] os.environ.setdefault("OTEL_INSTRUMENTATION_HTTP_CAPTURE_HEADERS_SERVER_RESPONSE", ".*") # Increase max attribute length to capture full messages (default is 250) os.environ.setdefault("OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT", "10000") - + # Try to enable ADK's telemetry (only succeeds if OTLP endpoint is set) # Even if it fails, instrumentation libraries will use our tracer provider otel_endpoint = os.getenv("OTEL_EXPORTER_OTLP_TRACES_ENDPOINT") or os.getenv("OTEL_EXPORTER_OTLP_ENDPOINT") logger.info(f"[DEBUG] OTLP endpoint: {otel_endpoint or 'Not set (using JSON file exporter only)'}") - + if otel_endpoint: try: from google.adk.telemetry.setup import maybe_set_otel_providers @@ -643,17 +643,17 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] logger.warning(f"Failed to enable ADK OTLP exporter: {e}") else: logger.info("[DEBUG] OTLP endpoint not set, ADK telemetry will use our JSON file exporter") - + # Debug: Log final tracer provider state final_provider = trace.get_tracer_provider() logger.info(f"[DEBUG] Final tracer provider: {type(final_provider).__name__}") if hasattr(final_provider, 'resource') and final_provider.resource: logger.info(f"[DEBUG] Tracer provider resource: {dict(final_provider.resource.attributes)}") - + logger.info("Tracer provider configured with JSON file exporter") - + logger.info("OpenTelemetry tracing setup completed") - + except Exception as e: logger.error(f"Failed to setup tracing: {e}", exc_info=True) # Don't raise exception, allow application to continue (tracing is optional) @@ -661,32 +661,32 @@ def setup_tracing(service_name: Optional[str] = None, trace_file: Optional[str] def setup_metrics(service_name: Optional[str] = None, metrics_file: Optional[str] = None, enable_system_metrics: bool = True): """Setup OpenTelemetry metrics with system resource monitoring and local JSON file export. - + This function configures OpenTelemetry Metrics to collect: - CPU usage (percentage) - Memory usage (percentage and absolute values) - Process-specific metrics - + Metrics are exported to local JSON files, similar to trace export. - + Note: In containerized environments, psutil will report container-level metrics, not host-level metrics. For host-level metrics, use cadvisor. - + Args: service_name: Service name for resource attributes metrics_file: Metrics file path, defaults to OTEL_METRICS_FILE env var or auto-generated enable_system_metrics: Whether to enable CPU/memory monitoring (default: True) """ global _metrics_initialized, _metrics_thread - + if _metrics_initialized: logger.warning("Metrics already initialized, skipping") return - + try: service_name = service_name or os.getenv("OTEL_SERVICE_NAME", "adk-agent") metrics_file = metrics_file or os.getenv("OTEL_METRICS_FILE") - + if not metrics_file: # Default file path: metrics directory in current working directory metrics_dir = Path.cwd() / "metrics" @@ -694,35 +694,35 @@ def setup_metrics(service_name: Optional[str] = None, metrics_file: Optional[str # Use unified timestamp if available, otherwise generate new one timestamp = os.getenv("OTEL_RUN_TIMESTAMP") or datetime.now().strftime("%Y%m%d_%H%M%S") metrics_file = str(metrics_dir / f"{service_name}_{timestamp}.jsonl") - + logger.info(f"Setting up OpenTelemetry metrics for {service_name}") logger.info(f"Metrics file: {metrics_file}") - + # Create resource resource = Resource.create({ "service.name": service_name, "service.version": os.getenv("OTEL_SERVICE_VERSION", "1.0.0"), "deployment.environment": os.getenv("OTEL_DEPLOYMENT_ENVIRONMENT", "local"), }) - + # Create JSON file exporter json_exporter = JsonFileMetricExporter(metrics_file) - + # Create metric reader with periodic export (every 1 seconds) reader = PeriodicExportingMetricReader( exporter=json_exporter, export_interval_millis=1000, # Export every 1 seconds ) - + # Create meter provider meter_provider = MeterProvider( resource=resource, metric_readers=[reader], ) metrics.set_meter_provider(meter_provider) - + meter = metrics.get_meter(__name__) - + # Create custom metrics for agent communication # These will be updated via span processor hooks communication_counter = meter.create_counter( @@ -730,25 +730,25 @@ def setup_metrics(service_name: Optional[str] = None, metrics_file: Optional[str description="Number of agent-to-agent communications", unit="1", ) - + communication_duration = meter.create_histogram( name="agent.communication.duration", description="Duration of agent-to-agent communications", unit="ms", ) - + communication_message_size = meter.create_histogram( name="agent.communication.message_size", description="Size of messages passed between agents", unit="bytes", ) - + logger.info("Custom communication metrics created") - + if enable_system_metrics: try: import psutil - + # Process-specific metrics only (system metrics removed per user request) process_cpu_gauge = meter.create_observable_gauge( name="process.cpu.usage", @@ -756,26 +756,26 @@ def setup_metrics(service_name: Optional[str] = None, metrics_file: Optional[str unit="%", callbacks=[_get_process_cpu_usage], ) - + process_memory_gauge = meter.create_observable_gauge( name="process.memory.usage_bytes", description="Process memory usage in bytes", unit="bytes", callbacks=[_get_process_memory_usage], ) - + logger.info("Process metrics (CPU/memory) enabled using psutil") logger.info("Note: System-level metrics removed per user request") - + except ImportError: logger.warning("psutil not installed, skipping system metrics. Install with: pip install psutil") except Exception as e: logger.warning(f"Failed to setup system metrics: {e}") - + _metrics_initialized = True logger.info("Meter provider configured with JSON file exporter") logger.info("OpenTelemetry metrics setup completed") - + except Exception as e: logger.error(f"Failed to setup metrics: {e}", exc_info=True) # Don't raise exception, allow application to continue (metrics are optional) @@ -804,7 +804,7 @@ def _get_memory_usage_bytes(callback_options): def _get_process_cpu_usage(callback_options): """Callback to get process CPU usage percentage. - + Note: psutil.Process().cpu_percent(interval=None) requires two calls to calculate. First call returns 0.0, subsequent calls return the percentage since last call. We use interval=0.1 to get immediate accurate reading. @@ -813,13 +813,13 @@ def _get_process_cpu_usage(callback_options): try: import psutil from opentelemetry.metrics import Observation - + if _process_obj is None: _process_obj = psutil.Process() # First call to initialize, returns 0.0 _process_obj.cpu_percent(interval=0.5) return [Observation(0.0)] - + # Subsequent calls return actual CPU usage # Use interval=0.1 for more accurate reading cpu_percent = _process_obj.cpu_percent(interval=0.5) @@ -839,4 +839,3 @@ def _get_process_memory_usage(callback_options): return [Observation(memory_info.rss)] # Resident Set Size except Exception: return [] - diff --git a/examples/adk/image-scoring/image_scoring/tools/loop_condition_tool.py b/examples/adk/image-scoring/image_scoring/tools/loop_condition_tool.py index 26fc6aa..b64e684 100644 --- a/examples/adk/image-scoring/image_scoring/tools/loop_condition_tool.py +++ b/examples/adk/image-scoring/image_scoring/tools/loop_condition_tool.py @@ -5,7 +5,7 @@ def check_condition_and_escalate_tool(tool_context: ToolContext) -> dict: """Checks the loop termination condition and escalates if met or max count reached.""" - + # Increment loop iteration count using state current_loop_count = tool_context.state.get("loop_iteration", 0) diff --git a/examples/adk/marketing-agency/demo_html/about.html b/examples/adk/marketing-agency/demo_html/about.html index 4a47efb..36f5786 100644 --- a/examples/adk/marketing-agency/demo_html/about.html +++ b/examples/adk/marketing-agency/demo_html/about.html @@ -93,4 +93,4 @@

Customer Joy

- \ No newline at end of file + diff --git a/examples/adk/marketing-agency/demo_html/contact.html b/examples/adk/marketing-agency/demo_html/contact.html index 344b60b..a444c2f 100644 --- a/examples/adk/marketing-agency/demo_html/contact.html +++ b/examples/adk/marketing-agency/demo_html/contact.html @@ -101,4 +101,4 @@

Business Hours

- \ No newline at end of file + diff --git a/examples/adk/marketing-agency/demo_html/gallery.html b/examples/adk/marketing-agency/demo_html/gallery.html index a2d8896..fda9cf4 100644 --- a/examples/adk/marketing-agency/demo_html/gallery.html +++ b/examples/adk/marketing-agency/demo_html/gallery.html @@ -129,4 +129,4 @@

[Assorted Cupcakes]

- \ No newline at end of file + diff --git a/examples/adk/marketing-agency/demo_html/index.html b/examples/adk/marketing-agency/demo_html/index.html index c241082..bbd6162 100644 --- a/examples/adk/marketing-agency/demo_html/index.html +++ b/examples/adk/marketing-agency/demo_html/index.html @@ -118,4 +118,4 @@

Ready for the Perfect Cake?

- \ No newline at end of file + diff --git a/examples/adk/marketing-agency/demo_html/script.js b/examples/adk/marketing-agency/demo_html/script.js index ccbd729..affdb92 100644 --- a/examples/adk/marketing-agency/demo_html/script.js +++ b/examples/adk/marketing-agency/demo_html/script.js @@ -55,4 +55,4 @@ document.addEventListener('DOMContentLoaded', function() { }); }); -}); \ No newline at end of file +}); diff --git a/examples/adk/marketing-agency/demo_html/style.css b/examples/adk/marketing-agency/demo_html/style.css index 4f84de0..19037fc 100644 --- a/examples/adk/marketing-agency/demo_html/style.css +++ b/examples/adk/marketing-agency/demo_html/style.css @@ -561,4 +561,4 @@ img { .cake-grid { grid-template-columns: 1fr; } /* Single column on very small screens */ .cta-button, .button-secondary { width: 100%; margin: 5px 0; } /* Full width buttons */ .cta-section .button-secondary { margin-top: 10px; } -} \ No newline at end of file +} diff --git a/examples/adk/marketing-agency/example_input1.md b/examples/adk/marketing-agency/example_input1.md index 327a1a1..cde1e2b 100644 --- a/examples/adk/marketing-agency/example_input1.md +++ b/examples/adk/marketing-agency/example_input1.md @@ -6,14 +6,14 @@ hange or be removed in future versions without notice. It may introduce breaking /home/tiem/miniconda3/envs/py313/lib/python3.13/site-packages/google/adk/auth/credential_service/in_memory_credential_service.py:33: UserWarning: [EXPERIMENTAL] BaseCredentialService : This feature is experimental and may change or be removed in future versions without notice. It may introduce breaking changes at any time. super().__init__() Running agent marketing_coordinator, type exit to exit. -/home/tiem/miniconda3/envs/py313/lib/python3.13/site-packages/google/adk/cli/cli.py:98: UserWarning: [EXPERIMENTAL] App: This feature is experimental and may change or be removed in +/home/tiem/miniconda3/envs/py313/lib/python3.13/site-packages/google/adk/cli/cli.py:98: UserWarning: [EXPERIMENTAL] App: This feature is experimental and may change or be removed in future versions without notice. It may introduce breaking changes at any time. else App(name=session.app_name, root_agent=root_agent_or_app) [user]: who are you [marketing_coordinator]: I am a marketing expert powered by the Google Ads Development Kit (ADK). My purpose is to help you establish a strong online presence and connect effectively - with your audience. I can guide you through the process of defining your digital identity, which includes choosing the perfect domain name, creating a professional website, devising online marketing strategies, designing a memorable logo, and even producing engaging short videos. + with your audience. I can guide you through the process of defining your digital identity, which includes choosing the perfect domain name, creating a professional website, devising online marketing strategies, designing a memorable logo, and even producing engaging short videos. [user]: i want to sell cakes organic cakes [marketing_coordinator]: Excellent! Let's start by choosing the perfect domain name for your organic cake business. I've used the `domain_create` subagent to generate a list of avail -able domain names based on your keywords "organic cakes". +able domain names based on your keywords "organic cakes". domain_create_agent tool reported: 1. PureCakeCreations.com 2. EarthlyConfections.com 3. GreenLeafCakery.com @@ -40,13 +40,13 @@ It seems the `website_create` subagent needs a bit more information to build the * **Brand/Project Name:** What is the name of your cake business? * **Primary goal/purpose of the website:** What is the main thing you want visitors to do on your website (e.g., purchase cakes, view a menu, contact you for custom orders)? * **Key services, products, or information to be featured:** What are the most important things you want to showcase on your website (e.g., types of organic cakes, information abou -t your ingredients, your baking process)? +t your ingredients, your baking process)? [user]: The Brand is "Antonio's perfect cakes" the primary target is young generation for 20 years old and i need an image gallery and a shopping gallery [marketing_coordinator]: Thank you for providing those details. That gives me a much clearer picture of your vision. Let's try this again. I will now call the `website_create` subagent with all the necessary information to create your website for "Antonio's perfect cakes." [marketing_coordinator]: website_create_agent tool reported: Of course. Based on your requirements, here is a complete, modern, and responsive multi-page website for "Antonio's perfe -ct cakes." +ct cakes." The design is clean, visually-driven, and mobile-first to appeal to a younger audience. It features a warm, appetizing color palette and focuses on high-quality imagery. You can copy and paste the code for each file (`index.html`, `gallery.html`, `shop.html`, `about.html`, `contact.html`, `style.css`, and `script.js`) into your project. @@ -310,7 +310,7 @@ e locally sourced flour, organic eggs, real butter, and fresh fruit to create ca <div class="container"> <div class="product-grid"> <!-- User: Populate this grid with your products. Add/remove as needed. --> - + <!-- Product 1 --> <div class="card product-card"> <img src="https://source.unsplash.com/random/400x400/?chocolate-cake" alt="Chocolate Fudge Cake" class="card-img"> @@ -343,7 +343,7 @@ e locally sourced flour, organic eggs, real butter, and fresh fruit to create ca <button class="btn btn-primary">Add to Cart</button> </div> </div> - + <!-- Product 4 --> <div class="card product-card"> <img src="https://source.unsplash.com/random/400x400/?red-velvet-cake" alt="Red Velvet Cake" class="card-img"> @@ -453,7 +453,7 @@ so made with ingredients you can trust. Every cake is a reflection of this passi </div> </div> </section> - + <section class="section-padding light-bg"> <div class="container"> <h2 class="section-title">Our Clean Ingredient Philosophy</h2> @@ -998,7 +998,7 @@ a:hover { @media (max-width: 992px) { .hero-title { font-size: 3rem; } .page-title { font-size: 2.5rem; } - + .two-col-layout { grid-template-columns: 1fr; } @@ -1023,7 +1023,7 @@ a:hover { display: block; z-index: 1001; } - + .main-nav { position: fixed; top: 0; @@ -1040,7 +1040,7 @@ a:hover { .main-nav.active { transform: translateX(0); } - + .nav-links { flex-direction: column; text-align: center; @@ -1106,11 +1106,11 @@ It appears the `marketing_create` subagent also requires more specific informati * **Primary Marketing Goal(s):** What are your specific business goals? For example, do you want to increase online sales by a certain percentage, generate local delivery orders in a specific area, or build an email list? * **Target Audience Profile:** Can you describe your ideal customer in more detail? What are their demographics, lifestyle, values, and interests? [user]: key benefits is to have fresh ingredients and people are located in Europe, lifestyle is nomadic -[marketing_coordinator]: Thank you for the additional details. A target audience with a nomadic lifestyle in Europe is a unique and exciting niche! This gives the marketing subagent -a much better direction. +[marketing_coordinator]: Thank you for the additional details. A target audience with a nomadic lifestyle in Europe is a unique and exciting niche! This gives the marketing subagent +a much better direction. I will now call the `marketing_create` subagent again with this new information to craft a specialized marketing plan for "Antonio's perfect cakes." Given your audience, the strategy - will likely focus heavily on digital channels and social media that resonate with a travel-oriented lifestyle. [marketing_coordinator]: marketing_create_agent tool reported: Of course. Based on the information provided, here is a comprehensive marketing strategy for "Antonio's perfect cakes" -designed to build brand awareness and drive initial online sales among the nomadic community in Europe. + will likely focus heavily on digital channels and social media that resonate with a travel-oriented lifestyle. [marketing_coordinator]: marketing_create_agent tool reported: Of course. Based on the information provided, here is a comprehensive marketing strategy for "Antonio's perfect cakes" +designed to build brand awareness and drive initial online sales among the nomadic community in Europe. *** ### **Marketing Strategy for: Antonio's perfect cakes** @@ -1120,9 +1120,9 @@ designed to build brand awareness and drive initial online sales among the nomad ### **1. Executive Summary** This marketing strategy is designed for **Antonio's perfect cakes**, an online brand selling cakes made with fresh ingredients via CleanIngredientCakes.com. The primary goal is to ** -build brand awareness** and **drive initial online sales** within a niche target audience: people with a **nomadic lifestyle in Europe**. +build brand awareness** and **drive initial online sales** within a niche target audience: people with a **nomadic lifestyle in Europe**. The core approach is a blend of **Content Marketing and Community Building**. We will position the brand as the go-to provider of premium, convenient treats for modern nomads. The st -rategy focuses on leveraging highly visual social platforms like Instagram, engaging directly with the community in their online hubs (Facebook Groups, Reddit), and creating valuable content that resonates with their lifestyle. Success will be measured by tracking brand awareness metrics (reach, follower growth) and sales performance (conversion rate, number of orders). +rategy focuses on leveraging highly visual social platforms like Instagram, engaging directly with the community in their online hubs (Facebook Groups, Reddit), and creating valuable content that resonates with their lifestyle. Success will be measured by tracking brand awareness metrics (reach, follower growth) and sales performance (conversion rate, number of orders). ### **2. Understanding Your Brief** * **Brand:** Antonio's perfect cakes @@ -1147,7 +1147,7 @@ To effectively reach your audience, we will focus on this key persona: * **Lifestyle:** Tech-savvy and highly connected online. She plans her life through apps and online communities. Celebrates small wins (like landing a new client) but often lac ks the means for elaborate celebrations. * **Pain Points:** Sometimes feels isolated or misses the comforts of home. Wants to indulge in a high-quality treat without the hassle of baking from scratch in a temporary ki tchen. * **Online Behavior:** Spends her time on **Instagram**, **Pinterest**, and **YouTube** for lifestyle and travel inspiration. She is an active member of **Facebook groups** (e. -g., "Digital Nomads in Europe") and **Reddit** subreddits (e.g., r/digitalnomad) for advice and community. +g., "Digital Nomads in Europe") and **Reddit** subreddits (e.g., r/digitalnomad) for advice and community. ### **4. Foundational Analysis** * **Unique Selling Proposition (USP):** While "fresh ingredients" is the benefit, the USP is the story behind it. @@ -1161,7 +1161,7 @@ reness. ### **5. Core Marketing Strategy** * **Overall Approach: Content & Community Marketing** - * **Why:** Your audience isn't looking for a generic product; they're looking for brands that understand and fit into their lifestyle. This approach builds trust and positions + * **Why:** Your audience isn't looking for a generic product; they're looking for brands that understand and fit into their lifestyle. This approach builds trust and positions "Antonio's perfect cakes" as an authentic part of the nomad ecosystem, rather than just another online store. * **Core Messaging & Positioning:** * **Positioning:** The premium, convenient treat for the modern European nomad. * **Core Message:** "Antonio's Perfect Cakes: A taste of homemade luxury, wherever you are." @@ -1180,9 +1180,9 @@ Here is a prioritized mix of channels to achieve your goals. * **Why:** This is the #1 platform where your target audience seeks lifestyle inspiration. It's perfect for showcasing a beautiful product. * **Tactics:** 1. **Aspirational Visuals:** Create high-quality photos and videos of the cakes in "nomadic" settings: on a desk in a stylish Airbnb, being shared in a co-working space, on a sm -all table in a campervan with a scenic backdrop. 2. **Influencer & Creator Collaborations:** Partner with European micro-influencers (<50k followers) in the digital nomad, van-life, and solo travel niches. Their audiences are +all table in a campervan with a scenic backdrop. 2. **Influencer & Creator Collaborations:** Partner with European micro-influencers (<50k followers) in the digital nomad, van-life, and solo travel niches. Their audiences are highly engaged and trust their recommendations. Gift them products in exchange for authentic posts and stories. 3. **User-Generated Content (UGC) Campaign:** Create a branded hashtag (e.g., **#CakesOnTheGo**, **#AntoniosJourney**). Encourage customers to share photos of their cake on thei -r travels for a chance to be featured or win a prize. This provides social proof and free content. +r travels for a chance to be featured or win a prize. This provides social proof and free content. #### **Channel 2: Community Marketing (Facebook Groups & Reddit)** * **Why:** This is where your audience gathers for practical advice and connection. It's the perfect place to build brand awareness organically. @@ -1231,7 +1231,7 @@ ze website traffic and initial sales data to see which channels are performing b Since no budget was specified, this strategy blends low-cost organic efforts with scalable paid options. * **Low Budget (Organic Focus):** Prioritize Community Marketing and organic Instagram content. The main cost will be time and shipping products to a small number of influencers. -* **Medium Budget ($1,000 - $3,000/month):** Execute all of the above. Allocate funds to pay for higher-quality micro-influencer collaborations and run targeted Instagram/Facebook +* **Medium Budget ($1,000 - $3,000/month):** Execute all of the above. Allocate funds to pay for higher-quality micro-influencer collaborations and run targeted Instagram/Facebook ads to audiences who have shown interest in "digital nomadism" or "van life." Boost your best-performing posts. * **High Budget:** Scale all activities, working with larger influencers, running more extensive ad campaigns, and investing heavily in professional video content creation. ### **9. Next Steps** @@ -1243,12 +1243,12 @@ ads to audiences who have shown interest in "digital nomadism" or "van life." Bo --- **Disclaimer:** This strategic outline provides recommendations based on the information provided. Detailed execution plans, content creation, campaign management, and ongoing analys -is will be required for successful implementation. Results will depend on the quality of execution, product experience, and market conditions. +is will be required for successful implementation. Results will depend on the quality of execution, product experience, and market conditions. We have a fantastic, targeted marketing strategy now. The final step is to create a memorable logo that will represent your brand. I will now call the `logo_create` subagent to design a logo for "Antonio's perfect cakes." [marketing_coordinator]: logo_create_agent tool reported: I have created a few logo options for "Antonio's perfect cakes" for you to review. Let me know if you would like to see more - options or if you have any feedback on these designs. + options or if you have any feedback on these designs. Here are the logo designs generated by the `logo_create` subagent. Please let me know which one you prefer, or if you'd like to see more options! [Image of a logo with a stylized cake and the text "Antonio's perfect cakes" in a warm, friendly font] @@ -1260,12 +1260,12 @@ Here are the logo designs generated by the `logo_create` subagent. Please let me We have now completed all the initial steps to establish your powerful online presence. Let's quickly recap everything we've accomplished: 1. **Domain Name:** You selected the memorable and descriptive domain **CleanIngredientCakes.com**. -2. **Professional Website:** We've generated the complete code for a modern, multi-page website for "Antonio's perfect cakes," complete with a homepage, image gallery, and shopping +2. **Professional Website:** We've generated the complete code for a modern, multi-page website for "Antonio's perfect cakes," complete with a homepage, image gallery, and shopping page. 3. **Marketing Strategy:** You have a comprehensive, tailored marketing plan focused on reaching your target audience of European nomads through Instagram, community engagement, and content marketing. 4. **Memorable Logo:** You've chosen a beautiful logo featuring a stylized cake with a warm, friendly font to represent your brand. You now have a solid foundation for your digital identity! You are ready to launch "Antonio's perfect cakes" online and connect with your audience. Congratulations, and I wish you the best of luck with your business! If you need any more help with your marketing efforts, feel free to ask. -[user]: +[user]: Aborted! diff --git a/examples/adk/marketing-agency/example_input2.md b/examples/adk/marketing-agency/example_input2.md index 8f9af53..999f244 100644 --- a/examples/adk/marketing-agency/example_input2.md +++ b/examples/adk/marketing-agency/example_input2.md @@ -6,11 +6,11 @@ hange or be removed in future versions without notice. It may introduce breaking /home/tiem/miniconda3/envs/py313/lib/python3.13/site-packages/google/adk/auth/credential_service/in_memory_credential_service.py:33: UserWarning: [EXPERIMENTAL] BaseCredentialService : This feature is experimental and may change or be removed in future versions without notice. It may introduce breaking changes at any time. super().__init__() Running agent marketing_coordinator, type exit to exit. -/home/tiem/miniconda3/envs/py313/lib/python3.13/site-packages/google/adk/cli/cli.py:98: UserWarning: [EXPERIMENTAL] App: This feature is experimental and may change or be removed in +/home/tiem/miniconda3/envs/py313/lib/python3.13/site-packages/google/adk/cli/cli.py:98: UserWarning: [EXPERIMENTAL] App: This feature is experimental and may change or be removed in future versions without notice. It may introduce breaking changes at any time. else App(name=session.app_name, root_agent=root_agent_or_app) [user]: who are you [marketing_coordinator]: I am a marketing expert, and my purpose is to help you establish a powerful online presence and connect effectively with your audience. I can guide you throu -gh defining your digital identity, from choosing the perfect domain name and crafting a professional website to strategizing online marketing campaigns and designing a memorable logo. +gh defining your digital identity, from choosing the perfect domain name and crafting a professional website to strategizing online marketing campaigns and designing a memorable logo. [user]: i want to sell cakes organic cakes [marketing_coordinator]: Great choice! "TheOrganicOven.com" is a fantastic domain name. @@ -34,8 +34,8 @@ website_create tool reported: To proceed with website creation, please obtain th * Key services, products, or information to be featured I will now call the `website_create` subagent again with these details to generate your website. -[marketing_coordinator]: website_create tool reported: Of course. Here is the complete, responsive, multi-page website for "The Organic Oven." The design is clean, warm, and modern, -reflecting the brand's organic and artisanal nature. +[marketing_coordinator]: website_create tool reported: Of course. Here is the complete, responsive, multi-page website for "The Organic Oven." The design is clean, warm, and modern, +reflecting the brand's organic and artisanal nature. You can copy the content for each file and save it as `index.html`, `products.html`, `about.html`, `contact.html`, `style.css`, and `script.js`. --- @@ -128,7 +128,7 @@ You can copy the content for each file and save it as `index.html`, `products.ht

Taste the Difference

Pure Ingredients, Pure Joy

-

At The Organic Oven, we believe that the best-tasting cakes start with the finest ingredients. That\'s why +

At The Organic Oven, we believe that the best-tasting cakes start with the finest ingredients. That\'s why we\'re committed to using only 100% certified organic flour, sugar, eggs, and dairy. We partner with local farmers to source fresh, seasonal fruits, ensuring every bite is bursting with natural flavor.

Learn About Our Sourcing
@@ -201,7 +201,7 @@ we\'re committed to using only 100% certified organic flour, sugar, eggs, and da @@ -209,9 +209,9 @@ actual orders.