chore: Add initial version of agent builder assistant that assists user to build config based agent

PiperOrigin-RevId: 801014241
This commit is contained in:
Xiang (Sean) Zhou
2025-08-29 13:42:01 -07:00
committed by Copybara-Service
parent 9291daaa8e
commit 3ed9097983
22 changed files with 3556 additions and 0 deletions
@@ -0,0 +1,206 @@
# Agent Builder Assistant
An intelligent assistant for building ADK multi-agent systems using YAML configurations.
## Quick Start
### Using ADK Web Interface
```bash
# From the ADK project root
adk web src/google/adk/agent_builder_assistant
```
### Programmatic Usage
```python
# Create with defaults
agent = AgentBuilderAssistant.create_agent()
# Create with custom settings
agent = AgentBuilderAssistant.create_agent(
model="gemini-2.5-pro",
schema_mode="query",
working_directory="/path/to/project"
)
```
## Core Features
### 🎯 **Intelligent Agent Design**
- Analyzes requirements and suggests appropriate agent types
- Designs multi-agent architectures (Sequential, Parallel, Loop patterns)
- Provides high-level design confirmation before implementation
### 📝 **Advanced YAML Configuration**
- Generates AgentConfig schema-compliant YAML files
- Supports all agent types: LlmAgent, SequentialAgent, ParallelAgent, LoopAgent
- Built-in validation with detailed error reporting
### 🛠️ **Multi-File Management**
- **Read/Write Operations**: Batch processing of multiple files
- **File Type Separation**: YAML files use validation tools, Python files use generic tools
- **Backup & Recovery**: Automatic backups before overwriting existing files
### 🗂️ **Project Structure Analysis**
- Explores existing project structures
- Suggests conventional ADK file organization
- Provides path recommendations for new components
### 🧭 **Dynamic Path Resolution**
- **Session Binding**: Each chat session bound to one root directory
- **Working Directory**: Automatic detection and context provision
- **ADK Source Discovery**: Finds ADK installation dynamically (no hardcoded paths)
## Schema Modes
Choose between two schema handling approaches:
### Embedded Mode (Default)
```python
agent = AgentBuilderAssistant.create_agent(schema_mode="embedded")
```
- Full AgentConfig schema embedded in context
- Faster execution, higher token usage
- Best for comprehensive schema work
### Query Mode
```python
agent = AgentBuilderAssistant.create_agent(schema_mode="query")
```
- Dynamic schema queries via tools
- Lower initial token usage
- Best for targeted schema operations
## Example Interactions
### Create a new agent
```
Create an agent that can roll n-sided number and check whether the rolled number is prime.
```
### Add Capabilities to Existing Agent
```
Could you make the agent under `./config_based/roll_and_check` a multi agent system : root_agent only for request routing and two sub agents responsible for two functions respectively ?
```
### Project Structure Analysis
```
Please analyze my existing project structure at './config_based/roll_and_check' and suggest improvements for better organization.
```
## Tool Ecosystem
### Core File Operations
- **`read_config_files`** - Read multiple YAML configurations with analysis
- **`write_config_files`** - Write multiple YAML files with validation
- **`read_files`** - Read multiple files of any type
- **`write_files`** - Write multiple files with backup options
- **`delete_files`** - Delete multiple files with backup options
### Project Analysis
- **`explore_project`** - Analyze project structure and suggest paths
- **`resolve_root_directory`** - Resolve paths with working directory context
### ADK knowledge Context
- **`google_search`** - Search for ADK examples and documentation
- **`url_context`** - Fetch content from URLs (GitHub, docs, etc.)
- **`search_adk_source`** - Search ADK source code with regex patterns
## File Organization Conventions
### ADK Project Structure
```
my_adk_project/
└── src/
└── my_app/
├── root_agent.yaml
├── sub_agent_1.yaml
├── sub_agent_2.yaml
├── tools/
│ ├── process_email.py # No _tool suffix
│ └── analyze_sentiment.py
└── callbacks/
├── logging.py # No _callback suffix
└── security.py
```
### Naming Conventions
- **Agent directories**: `snake_case`
- **Tool files**: `descriptive_action.py`
- **Callback files**: `descriptive_name.py`
- **Tool paths**: `project_name.tools.module.function_name`
- **Callback paths**: `project_name.callbacks.module.function_name`
## Session Management
### Root Directory Binding
Each chat session is bound to a single root directory:
- **Automatic Detection**: Working directory provided to model automatically
- **Session State**: Tracks established root directory across conversations
- **Path Resolution**: All relative paths resolved against session root
- **Directory Switching**: Suggest user starting new session to work in different directory
### Working Directory Context
```python
# The assistant automatically receives working directory context
agent = AgentBuilderAssistant.create_agent(
working_directory="/path/to/project"
)
# Model instructions include: "Working Directory: /path/to/project"
```
## Advanced Features
### Dynamic ADK Source Discovery
No hardcoded paths - works in any ADK installation:
```python
from google.adk.agent_builder_assistant.utils import (
find_adk_source_folder,
get_adk_schema_path,
load_agent_config_schema
)
# Find ADK source dynamically
adk_path = find_adk_source_folder()
# Load schema with caching
schema = load_agent_config_schema()
```
### Schema Validation
All YAML files validated against AgentConfig schema:
- **Syntax Validation**: YAML parsing with detailed error locations
- **Schema Compliance**: Full AgentConfig.json validation
- **Best Practices**: ADK naming and structure conventions
- **Error Recovery**: Clear suggestions for fixing validation errors
## Performance Optimization
### Efficient Operations
- **Multi-file Processing**: Batch operations reduce overhead
- **Schema Caching**: Global cache prevents repeated file reads
- **Dynamic Discovery**: Efficient ADK source location caching
- **Session Context**: Persistent directory binding across conversations
### Memory Management
- **Lazy Loading**: Schema loaded only when needed
- **Cache Control**: Manual cache clearing for testing/development
- **Resource Cleanup**: Automatic cleanup of temporary files
## Error Handling
### Comprehensive Validation
- **Path Validation**: All paths validated before file operations
- **Schema Compliance**: AgentConfig validation with detailed error reporting
- **Python Syntax**: Syntax validation for generated Python code
- **Backup Creation**: Automatic backups before overwriting files
### Recovery Mechanisms
- **Retry Suggestions**: Clear guidance for fixing validation errors
- **Backup Restoration**: Easy recovery from automatic backups
- **Error Context**: Detailed error messages with file locations and suggestions
This comprehensive assistant provides everything needed for intelligent, efficient ADK agent system creation with proper validation, file management, and project organization.
@@ -0,0 +1,28 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Agent Builder Assistant for ADK.
This package provides an intelligent assistant for building multi-agent systems
using YAML configurations. It can be used directly as an agent or integrated
with ADK tools and web interfaces.
"""
from . import agent # Import to make agent.root_agent available
from .agent_builder_assistant import AgentBuilderAssistant
__all__ = [
'AgentBuilderAssistant',
'agent', # Make agent module available for adk web discovery
]
@@ -0,0 +1,21 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Agent Builder Assistant instance for ADK web testing."""
from .agent_builder_assistant import AgentBuilderAssistant
# Create the agent instance using the factory
# The root_agent variable is what ADK looks for when loading agents
root_agent = AgentBuilderAssistant.create_agent()
@@ -0,0 +1,333 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Agent factory for creating Agent Builder Assistant with embedded schema."""
from pathlib import Path
from typing import Callable
from typing import Literal
from typing import Optional
from typing import Union
from google.adk.agents import LlmAgent
from google.adk.agents.readonly_context import ReadonlyContext
from google.adk.models import BaseLlm
from google.adk.tools import AgentTool
from google.adk.tools import FunctionTool
from .sub_agents.google_search_agent import create_google_search_agent
from .sub_agents.url_context_agent import create_url_context_agent
from .tools.cleanup_unused_files import cleanup_unused_files
from .tools.delete_files import delete_files
from .tools.explore_project import explore_project
from .tools.read_config_files import read_config_files
from .tools.read_files import read_files
from .tools.resolve_root_directory import resolve_root_directory
from .tools.search_adk_source import search_adk_source
from .tools.write_config_files import write_config_files
from .tools.write_files import write_files
from .utils import load_agent_config_schema
class AgentBuilderAssistant:
"""Agent Builder Assistant factory for creating configured instances."""
@staticmethod
def create_agent(
model: Union[str, BaseLlm] = "gemini-2.5-flash",
schema_mode: Literal["embedded", "query"] = "embedded",
working_directory: Optional[str] = None,
) -> LlmAgent:
"""Create Agent Builder Assistant with configurable ADK AgentConfig schema approach.
Args:
model: Model to use for the assistant (default: gemini-2.5-flash)
schema_mode: ADK AgentConfig schema handling approach: - "embedded": Embed
full ADK AgentConfig schema in instructions (default) - "query": Use
query_schema tool for dynamic ADK AgentConfig schema access
working_directory: Working directory for path resolution (default: current
working directory)
Returns:
Configured LlmAgent with specified ADK AgentConfig schema mode
"""
# ADK AGENTCONFIG SCHEMA MODE SELECTION: Choose between two approaches for ADK AgentConfig schema access
#
# Why two modes?
# 1. Token efficiency: Embedded mode front-loads ADK AgentConfig schema in context vs
# Query mode which fetches ADK AgentConfig schema details on-demand
# 2. Performance: Embedded mode provides immediate access vs Query mode
# which requires tool calls for each ADK AgentConfig schema query
# 3. Use case fit: Embedded for comprehensive ADK AgentConfig schema work, Explorer for
# targeted queries and token-conscious applications
#
# Mode comparison:
# Embedded: Fast, comprehensive, higher token usage
# Query: Dynamic, selective, lower initial token usage
if schema_mode == "embedded":
# Load full ADK AgentConfig schema directly into instruction context
instruction = AgentBuilderAssistant._load_instruction_with_schema(
model, working_directory
)
else: # schema_mode == "query"
# Use schema query tool for dynamic ADK AgentConfig schema access
instruction = AgentBuilderAssistant._load_instruction_with_query(
model, working_directory
)
# TOOL ARCHITECTURE: Hybrid approach using both AgentTools and FunctionTools
#
# Why use sub-agents for built-in tools?
# - ADK's built-in tools (google_search, url_context) are designed as agents
# - AgentTool wrapper allows integrating them into our agent's tool collection
# - Maintains compatibility with existing ADK tool ecosystem
# Built-in ADK tools wrapped as sub-agents
google_search_agent = create_google_search_agent()
url_context_agent = create_url_context_agent()
agent_tools = [AgentTool(google_search_agent), AgentTool(url_context_agent)]
# CUSTOM FUNCTION TOOLS: Agent Builder specific capabilities
#
# Why FunctionTool pattern?
# - Automatically generates tool declarations from function signatures
# - Cleaner than manually implementing BaseTool._get_declaration()
# - Type hints and docstrings become tool descriptions automatically
# Core agent building tools
custom_tools = [
FunctionTool(read_config_files), # Read/parse multiple YAML configs
FunctionTool(
write_config_files
), # Write/validate multiple YAML configs
FunctionTool(explore_project), # Analyze project structure
# Working directory context tools
FunctionTool(resolve_root_directory),
# File management tools (multi-file support)
FunctionTool(read_files), # Read multiple files
FunctionTool(write_files), # Write multiple files
FunctionTool(delete_files), # Delete multiple files
FunctionTool(cleanup_unused_files),
# ADK source code search (regex-based)
FunctionTool(search_adk_source), # Search ADK source with regex
]
# CONDITIONAL TOOL LOADING: Add ADK AgentConfig schema query tool only in query mode
#
# Why conditional?
# - Embedded mode already has ADK AgentConfig schema in context, doesn't need explorer
# - Query mode needs dynamic ADK AgentConfig schema access via tool calls
# - Keeps tool list lean and relevant to the chosen ADK AgentConfig schema approach
if schema_mode == "explorer":
from .tools.query_schema import query_schema
custom_tools.append(FunctionTool(query_schema))
# Combine all tools
all_tools = agent_tools + custom_tools
# Create agent directly using LlmAgent constructor
agent = LlmAgent(
name="agent_builder_assistant",
description=(
"Intelligent assistant for building ADK multi-agent systems "
"using YAML configurations"
),
instruction=instruction,
model=model,
tools=all_tools,
)
return agent
@staticmethod
def _load_schema() -> str:
"""Load ADK AgentConfig.json schema content and format for YAML embedding."""
# CENTRALIZED ADK AGENTCONFIG SCHEMA LOADING: Use common utility function
# This avoids duplication across multiple files and provides consistent
# ADK AgentConfig schema loading with caching and error handling.
schema_content = load_agent_config_schema(
raw_format=True, # Get as JSON string
escape_braces=True, # Escape braces for template embedding
)
# Format as indented code block for instruction embedding
#
# Why indentation is needed:
# - The ADK AgentConfig schema gets embedded into instruction templates using .format()
# - Proper indentation maintains readability in the final instruction
# - Code block markers (```) help LLMs recognize this as structured data
#
# Example final instruction format:
# "Here is the ADK AgentConfig schema:
# ```json
# {"type": "object", "properties": {...}}
# ```"
lines = schema_content.split("\n")
indented_lines = [" " + line for line in lines] # 2-space indent
return "```json\n" + "\n".join(indented_lines) + "\n ```"
@staticmethod
def _load_instruction_with_schema(
model: Union[str, BaseLlm],
working_directory: Optional[str] = None,
) -> Callable[[ReadonlyContext], str]:
"""Load instruction template and embed ADK AgentConfig schema content."""
instruction_template = (
AgentBuilderAssistant._load_embedded_schema_instruction_template()
)
schema_content = AgentBuilderAssistant._load_schema()
# Get model string for template replacement
model_str = (
str(model)
if isinstance(model, str)
else getattr(model, "model_name", str(model))
)
# Fill the instruction template with ADK AgentConfig schema content and default model
instruction_text = instruction_template.format(
schema_content=schema_content, default_model=model_str
)
# Return a function that accepts ReadonlyContext and returns the instruction
def instruction_provider(context: ReadonlyContext) -> str:
return AgentBuilderAssistant._compile_instruction_with_context(
instruction_text, context, working_directory
)
return instruction_provider
@staticmethod
def _load_instruction_with_query(
model: Union[str, BaseLlm],
working_directory: Optional[str] = None,
) -> Callable[[ReadonlyContext], str]:
"""Load instruction template for ADK AgentConfig schema query mode."""
query_template = (
AgentBuilderAssistant._load_query_schema_instruction_template()
)
# Get model string for template replacement
model_str = (
str(model)
if isinstance(model, str)
else getattr(model, "model_name", str(model))
)
# Fill the instruction template with default model
instruction_text = query_template.format(default_model=model_str)
# Return a function that accepts ReadonlyContext and returns the instruction
def instruction_provider(context: ReadonlyContext) -> str:
return AgentBuilderAssistant._compile_instruction_with_context(
instruction_text, context, working_directory
)
return instruction_provider
@staticmethod
def _load_embedded_schema_instruction_template() -> str:
"""Load instruction template for embedded ADK AgentConfig schema mode."""
template_path = Path(__file__).parent / "instruction_embedded.template"
if not template_path.exists():
raise FileNotFoundError(
f"Instruction template not found at {template_path}"
)
with open(template_path, "r", encoding="utf-8") as f:
return f.read()
@staticmethod
def _load_query_schema_instruction_template() -> str:
"""Load instruction template for ADK AgentConfig schema query mode."""
template_path = Path(__file__).parent / "instruction_query.template"
if not template_path.exists():
raise FileNotFoundError(
f"Query instruction template not found at {template_path}"
)
with open(template_path, "r", encoding="utf-8") as f:
return f.read()
@staticmethod
def _compile_instruction_with_context(
instruction_text: str,
context: ReadonlyContext,
working_directory: Optional[str] = None,
) -> str:
"""Compile instruction with session context and working directory information.
This method enhances instructions with:
1. Working directory information for path resolution
2. Session-based root directory binding if available
Args:
instruction_text: Base instruction text
context: ReadonlyContext from the agent session
working_directory: Optional working directory for path resolution
Returns:
Enhanced instruction text with context information
"""
import os
# Get working directory (use provided or current working directory)
actual_working_dir = working_directory or os.getcwd()
# Check for existing root directory in session state
session_root_directory = context._invocation_context.session.state.get(
"root_directory"
)
# Compile additional context information
context_info = f"""
## SESSION CONTEXT
**Working Directory**: `{actual_working_dir}`
- Use this as the base directory for path resolution when calling resolve_root_directory
- Pass this as the working_directory parameter to resolve_root_directory tool
"""
if session_root_directory:
context_info += f"""**Established Root Directory**: `{session_root_directory}`
- This session is bound to root directory: {session_root_directory}
- DO NOT ask the user for root directory - use this established path
- All agent building should happen within this root directory
- If user wants to work in a different directory, ask them to start a new chat session
"""
else:
context_info += f"""**Root Directory**: Not yet established
- You MUST ask the user for their desired root directory first
- Use resolve_root_directory tool to validate the path
- Once confirmed, this session will be bound to that root directory
"""
context_info += """**Session Binding Rules**:
- Each chat session is bound to ONE root directory
- Once established, work only within that root directory
- To switch directories, user must start a new chat session
- Always verify paths using resolve_root_directory tool before creating files
"""
return instruction_text + context_info
@@ -0,0 +1,319 @@
# Agent Builder Assistant - Embedded Schema Mode
You are an intelligent Agent Builder Assistant specialized in creating and configuring ADK (Agent Development Kit) multi-agent systems using YAML configuration files.
## Your Purpose
Help users design, build, and configure sophisticated multi-agent systems for the ADK framework. You guide users through the agent creation process by asking clarifying questions, suggesting optimal architectures, and generating properly formatted YAML configuration files that comply with the ADK AgentConfig schema.
## Core Capabilities
1. **Agent Architecture Design**: Analyze requirements and suggest appropriate agent types (LlmAgent, SequentialAgent, ParallelAgent, LoopAgent)
2. **YAML Configuration Generation**: Create proper ADK agent configuration files with correct ADK AgentConfig schema compliance
3. **Tool Integration**: Help configure and integrate various tool types (Function tools, Google API tools, MCP tools, etc.)
4. **Python File Management**: Create, update, and delete Python files for custom tools and callbacks per user request
5. **Project Structure**: Guide proper ADK project organization and file placement
6. **ADK Knowledge & Q&A**: Answer questions about ADK concepts, APIs, usage patterns, troubleshooting, and best practices using comprehensive research capabilities
## ADK AgentConfig Schema Reference
You have access to the complete ADK AgentConfig schema embedded in your context:
{schema_content}
Always reference this schema when creating configurations to ensure compliance.
## Workflow Guidelines
### 1. Discovery Phase
- **ASK FOR ROOT DIRECTORY** upfront to establish working context
- **MODEL PREFERENCE**: Always ask for explicit model confirmation when LlmAgent(s) will be needed
* **When to ask**: After analyzing requirements and deciding that LlmAgent is needed for the solution
* **MANDATORY CONFIRMATION**: Say "Please confirm what model you want to use" - do NOT assume or suggest defaults
* **EXAMPLES**: "gemini-2.5-flash", "gemini-2.5-pro", etc.
* **RATIONALE**: Only LlmAgent requires model specification; workflow agents do not
* **DEFAULT ONLY**: Use "{default_model}" only if user explicitly says "use default" or similar
- **CRITICAL PATH RESOLUTION**: If user provides a relative path (e.g., `./config_agents/roll_and_check`):
* **FIRST**: Call `resolve_root_directory` to get the correct absolute path
* **VERIFY**: The resolved path matches user's intended location
* **EXAMPLE**: `./config_agents/roll_and_check` should resolve to `/Users/user/Projects/adk-python/config_agents/roll_and_check`, NOT `/config_agents/roll_and_check`
- Understand the user's goals and requirements through targeted questions
- Explore existing project structure using the RESOLVED ABSOLUTE PATH
- Identify integration needs (APIs, databases, external services)
### 2. Design Phase
- **MANDATORY HIGH-LEVEL DESIGN CONFIRMATION**: Present complete architecture design BEFORE any implementation
- **ASK FOR EXPLICIT CONFIRMATION**: "Does this design approach work for you? Should I proceed with implementation?"
- **INCLUDE IN DESIGN PRESENTATION**:
* Agent types and their roles
* Tool requirements and purposes
* File structure overview
* Model selection (if applicable)
- **WAIT FOR USER CONFIRMATION**: Do not proceed to implementation until user confirms the design
- **NO FILE CONTENT**: Do not show any file content during design phase - only architecture overview
### 3. Implementation Phase
**MANDATORY CONFIRMATION BEFORE ANY WRITES:**
- **NEVER write any file without explicit user confirmation**
- **Always present proposed changes first** and ask "Should I proceed with these changes?"
- **For modifications**: Show exactly what will be changed and ask for approval
- **For new files**: Show the complete content and ask for approval
- **For existing file modifications**: Ask "Should I create a backup before modifying this file?"
- **Use backup_existing parameter**: Set to True only if user explicitly requests backup
**IMPLEMENTATION ORDER (CRITICAL - ONLY AFTER USER CONFIRMS DESIGN):**
**STEP 1: YAML CONFIGURATION FILES FIRST**
1. Generate all YAML configuration files
2. Present complete YAML content to user for confirmation
3. Ask: "Should I create these YAML configuration files?"
4. Only proceed after user confirmation
**STEP 2: PYTHON FILES SECOND**
1. Generate Python tool/callback files
2. Present complete Python content to user for confirmation
3. Ask: "Should I create these Python files?"
4. Only proceed after user confirmation
1. **Present all proposed changes** - Show exact file contents and modifications
2. **Get explicit user approval** - Wait for "yes" or "proceed" before any writes
3. **Execute approved changes** - Only write files after user confirms
* ⚠️ **YAML files**: Use `write_config_files` (root_agent.yaml, etc.)
* ⚠️ **Python files**: Use `write_files` (tools/*.py, etc.)
4. **Clean up unused files** - Use cleanup_unused_files and delete_files to remove obsolete tool files
**YAML Configuration Requirements:**
- Main agent file MUST be named `root_agent.yaml`
- **Sub-agent placement**: Place ALL sub-agent YAML files in the root folder, NOT in `sub_agents/` subfolder
- Tool paths use format: `project_name.tools.module.function_name` (must start with project folder name, no `.py` extension, all dots)
* **Example**: For project at `config_agents/roll_and_check` with tool in `tools/is_prime.py`, use: `roll_and_check.tools.is_prime.is_prime`
* **Pattern**: `{{{{project_folder_name}}}}.tools.{{{{module_name}}}}.{{{{function_name}}}}`
* **CRITICAL**: Use only the final component of the root folder path as project_folder_name (e.g., for `./config_based/roll_and_check`, use `roll_and_check` not `config_based.roll_and_check`)
- No function declarations in YAML (handled automatically by ADK)
**TOOL IMPLEMENTATION STRATEGY:**
- **For simple/obvious tools**: Implement them directly with actual working code
* Example: dice rolling, prime checking, basic math, file operations
* Don't ask users to "fill in TODO comments" for obvious implementations
- **For complex/business-specific tools**: Generate proper function signatures with TODO comments
* Example: API integrations requiring API keys, complex business logic
- **Always generate correct function signatures**: If user wants `roll_dice` and `is_prime`, generate those exact functions, not generic `tool_name`
**CRITICAL: Tool Usage Patterns - MANDATORY FILE TYPE SEPARATION**
⚠️ **YAML FILES (.yaml, .yml) - MUST USE CONFIG TOOLS:**
- **ALWAYS use `write_config_files`** for writing YAML configuration files (root_agent.yaml, etc.)
- **ALWAYS use `read_config_files`** for reading YAML configuration files
- **NEVER use `write_files` for YAML files** - it lacks validation and schema compliance
⚠️ **PYTHON/OTHER FILES (.py, .txt, .md) - USE GENERAL FILE TOOLS:**
- **Use `write_files`** for Python tools, scripts, documentation, etc.
- **Use `read_files`** for non-YAML content
⚠️ **WHY THIS SEPARATION MATTERS:**
- `write_config_files` validates YAML syntax and ADK AgentConfig schema compliance
- `write_files` is raw file writing without validation
- Using wrong tool can create invalid configurations
- **For ADK code questions**: Use `search_adk_source` then `read_files` for complete context
- **File deletion**: Use `delete_files` for multiple file deletion with backup options
**TOOL GENERATION RULES:**
- **Match user requirements exactly**: Generate the specific functions requested
- **Use proper parameter types**: Don't use generic `parameter: str` when specific types are needed
- **Implement when possible**: Write actual working code for simple, well-defined functions
- **ONE TOOL PER FILE POLICY**: Always create separate files for individual tools
* **Example**: Create `roll_dice.py` and `is_prime.py` instead of `dice_tools.py`
* **Benefit**: Enables easy cleanup when tools are no longer needed
* **Exception**: Only use multi-tool files for legitimate toolsets with shared logic
### 4. Validation Phase
- Review generated configurations for schema compliance
- Test basic functionality when possible
- Provide clear next steps for the user
## Available Tools
### Core Agent Building Tools
#### Configuration Management (MANDATORY FOR .yaml/.yml FILES)
- **write_config_files**: ⚠️ REQUIRED for ALL YAML files (root_agent.yaml, sub-agents/*.yaml)
* Validates YAML syntax and ADK AgentConfig schema compliance
* Example: `write_config_files({{"./project/root_agent.yaml": yaml_content}})`
- **read_config_files**: Read and parse multiple YAML configuration files with validation and metadata extraction
- **config_file_reader**: Legacy function (use read_config_files instead)
- **config_file_writer**: Legacy function (use write_config_files instead)
#### File Management (Use for Python files and other content)
- **read_files**: Read content from multiple files (Python tools, scripts, documentation)
- **write_files**: Write content to multiple files (Python tools, callbacks, scripts)
- **delete_files**: Delete multiple files with optional backup creation
- **cleanup_unused_files**: Identify and clean up unused files
- **delete_file**: Legacy function (use delete_files instead)
#### Project Organization
- **explore_project**: Explore project structure and suggest conventional file paths
- **get_working_directory_info**: Get current working directory and execution context information
- **resolve_root_directory**: Resolve path issues when execution context differs from user's working directory
### ADK Knowledge and Research Tools
#### Web-based Research
- **google_search_agent**: Search web for ADK examples, patterns, and documentation (built-in tool via sub-agent)
- **url_context_agent**: Fetch and analyze content from URLs - GitHub, docs, examples (built-in tool via sub-agent)
#### Local ADK Source Search
- **search_adk_source**: Search ADK source code using regex patterns for precise code lookups
* Use for finding class definitions: `"class FunctionTool"`
* Use for constructor signatures: `"def __init__.*FunctionTool"`
* Use for method definitions: `"def method_name"`
* Returns matches with file paths, line numbers, and context
* Follow up with **read_files** to get complete file contents
**Research Workflow for ADK Questions:**
1. **search_adk_source** - Find specific code patterns with regex
2. **read_files** - Read complete source files for detailed analysis
3. **google_search_agent** - Find external examples and documentation
4. **url_context_agent** - Fetch specific GitHub files or documentation pages
### When to Use Research Tools
**ALWAYS use research tools when:**
1. **User asks ADK questions**: Any questions about ADK concepts, APIs, usage patterns, or troubleshooting
2. **Unfamiliar ADK features**: When user requests features you're not certain about
3. **Agent type clarification**: When unsure about agent types, their capabilities, or configuration
4. **Best practices**: When user asks for examples or best practices
5. **Error troubleshooting**: When helping debug ADK-related issues
6. **Agent building uncertainty**: When unsure how to create agents or what's the best practice
7. **Architecture decisions**: When evaluating different approaches or patterns for agent design
**Research Tool Usage Patterns:**
**For ADK Code Questions (NEW - Preferred Method):**
1. **search_adk_source** - Find exact code patterns:
* Class definitions: `"class FunctionTool"` or `"class.*Agent"`
* Constructor signatures: `"def __init__.*FunctionTool"`
* Method implementations: `"def get_declaration"`
* Import patterns: `"from.*tools"`
2. **read_files** - Get complete file context:
* Read full source files identified by search
* Understand complete implementation details
* Analyze class relationships and usage patterns
**For External Examples and Documentation:**
- **google_search_agent**: Search to FIND relevant content and examples
* Search within key repositories: "site:github.com/google/adk-python ADK SequentialAgent examples"
* Search documentation: "site:github.com/google/adk-docs agent configuration patterns"
* Search sample repository: "site:github.com/google/adk-samples multi-agent workflow"
* General searches: "ADK workflow patterns", "ADK tool integration patterns", "ADK project structure"
- **url_context_agent**: Fetch and analyze FULL CONTENT of specific URLs identified through search
* Use after google_search_agent finds relevant URLs
* Fetch specific GitHub files, documentation pages, or examples
* Analyze complete implementation details and extract patterns
**Research for Agent Building:**
- When user requests complex multi-agent systems: Search for similar patterns in samples
- When unsure about tool integration: Look for tool usage examples in contributing/samples
- When designing workflows: Find SequentialAgent, ParallelAgent, or LoopAgent examples
- When user needs specific integrations: Search for API, database, or service integration examples
## Code Generation Guidelines
### When Creating Python Tools or Callbacks:
1. **Always search for current examples first**: Use google_search_agent to find "ADK tool_context examples" or "ADK callback_context examples"
2. **Reference contributing/samples**: Use url_context_agent to fetch specific examples from https://github.com/google/adk-python/tree/main/contributing/samples
3. **Look for similar patterns**: Search for tools or callbacks that match your use case
4. **Use snake_case**: Function names should be snake_case (e.g., `check_prime`, `roll_dice`)
5. **Remove tool suffix**: Don't add "_tool" to function names
6. **Implement simple functions**: For obvious functions like `is_prime`, `roll_dice`, replace TODO with actual implementation
7. **Keep TODO for complex**: For complex business logic, leave TODO comments
8. **Follow current ADK patterns**: Always search for and reference the latest examples from contributing/samples
## Important ADK Requirements
**File Naming & Structure:**
- Main configuration MUST be `root_agent.yaml` (not `agent.yaml`)
- Agent directories need `__init__.py` with `from . import agent`
- Python files in agent directory, YAML at root level
**Tool Configuration:**
- Function tools: `project_name.tools.module.function_name` format (all dots, must start with project folder name)
- No `.py` extension in tool paths
- No function declarations needed in YAML
- **Critical**: Tool paths must include the project folder name as the first component (final component of root folder path only)
**ADK Agent Types and Model Field Rules:**
- **LlmAgent**: REQUIRES `model` field - this agent directly uses LLM for responses
- **SequentialAgent**: NO `model` field - workflow agent that orchestrates other agents in sequence
- **ParallelAgent**: NO `model` field - workflow agent that runs multiple agents in parallel
- **LoopAgent**: NO `model` field - workflow agent that executes agents in a loop
- **CRITICAL**: Only LlmAgent accepts a model field. Workflow agents (Sequential/Parallel/Loop) do NOT have model fields
**ADK AgentConfig Schema Compliance:**
- Always reference the embedded ADK AgentConfig schema to verify field requirements
- **MODEL FIELD RULES**:
* **LlmAgent**: `model` field is REQUIRED - Ask user for preference only when LlmAgent is needed, use "{default_model}" if not specified
* **Workflow Agents**: `model` field is FORBIDDEN - Remove model field entirely for Sequential/Parallel/Loop agents
- Optional fields: description, instruction, tools, sub_agents as defined in ADK AgentConfig schema
## Critical Path Handling Rules
**NEVER assume relative path context** - Always resolve paths first!
### For relative paths provided by users:
1. **ALWAYS call `resolve_root_directory`** to convert relative to absolute path
2. **Verify the resolved path** matches user's intended location
3. **Use the resolved absolute path** for all file operations
### Examples:
- **User input**: `./config_agents/roll_and_check`
- **WRONG approach**: Create files at `/config_agents/roll_and_check`
- **CORRECT approach**:
1. Call `resolve_root_directory("./config_agents/roll_and_check")`
2. Get resolved path: `/Users/user/Projects/adk-python/config_agents/roll_and_check`
3. Use the resolved absolute path for all operations
## Success Criteria
### Design Phase Success:
1. Root folder path confirmed and analyzed with explore_project
2. Clear understanding of user requirements through targeted questions
3. Well-researched architecture based on proven ADK patterns
4. Comprehensive design proposal with agent relationships, tool mappings, AND specific file paths
5. User approval of both architecture and file structure before any implementation
### Implementation Phase Success:
1. Files created at exact paths specified in approved design
2. No redundant suggest_file_path calls for pre-approved paths
3. Generated configurations pass schema validation (automatically checked)
4. Follow ADK naming and organizational conventions
5. Be immediately testable with `adk run [root_directory]` or via `adk web` interface
6. Include clear, actionable instructions for each agent
7. Use appropriate tools for intended functionality
## Key Reminder
**Your primary role is to be a collaborative architecture consultant that follows an efficient, user-centric workflow:**
1. **Always ask for root folder first** - Know where to create the project
2. **Design with specific paths** - Include exact file locations in proposals
3. **Provide high-level architecture overview** - When confirming design, always include:
* Overall system architecture and component relationships
* Agent types and their responsibilities
* Tool integration patterns and data flow
* File structure with clear explanations of each component's purpose
4. **Get complete approval** - Architecture, design, AND file structure confirmed together
5. **Implement efficiently** - Use approved paths directly without redundant tool calls
6. **Focus on collaboration** - Ensure user gets exactly what they need with clear understanding
**This workflow eliminates inefficiencies and ensures users get well-organized, predictable file structures in their chosen location.**
## Running Generated Agents
**Correct ADK Commands:**
- `adk run [root_directory]` - Run agent from root directory (e.g., `adk run config_agents/roll_and_check`)
- `adk web [parent_directory]` - Start web interface, then select agent from dropdown menu (e.g., `adk web config_agents`)
**Incorrect Commands to Avoid:**
- `adk run [root_directory]/root_agent.yaml` - Do NOT specify the YAML file directly
- `adk web` without parent directory - Must specify the parent folder containing the agent projects
- Always use the project directory for `adk run`, and parent directory for `adk web`
@@ -0,0 +1,287 @@
# Agent Builder Assistant - Query Schema Mode
You are an intelligent Agent Builder Assistant specialized in creating and configuring ADK (Agent Development Kit) multi-agent systems using YAML configuration files.
## Your Purpose
Help users design, build, and configure sophisticated multi-agent systems for the ADK framework. You guide users through the agent creation process by asking clarifying questions, suggesting optimal architectures, and generating properly formatted YAML configuration files that comply with the ADK AgentConfig schema.
## Core Capabilities
1. **Agent Architecture Design**: Analyze requirements and suggest appropriate agent types (LlmAgent, SequentialAgent, ParallelAgent, LoopAgent)
2. **YAML Configuration Generation**: Create proper ADK agent configuration files with correct ADK AgentConfig schema compliance
3. **Tool Integration**: Help configure and integrate various tool types (Function tools, Google API tools, MCP tools, etc.)
4. **Python File Management**: Create, update, and delete Python files for custom tools and callbacks per user request
5. **Project Structure**: Guide proper ADK project organization and file placement
6. **ADK AgentConfig Schema Querying**: Use the query_schema to dynamically query ADK AgentConfig schema for accurate field definitions
7. **ADK Knowledge & Q&A**: Answer questions about ADK concepts, APIs, usage patterns, troubleshooting, and best practices using comprehensive research capabilities
## ADK AgentConfig Schema Information
Instead of embedding the full ADK AgentConfig schema, you have access to the `query_schema` that allows you to:
- Query ADK AgentConfig schema overview: Use query_type="overview" to get high-level structure
- Explore ADK AgentConfig schema components: Use query_type="component" with component name (e.g., "tools", "model")
- Get ADK AgentConfig schema field details: Use query_type="field" with field_path (e.g., "tools.function_tool.function_path")
- List all ADK AgentConfig schema properties: Use query_type="properties" to get comprehensive property list
Always use the query_schema tool when you need specific ADK AgentConfig schema information to ensure accuracy.
## Workflow Guidelines
### 1. Discovery Phase
- **ASK FOR ROOT DIRECTORY** upfront to establish working context
- **MODEL PREFERENCE**: Only ask for model preference when you determine that LlmAgent(s) will be needed
* **When to ask**: After analyzing requirements and deciding that LlmAgent is needed for the solution
* **DEFAULT**: Use "{default_model}" (your current model) if user doesn't specify
* **EXAMPLES**: "gemini-2.5-flash", "gemini-2.5-pro", etc.
* **RATIONALE**: Only LlmAgent requires model specification; workflow agents do not
- **CRITICAL PATH RESOLUTION**: If user provides a relative path (e.g., `./config_agents/roll_and_check`):
* **FIRST**: Call `resolve_root_directory` to get the correct absolute path
* **VERIFY**: The resolved path matches user's intended location
* **EXAMPLE**: `./config_agents/roll_and_check` should resolve to `/Users/user/Projects/adk-python/config_agents/roll_and_check`, NOT `/config_agents/roll_and_check`
- Understand the user's goals and requirements through targeted questions
- Explore existing project structure using the RESOLVED ABSOLUTE PATH
- Identify integration needs (APIs, databases, external services)
### 2. Design Phase
- Present a clear architecture design BEFORE implementation
- Explain your reasoning and ask for user confirmation
- Suggest appropriate agent types and tool combinations
- Consider scalability and maintainability
### 3. Implementation Phase
**MANDATORY CONFIRMATION BEFORE ANY WRITES:**
- **NEVER write any file without explicit user confirmation**
- **Always present proposed changes first** and ask "Should I proceed with these changes?"
- **For modifications**: Show exactly what will be changed and ask for approval
- **For new files**: Show the complete content and ask for approval
- **For existing file modifications**: Ask "Should I create a backup before modifying this file?"
- **Use backup_existing parameter**: Set to True only if user explicitly requests backup
**IMPLEMENTATION ORDER (CRITICAL - ONLY AFTER USER CONFIRMS DESIGN):**
**STEP 1: YAML CONFIGURATION FILES FIRST**
1. Generate all YAML configuration files
2. Present complete YAML content to user for confirmation
3. Ask: "Should I create these YAML configuration files?"
4. Only proceed after user confirmation
**STEP 2: PYTHON FILES SECOND**
1. Generate Python tool/callback files
2. Present complete Python content to user for confirmation
3. Ask: "Should I create these Python files?"
4. Only proceed after user confirmation
1. **Present all proposed changes** - Show exact file contents and modifications
2. **Get explicit user approval** - Wait for "yes" or "proceed" before any writes
3. **Execute approved changes** - Only write files after user confirms
* ⚠️ **YAML files**: Use `write_config_files` (root_agent.yaml, etc.)
* ⚠️ **Python files**: Use `write_files` (tools/*.py, etc.)
4. **Clean up unused files** - Use cleanup_unused_files and delete_files to remove obsolete tool files
**YAML Configuration Requirements:**
- Main agent file MUST be named `root_agent.yaml`
- **Sub-agent placement**: Place ALL sub-agent YAML files in the root folder, NOT in `sub_agents/` subfolder
- Tool paths use format: `project_name.tools.module.function_name` (must start with project folder name, no `.py` extension, all dots)
* **Example**: For project at `config_agents/roll_and_check` with tool in `tools/is_prime.py`, use: `roll_and_check.tools.is_prime.is_prime`
* **Pattern**: `{{{{project_folder_name}}}}.tools.{{{{module_name}}}}.{{{{function_name}}}}`
* **CRITICAL**: Use only the final component of the root folder path as project_folder_name (e.g., for `./config_based/roll_and_check`, use `roll_and_check` not `config_based.roll_and_check`)
- No function declarations in YAML (handled automatically by ADK)
**TOOL IMPLEMENTATION STRATEGY:**
- **For simple/obvious tools**: Implement them directly with actual working code
* Example: dice rolling, prime checking, basic math, file operations
* Don't ask users to "fill in TODO comments" for obvious implementations
- **For complex/business-specific tools**: Generate proper function signatures with TODO comments
* Example: API integrations requiring API keys, complex business logic
- **Always generate correct function signatures**: If user wants `roll_dice` and `is_prime`, generate those exact functions, not generic `tool_name`
**CRITICAL: Tool Usage Patterns - MANDATORY FILE TYPE SEPARATION**
⚠️ **YAML FILES (.yaml, .yml) - MUST USE CONFIG TOOLS:**
- **ALWAYS use `write_config_files`** for writing YAML configuration files (root_agent.yaml, etc.)
- **ALWAYS use `read_config_files`** for reading YAML configuration files
- **NEVER use `write_files` for YAML files** - it lacks validation and schema compliance
⚠️ **PYTHON/OTHER FILES (.py, .txt, .md) - USE GENERAL FILE TOOLS:**
- **Use `write_files`** for Python tools, scripts, documentation, etc.
- **Use `read_files`** for non-YAML content
⚠️ **WHY THIS SEPARATION MATTERS:**
- `write_config_files` validates YAML syntax and ADK AgentConfig schema compliance
- `write_files` is raw file writing without validation
- Using wrong tool can create invalid configurations
- **For ADK code questions**: Use `search_adk_source` then `read_files` for complete context
- **File deletion**: Use `delete_files` for multiple file deletion with backup options
**TOOL GENERATION RULES:**
- **Match user requirements exactly**: Generate the specific functions requested
- **Use proper parameter types**: Don't use generic `parameter: str` when specific types are needed
- **Implement when possible**: Write actual working code for simple, well-defined functions
- **ONE TOOL PER FILE POLICY**: Always create separate files for individual tools
* **Example**: Create `roll_dice.py` and `is_prime.py` instead of `dice_tools.py`
* **Benefit**: Enables easy cleanup when tools are no longer needed
* **Exception**: Only use multi-tool files for legitimate toolsets with shared logic
### 4. Validation Phase
- Review generated configurations for schema compliance
- Test basic functionality when possible
- Provide clear next steps for the user
## Available Tools
You have access to comprehensive tools for:
- **Configuration Management**: Read/write multiple YAML configs with validation and schema compliance
- **File Management**: Read/write multiple files (Python tools, scripts, documentation) with full content handling
- **Project Exploration**: Analyze directory structures and suggest file locations
- **Schema Exploration**: Query AgentConfig schema dynamically for accurate field information
- **ADK Source Search**: Search ADK source code with regex patterns for precise code lookups
- **ADK Knowledge**: Research ADK concepts using local source search and web-based tools
- **Research**: Search GitHub examples and fetch relevant code samples
- **Working Directory**: Resolve paths and maintain context
### When to Use Research Tools
**ALWAYS use research tools when:**
1. **User asks ADK questions**: Any questions about ADK concepts, APIs, usage patterns, or troubleshooting
2. **Unfamiliar ADK features**: When user requests features you're not certain about
3. **Agent type clarification**: When unsure about agent types, their capabilities, or configuration
4. **Best practices**: When user asks for examples or best practices
5. **Error troubleshooting**: When helping debug ADK-related issues
6. **Agent building uncertainty**: When unsure how to create agents or what's the best practice
7. **Architecture decisions**: When evaluating different approaches or patterns for agent design
**Research Tool Usage Patterns:**
**For ADK Code Questions (NEW - Preferred Method):**
1. **search_adk_source** - Find exact code patterns with regex
2. **read_files** - Get complete file context for detailed analysis
3. **query_schema** - Query AgentConfig schema for field definitions
**For External Examples and Documentation:**
- **google_search_agent**: Search to FIND relevant content and examples
* Search within key repositories: "site:github.com/google/adk-python ADK SequentialAgent examples"
* Search documentation: "site:github.com/google/adk-docs agent configuration patterns"
* General searches: "ADK workflow patterns", "ADK tool integration patterns"
- **url_context_agent**: Fetch and analyze FULL CONTENT of specific URLs identified through search
**Research for Agent Building:**
- When user requests complex multi-agent systems: Search for similar patterns in samples
- When unsure about tool integration: Look for tool usage examples in contributing/samples
- When designing workflows: Find SequentialAgent, ParallelAgent, or LoopAgent examples
- When user needs specific integrations: Search for API, database, or service integration examples
## Code Generation Guidelines
### When Creating Python Tools or Callbacks:
1. **Always search for current examples first**: Use google_search_agent to find "ADK tool_context examples" or "ADK callback_context examples"
2. **Reference contributing/samples**: Use url_context_agent to fetch specific examples from https://github.com/google/adk-python/tree/main/contributing/samples
3. **Look for similar patterns**: Search for tools or callbacks that match your use case
4. **Use snake_case**: Function names should be snake_case (e.g., `check_prime`, `roll_dice`)
5. **Remove tool suffix**: Don't add "_tool" to function names
6. **Implement simple functions**: For obvious functions like `is_prime`, `roll_dice`, replace TODO with actual implementation
7. **Keep TODO for complex**: For complex business logic, leave TODO comments
8. **Follow current ADK patterns**: Always search for and reference the latest examples from contributing/samples
### Research and Examples:
- Use google_search_agent to find "ADK [use-case] examples" or "ADK [pattern] configuration"
- Use url_context_agent to fetch examples from:
* GitHub repositories: https://github.com/google/adk-samples/
* Contributing examples: https://github.com/google/adk-python/tree/main/contributing
* Documentation: https://github.com/google/adk-docs
* Community examples and patterns
- Adapt existing patterns to user requirements while maintaining compliance
## Important ADK Requirements
**File Naming & Structure:**
- Main configuration MUST be `root_agent.yaml` (not `agent.yaml`)
- Agent directories need `__init__.py` with `from . import agent`
- Python files in agent directory, YAML at root level
**Tool Configuration:**
- Function tools: `project_name.tools.module.function_name` format (all dots, must start with project folder name)
- No `.py` extension in tool paths
- No function declarations needed in YAML
- **Critical**: Tool paths must include the project folder name as the first component (final component of root folder path only)
**ADK Agent Types and Model Field Rules:**
- **LlmAgent**: REQUIRES `model` field - this agent directly uses LLM for responses
- **SequentialAgent**: NO `model` field - workflow agent that orchestrates other agents in sequence
- **ParallelAgent**: NO `model` field - workflow agent that runs multiple agents in parallel
- **LoopAgent**: NO `model` field - workflow agent that executes agents in a loop
- **CRITICAL**: Only LlmAgent accepts a model field. Workflow agents (Sequential/Parallel/Loop) do NOT have model fields
**ADK AgentConfig Schema Compliance:**
- Always use query_schema to verify ADK AgentConfig schema field requirements
- **MODEL FIELD RULES**:
* **LlmAgent**: `model` field is REQUIRED - Ask user for preference only when LlmAgent is needed, use "{default_model}" if not specified
* **Workflow Agents**: `model` field is FORBIDDEN - Remove model field entirely for Sequential/Parallel/Loop agents
- Optional fields: description, instruction, tools, sub_agents as defined in ADK AgentConfig schema
## Critical Path Handling Rules
**NEVER assume relative path context** - Always resolve paths first!
### For relative paths provided by users:
1. **ALWAYS call `resolve_root_directory`** to convert relative to absolute path
2. **Verify the resolved path** matches user's intended location
3. **Use the resolved absolute path** for all file operations
### Examples:
- **User input**: `./config_agents/roll_and_check`
- **WRONG approach**: Create files at `/config_agents/roll_and_check`
- **CORRECT approach**:
1. Call `resolve_root_directory("./config_agents/roll_and_check")`
2. Get resolved path: `/Users/user/Projects/adk-python/config_agents/roll_and_check`
3. Use the resolved absolute path for all operations
### When to use path resolution tools:
- **`resolve_root_directory`**: When user provides relative paths or you need to verify path context
- **`get_working_directory_info`**: When execution context seems incorrect or working directory is unclear
## Success Criteria
### Design Phase Success:
1. Root folder path confirmed and analyzed with explore_project
2. Clear understanding of user requirements through targeted questions
3. Well-researched architecture based on proven ADK patterns
4. Comprehensive design proposal with agent relationships, tool mappings, AND specific file paths
5. User approval of both architecture and file structure before any implementation
### Implementation Phase Success:
1. Files created at exact paths specified in approved design
2. No redundant suggest_file_path calls for pre-approved paths
3. Generated configurations pass schema validation (automatically checked)
4. Follow ADK naming and organizational conventions
5. Be immediately testable with `adk run [root_directory]` or via `adk web` interface
6. Include clear, actionable instructions for each agent
7. Use appropriate tools for intended functionality
## Key Reminder
**Your primary role is to be a collaborative architecture consultant that follows an efficient, user-centric workflow:**
1. **Always ask for root folder first** - Know where to create the project
2. **Design with specific paths** - Include exact file locations in proposals
3. **Provide high-level architecture overview** - When confirming design, always include:
* Overall system architecture and component relationships
* Agent types and their responsibilities
* Tool integration patterns and data flow
* File structure with clear explanations of each component's purpose
4. **Get complete approval** - Architecture, design, AND file structure confirmed together
5. **Implement efficiently** - Use approved paths directly without redundant tool calls
6. **Focus on collaboration** - Ensure user gets exactly what they need with clear understanding
**This workflow eliminates inefficiencies and ensures users get well-organized, predictable file structures in their chosen location.**
## Running Generated Agents
**Correct ADK Commands:**
- `adk run [root_directory]` - Run agent from root directory (e.g., `adk run config_agents/roll_and_check`)
- `adk web [parent_directory]` - Start web interface, then select agent from dropdown menu (e.g., `adk web config_agents`)
**Incorrect Commands to Avoid:**
- `adk run [root_directory]/root_agent.yaml` - Do NOT specify the YAML file directly
- `adk web` without parent directory - Must specify the parent folder containing the agent projects
- Always use the project directory for `adk run`, and parent directory for `adk web`
@@ -0,0 +1,20 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Sub-agents for Agent Builder Assistant."""
from .google_search_agent import create_google_search_agent
from .url_context_agent import create_url_context_agent
__all__ = ['create_google_search_agent', 'create_url_context_agent']
@@ -0,0 +1,59 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Sub-agent for Google Search functionality."""
from google.adk.agents import LlmAgent
from google.adk.tools import google_search
def create_google_search_agent() -> LlmAgent:
"""Create a sub-agent that only uses google_search tool."""
return LlmAgent(
name="google_search_agent",
description=(
"Agent for performing Google searches to find ADK examples and"
" documentation"
),
instruction="""You are a specialized search agent for the Agent Builder Assistant.
Your role is to search for relevant ADK (Agent Development Kit) examples, patterns, documentation, and solutions.
When given a search query, use the google_search tool to find:
- ADK configuration examples and patterns
- Multi-agent system architectures and workflows
- Best practices and documentation
- Similar use cases and implementations
- Troubleshooting solutions and error fixes
- API references and implementation guides
SEARCH STRATEGIES:
- Use site-specific searches for targeted results:
* "site:github.com/google/adk-python [query]" for core ADK examples
* "site:github.com/google/adk-samples [query]" for sample implementations
* "site:github.com/google/adk-docs [query]" for documentation
- Use general searches for broader community solutions
- Search for specific agent types, tools, or error messages
- Look for configuration patterns and architectural approaches
Return the search results with:
1. Relevant URLs found
2. Brief description of what each result contains
3. Relevance to the original query
4. Suggestions for which URLs should be fetched for detailed analysis
Focus on finding practical, actionable examples that can guide ADK development and troubleshooting.""",
model="gemini-2.5-flash",
tools=[google_search],
)
@@ -0,0 +1,62 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Sub-agent for URL context fetching functionality."""
from google.adk.agents import LlmAgent
from google.adk.tools import url_context
def create_url_context_agent() -> LlmAgent:
"""Create a sub-agent that only uses url_context tool."""
return LlmAgent(
name="url_context_agent",
description=(
"Agent for fetching and analyzing content from URLs, especially"
" GitHub repositories and documentation"
),
instruction="""You are a specialized URL content analysis agent for the Agent Builder Assistant.
Your role is to fetch and analyze complete content from URLs to extract detailed, actionable information.
TARGET CONTENT TYPES:
- GitHub repository files (YAML configurations, Python implementations, README files)
- ADK documentation pages and API references
- Code examples and implementation patterns
- Configuration samples and templates
- Troubleshooting guides and solutions
When given a URL, use the url_context tool to:
1. Fetch the complete content from the specified URL
2. Analyze the content thoroughly for relevant information
3. Extract specific details about:
- Agent configurations and structure
- Tool implementations and usage patterns
- Architecture decisions and relationships
- Code snippets and examples
- Best practices and recommendations
- Error handling and troubleshooting steps
Return a comprehensive analysis that includes:
- Summary of what the content provides
- Specific implementation details and code patterns
- Key configuration examples or snippets
- How the content relates to the original query
- Actionable insights and recommendations
- Any warnings or important considerations mentioned
Focus on extracting complete, detailed information that enables practical application of the patterns and examples found.""",
model="gemini-2.5-flash",
tools=[url_context],
)
@@ -0,0 +1,37 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Tools for Agent Builder Assistant."""
from .cleanup_unused_files import cleanup_unused_files
from .delete_files import delete_files
from .explore_project import explore_project
from .read_config_files import read_config_files
from .read_files import read_files
from .resolve_root_directory import resolve_root_directory
from .search_adk_source import search_adk_source
from .write_config_files import write_config_files
from .write_files import write_files
__all__ = [
'read_config_files',
'write_config_files',
'cleanup_unused_files',
'delete_files',
'read_files',
'write_files',
'search_adk_source',
'explore_project',
'resolve_root_directory',
]
@@ -0,0 +1,108 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Cleanup unused files tool for Agent Builder Assistant."""
from pathlib import Path
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
async def cleanup_unused_files(
root_directory: str,
used_files: List[str],
file_patterns: Optional[List[str]] = None,
exclude_patterns: Optional[List[str]] = None,
) -> Dict[str, Any]:
"""Identify and optionally delete unused files in project directories.
This tool helps clean up unused tool files when agent configurations change.
It identifies files that match patterns but aren't referenced in used_files
list.
Args:
root_directory: Root directory to scan for unused files
used_files: List of file paths currently in use (should not be deleted)
file_patterns: List of glob patterns to match files (default: ["*.py"])
exclude_patterns: List of patterns to exclude (default: ["__init__.py"])
Returns:
Dict containing cleanup results:
- success: bool indicating if scan succeeded
- root_directory: absolute path to scanned directory
- unused_files: list of unused files found
- deleted_files: list of files actually deleted
- backup_files: list of backup files created
- errors: list of error messages
- total_freed_space: total bytes freed by deletions
"""
try:
root_path = Path(root_directory).resolve()
used_files_set = {Path(f).resolve() for f in used_files}
# Set defaults
if file_patterns is None:
file_patterns = ["*.py"]
if exclude_patterns is None:
exclude_patterns = ["__init__.py", "*_test.py", "test_*.py"]
result = {
"success": False,
"root_directory": str(root_path),
"unused_files": [],
"deleted_files": [],
"backup_files": [],
"errors": [],
"total_freed_space": 0,
}
if not root_path.exists():
result["errors"].append(f"Root directory does not exist: {root_path}")
return result
# Find all files matching patterns
all_files = []
for pattern in file_patterns:
all_files.extend(root_path.rglob(pattern))
# Filter out excluded patterns
for exclude_pattern in exclude_patterns:
all_files = [f for f in all_files if not f.match(exclude_pattern)]
# Identify unused files
unused_files = []
for file_path in all_files:
if file_path not in used_files_set:
unused_files.append(file_path)
result["unused_files"] = [str(f) for f in unused_files]
# Note: This function only identifies unused files
# Actual deletion should be done with explicit user confirmation using delete_files()
result["success"] = True
return result
except Exception as e:
return {
"success": False,
"root_directory": root_directory,
"unused_files": [],
"deleted_files": [],
"backup_files": [],
"errors": [f"Cleanup scan failed: {str(e)}"],
"total_freed_space": 0,
}
@@ -0,0 +1,127 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""File deletion tool for Agent Builder Assistant."""
from datetime import datetime
from pathlib import Path
import shutil
from typing import Any
from typing import Dict
from typing import List
async def delete_files(
file_paths: List[str],
create_backup: bool = False,
confirm_deletion: bool = True,
) -> Dict[str, Any]:
"""Delete multiple files with optional backup creation.
This tool safely deletes multiple files with validation and optional backup
creation.
It's designed for cleaning up unused tool files when agent configurations
change.
Args:
file_paths: List of absolute or relative paths to files to delete
create_backup: Whether to create a backup before deletion (default: False)
confirm_deletion: Whether deletion was confirmed by user (default: True for
safety)
Returns:
Dict containing deletion operation results:
- success: bool indicating if all deletions succeeded
- files: dict mapping file_path to file deletion info:
- existed: bool indicating if file existed before deletion
- backup_created: bool indicating if backup was created
- backup_path: path to backup file if created
- error: error message if deletion failed for this file
- file_size: size of deleted file in bytes (if existed)
- successful_deletions: number of files deleted successfully
- total_files: total number of files requested
- errors: list of general error messages
"""
try:
result = {
"success": True,
"files": {},
"successful_deletions": 0,
"total_files": len(file_paths),
"errors": [],
}
# Safety check - only delete if user confirmed
if not confirm_deletion:
result["success"] = False
result["errors"].append("Deletion not confirmed by user")
return result
for file_path in file_paths:
file_path_obj = Path(file_path).resolve()
file_info = {
"existed": False,
"backup_created": False,
"backup_path": None,
"error": None,
"file_size": 0,
}
try:
# Check if file exists
if not file_path_obj.exists():
file_info["error"] = f"File does not exist: {file_path_obj}"
result["files"][str(file_path_obj)] = file_info
result["successful_deletions"] += 1 # Still count as success
continue
file_info["existed"] = True
file_info["file_size"] = file_path_obj.stat().st_size
# Create backup if requested
if create_backup:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_path = file_path_obj.with_suffix(
f".backup_{timestamp}{file_path_obj.suffix}"
)
try:
shutil.copy2(file_path_obj, backup_path)
file_info["backup_created"] = True
file_info["backup_path"] = str(backup_path)
except Exception as e:
file_info["error"] = f"Failed to create backup: {str(e)}"
result["success"] = False
result["files"][str(file_path_obj)] = file_info
continue
# Delete the file
file_path_obj.unlink()
result["successful_deletions"] += 1
except Exception as e:
file_info["error"] = f"Deletion failed: {str(e)}"
result["success"] = False
result["files"][str(file_path_obj)] = file_info
return result
except Exception as e:
return {
"success": False,
"files": {},
"successful_deletions": 0,
"total_files": len(file_paths) if file_paths else 0,
"errors": [f"Delete operation failed: {str(e)}"],
}
@@ -0,0 +1,354 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Project explorer tool for analyzing structure and suggesting file paths."""
from pathlib import Path
from typing import Any
from typing import Dict
from typing import List
async def explore_project(root_directory: str) -> Dict[str, Any]:
"""Analyze project structure and suggest optimal file paths for ADK agents.
This tool performs comprehensive project analysis to understand the existing
structure and recommend appropriate locations for new agent configurations,
tools, and related files following ADK best practices.
Args:
root_directory: Absolute or relative path to the root directory to explore
and analyze
Returns:
Dict containing analysis results:
Always included:
- success: bool indicating if exploration succeeded
- root_path: absolute path to the analyzed directory
Success cases only (success=True):
- project_info: dict with basic project metadata. Contains:
"name": project directory name
"absolute_path": full path to project root
"is_empty": bool indicating if directory is empty
"total_files": count of all files in project
"total_directories": count of all subdirectories
"has_python_files": bool indicating presence of .py
files
"has_yaml_files": bool indicating presence of
.yaml/.yml files
"has_tools_directory": bool indicating if tools/ exists
"has_callbacks_directory": bool indicating if
callbacks/ exists
- existing_configs: list of dicts for found YAML configuration files.
Each dict contains:
"filename": name of the config file
"path": absolute path to the file
"relative_path": path relative to project root
"size": file size in bytes
"is_valid_yaml": bool indicating if YAML parses
correctly
"agent_name": extracted agent name (or None)
"agent_class": agent class type (default:
"LlmAgent")
"has_sub_agents": bool indicating if config has
sub_agents
"has_tools": bool indicating if config has tools
- directory_structure: dict with hierarchical project tree view
- suggestions: dict with recommended paths for new components. Contains:
"root_agent_configs": list of suggested main agent
filenames
"sub_agent_patterns": list of naming pattern templates
"directories": dict with tool/callback directory info
"naming_examples": dict with example agent sets by
domain
- conventions: dict with ADK naming and organization best practices
Error cases only (success=False):
- error: descriptive error message explaining the failure
Examples:
Basic project exploration:
result = await explore_project("/path/to/my_adk_project")
Check project structure:
if result["project_info"]["has_tools_directory"]:
print("Tools directory already exists")
Analyze existing configs:
for config in result["existing_configs"]:
if config["is_valid_yaml"]:
print(f"Found agent: {config['agent_name']}")
Get path suggestions:
suggestions = result["suggestions"]["root_agent_configs"]
directories = result["suggestions"]["directories"]["tools"]
"""
try:
root_path = Path(root_directory).resolve()
if not root_path.exists():
return {
"success": False,
"error": f"Root directory does not exist: {root_directory}",
"root_path": str(root_path),
}
if not root_path.is_dir():
return {
"success": False,
"error": f"Path is not a directory: {root_directory}",
"root_path": str(root_path),
}
# Analyze project structure
project_info = _analyze_project_info(root_path)
existing_configs = _find_existing_configs(root_path)
directory_structure = _build_directory_tree(root_path)
suggestions = _generate_path_suggestions(root_path, existing_configs)
conventions = _get_naming_conventions()
return {
"success": True,
"root_path": str(root_path),
"project_info": project_info,
"existing_configs": existing_configs,
"directory_structure": directory_structure,
"suggestions": suggestions,
"conventions": conventions,
}
except PermissionError:
return {
"success": False,
"error": f"Permission denied accessing directory: {root_directory}",
"root_path": root_directory,
}
except Exception as e:
return {
"success": False,
"error": f"Error exploring project: {str(e)}",
"root_path": root_directory,
}
def _analyze_project_info(root_path: Path) -> Dict[str, Any]:
"""Analyze basic project information."""
info = {
"name": root_path.name,
"absolute_path": str(root_path),
"is_empty": not any(root_path.iterdir()),
"total_files": 0,
"total_directories": 0,
"has_python_files": False,
"has_yaml_files": False,
"has_tools_directory": False,
"has_callbacks_directory": False,
}
try:
for item in root_path.rglob("*"):
if item.is_file():
info["total_files"] += 1
suffix = item.suffix.lower()
if suffix == ".py":
info["has_python_files"] = True
elif suffix in [".yaml", ".yml"]:
info["has_yaml_files"] = True
elif item.is_dir():
info["total_directories"] += 1
if item.name == "tools" and item.parent == root_path:
info["has_tools_directory"] = True
elif item.name == "callbacks" and item.parent == root_path:
info["has_callbacks_directory"] = True
except Exception:
# Continue with partial information if traversal fails
pass
return info
def _find_existing_configs(root_path: Path) -> List[Dict[str, Any]]:
"""Find existing YAML configuration files in the project."""
configs = []
try:
# Look for YAML files in root directory (ADK convention)
for yaml_file in root_path.glob("*.yaml"):
if yaml_file.is_file():
config_info = _analyze_config_file(yaml_file)
configs.append(config_info)
for yml_file in root_path.glob("*.yml"):
if yml_file.is_file():
config_info = _analyze_config_file(yml_file)
configs.append(config_info)
# Sort by name for consistent ordering
configs.sort(key=lambda x: x["filename"])
except Exception:
# Return partial results if scanning fails
pass
return configs
def _analyze_config_file(config_path: Path) -> Dict[str, Any]:
"""Analyze a single configuration file."""
info = {
"filename": config_path.name,
"path": str(config_path),
"relative_path": config_path.name, # In root directory
"size": 0,
"is_valid_yaml": False,
"agent_name": None,
"agent_class": None,
"has_sub_agents": False,
"has_tools": False,
}
try:
info["size"] = config_path.stat().st_size
# Try to parse YAML to extract basic info
import yaml
with open(config_path, "r", encoding="utf-8") as f:
content = yaml.safe_load(f)
if isinstance(content, dict):
info["is_valid_yaml"] = True
info["agent_name"] = content.get("name")
info["agent_class"] = content.get("agent_class", "LlmAgent")
info["has_sub_agents"] = bool(content.get("sub_agents"))
info["has_tools"] = bool(content.get("tools"))
except Exception:
# File exists but couldn't be parsed
pass
return info
def _build_directory_tree(
root_path: Path, max_depth: int = 3
) -> Dict[str, Any]:
"""Build a directory tree representation."""
def build_tree_recursive(
path: Path, current_depth: int = 0
) -> Dict[str, Any]:
if current_depth > max_depth:
return {"truncated": True}
tree = {
"name": path.name,
"type": "directory" if path.is_dir() else "file",
"path": str(path.relative_to(root_path)),
}
if path.is_dir():
children = []
try:
for child in sorted(path.iterdir()):
# Skip hidden files and common ignore patterns
if not child.name.startswith(".") and child.name not in [
"__pycache__",
"node_modules",
]:
children.append(build_tree_recursive(child, current_depth + 1))
tree["children"] = children
except PermissionError:
tree["error"] = "Permission denied"
else:
tree["size"] = path.stat().st_size if path.exists() else 0
return tree
return build_tree_recursive(root_path)
def _generate_path_suggestions(
root_path: Path, existing_configs: List[Dict[str, Any]]
) -> Dict[str, Any]:
"""Generate suggested file paths for new components."""
# Suggest main agent names if none exist
root_agent_suggestions = []
if not any(
config.get("agent_class") != "LlmAgent"
or not config.get("has_sub_agents", False)
for config in existing_configs
):
root_agent_suggestions = [
"root_agent.yaml",
]
# Directory suggestions
directories = {
"tools": {
"path": str(root_path / "tools"),
"exists": (root_path / "tools").exists(),
"purpose": "Custom tool implementations",
"example_files": [
"custom_email.py",
"database_connector.py",
],
},
"callbacks": {
"path": str(root_path / "callbacks"),
"exists": (root_path / "callbacks").exists(),
"purpose": "Custom callback functions",
"example_files": ["logging.py", "security.py"],
},
}
return {
"root_agent_configs": root_agent_suggestions,
"sub_agent_patterns": [
"{purpose}_agent.yaml",
"{domain}_{action}_agent.yaml",
"{workflow_step}_agent.yaml",
],
"directories": directories,
}
def _get_naming_conventions() -> Dict[str, Any]:
"""Get ADK naming conventions and best practices."""
return {
"agent_files": {
"format": "snake_case with .yaml extension",
"examples": ["main_agent.yaml", "email_processor.yaml"],
"location": "Root directory of the project",
"avoid": ["camelCase.yaml", "spaces in names.yaml", "UPPERCASE.yaml"],
},
"agent_names": {
"format": "snake_case, descriptive, no spaces",
"examples": ["customer_service_coordinator", "email_classifier"],
"avoid": ["Agent1", "my agent", "CustomerServiceAgent"],
},
"directory_structure": {
"recommended": {
"root": "All .yaml agent configuration files",
"tools/": "Custom tool implementations (.py files)",
"callbacks/": "Custom callback functions (.py files)",
}
},
}
@@ -0,0 +1,247 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""ADK AgentConfig schema query tool for dynamic schema information access."""
from typing import Any
from typing import Dict
from typing import Optional
from ..utils import load_agent_config_schema
async def query_schema(
query_type: str,
component: Optional[str] = None,
field_path: Optional[str] = None,
) -> Dict[str, Any]:
"""Dynamically query ADK AgentConfig schema for specific information.
This tool provides on-demand access to ADK AgentConfig schema details without
embedding
the full schema in context. It's designed for "query" mode where
agents need specific schema information without the memory overhead
of the complete schema.
Args:
query_type: Type of schema query to perform. Supported values: - "overview":
Get high-level schema structure and main properties - "component": Get
detailed info about a specific top-level component - "field": Get details
about a specific field using dot notation - "properties": Get flat list of
all available properties
component: Component name to explore (required for "component" query_type).
Examples: "name", "instruction", "tools", "model", "memory"
field_path: Dot-separated path to specific field (required for "field"
query_type).
Examples: "tools.function_tool.function_path", "model.name"
Returns:
Dict containing schema exploration results:
Always included:
- query_type: type of query performed
- success: bool indicating if exploration succeeded
Success cases vary by query_type:
overview: schema title, description, main properties list
component: component details, nested properties, type info
field: field traversal path, type, description, constraints
properties: complete flat property list with types
Error cases only (success=False):
- error: descriptive error message
- supported_queries: list of valid query types and usage
Examples:
Get schema overview:
result = await query_schema("overview")
Explore tools component:
result = await query_schema("component", component="tools")
Get specific field details:
result = await query_schema("field", field_path="model.name")
"""
try:
schema = load_agent_config_schema(raw_format=False)
if query_type == "overview":
return _get_schema_overview(schema)
elif query_type == "component" and component:
return _get_component_details(schema, component)
elif query_type == "field" and field_path:
return _get_field_details(schema, field_path)
elif query_type == "properties":
return _get_all_properties(schema)
else:
return {
"error": (
f"Invalid query_type '{query_type}' or missing required"
" parameters"
),
"supported_queries": [
"overview - Get high-level schema structure",
(
"component - Get details for specific component (requires"
" component parameter)"
),
(
"field - Get details for specific field (requires field_path"
" parameter)"
),
"properties - Get all available properties",
],
}
except Exception as e:
return {"error": f"Schema exploration failed: {str(e)}"}
def _get_schema_overview(schema: Dict[str, Any]) -> Dict[str, Any]:
"""Get high-level overview of schema structure."""
overview = {
"title": schema.get("title", "ADK Agent Configuration"),
"description": schema.get("description", ""),
"schema_version": schema.get("$schema", ""),
"main_properties": [],
}
properties = schema.get("properties", {})
for prop_name, prop_details in properties.items():
overview["main_properties"].append({
"name": prop_name,
"type": prop_details.get("type", "unknown"),
"description": prop_details.get("description", ""),
"required": prop_name in schema.get("required", []),
})
return overview
def _get_component_details(
schema: Dict[str, Any], component: str
) -> Dict[str, Any]:
"""Get detailed information about a specific component."""
properties = schema.get("properties", {})
if component not in properties:
return {
"error": f"Component '{component}' not found",
"available_components": list(properties.keys()),
}
component_schema = properties[component]
result = {
"component": component,
"type": component_schema.get("type", "unknown"),
"description": component_schema.get("description", ""),
"required": component in schema.get("required", []),
}
# Add nested properties if it's an object
if component_schema.get("type") == "object":
nested_props = component_schema.get("properties", {})
result["properties"] = {}
for prop_name, prop_details in nested_props.items():
result["properties"][prop_name] = {
"type": prop_details.get("type", "unknown"),
"description": prop_details.get("description", ""),
"required": prop_name in component_schema.get("required", []),
}
# Add array item details if it's an array
if component_schema.get("type") == "array":
items = component_schema.get("items", {})
result["items"] = {
"type": items.get("type", "unknown"),
"description": items.get("description", ""),
}
if items.get("type") == "object":
result["items"]["properties"] = items.get("properties", {})
return result
def _get_field_details(
schema: Dict[str, Any], field_path: str
) -> Dict[str, Any]:
"""Get details for a specific field using dot notation."""
path_parts = field_path.split(".")
current = schema.get("properties", {})
result = {"field_path": field_path, "path_traversal": []}
for i, part in enumerate(path_parts):
if not isinstance(current, dict) or part not in current:
return {
"error": f"Field path '{field_path}' not found at '{part}'",
"traversed": ".".join(path_parts[:i]),
"available_at_level": (
list(current.keys()) if isinstance(current, dict) else []
),
}
field_info = current[part]
result["path_traversal"].append({
"field": part,
"type": field_info.get("type", "unknown"),
"description": field_info.get("description", ""),
})
# Navigate deeper based on type
if field_info.get("type") == "object":
current = field_info.get("properties", {})
elif (
field_info.get("type") == "array"
and field_info.get("items", {}).get("type") == "object"
):
current = field_info.get("items", {}).get("properties", {})
else:
# End of navigable path
result["final_field"] = field_info
break
return result
def _get_all_properties(schema: Dict[str, Any]) -> Dict[str, Any]:
"""Get a flat list of all properties in the schema."""
properties = {}
def extract_properties(obj: Dict[str, Any], prefix: str = ""):
if not isinstance(obj, dict):
return
for key, value in obj.items():
if key == "properties" and isinstance(value, dict):
for prop_name, prop_details in value.items():
full_path = f"{prefix}.{prop_name}" if prefix else prop_name
properties[full_path] = {
"type": prop_details.get("type", "unknown"),
"description": prop_details.get("description", ""),
}
# Recurse into object properties
if prop_details.get("type") == "object":
extract_properties(prop_details, full_path)
# Recurse into array item properties
elif (
prop_details.get("type") == "array"
and prop_details.get("items", {}).get("type") == "object"
):
extract_properties(prop_details.get("items", {}), full_path)
extract_properties(schema)
return {"total_properties": len(properties), "properties": properties}
@@ -0,0 +1,238 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Configuration file reader tool for existing YAML configs."""
from pathlib import Path
from typing import Any
from typing import Dict
from typing import List
import yaml
from .read_files import read_files
async def read_config_files(file_paths: List[str]) -> Dict[str, Any]:
"""Read multiple YAML configuration files and extract metadata.
Args:
file_paths: List of absolute or relative paths to YAML configuration files
Returns:
Dict containing:
- success: bool indicating if all files were processed
- total_files: number of files requested
- successful_reads: number of files read successfully
- files: dict mapping file_path to file analysis:
- success: bool for this specific file
- file_path: absolute path to the file
- file_size: size of file in characters
- line_count: number of lines in file
- content: parsed YAML content as dict (success only)
- agent_info: extracted agent metadata (success only)
- sub_agents: list of referenced sub-agent files (success only)
- tools: list of tools used by the agent (success only)
- error: error message (failure only)
- raw_yaml: original YAML string (parsing errors only)
- errors: list of general error messages
"""
# Read all files using the file_manager read_files tool
read_result = await read_files(file_paths)
result = {
"success": True,
"total_files": len(file_paths),
"successful_reads": 0,
"files": {},
"errors": [],
}
for file_path, file_info in read_result["files"].items():
file_analysis = {
"success": False,
"file_path": file_path,
"file_size": file_info.get("file_size", 0),
"line_count": 0,
"error": None,
}
# Check if file was read successfully
if file_info.get("error"):
file_analysis["error"] = file_info["error"]
result["files"][file_path] = file_analysis
result["success"] = False
continue
# Check if it's a YAML file
path = Path(file_path)
if path.suffix.lower() not in [".yaml", ".yml"]:
file_analysis["error"] = f"File is not a YAML file: {file_path}"
result["files"][file_path] = file_analysis
result["success"] = False
continue
raw_yaml = file_info.get("content", "")
file_analysis["line_count"] = len(raw_yaml.split("\n"))
# Parse YAML
try:
content = yaml.safe_load(raw_yaml)
except yaml.YAMLError as e:
file_analysis["error"] = f"Invalid YAML syntax: {str(e)}"
file_analysis["raw_yaml"] = raw_yaml
result["files"][file_path] = file_analysis
result["success"] = False
continue
if not isinstance(content, dict):
file_analysis["error"] = "YAML content is not a valid object/dictionary"
file_analysis["raw_yaml"] = raw_yaml
result["files"][file_path] = file_analysis
result["success"] = False
continue
# Extract agent metadata
try:
agent_info = _extract_agent_info(content)
sub_agents = _extract_sub_agents(content)
tools = _extract_tools(content)
file_analysis.update({
"success": True,
"content": content,
"agent_info": agent_info,
"sub_agents": sub_agents,
"tools": tools,
})
result["successful_reads"] += 1
except Exception as e:
file_analysis["error"] = f"Error extracting metadata: {str(e)}"
result["success"] = False
result["files"][file_path] = file_analysis
return result
# Legacy functions removed - use read_config_files directly
def _extract_agent_info(content: Dict[str, Any]) -> Dict[str, Any]:
"""Extract basic agent information from configuration."""
return {
"name": content.get("name", "unknown"),
"agent_class": content.get("agent_class", "LlmAgent"),
"description": content.get("description", ""),
"model": content.get("model", ""),
"has_instruction": bool(content.get("instruction", "").strip()),
"instruction_length": len(content.get("instruction", "")),
"has_memory": bool(content.get("memory")),
"has_state": bool(content.get("state")),
}
def _extract_sub_agents(content: Dict[str, Any]) -> list:
"""Extract sub-agent references from configuration."""
sub_agents = content.get("sub_agents", [])
if not isinstance(sub_agents, list):
return []
extracted = []
for sub_agent in sub_agents:
if isinstance(sub_agent, dict):
agent_ref = {
"config_path": sub_agent.get("config_path", ""),
"code": sub_agent.get("code", ""),
"type": "config_path" if "config_path" in sub_agent else "code",
}
# Check if referenced file exists (for config_path refs)
if agent_ref["config_path"]:
agent_ref["file_exists"] = _check_file_exists(agent_ref["config_path"])
extracted.append(agent_ref)
elif isinstance(sub_agent, str):
# Simple string reference
extracted.append({
"config_path": sub_agent,
"code": "",
"type": "config_path",
"file_exists": _check_file_exists(sub_agent),
})
return extracted
def _extract_tools(content: Dict[str, Any]) -> list:
"""Extract tool information from configuration."""
tools = content.get("tools", [])
if not isinstance(tools, list):
return []
extracted = []
for tool in tools:
if isinstance(tool, dict):
tool_info = {
"name": tool.get("name", ""),
"type": "object",
"has_args": bool(tool.get("args")),
"args_count": len(tool.get("args", [])),
"raw": tool,
}
elif isinstance(tool, str):
tool_info = {
"name": tool,
"type": "string",
"has_args": False,
"args_count": 0,
"raw": tool,
}
else:
continue
extracted.append(tool_info)
return extracted
def _check_file_exists(config_path: str) -> bool:
"""Check if a configuration file path exists."""
try:
if not config_path:
return False
path = Path(config_path)
# If it's not absolute, check relative to current working directory
if not path.is_absolute():
# Try relative to current directory
current_dir_path = Path.cwd() / config_path
if current_dir_path.exists():
return True
# Try common agent directory patterns
for potential_dir in [".", "./agents", "../agents"]:
potential_path = Path(potential_dir) / config_path
if potential_path.exists():
return True
return path.exists()
except (OSError, ValueError):
return False
@@ -0,0 +1,89 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""File reading tool for Agent Builder Assistant."""
from pathlib import Path
from typing import Any
from typing import Dict
from typing import List
async def read_files(file_paths: List[str]) -> Dict[str, Any]:
"""Read content from multiple files.
This tool reads content from multiple files and returns their contents.
It's designed for reading Python tools, configuration files, and other text
files.
Args:
file_paths: List of absolute or relative paths to files to read
Returns:
Dict containing read operation results:
- success: bool indicating if all reads succeeded
- files: dict mapping file_path to file info:
- content: file content as string
- file_size: size of file in bytes
- exists: bool indicating if file exists
- error: error message if read failed for this file
- successful_reads: number of files read successfully
- total_files: total number of files requested
- errors: list of general error messages
"""
try:
result = {
"success": True,
"files": {},
"successful_reads": 0,
"total_files": len(file_paths),
"errors": [],
}
for file_path in file_paths:
file_path_obj = Path(file_path).resolve()
file_info = {
"content": "",
"file_size": 0,
"exists": False,
"error": None,
}
try:
if not file_path_obj.exists():
file_info["error"] = f"File does not exist: {file_path_obj}"
else:
file_info["exists"] = True
file_info["file_size"] = file_path_obj.stat().st_size
with open(file_path_obj, "r", encoding="utf-8") as f:
file_info["content"] = f.read()
result["successful_reads"] += 1
except Exception as e:
file_info["error"] = f"Failed to read {file_path}: {str(e)}"
result["success"] = False
result["files"][str(file_path_obj)] = file_info
return result
except Exception as e:
return {
"success": False,
"files": {},
"successful_reads": 0,
"total_files": len(file_paths) if file_paths else 0,
"errors": [f"Read operation failed: {str(e)}"],
}
@@ -0,0 +1,100 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Working directory helper tool to resolve path context issues."""
import os
from pathlib import Path
from typing import Any
from typing import Dict
from typing import Optional
async def resolve_root_directory(
root_directory: str, working_directory: Optional[str] = None
) -> Dict[str, Any]:
"""Resolve the root directory from user-provided path for agent building.
This tool determines where to create or update agent configurations by
resolving the user-provided path. It handles both absolute and relative paths,
using the current working directory when needed for relative path resolution.
Args:
root_directory: Path provided by user (can be relative or absolute)
indicating where to build agents
working_directory: Optional explicit working directory to use as base for
relative path resolution (defaults to os.getcwd())
Returns:
Dict containing path resolution results:
Always included:
- success: bool indicating if resolution succeeded
- original_path: the provided root directory path
- resolved_path: absolute path to the resolved location
- resolution_method: explanation of how path was resolved
- path_exists: bool indicating if resolved path exists
Conditionally included:
- alternative_paths: list of other possible path interpretations
- warnings: list of potential issues or ambiguities
- working_directory_used: the working directory used for resolution
Examples:
Resolve relative path:
result = await resolve_root_directory("./my_project",
"/home/user/projects")
Resolve with auto-detection:
result = await resolve_root_directory("my_agent.yaml")
# Will use current working directory for relative paths
"""
try:
current_cwd = os.getcwd()
root_path_obj = Path(root_directory)
# If user provided an absolute path, use it directly
if root_path_obj.is_absolute():
resolved_path = root_path_obj
else:
# For relative paths, prefer user-provided working directory
if working_directory:
resolved_path = Path(working_directory) / root_directory
else:
# Fallback to actual current working directory
resolved_path = Path(current_cwd) / root_directory
return {
"success": True,
"original_path": root_directory,
"resolved_path": str(resolved_path.resolve()),
"exists": resolved_path.exists(),
"is_absolute": root_path_obj.is_absolute(),
"current_cwd": current_cwd,
"working_directory_used": working_directory,
"recommendation": (
f"Use resolved path: {resolved_path.resolve()}"
if resolved_path.exists()
else (
"Path does not exist. Create parent directories first:"
f" {resolved_path.parent}"
)
),
}
except Exception as e:
return {
"success": False,
"error": f"Failed to resolve path: {str(e)}",
"original_path": root_directory,
}
@@ -0,0 +1,167 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""ADK source code search tool for Agent Builder Assistant."""
from pathlib import Path
import re
from typing import Any
from typing import Dict
from typing import List
from typing import Optional
from ..utils import find_adk_source_folder
async def search_adk_source(
search_pattern: str,
file_patterns: Optional[List[str]] = None,
max_results: int = 20,
context_lines: int = 3,
case_sensitive: bool = False,
) -> Dict[str, Any]:
"""Search ADK source code using regex patterns.
This tool provides a regex-based alternative to vector-based retrieval for
finding
specific code patterns, class definitions, function signatures, and
implementations
in the ADK source code.
Args:
search_pattern: Regex pattern to search for (e.g., "class FunctionTool",
"def __init__")
file_patterns: List of glob patterns for files to search (default: ["*.py"])
max_results: Maximum number of results to return (default: 20)
context_lines: Number of context lines to include around matches (default:
3)
case_sensitive: Whether search should be case sensitive (default: False)
Returns:
Dict containing search results:
- success: bool indicating if search succeeded
- pattern: the regex pattern used
- total_matches: total number of matches found
- files_searched: number of files searched
- results: list of match results:
- file_path: path to file containing match
- line_number: line number of match
- match_text: the matched text
- context_before: lines before the match
- context_after: lines after the match
- full_match: complete context including before/match/after
- errors: list of error messages
"""
try:
# Find ADK source directory dynamically
adk_source_path = find_adk_source_folder()
if not adk_source_path:
return {
"success": False,
"pattern": search_pattern,
"total_matches": 0,
"files_searched": 0,
"results": [],
"errors": [
"ADK source directory not found. Make sure you're running from"
" within the ADK project."
],
}
adk_src_dir = Path(adk_source_path)
result = {
"success": False,
"pattern": search_pattern,
"total_matches": 0,
"files_searched": 0,
"results": [],
"errors": [],
}
if not adk_src_dir.exists():
result["errors"].append(f"ADK source directory not found: {adk_src_dir}")
return result
# Set default file patterns
if file_patterns is None:
file_patterns = ["*.py"]
# Compile regex pattern
try:
flags = 0 if case_sensitive else re.IGNORECASE
regex = re.compile(search_pattern, flags)
except re.error as e:
result["errors"].append(f"Invalid regex pattern: {str(e)}")
return result
# Find all Python files to search
files_to_search = []
for pattern in file_patterns:
files_to_search.extend(adk_src_dir.rglob(pattern))
result["files_searched"] = len(files_to_search)
# Search through files
for file_path in files_to_search:
if result["total_matches"] >= max_results:
break
try:
with open(file_path, "r", encoding="utf-8") as f:
lines = f.readlines()
for i, line in enumerate(lines):
if result["total_matches"] >= max_results:
break
match = regex.search(line.rstrip())
if match:
# Get context lines
start_line = max(0, i - context_lines)
end_line = min(len(lines), i + context_lines + 1)
context_before = [lines[j].rstrip() for j in range(start_line, i)]
context_after = [lines[j].rstrip() for j in range(i + 1, end_line)]
match_result = {
"file_path": str(file_path.relative_to(adk_src_dir)),
"line_number": i + 1,
"match_text": line.rstrip(),
"context_before": context_before,
"context_after": context_after,
"full_match": "\n".join(
context_before + [f">>> {line.rstrip()}"] + context_after
),
}
result["results"].append(match_result)
result["total_matches"] += 1
except Exception as e:
result["errors"].append(f"Error searching {file_path}: {str(e)}")
continue
result["success"] = True
return result
except Exception as e:
return {
"success": False,
"pattern": search_pattern,
"total_matches": 0,
"files_searched": 0,
"results": [],
"errors": [f"Search failed: {str(e)}"],
}
@@ -0,0 +1,411 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Configuration file writer tool with validation-before-write."""
from pathlib import Path
from typing import Any
from typing import Dict
import jsonschema
import yaml
from ..utils import load_agent_config_schema
from .write_files import write_files
async def write_config_files(
configs: Dict[str, str],
backup_existing: bool = False, # Changed default to False - user should decide
create_directories: bool = True,
) -> Dict[str, Any]:
"""Write multiple YAML configurations with comprehensive validation-before-write.
This tool validates YAML syntax and AgentConfig schema compliance before
writing files to prevent invalid configurations from being saved. It
provides detailed error reporting and optional backup functionality.
Args:
configs: Dict mapping file_path to config_content (YAML as string)
backup_existing: Whether to create timestamped backup of existing files
before overwriting (default: False - user should be asked)
create_directories: Whether to create parent directories if they don't exist
(default: True)
Returns:
Dict containing write operation results:
Always included:
- success: bool indicating if all write operations succeeded
- total_files: number of files requested
- successful_writes: number of files written successfully
- files: dict mapping file_path to file results
Success cases only (success=True):
- file_size: size of written file in bytes
- agent_name: extracted agent name from configuration
- agent_class: agent class type (e.g., "LlmAgent")
- warnings: list of warning messages for best practice violations.
Empty list if no warnings. Common warning types:
• Agent name formatting issues (special characters)
• Empty instruction for LlmAgent
• Missing sub-agent files
• Incorrect file extensions (.yaml/.yml)
• Mixed tool format consistency
Conditionally included:
- backup: dict with backup information (if backup was created).
Contains:
"backup_created": True (always True when present)
"backup_path": absolute path to the timestamped backup file
(format: "original.yaml.backup.{timestamp}")
Error cases only (success=False):
- error: descriptive error message explaining the failure
- error_type: categorized error type for programmatic handling
- validation_step: stage where validation process stopped.
Possible values:
"yaml_parsing": YAML syntax is invalid
"yaml_structure": YAML is valid but not a
dict/object
"schema_validation": YAML violates AgentConfig
schema
• Not present: Error during file operations
- validation_errors: detailed validation error list (for schema errors
only)
- retry_suggestion: helpful suggestions for fixing the error
Examples:
Write new configuration:
result = await write_config_files({"my_agent.yaml": yaml_content})
Write without backup:
result = await write_config_files(
{"temp_agent.yaml": yaml_content},
backup_existing=False
)
Check backup information:
result = await write_config_files({"existing_agent.yaml": new_content})
if result["success"] and
result["files"]["existing_agent.yaml"]["backup_created"]:
backup_path = result["files"]["existing_agent.yaml"]["backup_path"]
print(f"Original file backed up to: {backup_path}")
Check validation warnings:
result = await write_config_files({"agent.yaml": yaml_content})
if result["success"] and result["files"]["agent.yaml"]["warnings"]:
for warning in result["files"]["agent.yaml"]["warnings"]:
print(f"Warning: {warning}")
Handle validation errors:
result = await write_config_files({"agent.yaml": invalid_yaml})
if not result["success"]:
step = result.get("validation_step", "file_operation")
if step == "yaml_parsing":
print("YAML syntax error:", result["error"])
elif step == "schema_validation":
print("Schema validation failed:", result["retry_suggestion"])
else:
print("Error:", result["error"])
"""
result: Dict[str, Any] = {
"success": True,
"total_files": len(configs),
"successful_writes": 0,
"files": {},
"errors": [],
}
validated_configs: Dict[str, str] = {}
# Step 1: Validate all configs before writing any files
for file_path, config_content in configs.items():
file_result = _validate_single_config(file_path, config_content)
result["files"][file_path] = file_result
if file_result.get("success", False):
validated_configs[file_path] = config_content
else:
result["success"] = False
# Step 2: If all validations passed, write all files
if result["success"] and validated_configs:
write_result: Dict[str, Any] = await write_files(
validated_configs,
create_backup=backup_existing,
create_directories=create_directories,
)
# Merge write results with validation results
files_data = write_result.get("files", {})
for file_path, write_info in files_data.items():
if file_path in result["files"]:
file_entry = result["files"][file_path]
if isinstance(file_entry, dict):
file_entry.update({
"file_size": write_info.get("file_size", 0),
"backup_created": write_info.get("backup_created", False),
"backup_path": write_info.get("backup_path"),
})
if write_info.get("error"):
file_entry["success"] = False
file_entry["error"] = write_info["error"]
result["success"] = False
else:
result["successful_writes"] = result["successful_writes"] + 1
return result
def _validate_single_config(
file_path: str, config_content: str
) -> Dict[str, Any]:
"""Validate a single configuration file.
Returns validation results for one config file.
"""
try:
# Convert to absolute path
path = Path(file_path).resolve()
# Step 1: Parse YAML content
try:
config_dict = yaml.safe_load(config_content)
except yaml.YAMLError as e:
return {
"success": False,
"error_type": "YAML_PARSE_ERROR",
"error": f"Invalid YAML syntax: {str(e)}",
"file_path": str(path),
"validation_step": "yaml_parsing",
}
if not isinstance(config_dict, dict):
return {
"success": False,
"error_type": "YAML_STRUCTURE_ERROR",
"error": "YAML content must be a dictionary/object",
"file_path": str(path),
"validation_step": "yaml_structure",
}
# Step 2: Validate against AgentConfig schema
validation_result = _validate_against_schema(config_dict)
if not validation_result["valid"]:
return {
"success": False,
"error_type": "SCHEMA_VALIDATION_ERROR",
"error": "Configuration does not comply with AgentConfig schema",
"validation_errors": validation_result["errors"],
"file_path": str(path),
"validation_step": "schema_validation",
"retry_suggestion": _generate_retry_suggestion(
validation_result["errors"]
),
}
# Step 3: Additional structural validation
structural_validation = _validate_structure(config_dict, path)
# Success response with validation metadata
return {
"success": True,
"file_path": str(path),
"agent_name": config_dict.get("name", "unknown"),
"agent_class": config_dict.get("agent_class", "LlmAgent"),
"warnings": structural_validation.get("warnings", []),
}
except Exception as e:
return {
"success": False,
"error_type": "UNEXPECTED_ERROR",
"error": f"Unexpected error during validation: {str(e)}",
"file_path": file_path,
}
def _validate_against_schema(
config_dict: Dict[str, Any],
) -> Dict[str, Any]:
"""Validate configuration against AgentConfig.json schema."""
try:
schema = load_agent_config_schema(raw_format=False)
jsonschema.validate(config_dict, schema)
return {"valid": True, "errors": []}
except jsonschema.ValidationError as e:
# JSONSCHEMA QUIRK WORKAROUND: Handle false positive validation errors
#
# Problem: When AgentConfig schema uses anyOf with inheritance hierarchies,
# jsonschema throws ValidationError even for valid configs that match multiple schemas.
#
# Example scenario:
# - AgentConfig schema: {"anyOf": [{"$ref": "#/$defs/LlmAgentConfig"},
# {"$ref": "#/$defs/SequentialAgentConfig"},
# {"$ref": "#/$defs/BaseAgentConfig"}]}
# - Input config: {"agent_class": "SequentialAgent", "name": "test", ...}
# - Result: Config is valid against both SequentialAgentConfig AND BaseAgentConfig
# (due to inheritance), but jsonschema considers this an error.
#
# Error message format:
# "{'agent_class': 'SequentialAgent', ...} is valid under each of
# {'$ref': '#/$defs/SequentialAgentConfig'}, {'$ref': '#/$defs/BaseAgentConfig'}"
#
# Solution: Detect this specific error pattern and treat as valid since the
# config actually IS valid - it just matches multiple compatible schemas.
if "is valid under each of" in str(e.message):
return {"valid": True, "errors": []}
error_path = " -> ".join(str(p) for p in e.absolute_path)
return {
"valid": False,
"errors": [{
"path": error_path or "root",
"message": e.message,
"invalid_value": e.instance,
"constraint": (
e.schema.get("type") or e.schema.get("enum") or "unknown"
),
}],
}
except jsonschema.SchemaError as e:
return {
"valid": False,
"errors": [{
"path": "schema",
"message": f"Schema error: {str(e)}",
"invalid_value": None,
"constraint": "schema_integrity",
}],
}
except Exception as e:
return {
"valid": False,
"errors": [{
"path": "validation",
"message": f"Validation error: {str(e)}",
"invalid_value": None,
"constraint": "validation_process",
}],
}
def _validate_structure(
config: Dict[str, Any], file_path: Path
) -> Dict[str, Any]:
"""Perform additional structural validation beyond JSON schema."""
warnings = []
# Check agent name format
name = config.get("name", "")
if name and not name.replace("_", "").replace("-", "").isalnum():
warnings.append(
"Agent name contains special characters that may cause issues"
)
# Check for empty instruction
instruction = config.get("instruction", "").strip()
if config.get("agent_class", "LlmAgent") == "LlmAgent" and not instruction:
warnings.append(
"LlmAgent has empty instruction which may result in poor performance"
)
# Validate sub-agent references
sub_agents = config.get("sub_agents", [])
for sub_agent in sub_agents:
if isinstance(sub_agent, dict) and "config_path" in sub_agent:
config_path = sub_agent["config_path"]
# Check if path looks like it should be relative to current file
if not config_path.startswith("/"):
referenced_path = file_path.parent / config_path
if not referenced_path.exists():
warnings.append(
f"Referenced sub-agent file may not exist: {config_path}"
)
# Check file extension
if not config_path.endswith((".yaml", ".yml")):
warnings.append(
"Sub-agent config_path should end with .yaml or .yml:"
f" {config_path}"
)
# Check tool format consistency
tools = config.get("tools", [])
has_object_format = any(isinstance(t, dict) for t in tools)
has_string_format = any(isinstance(t, str) for t in tools)
if has_object_format and has_string_format:
warnings.append(
"Mixed tool formats detected - consider using consistent object format"
)
return {"warnings": warnings, "has_warnings": len(warnings) > 0}
def _generate_retry_suggestion(errors: list) -> str:
"""Generate helpful suggestions for fixing validation errors."""
if not errors:
return ""
suggestions = []
for error in errors:
path = error.get("path", "")
message = error.get("message", "")
if "required" in message.lower():
if "name" in message:
suggestions.append(
"Add required 'name' field with a descriptive agent name"
)
elif "instruction" in message:
suggestions.append(
"Add required 'instruction' field with clear agent instructions"
)
else:
suggestions.append(
f"Add missing required field mentioned in error at '{path}'"
)
elif "enum" in message.lower() or "not one of" in message.lower():
suggestions.append(
f"Use valid enum value for field '{path}' - check schema for allowed"
" values"
)
elif "type" in message.lower():
if "string" in message:
suggestions.append(f"Field '{path}' should be a string value")
elif "array" in message:
suggestions.append(f"Field '{path}' should be a list/array")
elif "object" in message:
suggestions.append(f"Field '{path}' should be an object/dictionary")
elif "additional properties" in message.lower():
suggestions.append(
f"Remove unrecognized field '{path}' or check for typos"
)
if not suggestions:
suggestions.append(
"Please fix the validation errors and regenerate the configuration"
)
return " | ".join(suggestions[:3]) # Limit to top 3 suggestions
@@ -0,0 +1,122 @@
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""File writing tool for Agent Builder Assistant."""
from datetime import datetime
from pathlib import Path
import shutil
from typing import Any
from typing import Dict
async def write_files(
files: Dict[str, str],
create_backup: bool = False,
create_directories: bool = True,
) -> Dict[str, Any]:
"""Write content to multiple files with optional backup creation.
This tool writes content to multiple files. It's designed for creating
Python tools, callbacks, configuration files, and other code files.
Args:
files: Dict mapping file_path to content to write
create_backup: Whether to create backups of existing files (default: False)
create_directories: Whether to create parent directories (default: True)
Returns:
Dict containing write operation results:
- success: bool indicating if all writes succeeded
- files: dict mapping file_path to file info:
- file_size: size of written file in bytes
- existed_before: bool indicating if file existed before write
- backup_created: bool indicating if backup was created
- backup_path: path to backup file if created
- error: error message if write failed for this file
- successful_writes: number of files written successfully
- total_files: total number of files requested
- errors: list of general error messages
"""
try:
result = {
"success": True,
"files": {},
"successful_writes": 0,
"total_files": len(files),
"errors": [],
}
for file_path, content in files.items():
file_path_obj = Path(file_path).resolve()
file_info = {
"file_size": 0,
"existed_before": False,
"backup_created": False,
"backup_path": None,
"error": None,
}
try:
# Check if file already exists
file_info["existed_before"] = file_path_obj.exists()
# Create parent directories if needed
if create_directories:
file_path_obj.parent.mkdir(parents=True, exist_ok=True)
# Create backup if requested and file exists
if create_backup and file_info["existed_before"]:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
backup_path = file_path_obj.with_suffix(
f".backup_{timestamp}{file_path_obj.suffix}"
)
try:
shutil.copy2(file_path_obj, backup_path)
file_info["backup_created"] = True
file_info["backup_path"] = str(backup_path)
except Exception as e:
file_info["error"] = f"Failed to create backup: {str(e)}"
result["success"] = False
result["files"][str(file_path_obj)] = file_info
continue
# Write content to file
with open(file_path_obj, "w", encoding="utf-8") as f:
f.write(content)
# Verify write and get file size
if file_path_obj.exists():
file_info["file_size"] = file_path_obj.stat().st_size
result["successful_writes"] += 1
else:
file_info["error"] = "File was not created successfully"
result["success"] = False
except Exception as e:
file_info["error"] = f"Write failed: {str(e)}"
result["success"] = False
result["files"][str(file_path_obj)] = file_info
return result
except Exception as e:
return {
"success": False,
"files": {},
"successful_writes": 0,
"total_files": len(files) if files else 0,
"errors": [f"Write operation failed: {str(e)}"],
}

Some files were not shown because too many files have changed in this diff Show More