From 3be9cd844cfe83e0c3f029a294700306b85d00b5 Mon Sep 17 00:00:00 2001 From: "Xiang (Sean) Zhou" Date: Tue, 7 Oct 2025 08:10:29 -0700 Subject: [PATCH] chore: Let tools to handle root directory resolvement and model only knows the project name and always use relative path PiperOrigin-RevId: 816213558 --- .../agent_builder_assistant.py | 114 +++++------------- .../instruction_embedded.template | 93 +++++++------- .../tools/__init__.py | 2 - .../tools/delete_files.py | 13 +- .../tools/explore_project.py | 56 +++++---- .../tools/read_config_files.py | 7 +- .../tools/read_files.py | 18 ++- .../tools/resolve_root_directory.py | 100 --------------- .../tools/write_config_files.py | 3 + .../tools/write_files.py | 12 +- .../utils/resolve_root_directory.py | 87 +++++++++++++ 11 files changed, 237 insertions(+), 268 deletions(-) delete mode 100644 contributing/samples/adk_agent_builder_assistant/tools/resolve_root_directory.py create mode 100644 contributing/samples/adk_agent_builder_assistant/utils/resolve_root_directory.py diff --git a/contributing/samples/adk_agent_builder_assistant/agent_builder_assistant.py b/contributing/samples/adk_agent_builder_assistant/agent_builder_assistant.py index 4bb6d592..69f34a2a 100644 --- a/contributing/samples/adk_agent_builder_assistant/agent_builder_assistant.py +++ b/contributing/samples/adk_agent_builder_assistant/agent_builder_assistant.py @@ -34,7 +34,6 @@ 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 @@ -60,9 +59,7 @@ class AgentBuilderAssistant: Configured LlmAgent with embedded ADK AgentConfig schema """ # Load full ADK AgentConfig schema directly into instruction context - instruction = AgentBuilderAssistant._load_instruction_with_schema( - model, working_directory - ) + instruction = AgentBuilderAssistant._load_instruction_with_schema(model) # TOOL ARCHITECTURE: Hybrid approach using both AgentTools and FunctionTools # @@ -95,8 +92,6 @@ class AgentBuilderAssistant: 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 @@ -135,7 +130,6 @@ class AgentBuilderAssistant: # 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 @@ -157,7 +151,6 @@ class AgentBuilderAssistant: @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 = ( @@ -172,19 +165,43 @@ class AgentBuilderAssistant: 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 + # Extract project folder name from session state + project_folder_name = AgentBuilderAssistant._extract_project_folder_name( + context ) + # Fill the instruction template with all variables + instruction_text = instruction_template.format( + schema_content=schema_content, + default_model=model_str, + project_folder_name=project_folder_name, + ) + return instruction_text + return instruction_provider + @staticmethod + def _extract_project_folder_name(context: ReadonlyContext) -> str: + """Extract project folder name from session state using resolve_file_path.""" + from .utils.resolve_root_directory import resolve_file_path + + session_state = context._invocation_context.session.state + + # Use resolve_file_path to get the full resolved path for "." + # This handles all the root_directory resolution logic consistently + resolved_path = resolve_file_path(".", session_state) + + # Extract the project folder name from the resolved path + project_folder_name = resolved_path.name + + # Fallback to "project" if we somehow get an empty name + if not project_folder_name: + project_folder_name = "project" + + return project_folder_name + @staticmethod def _load_embedded_schema_instruction_template() -> str: """Load instruction template for embedded ADK AgentConfig schema mode.""" @@ -197,70 +214,3 @@ class AgentBuilderAssistant: 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 diff --git a/contributing/samples/adk_agent_builder_assistant/instruction_embedded.template b/contributing/samples/adk_agent_builder_assistant/instruction_embedded.template index 81303332..df47e071 100644 --- a/contributing/samples/adk_agent_builder_assistant/instruction_embedded.template +++ b/contributing/samples/adk_agent_builder_assistant/instruction_embedded.template @@ -29,11 +29,15 @@ You have access to the complete ADK AgentConfig schema embedded in your context: Always reference this schema when creating configurations to ensure compliance. +## Current Context + +**Current Project Folder Name**: `{project_folder_name}` + ## Workflow Guidelines ### 1. Discovery Phase - **DETERMINE USER INTENT FIRST**: - * **INFORMATIONAL QUESTIONS** (Answer directly WITHOUT asking for root directory): + * **INFORMATIONAL QUESTIONS** (Answer directly): - "Could you find me examples of..." / "Find me samples of..." - "Show me how to..." / "How do I..." - "What is..." / "What are..." / "Explain..." @@ -41,34 +45,21 @@ Always reference this schema when creating configurations to ensure compliance. - "I'm looking for information about..." / "I need to understand..." - Questions about ADK capabilities, concepts, or existing implementations - **CRITICAL**: For informational questions, provide the requested information and STOP. Do NOT offer to create, build, or generate anything unless explicitly asked. - * **CREATION/BUILDING INTENT** (Only then ask for root directory): + * **CREATION/BUILDING INTENT**: - "Create a new agent..." / "Build me an agent..." - "Generate an agent..." / "Implement an agent..." - "Update my agent..." / "Modify my agent..." / "Change my agent..." - "I want to create..." / "Help me build..." / "Help me update..." - "Set up a project..." / "Make me an agent..." -**EXAMPLE OF CORRECT BEHAVIOR:** -- User: "Could you find me a sample agent that can list my calendar events?" -- ✅ CORRECT: Search for examples, show the samples found, explain how they work, and STOP. -- ❌ WRONG: "Before I proceed with creating an agent..." or asking for root directory. -- **ROOT DIRECTORY ESTABLISHMENT** (Only for Creation/Building): - * **FIRST**: Check SESSION CONTEXT section below for "Established Root Directory" - * **IF ESTABLISHED**: Use the existing root directory - **🚨 NEVER ASK FOR ROOT DIRECTORY AGAIN** - * **IF NOT ESTABLISHED**: Ask user for root directory to establish working context - * **🚨 CRITICAL**: If SESSION CONTEXT shows an established root directory, NEVER ask "What is the root directory?" or similar questions - **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 +- Explore existing project structure using the explore_project tool - Identify integration needs (APIs, databases, external services) ### 2. Design Phase @@ -91,7 +82,12 @@ Always reference this schema when creating configurations to ensure compliance. - **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 -- **🚨 PATH DISPLAY RULE**: When root directory is established (shown in SESSION CONTEXT), ALWAYS show relative paths in responses (e.g., `root_agent.yaml`, `tools/dice_tool.py`) instead of full absolute paths +- **🚨 PATH DISPLAY RULE**: ALWAYS show relative paths in responses (e.g., `root_agent.yaml`, `tools/dice_tool.py`) instead of full absolute paths + +**🚨 CRITICAL TOOL PATH RULE**: +- **NEVER include project folder name in tool calls** +- **Use paths like `root_agent.yaml`, NOT `{project_folder_name}/root_agent.yaml`** +- **Tools automatically resolve relative to project folder** **IMPLEMENTATION ORDER (CRITICAL - ONLY AFTER USER CONFIRMS DESIGN):** @@ -109,18 +105,18 @@ Always reference this schema when creating configurations to ensure compliance. 5. **Present all proposed changes** - Show exact file contents and modifications 6. **Get explicit user approval** - Wait for "yes" or "proceed" before any writes 7. **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.) + * ⚠️ **YAML files**: Use `write_config_files` with paths like `"root_agent.yaml"` (NO project folder prefix) + * ⚠️ **Python files**: Use `write_files` with paths like `"tools/dice_tool.py"` (NO project folder prefix) 8. **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 +- **Sub-agent placement**: Place ALL sub-agent YAML files in the main project 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 TOOL NAMING RULE**: Use ONLY the FINAL/LAST component of the root folder path as project_folder_name - - ✅ CORRECT: For root directory `projects/workspace/my_agent`, use `my_agent` (last component) + * **Pattern**: `{{project_folder_name}}.tools.{{module_name}}.{{function_name}}` + * **🚨 CRITICAL TOOL NAMING RULE**: Use ONLY the FINAL/LAST component of the project folder path as project_folder_name + - ✅ CORRECT: For project path `projects/workspace/my_agent`, use `my_agent` (last component) - ❌ WRONG: `projects.workspace.my_agent` (full dotted path) - ✅ CORRECT: For `./config_based/roll_and_check`, use `roll_and_check` (last component) - ❌ WRONG: `config_based.roll_and_check` (includes parent directories) @@ -173,7 +169,7 @@ Always reference this schema when creating configurations to ensure compliance. ### Core Agent Building Tools #### Configuration Management (MANDATORY FOR .yaml/.yml FILES) -- **write_config_files**: ⚠️ REQUIRED for ALL YAML agent configuration files (root_agent.yaml, any sub-agent YAML files in root folder) +- **write_config_files**: ⚠️ REQUIRED for ALL YAML agent configuration files (root_agent.yaml, any sub-agent YAML files in main project folder) * Validates YAML syntax and ADK AgentConfig schema compliance * Example: `write_config_files({{"./project/root_agent.yaml": yaml_content, "./project/researcher_agent.yaml": sub_agent_content}})` * **CRITICAL**: All agent YAML files must be in the root project folder, NOT in a sub_agents/ subdirectory @@ -190,7 +186,6 @@ Always reference this schema when creating configurations to ensure compliance. #### Project Organization - **explore_project**: Explore project structure and suggest conventional file paths -- **resolve_root_directory**: Resolve path issues when execution context differs from user's working directory ### ADK Knowledge and Research Tools @@ -358,7 +353,7 @@ def log_tool_result(tool: BaseTool, args: Dict[str, Any], context: ToolContext, - 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) +- **Critical**: Tool paths must include the project folder name as the first component (final component of project folder path only) **ADK Agent Types and Model Field Rules:** - **LlmAgent**: REQUIRES `model` field (unless inherited from ancestor) - this agent directly uses LLM for responses @@ -374,31 +369,31 @@ def log_tool_result(tool: BaseTool, args: Dict[str, Any], context: ToolContext, * **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 +## File Operation Guidelines -**NEVER assume relative path context** - Always resolve paths first! +**CRITICAL PATH RULE FOR TOOL CALLS**: +- **NEVER include the project folder name in paths when calling tools** +- **Tools automatically resolve paths relative to the project folder** +- **Use simple relative paths like `root_agent.yaml`, `tools/dice_tool.py`** +- **WRONG**: `{project_folder_name}/root_agent.yaml` (includes project folder name) +- **CORRECT**: `root_agent.yaml` (just the file path within project) -### 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 +**Examples**: +- Current project folder: `basic` +- ✅ **CORRECT tool calls**: + * `write_config_files({{"root_agent.yaml": "..."}})` + * `write_files({{"tools/dice_tool.py": "..."}})` +- ❌ **WRONG tool calls**: + * `write_config_files({{"basic/root_agent.yaml": "..."}})` (duplicates project folder!) + * This would create `projects/basic/basic/root_agent.yaml` instead of `projects/basic/root_agent.yaml` ## 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 +1. Clear understanding of user requirements through targeted questions +2. Well-researched architecture based on proven ADK patterns +3. Comprehensive design proposal with agent relationships, tool mappings, AND specific file paths +4. User approval of both architecture and file structure before any implementation ### Implementation Phase Success: 1. Files created at exact paths specified in approved design @@ -413,8 +408,8 @@ def log_tool_result(tool: BaseTool, args: Dict[str, Any], context: ToolContext, **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 +1. **Understand requirements first** - Know what the user wants to build +2. **Design the architecture** - Plan the agent structure and components 3. **Provide high-level architecture overview** - When confirming design, always include: * Overall system architecture and component relationships * Agent types and their responsibilities @@ -429,10 +424,10 @@ def log_tool_result(tool: BaseTool, args: Dict[str, Any], context: ToolContext, ## 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 run [project_directory]` - Run agent from project 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 run [project_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` diff --git a/contributing/samples/adk_agent_builder_assistant/tools/__init__.py b/contributing/samples/adk_agent_builder_assistant/tools/__init__.py index 66d5bbd7..c282cfa4 100644 --- a/contributing/samples/adk_agent_builder_assistant/tools/__init__.py +++ b/contributing/samples/adk_agent_builder_assistant/tools/__init__.py @@ -19,7 +19,6 @@ 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 @@ -33,5 +32,4 @@ __all__ = [ 'write_files', 'search_adk_source', 'explore_project', - 'resolve_root_directory', ] diff --git a/contributing/samples/adk_agent_builder_assistant/tools/delete_files.py b/contributing/samples/adk_agent_builder_assistant/tools/delete_files.py index 18a68018..170f8e9e 100644 --- a/contributing/samples/adk_agent_builder_assistant/tools/delete_files.py +++ b/contributing/samples/adk_agent_builder_assistant/tools/delete_files.py @@ -21,9 +21,14 @@ from typing import Any from typing import Dict from typing import List +from google.adk.tools.tool_context import ToolContext + +from ..utils.resolve_root_directory import resolve_file_paths + async def delete_files( file_paths: List[str], + tool_context: ToolContext, create_backup: bool = False, confirm_deletion: bool = True, ) -> Dict[str, Any]: @@ -54,6 +59,10 @@ async def delete_files( - errors: list of general error messages """ try: + # Resolve file paths using session state + session_state = tool_context._invocation_context.session.state + resolved_paths = resolve_file_paths(file_paths, session_state) + result = { "success": True, "files": {}, @@ -68,8 +77,8 @@ async def delete_files( result["errors"].append("Deletion not confirmed by user") return result - for file_path in file_paths: - file_path_obj = Path(file_path).resolve() + for resolved_path in resolved_paths: + file_path_obj = resolved_path.resolve() file_info = { "existed": False, "backup_created": False, diff --git a/contributing/samples/adk_agent_builder_assistant/tools/explore_project.py b/contributing/samples/adk_agent_builder_assistant/tools/explore_project.py index 22ccc034..b9beb172 100644 --- a/contributing/samples/adk_agent_builder_assistant/tools/explore_project.py +++ b/contributing/samples/adk_agent_builder_assistant/tools/explore_project.py @@ -19,23 +19,24 @@ from typing import Any from typing import Dict from typing import List +from google.adk.tools.tool_context import ToolContext -async def explore_project(root_directory: str) -> Dict[str, Any]: +from ..utils.resolve_root_directory import resolve_file_path + + +async def explore_project(tool_context: ToolContext) -> 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 + The tool automatically determines the project directory from session state. Returns: - Dict containing analysis results: + Dict containing analysis results with ALL PATHS RELATIVE TO PROJECT FOLDER: 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: @@ -54,8 +55,7 @@ async def explore_project(root_directory: str) -> Dict[str, Any]: - 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 + • "relative_path": path relative to project folder • "size": file size in bytes • "is_valid_yaml": bool indicating if YAML parses correctly @@ -80,7 +80,7 @@ async def explore_project(root_directory: str) -> Dict[str, Any]: Examples: Basic project exploration: - result = await explore_project("/path/to/my_adk_project") + result = await explore_project(tool_context) Check project structure: if result["project_info"]["has_tools_directory"]: @@ -96,20 +96,21 @@ async def explore_project(root_directory: str) -> Dict[str, Any]: directories = result["suggestions"]["directories"]["tools"] """ try: - root_path = Path(root_directory).resolve() + # Resolve root directory using session state (use "." as current project directory) + session_state = tool_context._invocation_context.session.state + resolved_path = resolve_file_path(".", session_state) + root_path = resolved_path.resolve() if not root_path.exists(): return { "success": False, - "error": f"Root directory does not exist: {root_directory}", - "root_path": str(root_path), + "error": f"Project directory does not exist: {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), + "error": f"Path is not a directory: {root_path}", } # Analyze project structure @@ -121,7 +122,6 @@ async def explore_project(root_directory: str) -> Dict[str, Any]: return { "success": True, - "root_path": str(root_path), "project_info": project_info, "existing_configs": existing_configs, "directory_structure": directory_structure, @@ -132,14 +132,12 @@ async def explore_project(root_directory: str) -> Dict[str, Any]: except PermissionError: return { "success": False, - "error": f"Permission denied accessing directory: {root_directory}", - "root_path": root_directory, + "error": "Permission denied accessing project directory", } except Exception as e: return { "success": False, "error": f"Error exploring project: {str(e)}", - "root_path": root_directory, } @@ -191,12 +189,12 @@ def _find_existing_configs(root_path: Path) -> List[Dict[str, Any]]: # 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) + config_info = _analyze_config_file(yaml_file, root_path) configs.append(config_info) for yml_file in root_path.glob("*.yml"): if yml_file.is_file(): - config_info = _analyze_config_file(yml_file) + config_info = _analyze_config_file(yml_file, root_path) configs.append(config_info) # Sort by name for consistent ordering @@ -209,12 +207,18 @@ def _find_existing_configs(root_path: Path) -> List[Dict[str, Any]]: return configs -def _analyze_config_file(config_path: Path) -> Dict[str, Any]: +def _analyze_config_file(config_path: Path, root_path: Path) -> Dict[str, Any]: """Analyze a single configuration file.""" + # Compute relative path from project root + try: + relative_path = config_path.relative_to(root_path) + except ValueError: + # Fallback if not relative to root_path + relative_path = config_path.name + info = { "filename": config_path.name, - "path": str(config_path), - "relative_path": config_path.name, # In root directory + "relative_path": str(relative_path), "size": 0, "is_valid_yaml": False, "agent_name": None, @@ -300,10 +304,10 @@ def _generate_path_suggestions( "root_agent.yaml", ] - # Directory suggestions + # Directory suggestions (relative paths) directories = { "tools": { - "path": str(root_path / "tools"), + "path": "tools", "exists": (root_path / "tools").exists(), "purpose": "Custom tool implementations", "example_files": [ @@ -312,7 +316,7 @@ def _generate_path_suggestions( ], }, "callbacks": { - "path": str(root_path / "callbacks"), + "path": "callbacks", "exists": (root_path / "callbacks").exists(), "purpose": "Custom callback functions", "example_files": ["logging.py", "security.py"], diff --git a/contributing/samples/adk_agent_builder_assistant/tools/read_config_files.py b/contributing/samples/adk_agent_builder_assistant/tools/read_config_files.py index ad52c52e..63b1bc58 100644 --- a/contributing/samples/adk_agent_builder_assistant/tools/read_config_files.py +++ b/contributing/samples/adk_agent_builder_assistant/tools/read_config_files.py @@ -19,12 +19,15 @@ from typing import Any from typing import Dict from typing import List +from google.adk.tools.tool_context import ToolContext import yaml from .read_files import read_files -async def read_config_files(file_paths: List[str]) -> Dict[str, Any]: +async def read_config_files( + file_paths: List[str], tool_context: ToolContext +) -> Dict[str, Any]: """Read multiple YAML configuration files and extract metadata. Args: @@ -49,7 +52,7 @@ async def read_config_files(file_paths: List[str]) -> Dict[str, Any]: - errors: list of general error messages """ # Read all files using the file_manager read_files tool - read_result = await read_files(file_paths) + read_result = await read_files(file_paths, tool_context) result = { "success": True, diff --git a/contributing/samples/adk_agent_builder_assistant/tools/read_files.py b/contributing/samples/adk_agent_builder_assistant/tools/read_files.py index bd061340..4afaf271 100644 --- a/contributing/samples/adk_agent_builder_assistant/tools/read_files.py +++ b/contributing/samples/adk_agent_builder_assistant/tools/read_files.py @@ -19,8 +19,14 @@ from typing import Any from typing import Dict from typing import List +from google.adk.tools.tool_context import ToolContext -async def read_files(file_paths: List[str]) -> Dict[str, Any]: +from ..utils.resolve_root_directory import resolve_file_paths + + +async def read_files( + file_paths: List[str], tool_context: ToolContext +) -> Dict[str, Any]: """Read content from multiple files. This tool reads content from multiple files and returns their contents. @@ -43,6 +49,10 @@ async def read_files(file_paths: List[str]) -> Dict[str, Any]: - errors: list of general error messages """ try: + # Resolve file paths using session state + session_state = tool_context._invocation_context.session.state + resolved_paths = resolve_file_paths(file_paths, session_state) + result = { "success": True, "files": {}, @@ -51,8 +61,8 @@ async def read_files(file_paths: List[str]) -> Dict[str, Any]: "errors": [], } - for file_path in file_paths: - file_path_obj = Path(file_path).resolve() + for resolved_path in resolved_paths: + file_path_obj = resolved_path.resolve() file_info = { "content": "", "file_size": 0, @@ -72,7 +82,7 @@ async def read_files(file_paths: List[str]) -> Dict[str, Any]: result["successful_reads"] += 1 except Exception as e: - file_info["error"] = f"Failed to read {file_path}: {str(e)}" + file_info["error"] = f"Failed to read {file_path_obj}: {str(e)}" result["success"] = False result["files"][str(file_path_obj)] = file_info diff --git a/contributing/samples/adk_agent_builder_assistant/tools/resolve_root_directory.py b/contributing/samples/adk_agent_builder_assistant/tools/resolve_root_directory.py deleted file mode 100644 index 3483a78d..00000000 --- a/contributing/samples/adk_agent_builder_assistant/tools/resolve_root_directory.py +++ /dev/null @@ -1,100 +0,0 @@ -# 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, - } diff --git a/contributing/samples/adk_agent_builder_assistant/tools/write_config_files.py b/contributing/samples/adk_agent_builder_assistant/tools/write_config_files.py index 78f17240..8b56dc34 100644 --- a/contributing/samples/adk_agent_builder_assistant/tools/write_config_files.py +++ b/contributing/samples/adk_agent_builder_assistant/tools/write_config_files.py @@ -18,6 +18,7 @@ from pathlib import Path from typing import Any from typing import Dict +from google.adk.tools.tool_context import ToolContext import jsonschema import yaml @@ -27,6 +28,7 @@ from .write_files import write_files async def write_config_files( configs: Dict[str, str], + tool_context: ToolContext, backup_existing: bool = False, # Changed default to False - user should decide create_directories: bool = True, ) -> Dict[str, Any]: @@ -143,6 +145,7 @@ async def write_config_files( if result["success"] and validated_configs: write_result: Dict[str, Any] = await write_files( validated_configs, + tool_context, create_backup=backup_existing, create_directories=create_directories, ) diff --git a/contributing/samples/adk_agent_builder_assistant/tools/write_files.py b/contributing/samples/adk_agent_builder_assistant/tools/write_files.py index d610fe3e..088f8c41 100644 --- a/contributing/samples/adk_agent_builder_assistant/tools/write_files.py +++ b/contributing/samples/adk_agent_builder_assistant/tools/write_files.py @@ -20,9 +20,14 @@ import shutil from typing import Any from typing import Dict +from google.adk.tools.tool_context import ToolContext + +from ..utils.resolve_root_directory import resolve_file_path + async def write_files( files: Dict[str, str], + tool_context: ToolContext, create_backup: bool = False, create_directories: bool = True, ) -> Dict[str, Any]: @@ -50,6 +55,9 @@ async def write_files( - errors: list of general error messages """ try: + # Get session state for path resolution + session_state = tool_context._invocation_context.session.state + result = { "success": True, "files": {}, @@ -59,7 +67,9 @@ async def write_files( } for file_path, content in files.items(): - file_path_obj = Path(file_path).resolve() + # Resolve file path using session state + resolved_path = resolve_file_path(file_path, session_state) + file_path_obj = resolved_path.resolve() file_info = { "file_size": 0, "existed_before": False, diff --git a/contributing/samples/adk_agent_builder_assistant/utils/resolve_root_directory.py b/contributing/samples/adk_agent_builder_assistant/utils/resolve_root_directory.py new file mode 100644 index 00000000..4826b999 --- /dev/null +++ b/contributing/samples/adk_agent_builder_assistant/utils/resolve_root_directory.py @@ -0,0 +1,87 @@ +# 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 List +from typing import Optional + + +def resolve_file_path( + file_path: str, + session_state: Optional[Dict[str, Any]] = None, + working_directory: Optional[str] = None, +) -> Path: + """Resolve a file path using root directory from session state. + + This is a helper function that other tools can use to resolve file paths + without needing to be async or return detailed resolution information. + + Args: + file_path: File path (relative or absolute) + session_state: Session state dict that may contain root_directory + working_directory: Working directory to use as base (defaults to cwd) + + Returns: + Resolved absolute Path object + """ + file_path_obj = Path(file_path) + + # If already absolute, use as-is + if file_path_obj.is_absolute(): + return file_path_obj + + # Get root directory from session state, default to "./" + root_directory = "./" + if session_state and "root_directory" in session_state: + root_directory = session_state["root_directory"] + + # Use the same resolution logic as the main function + root_path_obj = Path(root_directory) + + if root_path_obj.is_absolute(): + resolved_root = root_path_obj + else: + if working_directory: + resolved_root = Path(working_directory) / root_directory + else: + resolved_root = Path(os.getcwd()) / root_directory + + # Resolve file path relative to root directory + return resolved_root / file_path + + +def resolve_file_paths( + file_paths: List[str], + session_state: Optional[Dict[str, Any]] = None, + working_directory: Optional[str] = None, +) -> List[Path]: + """Resolve multiple file paths using root directory from session state. + + Args: + file_paths: List of file paths (relative or absolute) + session_state: Session state dict that may contain root_directory + working_directory: Working directory to use as base (defaults to cwd) + + Returns: + List of resolved absolute Path objects + """ + return [ + resolve_file_path(path, session_state, working_directory) + for path in file_paths + ]